Repository: PennyLaneAI/pennylane

Issue:
Optimizers: standardize use of `accumulation`
Currently, the following optimizers: 

* adagrad
* momentum
* nesterov momentum
* rms prop

all have an `accumulation` attribute that tracks some sort of running quantity over multiple optimization steps.

On the other hand, Adam has `fm`, `sm` and `t` for first moment, second moment, and time.

We should standardize the use of the term `accumulation` across all optimizers.  In the case of `Adam`, we could have a named tuple or a dictionary to include the three different quantities.

To support backward compatibility, we could create `property` interfaces for `fm`, `sm`, and `t`.

I believe this would improve code cleanliness and consistency, but it may not be worth the effort to change.


Discussion:
Hey @albi3ro,

I'd love to work on this. I'm quite new to quantum computing and open source, but I have experience in working with python and optimizers so I think I should be able to tackle this.
Sure

You can look at our guide on [opening a pull request](https://pennylane.readthedocs.io/en/stable/development/guide/pullrequests.html) for more information about how our open source process works, and feel free to ask any questions either here or in your pull request once opened.

You can find the source code to the Adam optimizer [here](https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/optimize/adam.py).

Our [optimizer tests](https://github.com/PennyLaneAI/pennylane/blob/master/tests/optimize/test_optimize.py) are rather disordered, so I would recommend first fixing any tests that fail due to the change and then either adding new ones at the bottom or in a new `tests/optimize/test_adam.py` file.

Resolving patch:
diff --git a/pennylane/optimize/adam.py b/pennylane/optimize/adam.py
--- a/pennylane/optimize/adam.py
+++ b/pennylane/optimize/adam.py
@@ -58,9 +58,7 @@ def __init__(self, stepsize=0.01, beta1=0.9, beta2=0.99, eps=1e-8):
         self.beta1 = beta1
         self.beta2 = beta2
         self.eps = eps
-        self.fm = None
-        self.sm = None
-        self.t = 0
+        self.accumulation = None
 
     def apply_grad(self, grad, args):
         r"""Update the variables args to take a single optimization step. Flattens and unflattens
@@ -75,19 +73,19 @@ def apply_grad(self, grad, args):
             list: the new values :math:`x^{(t+1)}`
         """
         args_new = list(args)
-        self.t += 1
+
+        if self.accumulation is None:
+            self.accumulation = {"fm": [None] * len(args), "sm": [None] * len(args), "t": 0}
+
+        self.accumulation["t"] += 1
 
         # Update step size (instead of correcting for bias)
         new_stepsize = (
-            self.stepsize * math.sqrt(1 - self.beta2 ** self.t) / (1 - self.beta1 ** self.t)
+            self.stepsize
+            * math.sqrt(1 - self.beta2 ** self.accumulation["t"])
+            / (1 - self.beta1 ** self.accumulation["t"])
         )
 
-        if self.fm is None:
-            self.fm = [None] * len(args)
-
-        if self.sm is None:
-            self.sm = [None] * len(args)
-
         trained_index = 0
         for index, arg in enumerate(args):
             if getattr(arg, "requires_grad", True):
@@ -99,7 +97,9 @@ def apply_grad(self, grad, args):
 
                 x_new_flat = [
                     e - new_stepsize * f / (math.sqrt(s) + self.eps)
-                    for f, s, e in zip(self.fm[index], self.sm[index], x_flat)
+                    for f, s, e in zip(
+                        self.accumulation["fm"][index], self.accumulation["sm"][index], x_flat
+                    )
                 ]
                 args_new[index] = unflatten(x_new_flat, arg)
 
@@ -122,23 +122,47 @@ def _update_moments(self, index, grad_flat):
             grad_flat (list): the flattened gradient for that trainable param
         """
         # update first moment
-        if self.fm[index] is None:
-            self.fm[index] = grad_flat
+        if self.accumulation["fm"][index] is None:
+            self.accumulation["fm"][index] = grad_flat
         else:
-            self.fm[index] = [
-                self.beta1 * f + (1 - self.beta1) * g for f, g in zip(self.fm[index], grad_flat)
+            self.accumulation["fm"][index] = [
+                self.beta1 * f + (1 - self.beta1) * g
+                for f, g in zip(self.accumulation["fm"][index], grad_flat)
             ]
 
         # update second moment
-        if self.sm[index] is None:
-            self.sm[index] = [g * g for g in grad_flat]
+        if self.accumulation["sm"][index] is None:
+            self.accumulation["sm"][index] = [g * g for g in grad_flat]
         else:
-            self.sm[index] = [
-                self.beta2 * f + (1 - self.beta2) * g * g for f, g in zip(self.sm[index], grad_flat)
+            self.accumulation["sm"][index] = [
+                self.beta2 * f + (1 - self.beta2) * g * g
+                for f, g in zip(self.accumulation["sm"][index], grad_flat)
             ]
 
     def reset(self):
         """Reset optimizer by erasing memory of past steps."""
-        self.fm = None
-        self.sm = None
-        self.t = 0
+        self.accumulation = None
+
+    @property
+    def fm(self):
+        """Returns estimated first moments of gradient"""
+        if self.accumulation is None:
+            return None
+
+        return self.accumulation["fm"]
+
+    @property
+    def sm(self):
+        """Returns estimated second moments of gradient"""
+        if self.accumulation is None:
+            return None
+
+        return self.accumulation["sm"]
+
+    @property
+    def t(self):
+        """Returns accumulated timesteps"""
+        if self.accumulation is None:
+            return None
+
+        return self.accumulation["t"]


Test patch:
diff --git a/tests/optimize/test_optimize.py b/tests/optimize/test_optimize.py
--- a/tests/optimize/test_optimize.py
+++ b/tests/optimize/test_optimize.py
@@ -557,6 +557,22 @@ def test_adam_optimizer_multivar(self, bunch, tol):
                 )
                 assert x_twosteps == pytest.approx(x_twosteps_target, abs=tol)
 
+    def test_adam_optimizer_properties(self, bunch):
+        """Test the adam property interfaces"""
+        bunch.adam_opt.reset()
+        # check if None is returned when accumulation is empty
+        assert bunch.adam_opt.fm == None
+        assert bunch.adam_opt.sm == None
+        assert bunch.adam_opt.t == None
+
+        # Do some calculations to fill accumulation
+        bunch.adam_opt.step(np.sin, np.random.rand(1))
+
+        # Check the properties return the same values, stored in accumulation
+        assert bunch.adam_opt.fm == bunch.adam_opt.accumulation["fm"]
+        assert bunch.adam_opt.sm == bunch.adam_opt.accumulation["sm"]
+        assert bunch.adam_opt.t == bunch.adam_opt.accumulation["t"]
+
     @staticmethod
     def rotosolve_step(f, x):
         """Helper function to test the Rotosolve and Rotoselect optimizers"""