Repository: oemof/tespy

Issue:
get_comps of class Network does not work prior to first solve 
### Discussed in https://github.com/oemof/tespy/discussions/359

<div type='discussions-op-text'>

<sup>Originally posted by **GoldenPotatis** September 22, 2022</sup>
Hi!

I'm a new user of TESPy and still learning. I found this small issue and would like to share with the community.
I'm using Python3.8.9 and TESPy0.6.0

In the Network Class, it is possible to view the network connections and components by calling the `network.conns` attributes and `network.comps` attributes. The `network.conns` works perfectly fine. However, the user may find that `network.comps` does not work. It returns `AttributeError: 'Network' object has no attribute 'comps'`.

The example I'm using is a simple condenser cooled by air, same as in the [Condenser example code in tespy.readthedocs](https://tespy.readthedocs.io/en/main/api/tespy.components.html#module-tespy.components.heat_exchangers.condenser):

```python
from tespy.networks import Network
from tespy.components import Source, Sink, Condenser
from tespy.connections import Connection

# create a network and components
nw = Network(fluids=['water', 'air'], T_unit='C', p_unit='bar', h_unit='kJ / kg')
air_in = Source(label='air inlet')
air_out = Sink('air outlet')
waste_steam = Source('waste steam')
water = Sink('condensate water')
condenser = Condenser('condenser')

# create connections
air_con = Connection(air_in, 'out1', condenser, 'in2', label='air to condenser')
con_air = Connection(condenser, 'out2', air_out, 'in1', label='condenser to air')
ws_con = Connection(waste_steam, 'out1', condenser, 'in1', label='ws to con')
con_water = Connection(condenser, 'out1', water, 'in1')

nw.add_conns(air_con, con_air, ws_con, con_water)

```
When calling the `nw.conns` attributes, it will return a Pandas DataFrame. But calling `nw.comps` will result in `AttributeError: 'Network' object has no attribute 'comps'`.

![image](https://user-images.githubusercontent.com/30263046/191722120-8add9f70-7cc3-42a1-8498-dc3b3a799390.png)

Correspondingly, the `Network.get_comp()` doesn't work neither.

After looking into the source code of Network, it is found that the `network.conns` is initiated at the beginning under `def __init__(self, fluids, memorise_fluid_properties=True, **kwargs)` and `def set_defaults(self)`. But `network.comps` is not initiated. The first time `network.comps` is initiated is actually under `def check_network(self)`, which is:

```python
comps = pd.unique(self.conns[['source', 'target']].values.ravel())
# build the dataframe for components
```

After running `nw.check_network()`, the `nw.comps` then works perfectly fine, which gives a dataframe of the components.

![image](https://user-images.githubusercontent.com/30263046/191725300-efe870af-7092-41bd-a7fe-d511626e09e6.png)

It is therefore suggested to initiate the `.comps` at the beginning of the code to avoid such error.



Another attached question is under the example code of Condenser in readthedocs, why the air inlet is Sink and air outlet is Source?

```python
amb_in = Sink('ambient air inlet')
amb_out = Source('air outlet')
```

Based on the definition of Source and Sink, should it be like air inlet is Source and air outlet is Sink?
</div>

Comments:

Patch:
diff --git a/docs/whats_new/v0-6-1.rst b/docs/whats_new/v0-6-1.rst
index 5632686a..70e889fe 100644
--- a/docs/whats_new/v0-6-1.rst
+++ b/docs/whats_new/v0-6-1.rst
@@ -22,6 +22,14 @@ New Features
   :py:class:`tespy.connections.connection.Ref` class
   (`Discussion #352 <https://github.com/oemof/tespy/discussions/352>`__).
 
+Bug Fixes
+#########
+- The Network's component DataFrame is now available as soon as a connection
+  is added to the network. It is possible to use the
+  :py:meth:`tespy.networks.network.Network.get_comp` method prior to
+  initializing or solving
+  (`PR #362 <https://github.com/oemof/tespy/pull/362>`_).
+
 Documentation
 #############
 - Fix some typos in the online documentation
diff --git a/src/tespy/networks/network.py b/src/tespy/networks/network.py
index 3d638986..c8108970 100644
--- a/src/tespy/networks/network.py
+++ b/src/tespy/networks/network.py
@@ -175,7 +175,10 @@ class Network:
         # connection dataframe
         self.conns = pd.DataFrame(
             columns=['object', 'source', 'source_id', 'target', 'target_id'],
-            dtype='object')
+            dtype='object'
+        )
+        # component dataframe
+        self.comps = pd.DataFrame(dtype='object')
         # user defined function dictionary for fast access
         self.user_defined_eq = {}
         # bus dictionary
@@ -471,6 +474,7 @@ class Network:
             logging.debug(msg)
             # set status "checked" to false, if connection is added to network.
             self.checked = False
+        self._add_comps(*args)
 
     def del_conns(self, *args):
         """
@@ -525,6 +529,40 @@ class Network:
             logging.error(msg)
             raise hlp.TESPyNetworkError(msg)
 
+    def _add_comps(self, *args):
+        r"""
+        Add to network's component DataFrame from added connections.
+
+        Parameters
+        ----------
+        c : tespy.connections.connection.Connection
+            The connections, which have been added to the network. The
+            components are extracted from these information.
+        """
+        # get unique components in new connections
+        comps = list(set([cp for c in args for cp in [c.source, c.target]]))
+        # add to the dataframe of components
+        for comp in comps:
+            if comp.label in self.comps.index:
+                if self.comps.loc[comp.label, 'object'] == comp:
+                    continue
+                else:
+                    comp_type = comp.__class__.__name__
+                    other_obj = self.comps.loc[comp.label, "object"]
+                    other_comp_type = other_obj.__class__.__name__
+                    msg = (
+                        f"The component with the label {comp.label} of type "
+                        f"{comp_type} cannot be added to the network as a "
+                        f"different component of type {other_comp_type} with "
+                        "the same label has already been added. All "
+                        "components must have unique values!"
+                    )
+                    raise hlp.TESPyNetworkError(msg)
+
+            comp_type = comp.__class__.__name__
+            self.comps.loc[comp.label, 'comp_type'] = comp_type
+            self.comps.loc[comp.label, 'object'] = comp
+
     def add_ude(self, *args):
         r"""
         Add a user defined function to the network.
@@ -644,21 +682,21 @@ class Network:
         if len(self.conns) == 0:
             msg = (
                 'No connections have been added to the network, please make '
-                'sure to add your connections with the .add_conns() method.')
+                'sure to add your connections with the .add_conns() method.'
+            )
             logging.error(msg)
             raise hlp.TESPyNetworkError(msg)
 
         if len(self.fluids) == 0:
