Repository: PyPSA/PyPSA

Issue:
Components Class
Components are not pure dataframes but python classes which represent containers. For example, n.generators is a GeneratorContainer class or Generators class (an subclass of the ComponentContainer or Components) with various attributes like static for the static dataframe, dynamic for the timedependant dataframes, or even shortcuts, like n.generators.p which would directly return the time-dependent dispatch. These containers could have component specific front-end functions like add_ocgt which has all the technological data for OCGT power plants.

See discussion in #896 

Discussion:
FabianHofmann: yes, that is what I had in mind!

Patch:
diff --git a/doc/examples-index/other.rst b/doc/examples-index/other.rst
index 7efdaa8f..4ba9df19 100644
--- a/doc/examples-index/other.rst
+++ b/doc/examples-index/other.rst
@@ -15,3 +15,4 @@ This section provides examples related to different topics:
     ../examples/replace-generator-storage-units-with-store.ipynb
     ../examples/flow-plot.ipynb
     ../examples/stochastic-problem.ipynb
+    ../examples/experimental-components-class.ipynb
diff --git a/doc/examples/experimental-components-class.nblink b/doc/examples/experimental-components-class.nblink
new file mode 100644
index 00000000..813e05cc
--- /dev/null
+++ b/doc/examples/experimental-components-class.nblink
@@ -0,0 +1,1 @@
+{"path": "../../examples/notebooks/experimental-components-class.ipynb"}
diff --git a/doc/references/release-notes.rst b/doc/references/release-notes.rst
index 43a1d35d..5581ecd1 100644
--- a/doc/references/release-notes.rst
+++ b/doc/references/release-notes.rst
@@ -31,16 +31,19 @@ Features
 
   * This is an experimental feature, will be developed further and is not yet 
     recommended for general use. More features, documentation and examples will 
