Repository: netket/netket

Issue:
Save and load objects from python
The last main design issue to be solved for v2.0 concerns saving and loading objects from python.

Pybind11 has some [pickling support](https://pybind11.readthedocs.io/en/stable/advanced/classes.html#pickling-support).

However, the design issue to be addressed is how to serialize objects stored internally as pointers.
Basically, each pickable object needs to define a GetState function, returning a python tuple of the arguments needed to construct the object. 

```c++
py::tuple GetState(const Pickleable &p) { 
            return py::make_tuple(p.Field1(),p.Field2(),...);
}
```
However, if the `Pickeable` stores a pointer to some abstract object (say Hilbert), then one obviously cannot do: 
```c++
py::tuple GetState(const Pickleable &p) { 
            auto hilbert= p.GetHilbert(); //NO! 
            return py::make_tuple(p.Field1(),p.Field2(),hilbert);
}
```

Suggestions are welcome. 


Discussion:
For the moment, the easiest is to stick to the existing `Save` and `Load` functionality for machines, that basically write and load from a file, in json format. 

Resolving patch:
diff --git a/Tutorials/PyNetKet/machine.py b/Tutorials/PyNetKet/machine.py
--- a/Tutorials/PyNetKet/machine.py
+++ b/Tutorials/PyNetKet/machine.py
@@ -19,24 +19,20 @@
 from mpi4py import MPI
 import scipy.sparse as sparse
 
-#Constructing a 1d lattice
-g = nk.graph.Hypercube(L=4, ndim=1)
+# Constructing a 1d lattice
+g = nk.graph.Hypercube(length=4, n_dim=1)
 
 # Hilbert space of spins from given graph
 hi = nk.hilbert.Spin(s=0.5, graph=g)
 
-#Hamiltonian
+# Hamiltonian
 ha = nk.operator.Ising(h=1.0, hilbert=hi)
 
-#Machine
+# Machine
 ma = nk.machine.RbmSpin(hilbert=hi, alpha=1)
-ma.InitRandomPars(seed=1234, sigma=0.1)
-print(ma.GetParameters())
+ma.init_random_parameters(seed=1234, sigma=0.1)
 
-#Layer
-a = np.ones(3, dtype=complex)
-b = np.zeros(3, dtype=complex)
-act = nk.activation.Tanh()
-
-act(a, b)
-print(b)
+ma.save("test.wf")
+ma.parameters = np.zeros(ma.n_par)
+ma.load("test.wf")
+print(ma.parameters)


Test patch:
diff --git a/Test/Machine/test_machine.py b/Test/Machine/test_machine.py
--- a/Test/Machine/test_machine.py
+++ b/Test/Machine/test_machine.py
@@ -4,6 +4,7 @@
 import pytest
 from mpi4py import MPI
 from pytest import approx
+import os
 
 machines = {}
 
@@ -52,8 +53,8 @@
 ]
 
 # FFNN Machine
-# BUG
-machines["FFFN 1d Hypercube spin Convolutional Hypercube"] = nk.machine.FFNN(hi, layers)
+machines["FFFN 1d Hypercube spin Convolutional Hypercube"] = nk.machine.FFNN(
+    hi, layers)
 
 machines["MPS Diagonal 1d spin"] = nk.machine.MPSPeriodicDiagonal(
     hi, bond_dim=3)
@@ -78,18 +79,38 @@
 
 
 def log_val_f(par, machine, v):
-    machine.set_parameters(par)
+    machine.parameters = np.copy(par)
     return machine.log_val(v)
 
 
 def test_set_get_parameters():
     for name, machine in machines.items():
         print("Machine test: %s" % name)
-        assert(machine.n_par() > 0)
-        npar = machine.n_par()
+        assert(machine.n_par > 0)
+        npar = machine.n_par
         randpars = np.random.randn(npar) + 1.0j * np.random.randn(npar)
-        machine.set_parameters(randpars)
-        assert(np.array_equal(machine.get_parameters(), randpars))
+        machine.parameters = randpars
+        assert(np.array_equal(machine.parameters, randpars))
+
+
+def test_save_load_parameters(tmpdir):
+    for name, machine in machines.items():
+        print("Machine test: %s" % name)
+        assert(machine.n_par > 0)
+        n_par = machine.n_par
+        randpars = np.random.randn(n_par) + 1.0j * np.random.randn(n_par)
+
+        machine.parameters = np.copy(randpars)
+        fn = tmpdir.mkdir('datawf').join('test.wf')
+
+        filename = os.path.join(fn.dirname, fn.basename)
+
+        machine.save(filename)
+        machine.parameters = np.zeros(n_par, dtype=complex)
+        machine.load(filename)
+        os.remove(filename)
+        os.rmdir(fn.dirname)
+        assert(np.array_equal(machine.parameters, randpars))
 
 
 import numdifftools as nd
@@ -102,7 +123,7 @@ def test_log_derivative():
     for name, machine in machines.items():
         print("Machine test: %s" % name)
 
-        npar = machine.n_par()
+        npar = machine.n_par
 
         # random visibile state
         hi = machine.get_hilbert()
@@ -115,7 +136,7 @@ def test_log_derivative():
 
             randpars = 0.1 * (np.random.randn(npar) +
                               1.0j * np.random.randn(npar))
-            machine.set_parameters(randpars)
+            machine.parameters = randpars
             der_log = machine.der_log(v)
 
             if("Jastrow" in name):
@@ -135,9 +156,9 @@ def test_log_val_diff():
     for name, machine in machines.items():
         print("Machine test: %s" % name)
 
-        npar = machine.n_par()
+        npar = machine.n_par
         randpars = 0.5 * (np.random.randn(npar) + 1.0j * np.random.randn(npar))
-        machine.set_parameters(randpars)
+        machine.parameters = randpars
 
         hi = machine.get_hilbert()
 
@@ -195,4 +216,4 @@ def test_nvisible():
         print("Machine test: %s" % name)
         hi = machine.get_hilbert()
 
-        assert(machine.n_visible() == hi.size())
+        assert(machine.n_visible == hi.size())