-            msg = ('Network has no fluids, please specify a list with fluids '
-                   'on network creation.')
+            msg = (
+                'Network has no fluids, please specify a list with fluids on '
+                'network creation.'
+            )
             logging.error(msg)
             raise hlp.TESPyNetworkError(msg)
 
         self.check_conns()
-        # get unique components in connections dataframe
-        comps = pd.unique(self.conns[['source', 'target']].values.ravel())
-        # build the dataframe for components
-        self.init_components(comps)
+        self.init_components()
         # count number of incoming and outgoing connections and compare to
         # expected values
         for comp in self.comps['object']:
@@ -686,32 +724,9 @@ class Network:
         msg = 'Networkcheck successful.'
         logging.info(msg)
 
-    def init_components(self, comps):
-        r"""
-        Set up a dataframe for the network's components.
-
-        Additionally, check, if all components have unique labels.
-
-        Parameters
-        ----------
-        comps : pandas.core.frame.DataFrame
-            DataFrame containing all components of the network gathered from
-            the network's connection information.
-
-        Note
-        ----
-        The dataframe for the components is derived from the network's
-        connections. Thus it does not hold any additional information, the
-        dataframe is used to simplify the code, only.
-        """
-        self.comps = pd.DataFrame(dtype='object')
-
-        labels = []
-        for comp in comps:
-            # this is required for printing and saving
-            comp_type = comp.__class__.__name__
-            self.comps.loc[comp, 'comp_type'] = comp_type
-            self.comps.loc[comp, 'label'] = comp.label
+    def init_components(self):
+        r"""Set up necessary component information."""
+        for comp in self.comps["object"]:
             # get incoming and outgoing connections of a component
             sources = self.conns[self.conns['source'] == comp]
             sources = sources['source_id'].sort_values().index.tolist()
@@ -723,13 +738,14 @@ class Network:
             comp.outl = self.conns.loc[sources, 'object'].tolist()
             comp.num_i = len(comp.inlets())
             comp.num_o = len(comp.outlets())
-            labels += [comp.label]
 
             # save the connection locations to the components
             comp.conn_loc = []
             for c in comp.inl + comp.outl:
                 comp.conn_loc += [self.conns.index.get_loc(c.label)]
 
+            # set up restults and specification dataframes
+            comp_type = comp.__class__.__name__
             if comp_type not in self.results:
                 cols = [col for col, data in comp.variables.items()
                         if isinstance(data, dc_cp)]
@@ -751,18 +767,6 @@ class Network:
                     'properties': pd.DataFrame(columns=cols, dtype='bool')
                 }
 
-        self.comps = self.comps.reset_index().set_index('label')
-        self.comps.rename(columns={'index': 'object'}, inplace=True)
-
-        # check for duplicates in the component labels
-        if len(labels) != len(list(set(labels))):
-            duplicates = [
-                item for item, count in Counter(labels).items() if count > 1]
-            msg = ('All Components must have unique labels, duplicate labels '
-                   'are: "' + '", "'.join(duplicates) + '".')
-            logging.error(msg)
-            raise hlp.TESPyNetworkError(msg)
-
     def initialise(self):
         r"""
         Initilialise the network depending on calclation mode.
@@ -2627,9 +2631,12 @@ class Network:
                     if len(df) > 0:
                         # printout with tabulate
                         print('##### RESULTS (' + cp + ') #####')
-                        print(tabulate(
-                            df, headers='keys', tablefmt='psql',
-                            floatfmt='.2e'))
+                        print(
+                            tabulate(
+                                df, headers='keys', tablefmt='psql',
+                                floatfmt='.2e'
+                            )
+                        )
 
         # connection properties
         df = self.results['Connection'].loc[:, ['m', 'p', 'h', 'T']]
@@ -2648,7 +2655,8 @@ class Network:
         if len(df) > 0:
             print('##### RESULTS (Connection) #####')
             print(
-                tabulate(df, headers='keys', tablefmt='psql', floatfmt='.3e'))
+                tabulate(df, headers='keys', tablefmt='psql', floatfmt='.3e')
+            )
 
         for b in self.busses.values():
             if b.printout:
@@ -2661,8 +2669,12 @@ class Network:
                         coloring['set'] + str(df.loc['total', 'bus value']) +
                         coloring['end'])
                 print('##### RESULTS (Bus: ' + b.label + ') #####')
-                print(tabulate(df, headers='keys', tablefmt='psql',
-                               floatfmt='.3e'))
+                print(
+                    tabulate(
+                        df, headers='keys', tablefmt='psql',
+                        floatfmt='.3e'
+                    )
+                )
 
     def print_components(self, c, *args):
         """


Test patch:
diff --git a/tests/test_errors.py b/tests/test_errors.py
index b5a9419f..58a516b7 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -515,9 +515,8 @@ class TestNetworkErrors:
         source = Source('label')
         sink = Sink('label')
         a = Connection(source, 'out1', sink, 'in1')
-        self.nw.add_conns(a)
         with raises(TESPyNetworkError):
-            self.nw.check_network()
+            self.nw.add_conns(a)
 
     def test_missing_offdesign_path(self):
         source = Source('source')
diff --git a/tests/test_networks/test_network.py b/tests/test_networks/test_network.py
index ab856c3d..8078f2c2 100644
--- a/tests/test_networks/test_network.py
+++ b/tests/test_networks/test_network.py
@@ -253,6 +253,33 @@ class TestNetworks:
 
         shutil.rmtree('./tmp', ignore_errors=True)
 
+    def test_Network_get_comp_without_connections_added(self):
+        """Test if components are found prior to initialization."""
+        self.setup_Network_tests()
+        pi = Pipe('pipe')
+        a = Connection(self.source, 'out1', pi, 'in1')
+        b = Connection(pi, 'out1', self.sink, 'in1')
+        self.nw.add_conns(a)
+        msg = (
+            "A component with the label 'sink' has been created but must not "
+            "be part of the network as the respective connection has not "
+            "been added."
+        )
+        assert self.nw.get_comp("sink") == None, msg
+
+    def test_Network_get_comp_before_initialization(self):
+        """Test if components are found prior to initialization."""
+        self.setup_Network_tests()
+        pi = Pipe('pipe')
+        a = Connection(self.source, 'out1', pi, 'in1')
+        b = Connection(pi, 'out1', self.sink, 'in1')
+        self.nw.add_conns(a, b)
+        msg = (
+            "A component with the label 'pipe' is part of the network "
+            "and therefore must be found in the DataFrame."
+        )
+        assert self.nw.get_comp("pipe") == pi, msg
+
 
 class TestNetworkIndividualOffdesign: