From 541885b06197c519fd74fab655f1123748ea46f2 Mon Sep 17 00:00:00 2001
From: mili1247 <76153746+mili1247@users.noreply.github.com>
Date: Fri, 18 Dec 2020 09:12:04 +0100
Subject: [PATCH 1/2] python: Add to_dict() method in particles

Co-authored-by: Riccardo Frenner <riccardo.frenner@gmail.com>
---
 src/python/espressomd/particle_data.pyx | 76 ++++++++++++++++++++-----
 1 file changed, 62 insertions(+), 14 deletions(-)

diff --git a/src/python/espressomd/particle_data.pyx b/src/python/espressomd/particle_data.pyx
index 8a946b34fad..59f71bbe117 100644
--- a/src/python/espressomd/particle_data.pyx
+++ b/src/python/espressomd/particle_data.pyx
@@ -56,6 +56,33 @@ cdef class ParticleHandle:
     cdef int update_particle_data(self) except -1:
         self.particle_data = &get_particle_data(self._id)
 
+    def to_dict(self):
+        """
+        Returns the particle's attributes as a dictionary.
+
+        It includes the content of ``particle_attributes``, minus a few exceptions:
+
+        - :attr:`~ParticleHandle.dip`, :attr:`~ParticleHandle.director`:
+          Setting only the director will overwrite the orientation of the
+          particle around the axis parallel to dipole moment/director.
+          Quaternions contain the full info.
+        - :attr:`~ParticleHandle.image_box`, :attr:`~ParticleHandle.node`
+
+        """
+
+        pickle_attr = copy(particle_attributes)
+        for i in ["director", "dip", "image_box", "node"]:
+            if i in pickle_attr:
+                pickle_attr.remove(i)
+        IF MASS == 0:
+            pickle_attr.remove("mass")
+        pdict = {}
+
+        for property_ in pickle_attr:
+            pdict[property_] = ParticleHandle(
+                self.id).__getattribute__(property_)
+        return pdict
+
     def __str__(self):
         res = collections.OrderedDict()
         # Id and pos first, then the rest
@@ -1676,6 +1703,37 @@ class ParticleSlice(_ParticleSliceImpl):
                 f"ParticleHandle does not have the attribute {name}.")
         super().__setattr__(name, value)
 
+    def to_dict(self):
+        """
+        Returns the particles attributes as a dictionary.
+
+        It can be used to save the particle data and recover it by using
+
+        >>> p = system.part.add(...)
+        >>> particle_dict = p.to_dict()
+        >>> system.part.add(particle_dict)
+
+        It includes the content of ``particle_attributes``, minus a few exceptions:
+
+        - :attr:`~ParticleHandle.dip`, :attr:`~ParticleHandle.director`:
+          Setting only the director will overwrite the orientation of the
+          particle around the axis parallel to dipole moment/director.
+          Quaternions contain the full info.
+        - :attr:`~ParticleHandle.image_box`, :attr:`~ParticleHandle.node`
+
+        """
+
+        odict = {}
+        key_list = [p.id for p in self]
+        for particle_number in key_list:
+            pdict = ParticleHandle(particle_number).to_dict()
+            for p_key, p_value in pdict.items():
+                if p_key in odict:
+                    odict[p_key].append(p_value)
+                else:
+                    odict[p_key] = [p_value]
+        return odict
+
 
 cdef class ParticleList:
     """
@@ -1712,21 +1770,11 @@ cdef class ParticleList:
 
         """
 
-        pickle_attr = copy(particle_attributes)
-        for i in ["director", "dip", "id", "image_box", "node"]:
-            if i in pickle_attr:
-                pickle_attr.remove(i)
-        IF MASS == 0:
-            pickle_attr.remove("mass")
         odict = {}
-        key_list = [p.id for p in self]
-        for particle_number in key_list:
-            pdict = {}
-
-            for property_ in pickle_attr:
-                pdict[property_] = ParticleHandle(
-                    particle_number).__getattribute__(property_)
-            odict[particle_number] = pdict
+        for p in self:
+            pdict = p.to_dict()
+            del pdict["id"]
+            odict[p.id] = pdict
         return odict
 
     def __setstate__(self, params):

From de3f65975955ce08739629173c36ae415ae8ecaa Mon Sep 17 00:00:00 2001
From: mili1247 <76153746+mili1247@users.noreply.github.com>
Date: Fri, 18 Dec 2020 09:16:45 +0100
Subject: [PATCH 2/2] tests: Add check for to_dict()

Co-authored-by: Riccardo Frenner <riccardo.frenner@gmail.com>
---
 testsuite/python/particle.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/testsuite/python/particle.py b/testsuite/python/particle.py
index cf56ce1df22..b1d0e7cf262 100644
--- a/testsuite/python/particle.py
+++ b/testsuite/python/particle.py
@@ -377,6 +377,16 @@ def test_particle_slice(self):
         with self.assertRaises(TypeError):
             system.part[[ids[0], 1.2]]
 
+    def test_to_dict(self):
+        self.system.part.clear()
+        p = self.system.part.add(
+            pos=np.random.uniform(size=(10, 3)) * self.system.box_l)
+        pp = str(p)
+        pdict = p.to_dict()
+        p.remove()
+        self.system.part.add(pdict)
+        self.assertEqual(str(self.system.part.select()), pp)
+
 
 if __name__ == "__main__":
     ut.main()