-    follow. Most users will not notice any changes. If you wanna play around with
-    it, you could do so for example via: ``c = n.components.generators``.
-  
-  * While the changes try to maintain full backwards compatibility, there may be some 
-    breaking changes or bugs, especially if you use custom components or custom 
-    component attributes in your network attributes in your network. 
-  
-  * Please report any issues and bugs you might encounter
-    via the `issue tracker <https://github.com/PyPSA/PyPSA/issues/new>`__ on 
-    GitHub.
+    follow. Most users will not notice any changes.
+
+* Deprecation of custom components (https://github.com/PyPSA/PyPSA/pull/1130)
+
+   * This version of PyPSA deprecates custom components. While we don't see many use 
+     cases for them, they might be added in an improved way in future again. For a 
+     potential reimplementation we would be happy to hear your use case and 
+     requirements via the `issue tracker <https://www.github.com/PyPSA/PyPSA/issues>`_.
+   
+   * If you don't know what this is or have never used the ``override_components``
+     and ``override_component_attrs`` arguments during Network initialisation, you can
+     safely ignore this deprecation.
+     
 
 * New network attributes :meth:`n.timesteps <pypsa.networks.Network.timesteps>`, 
   :meth:`n.periods <pypsa.networks.Network.periods>` and 
diff --git a/doc/user-guide/components.rst b/doc/user-guide/components.rst
index 6475a152..73547aa8 100644
--- a/doc/user-guide/components.rst
+++ b/doc/user-guide/components.rst
@@ -492,3 +492,11 @@ correspond to the repository CSV files ``pypsa/data/components.csv`` and
 ``list_name``, ``description`` and ``type``. ``default_component_attrs`` is a special
 :meth:`Dict <pypsa.definitions.structures.Dict>` of pandas.DataFrame with the attribute
 properties for each component.  Just follow the formatting for the standard components.
+
+.. warning::
+
+   Version 0.33.0 of PyPSA, deprecates custom components. They most likely will be 
+   added in a different way in future again. If you rely on custom components,
+   please do not update to this version. For the reimplementation we would also be 
+   happy to hear your use case and requirements via the 
+   `issue tracker <https://www.github.com/PyPSA/PyPSA/issues>`_.
diff --git a/examples/notebooks/experimental-components-class.ipynb b/examples/notebooks/experimental-components-class.ipynb
new file mode 100644
index 00000000..b493416e
--- /dev/null
+++ b/examples/notebooks/experimental-components-class.ipynb
@@ -0,0 +1,253 @@
+{
+ "cells": [
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Components class\n",
+    "\n",
+    "Version `0.33` of PyPSA introduces a structural refactoring of how component data is stored and accessed. The new structure adds an extra layer to move all component-specific data from the networks class to a new component class. With version `0.33`, most of these changes will be unnoticeable to the user. \n",
+    "\n",
+    "But this makes it easy to add new features. Below are some simple examples to show which other features could be added in the future. If you have any ideas, wishes, feedback or suggestions, please let us know via https://github.com/PyPSA/PyPSA/issues.\n",
+    "\n",
+    "Note that this is experimental, features will be added and the newly introduced API may change. You can still use PyPSA as usual. Major API changes will not be introduced before version `1.0`.\n",
+    "Also, while all classes and methods have docstrings, there is no dedicated documentation yet."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## General"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import pypsa\n",
+    "\n",
+    "n = pypsa.examples.scigrid_de()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "So far, components data was directly attached to the network object (e.g. `n.generators`, `n.generators_t` etc.). While you still can access the data there, both actually sit now in a new `Components` class:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c = n.components.generators  # also via alias n.c.generators\n",
+    "c"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The datasets for static and dynamic data can be accessed via the class now, but also still via the old network properties:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c.static.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c.dynamic.keys()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Both ways refer to the same DataFrame/ Dict Container of\n",
+    "print(c.static is n.generators)\n",
+    "print(c.dynamic is n.generators_t)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Examples"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Simple alias properties"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Basic component information\n",
+    "print(f\"Component name: '{c.name}'\")\n",
+    "print(f\"Component list name: '{c.list_name}'\")\n",
+    "print(f\"Component type: '{c.type}'\")\n",
+    "\n",
+    "# Other maybe useful properties\n",
+    "print(f\"Nominal attribute: '{c.nominal_attr}'\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Quick access to attribute units\n",
+    "c.units.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Get ports of component (e.g. for multiport components)\n",
+    "n.c.links.ports"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Check if component is attached to network\n",
+    "if c.attached:\n",
+    "    print(f\"{c} is attached to {c.n}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Rename components and propagate new names through network"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Old names\n",
+    "print(f\"Old bus names: {', '.join(c.static.head(2).index)}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Rename \"1 Gas\" component\n",
+    "c = n.components.buses\n",
+    "rename_map = {\"1\": \"Super Bus\"}\n",
+    "c.rename_component_names(**rename_map)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# New names\n",
+    "print(f\"New bus names: {', '.join(c.static.head(2).index)}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Changes in other components of network\n",
+    "n.c.generators.static.head(2)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Calculate line length from attached buses"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "c = n.c.lines\n",
+    "c.calculate_line_length()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Those are just a couple of simple examples. Many other features could be added. If you have any ideas, wishes, feedback or suggestions, please let us know via the https://github.com/PyPSA/PyPSA/issues."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": []
+  }
+ ],
+ "metadata": {
+  "@webio": {
+   "lastCommId": null,
+   "lastKernelId": null
+  },
+  "kernelspec": {
+   "display_name": "",
+   "language": "python",
+   "name": ""
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.13.1"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/pypsa/components/abstract.py b/pypsa/components/abstract.py
index 36a3f57a..9c8004e6 100644
--- a/pypsa/components/abstract.py
+++ b/pypsa/components/abstract.py
@@ -118,7 +118,7 @@ class Components(ComponentsData, ABC):
         static, dynamic = self._get_data_containers(ctype)
         super().__init__(ctype, n=None, static=static, dynamic=dynamic)
 
-    def __repr__(self) -> str:
+    def __str__(self) -> str:
         """
         Get string representation of component.
 
@@ -130,43 +130,27 @@ class Components(ComponentsData, ABC):
         Examples
         --------
         >>> import pypsa
-        >>> c = pypsa.examples.ac_dc_meshed().components.generators
-        >>> c
-        PyPSA 'Generator' Components
-        ----------------------------
-        Attached to PyPSA Network 'AC-DC'
-        Components: 6
+        >>> n = pypsa.examples.ac_dc_meshed()
+        >>> str(n.components.generators)
+        "PyPSA 'Generator' Components"
 
         """
-        num_components = len(self.static)
-        if not num_components:
-            return f"Empty PyPSA {self.ctype.name} Components\n"
-        text = f"PyPSA '{self.ctype.name}' Components"
-        text += "\n" + "-" * len(text) + "\n"
+        return f"PyPSA '{self.ctype.name}' Components"
 
-        # Add attachment status
-        if self.attached:
-            network_name = f"'{self.n_save.name}'" if self.n_save.name else ""
-            text += f"Attached to PyPSA Network {network_name}\n"
-
-        text += f"Components: {len(self.static)}"
-
-        return text
-
-    def __str__(self) -> str:
+    def __repr__(self) -> str:
         """
-        Get string representation of component.
+        Get representation of component.
 
         Returns
         -------
         str
-            String representation of component.
+            Representation of component.
 
         Examples
         --------
         >>> import pypsa
-        >>> n = pypsa.examples.ac_dc_meshed()
-        >>> n.components.generators
+        >>> c = pypsa.examples.ac_dc_meshed().components.generators
+        >>> c
         PyPSA 'Generator' Components
         ----------------------------
         Attached to PyPSA Network 'AC-DC'
@@ -174,7 +158,16 @@ class Components(ComponentsData, ABC):
 
         """
         num_components = len(self.static)
-        text = f"{num_components} '{self.ctype.name}' Components"
+        if not num_components:
+            return f"Empty {self}"
+        text = f"{self}\n" + "-" * len(str(self)) + "\n"
+
+        # Add attachment status
+        if self.attached:
+            text += f"Attached to {str(self.n)}\n"
+
+        text += f"Components: {len(self.static)}"
+
         return text
 
     def __getitem__(self, key: str) -> Any:
@@ -566,6 +559,147 @@ class Components(ComponentsData, ABC):
         """
         return self.dynamic
 
+    @property
+    def units(self) -> pd.Series:
+        """
+        Get units of all attributes of components.
+
+        Returns
+        -------
+        pd.Series
+            Series with attribute names as index and units as values.
+
+        Examples
+        --------
+        >>> import pypsa
+        >>> c = pypsa.examples.ac_dc_meshed().components.generators
+        >>> c.units.head() # doctest: +SKIP
+                       unit
+        attribute
+        p_nom            MW
+        p_nom_mod        MW
+        p_nom_min        MW
+        p_nom_max        MW
+        p_min_pu   per unit
+
+        """
+        return self.defaults.unit[self.defaults.unit.notnull()].to_frame()
+
+    @property
+    def ports(self) -> list:
+        """
+        Get ports of all components.
+
+        Returns
+        -------
+        pd.Series
+            Series with attribute names as index and port names as values.
+
+        Examples
+        --------
+        >>> import pypsa
+        >>> c = pypsa.examples.ac_dc_meshed().components.lines
+        >>> c.ports
+        ['0', '1']
+
+        """
+        return [str(col)[3:] for col in self.static if str(col).startswith("bus")]
+
+    @property
+    def nominal_attr(self) -> str:
+        """
+        Get nominal attribute of component.
+
+        Returns
+        -------
+        str
+            Name of the nominal attribute of the component.
+
+        Examples
+        --------
+        >>> import pypsa
+        >>> c = pypsa.examples.ac_dc_meshed().components.generators
+        >>> c.nominal_attr
+        'p_nom'
+
+        """
+        # TODO: move to Component Specific class
+        nominal_attr = {
+            "Generator": "p_nom",
+            "Line": "s_nom",
+            "Transformer": "s_nom",
+            "Link": "p_nom",
+            "Store": "e_nom",
+            "StorageUnit": "p_nom",
+        }
+        try:
+            return nominal_attr[self.ctype.name]
+        except KeyError:
+            msg = f"Component type '{self.ctype.name}' has no nominal attribute."
+            raise AttributeError(msg)
+
+    # TODO move
+    def rename_component_names(self, **kwargs: str) -> None:
+        """
+        Rename component names.
+
+        Rename components and also update all cross-references of the component in
+        the network.
+
+        Parameters
+        ----------
+        **kwargs
+            Mapping of old names to new names.
+
+        Returns
+        -------
+        None
+
+        Examples
+        --------
+        Define some network
+        >>> import pypsa
+        >>> n = pypsa.Network()
+        >>> n.add("Bus", ["bus1"])
+        Index(['bus1'], dtype='object')
+        >>> n.add("Generator", ["gen1"], bus="bus1")
+        Index(['gen1'], dtype='object')
+        >>> c = n.c.buses
+
+        Now rename the bus
+
+        >>> c.rename_component_names(bus1="bus2")
+
+        Which updates the bus components
+
+        >>> c.static.index
+        Index(['bus2'], dtype='object', name='Bus')
+
+        and all references in the network
+
+        >>> n.generators.bus
+        Generator
+        gen1    bus2
+        Name: bus, dtype: object
+
+        """
+        if not all(isinstance(v, str) for v in kwargs.values()):
+            msg = "New names must be strings."
+            raise ValueError(msg)
+
+        # Rename component name definitions
+        self.static = self.static.rename(index=kwargs)
+        for k, v in self.dynamic.items():  # Modify in place
+            self.dynamic[k] = v.rename(columns=kwargs)
+
+        # Rename cross references in network (if attached to one)
+        if self.attached:
+            for c in self.n_save.components.values():
+                col_name = self.name.lower()  # TODO: Generalize
+                cols = [f"{col_name}{port}" for port in c.ports]
+                if cols and not c.static.empty:
+                    c.static[cols] = c.static[cols].replace(kwargs)
+
 
 class SubNetworkComponents:
     """
diff --git a/pypsa/components/components.py b/pypsa/components/components.py
index b7aa09dc..2c4f1e19 100644
--- a/pypsa/components/components.py
+++ b/pypsa/components/components.py
@@ -11,12 +11,14 @@ import logging
 import warnings
 from typing import Any
 
+import numpy as np
 import pandas as pd
 
 from pypsa.components.abstract import Components
 from pypsa.components.types import ComponentType
 from pypsa.components.types import get as get_component_type
 from pypsa.definitions.structures import Dict
+from pypsa.geo import haversine_pts
 
 logger = logging.getLogger(__name__)
 
@@ -101,8 +103,100 @@ class Generators(Components):
         super().__init__(*args, **kwargs)
 
 
+class Lines(Components):
+    """
+    Lines components class.
+
+    This class is used for line components. All functionality specific to
+    lines is implemented here. Functionality for all components is implemented in
+    the abstract base class.
+
+    .. warning::
+        This class is under ongoing development and will be subject to changes.
+        It is not recommended to use this class outside of PyPSA.
+
+    See Also
+    --------
+    pypsa.components.abstract.Components : Base class for all components.
+    pypsa.components.components.GenericComponents : Generic components class.
+
+    """
+
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        """
+        Initialize Generators class.
+
+        See :class:`pypsa.components.abstract.Components` for more information.
+
+        Parameters
+        ----------
+        args : Any
+            Arguments of base class.
+        kwargs : Any
+            Keyword arguments of base class.
+
+        Returns
+        -------
+        None
+
+        """
+        super().__init__(*args, **kwargs)
+
+    def calculate_line_length(self) -> pd.Series:
+        """
+        Get length of the lines in meters.
+
+        Based on coordinates of attached buses. Buses must have 'x' and 'y' attributes,
+        otherwise no line length can be calculated. By default the haversine formula is
+        used to calculate the distance between two points.
+
+        Returns
+        -------
+        pd.Series
+            Length of the lines.
+
+        See Also
+        --------
+        pypsa.geo.haversine : Function to calculate distance between two points.
+
+        Examples
+        --------
+        >>> import pypsa
+        >>> c = pypsa.examples.scigrid_de().c.lines
+        >>> ds = c.calculate_line_length()
+        >>> ds.head()
+        0    34432.796096
+        1    59701.666027
+        2    32242.741010
+        3    30559.154647
+        4    21574.543367
+        dtype: float64
+
+        """
+        return (
+            pd.Series(
+                haversine_pts(
+                    a=np.array(
+                        [
+                            self.static.bus0.map(self.n_save.buses.x),
+                            self.static.bus0.map(self.n_save.buses.y),
+                        ]
+                    ).T,
+                    b=np.array(
+                        [
+                            self.static.bus1.map(self.n_save.buses.x),
+                            self.static.bus1.map(self.n_save.buses.y),
+                        ]
+                    ).T,
+                )
+            )
+            * 1_000
+        )
+
+
 CLASS_MAPPING = {
     "Generator": Generators,
+    "Line": Lines,
 }
 
 
diff --git a/pypsa/components/types.py b/pypsa/components/types.py
index b001bafd..b383cdb2 100644
--- a/pypsa/components/types.py
+++ b/pypsa/components/types.py
@@ -7,7 +7,6 @@ Additional types can be added by the user.
 
 from __future__ import annotations
 
-from collections.abc import Sequence
 from pathlib import Path
 
 import numpy as np
@@ -222,35 +221,6 @@ def get(name: str) -> ComponentType:
         raise ValueError(msg)
 
 
-def check_if_added(components: str | Sequence) -> None:
-    """
-    Check if components are registered package-wide.
-
-    Parameters
-    ----------
-    components : list of str
-        List of component type names.
-
-    Raises
-    ------
-    ValueError
-        If a component is not registered package-wide.
-
-    """
-    if np.isscalar(components):
-        components = [components]
-
-    # Make sure any custom components are registered package-wide
-    if not_registered := [
-        c_name for c_name in components if c_name not in all_components.keys()
-    ]:
-        msg = (
-            f"Network contains custom components which are not registered "
-            f"package-wide. Add them first: {not_registered}components_"
-        )
-        raise ValueError(msg)
-
-
 # Load default component types
 _load_default_component_types(
     component_df=component_types_df,
diff --git a/pypsa/networks.py b/pypsa/networks.py
index 3740fa96..baadfb7e 100644
--- a/pypsa/networks.py
+++ b/pypsa/networks.py
@@ -34,9 +34,7 @@ from pypsa.common import as_index, deprecated_common_kwargs
 from pypsa.components.abstract import SubNetworkComponents
 from pypsa.components.components import Component
 from pypsa.components.types import (
-    check_if_added,
     component_types_df,
-    default_components,
 )
 from pypsa.components.types import (
     get as get_component_type,
@@ -262,7 +260,6 @@ class Network:
         import_name: str | Path = "",
         name: str = "",
         ignore_standard_types: bool = False,
-        custom_components: list[str] | None = None,
         override_components: pd.DataFrame | None = None,
         override_component_attrs: Dict | None = None,
         **kwargs: Any,
@@ -270,13 +267,11 @@ class Network:
         if override_components is not None or override_component_attrs is not None:
             msg = (
                 "The arguments `override_components` and `override_component_attrs` "
-                "are deprecated. Please use the #TODO"
+                "are deprecated. Please check the release notes: "
+                "https://pypsa.readthedocs.io/en/latest/references/release-notes.html#v0-33-0"
             )
             raise DeprecationWarning(msg)
 
-        if custom_components is None:
-            custom_components = []
-
         # Initialise root logger and set its level, if this has not been done before
         logging.basicConfig(level=logging.INFO)
 
@@ -307,7 +302,7 @@ class Network:
         # Define component sets
         self._initialize_component_sets()
 
-        self._initialize_components(custom_components=custom_components)
+        self._initialize_components()
 
         if not ignore_standard_types:
             self.read_in_default_standard_types()
@@ -329,9 +324,11 @@ class Network:
         for key, value in kwargs.items():
             setattr(self, key, value)
 
+    def __str__(self) -> str:
+        return f"PyPSA Network '{self.name}'" if self.name else "Unnamed PyPSA Network"
+
     def __repr__(self) -> str:
-        header = "PyPSA Network" + (f" '{self.name}'" if self.name else "")
-        header += "\n" + "-" * len(header)
+        header = f"{self}\n" + "-" * len(str(self))  # + "\n"
         comps = {
             c.name: f" - {c.name}: {len(c.static)}"
             for c in self.iterate_components()
@@ -398,8 +395,8 @@ class Network:
 
         self.all_components = set(component_types_df.index) - {"Network"}
 
-    def _initialize_components(self, custom_components: list) -> None:
-        components = component_types_df.index.to_list() + custom_components
+    def _initialize_components(self) -> None:
+        components = component_types_df.index.to_list()
 
         self.components = ComponentsStore()
         for c_name in components:
@@ -445,16 +442,6 @@ class Network:
         """
         return self.components
 
-    @property
-    def has_custom_components(self) -> bool:
-        """Check if network has custom components."""
-        return bool(set(self.components.keys()) - set(default_components))
-
-    @property
-    def custom_components(self) -> list[str]:
-        """List of custom components."""
-        return list(set(self.components.keys()) - set(default_components))
-
     @future_deprecation(details="Use `self.components.<component>.dynamic` instead.")
     def df(self, component_name: str) -> pd.DataFrame:
         """
@@ -1449,14 +1436,8 @@ class Network:
             )
             snapshots_ = pd.Index([], name="snapshot")
 
-        # Check if custom components are registered
-        check_if_added(self.custom_components)
-
         # Setup new network
-        n = self.__class__(
-            ignore_standard_types=ignore_standard_types,
-            custom_components=list(self.components.keys()) + self.custom_components,
-        )
+        n = self.__class__(ignore_standard_types=ignore_standard_types)
 
         # Copy components
         other_comps = sorted(self.all_components - {"Bus", "Carrier"})
@@ -1545,13 +1526,9 @@ class Network:
         else:
             time_i = slice(None)
 
-        # Check if custom components are registered
-        check_if_added(self.custom_components)
-
         # Setup new network
-        n = self.__class__(
-            custom_components=list(self.components.keys()) + self.custom_components
-        )
+        n = self.__class__()
+
         n.add(
             "Bus",
             pd.DataFrame(self.buses.loc[key]).assign(sub_network="").index,
@@ -1721,6 +1698,56 @@ class Network:
             if not (skip_empty and self.static(c_name).empty)
         )
 
+    def rename_component_names(
+        self, component: str | Components, **kwargs: str
+    ) -> None:
+        """
+        Rename component names.
+
+        Rename components of component type and also update all cross-references of
+        the component in network.
+
+        Parameters
+        ----------
+        component : str or pypsa.Components
+            Component type or instance of pypsa.Components.
+        **kwargs
+            Mapping of old names to new names.
+
+        Returns
+        -------
+        None
+
+        Examples
+        --------
+        Define some network
+        >>> import pypsa
+        >>> n = pypsa.Network()
+        >>> n.add("Bus", ["bus1"])
+        Index(['bus1'], dtype='object')
+        >>> n.add("Generator", ["gen1"], bus="bus1")
+        Index(['gen1'], dtype='object')
+
+        Now rename the bus component
+
+        >>> n.rename_component_names("Bus", bus1="bus2")
+
+        Which updates the bus components
+
+        >>> n.buses.index
+        Index(['bus2'], dtype='object', name='Bus')
+
+        and all references in the network
+
+        >>> n.generators.bus
+        Generator
+        gen1    bus2
+        Name: bus, dtype: object
+
+        """
+        c = as_components(self, component)
+        c.rename_component_names(**kwargs)
+
 
 class SubNetwork:
     """


Test patch:
diff --git a/test/test_components_abstract.py b/test/test_components_abstract.py
deleted file mode 100644
index 0b808e56..00000000
--- a/test/test_components_abstract.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import pytest
-
-import pypsa
-
-component_names = [
-    "Generator",
-    "Line",
-    "Link",
-    "StorageUnit",
-    "Load",
-    "Transformer",
-]
-
-
-@pytest.mark.parametrize("component_name", component_names)
-def test_alias_properties(ac_dc_network, component_name):
-    n = ac_dc_network
-    ct = pypsa.components.types.get(component_name)
-    c = n.components[component_name]
-
-    assert c.name == ct.name
-    assert c.list_name == ct.list_name
-    assert c.description == ct.description
-    assert c.category == ct.category
-    assert c.type == ct.category
-    assert c.defaults.equals(ct.defaults)
-    assert c.attrs.equals(ct.defaults)
-    if c.standard_types is not None:
-        assert c.standard_types.equals(ct.standard_types)
-
-    assert c.df is c.static
-    assert c.pnl is c.dynamic
-
-
-def test_network_attachments(ac_dc_network):
-    n = ac_dc_network.copy()
-
-    c = n.components.generators
-
-    assert c.n is n
-    assert c.n_save is n
-    assert c.attached is True
-    c.n = None
-    c.attached is False
-    with pytest.raises(AttributeError):
-        c.n_save
-
-
-def test_components_repr(ac_dc_network):
-    n = ac_dc_network
-    c = n.components.generators
-
-    assert repr(c).startswith("PyPSA 'Generator' Components")
diff --git a/test/test_components_custom.py b/test/test_components_custom.py
index d5963c89..f87a925e 100644
--- a/test/test_components_custom.py
+++ b/test/test_components_custom.py
@@ -1,7 +1,6 @@
 from pathlib import Path
 
 import pandas as pd
-import pytest
 
 import pypsa
 from pypsa.components.types import component_types_df, default_components, get
@@ -47,8 +46,8 @@ def test_custom_component_registration():
     assert custom_component.defaults.equals(defaults_df)
 
 
-def test_unregistered_custom_components():
-    import pypsa
+# def test_unregistered_custom_components():
+#     import pypsa
 
-    with pytest.raises(ValueError, match="Component type 'MyComponent' not found."):
-        pypsa.Network(custom_components=["MyComponent"])
+#     with pytest.raises(ValueError, match="Component type 'MyComponent' not found."):
+#         pypsa.Network(custom_components=["MyComponent"])
diff --git a/test/test_network.py b/test/test_network.py
index 5e9409cd..3d366b65 100644
--- a/test/test_network.py
+++ b/test/test_network.py
@@ -446,3 +446,49 @@ def test_components_referencing(ac_dc_network):
     assert id(ac_dc_network.buses) == id(ac_dc_network.components.buses.static)
     assert id(ac_dc_network.buses_t) == id(ac_dc_network.components.buses.dynamic)
     assert id(ac_dc_network.components.buses) == id(ac_dc_network.components.Bus)
+
+
+@pytest.mark.parametrize("use_component", [True, False])
+def test_rename_component_names(use_component):
+    n = pypsa.Network()
+    n.snapshots = [0, 1]
+
+    n.add("Bus", "bus1", v_mag_pu_set=[0.1, 0.2])
+    n.add("Bus", "bus2", v_mag_pu_set=[0.1, 0.2])
+    n.add("Line", "line1", bus0="bus1", bus1="bus2", s_max_pu=[0.1, 0.2])
+    n.add("Generator", "gen1", bus="bus1", p_min_pu=[0.1, 0.2])
+
+    if use_component:
+        c = n.c.buses
+        c.rename_component_names(bus1="bus3")
+    else:
+        n.rename_component_names("Bus", bus1="bus3")
+
+    with pytest.raises(ValueError):
+        n.rename_component_names("Bus", bus1=10)
+
+    assert "bus1" not in n.c.buses.static.index
+    assert "bus1" not in n.c.buses.dynamic.v_mag_pu_set.columns
+    assert "bus2" in n.c.buses.static.index
+    assert "bus2" in n.c.buses.dynamic.v_mag_pu_set.columns
+    assert "bus3" in n.c.buses.static.index
+    assert "bus3" in n.c.buses.dynamic.v_mag_pu_set.columns
+
+    assert "bus1" not in n.c.lines.static.bus0.to_list()
+    assert "bus1" not in n.c.lines.static.bus1.to_list()
+    assert "bus3" in n.c.lines.static.bus0.to_list()
+    assert "bus2" in n.c.lines.static.bus1.to_list()
+
+    assert "bus1" not in n.c.generators.static.bus.to_list()
+    assert "bus3" in n.c.generators.static.bus.to_list()
+
+
+def test_components_repr(ac_dc_network):
+    n = ac_dc_network
+
+    assert repr(n).startswith("PyPSA Network 'AC-DC'")
+    assert len(repr(n)) > len(str(n))
+
+    n = pypsa.Network()
+    assert repr(n).startswith("Empty Unnamed PyPSA Network")
+    assert len(repr(n)) > len(str(n))