Repository: e2nIEE/pandapower

Issue:
preserve dtypes when adding JSONSerializables to net
In the basic controller, dtypes are preserved upon creation. I am wondering if this is something that should be shifted to the JSONSerializableClass when an object is added to a net, because this resembles a create-function. For other tables with that kind of behavior, this would have to be implemented as well.


Discussion:
With https://github.com/SteffenMeinecke/pandapower/commit/6e7f9262fbbf5eee6ce218a7bd84601d5fc4e38d this should be fixed at least for the loading of files (FileIO). For creating entries to such object tables, this issue still exists.
Sorry, I forgot to add a code snippet. It would be added to the JSONSerializableClass method `add_to_net`:

Before:

    def add_to_net(self, net, element, index, column="object", overwrite=False):
        if element not in net:
            net[element] = pd.DataFrame(columns=[column])
        if index in net[element].index.values:
            obj = net[element].object.at[index]
            if overwrite or not isinstance(obj, JSONSerializableClass):
                logger.info("Updating %s with index %s" % (element, index))
            else:
                raise UserWarning("%s with index %s already exists" % (element, index))

Afterwards:

    def add_to_net(self, net, element, index, column="object", overwrite=False, preserve_dtypes=False):
        if element not in net:
            net[element] = pd.DataFrame(columns=[column])
        if index in net[element].index.values:
            obj = net[element].object.at[index]
            if overwrite or not isinstance(obj, JSONSerializableClass):
                logger.info("Updating %s with index %s" % (element, index))
            else:
                raise UserWarning("%s with index %s already exists" % (element, index))
        if preserve_dtypes:
            dtypes = net[element].dtypes
            net[element].at[index, column] = self
            _preserve_dtypes(net[element], dtypes)
        else:
            net[element].at[index, column] = self
This way, inheriting classes could make use of this functionality without having to take care of this for each other class, just by setting a flag.

Resolving patch:
diff --git a/pandapower/control/basic_controller.py b/pandapower/control/basic_controller.py
--- a/pandapower/control/basic_controller.py
+++ b/pandapower/control/basic_controller.py
@@ -4,9 +4,10 @@
 # and Energy System Technology (IEE), Kassel. All rights reserved.
 
 import copy
-from pandapower.auxiliary import get_free_id, _preserve_dtypes
+
+from pandapower.auxiliary import get_free_id
 from pandapower.control.util.auxiliary import \
-        drop_same_type_existing_controllers, log_same_type_existing_controllers
+    drop_same_type_existing_controllers, log_same_type_existing_controllers
 from pandapower.io_utils import JSONSerializableClass
 
 try:
@@ -31,8 +32,8 @@ def __init__(self, net, in_service=True, order=0, level=0, index=None, recycle=F
             index = get_free_id(net.controller)
         self.matching_params = dict() if matching_params is None else matching_params
         self.index = index
-        self.add_controller_to_net(net=net, in_service=in_service, initial_run=initial_run, order=order,
-                                   level=level, index=index, recycle=recycle,
+        self.add_controller_to_net(net=net, in_service=in_service, initial_run=initial_run,
+                                   order=order, level=level, index=index, recycle=recycle,
                                    drop_same_existing_ctrl=drop_same_existing_ctrl,
                                    overwrite=overwrite, matching_params=matching_params, **kwargs)
 
@@ -65,7 +66,6 @@ def __getstate__(self):
     def __setstate__(self, state):
         self.__dict__.update(state)
 
-
     def add_controller_to_net(self, net, in_service, initial_run, order, level, index, recycle,
                               drop_same_existing_ctrl, overwrite, **kwargs):
         """
@@ -78,7 +78,9 @@ def add_controller_to_net(self, net, in_service, initial_run, order, level, inde
 
             **index** (int) - index
 
-            **recycle** (bool) - if controller needs a new bbm (ppc, Ybus...) or if it can be used with prestored values. This is mostly needed for time series calculations
+            **recycle** (bool) - if controller needs a new bbm (ppc, Ybus...) or if it can be used \
+                                 with prestored values. This is mostly needed for time series \
+                                 calculations
 
         """
         if drop_same_existing_ctrl:
@@ -86,17 +88,11 @@ def add_controller_to_net(self, net, in_service, initial_run, order, level, inde
         else:
             log_same_type_existing_controllers(net, type(self), index=index, **kwargs)
 
-        dtypes = net.controller.dtypes
-
         # use base class method to raise an error if the object is in DF and overwrite = False
-        super().add_to_net(net=net, element='controller', index=index, overwrite=overwrite)
-
-        columns = ['object', 'in_service', 'initial_run', 'recycle']
-        net.controller.loc[index,columns] = (self, in_service, initial_run, recycle)
-        net.controller['order'][index] = order
-        net.controller['level'][index] = level
-
-        _preserve_dtypes(net.controller, dtypes)
+        fill_dict = {"in_service": in_service, "initial_run": initial_run, "recycle": recycle,
+                     "order": order, "level": level}
+        super().add_to_net(net=net, element='controller', index=index, overwrite=overwrite,
+                           fill_dict=fill_dict, preserve_dtypes=True)
 
     def time_step(self, net, time):
         """
diff --git a/pandapower/control/controller/characteristic_control.py b/pandapower/control/controller/characteristic_control.py
--- a/pandapower/control/controller/characteristic_control.py
+++ b/pandapower/control/controller/characteristic_control.py
@@ -47,13 +47,15 @@ class CharacteristicControl(Controller):
 
         **drop_same_existing_ctrl** (bool, False) - Indicates if already existing controllers of the same type and with the same matching parameters (e.g. at same element) should be dropped
     """
-    def __init__(self, net, output_element, output_variable, output_element_index, input_element, input_variable, input_element_index,
-                 characteristic, tol=1e-3, in_service=True, order=0, level=0, drop_same_existing_ctrl=False, matching_params=None,
-                 **kwargs):
+    def __init__(self, net, output_element, output_variable, output_element_index, input_element,
+                 input_variable, input_element_index, characteristic, tol=1e-3, in_service=True,
+                 order=0, level=0, drop_same_existing_ctrl=False, matching_params=None, **kwargs):
         if matching_params is None:
-            matching_params = {"element": output_element, "input_variable": input_variable, "output_variable": output_variable,
+            matching_params = {"element": output_element, "input_variable": input_variable,
+                               "output_variable": output_variable,
                                "element_index": input_element_index}
-        super().__init__(net, in_service=in_service, order=order, level=level, drop_same_existing_ctrl=drop_same_existing_ctrl,
+        super().__init__(net, in_service=in_service, order=order, level=level,
+                         drop_same_existing_ctrl=drop_same_existing_ctrl,
                          matching_params=matching_params, **kwargs)
         self.input_element = input_element
         self.input_variable = input_variable
diff --git a/pandapower/io_utils.py b/pandapower/io_utils.py
--- a/pandapower/io_utils.py
+++ b/pandapower/io_utils.py
@@ -45,7 +45,7 @@
 except:
     zlib_INSTALLED = False
 
-from pandapower.auxiliary import pandapowerNet, soft_dependency_error
+from pandapower.auxiliary import pandapowerNet, soft_dependency_error, _preserve_dtypes
 from pandapower.create import create_empty_network
 
 try:
@@ -620,7 +620,8 @@ def consider_callable(value):
              if key not in self.json_excludes}
         return d
 
-    def add_to_net(self, net, element, index, column="object", overwrite=False):
+    def add_to_net(self, net, element, index, column="object", overwrite=False,
+                   preserve_dtypes=False, fill_dict=None):
         if element not in net:
             net[element] = pd.DataFrame(columns=[column])
         if index in net[element].index.values:
@@ -629,8 +630,19 @@ def add_to_net(self, net, element, index, column="object", overwrite=False):
                 logger.info("Updating %s with index %s" % (element, index))
             else:
                 raise UserWarning("%s with index %s already exists" % (element, index))
+
+        dtypes = None
+        if preserve_dtypes:
+            dtypes = net[element].dtypes
+
+        if fill_dict is not None:
+            for k, v in fill_dict.items():
+                net[element].at[index, k] = v
         net[element].at[index, column] = self
 
+        if preserve_dtypes:
+            _preserve_dtypes(net[element], dtypes)
+
     def equals(self, other):
 
         class UnequalityFound(Exception):
@@ -731,7 +743,7 @@ def json_pandapowernet(obj):
     logger.debug('pandapowerNet')
     net_dict = {k: item for k, item in obj.items() if not k.startswith("_")}
     for k, item in net_dict.items():
-        if (isinstance(item, str) and '_module' in item):
+        if isinstance(item, str) and '_module' in item:
             net_dict[k] = json.loads(item)
     d = with_signature(obj, net_dict)
     return d


Test patch: