From 8307dcbbad84d07d31206d7ee7ada483bc9c4284 Mon Sep 17 00:00:00 2001
From: Jan Caron <j.caron@fz-juelich.de>
Date: Fri, 18 Aug 2017 15:28:58 +0200
Subject: [PATCH] Added basic framework for charge reconstruction.

---
 pyramid/dataset.py                            |  4 +-
 pyramid/fielddata.py                          |  4 +-
 pyramid/forwardmodel.py                       |  4 +
 pyramid/kernel.py                             |  4 +-
 pyramid/plottools.py                          |  2 +-
 pyramid/reconstruction.py                     | 55 +++++++++++
 .../utils/reconstruction_2d_from_phasemap.py  | 95 ++++++++++++++++++-
 .../utils/reconstruction_3d_from_magdata.py   |  2 +-
 8 files changed, 162 insertions(+), 8 deletions(-)

diff --git a/pyramid/dataset.py b/pyramid/dataset.py
index 408ac18..188f09b 100644
--- a/pyramid/dataset.py
+++ b/pyramid/dataset.py
@@ -140,7 +140,7 @@ class DataSet(object):
             'Dimension has to be a tuple of length 3!'
         self.a = a
         self.dim = dim
-        self.b_0 = b_0
+        self.b_0 = b_0  # TODO: Not very general!!! get rid off!
         self.mask = mask
         self.Se_inv = Se_inv
         self._phasemaps = []
@@ -169,6 +169,8 @@ class DataSet(object):
         # Create lookup key:
         # TODO: Think again if phasemappers should be given as attribute (seems to be faulty
         # TODO: currently... Also not very expensive, so keep outside?
+        # TODO: Streamline DataSet, only container, not so much work inside
+        # TODO: Generalise for arbitrary sets of phasemaps, independent of magnetic/charge/ramp!
         if phasemapper is not None:
             key = dim_uv  # Create standard phasemapper, dim_uv is enough for identification!
         else:
diff --git a/pyramid/fielddata.py b/pyramid/fielddata.py
index f295b48..76dd3e7 100644
--- a/pyramid/fielddata.py
+++ b/pyramid/fielddata.py
@@ -939,7 +939,7 @@ class VectorData(FieldData):
         else:
             cbar_mappable, cbar_label = None, None
         # Change background color:
-        axis.set_facecolor(bgcolor)
+        #axis.set_facecolor(bgcolor)  # TODO: Activate for matplotlib 2.0!
         # Show mask:
         if show_mask and not np.all(submask):  # Plot mask if desired and not trivial!
             vv, uu = np.indices(dim_uv) + 0.5  # shift to center of pixel
@@ -1042,7 +1042,7 @@ class VectorData(FieldData):
                     extent=(0, dim_uv[1], 0, dim_uv[0]))
         # Change background color:
         if bgcolor is not None:
-            axis.set_facecolor(bgcolor)
+            pass#axis.set_facecolor(bgcolor)  # TODO: Activate for matplotlib 2.0!
         # Show mask:
         if show_mask and not np.all(submask):  # Plot mask if desired and not trivial!
             vv, uu = np.indices(dim_uv) + 0.5  # shift to center of pixel
diff --git a/pyramid/forwardmodel.py b/pyramid/forwardmodel.py
index 17e1d98..9febbb4 100644
--- a/pyramid/forwardmodel.py
+++ b/pyramid/forwardmodel.py
@@ -18,6 +18,10 @@ from pyramid.ramp import Ramp
 __all__ = ['ForwardModel', 'DistributedForwardModel']
 
 
+# TODO: Ramp should be a forward model itself! Instead of hookpoints, each ForwardModel should
+# TODO: keep track of start and end in vector x (defaults: 0 and -1)!
+# TODO: Write CombinedForwardModel class!
+
 class ForwardModel(object):
     """Class for mapping 3D magnetic distributions to 2D phase maps.
 
diff --git a/pyramid/kernel.py b/pyramid/kernel.py
index 8f58314..dd6ef5a 100644
--- a/pyramid/kernel.py
+++ b/pyramid/kernel.py
@@ -106,8 +106,8 @@ class Kernel(object):
         v = np.linspace(-(v_dim - 1), v_dim - 1, num=2 * v_dim - 1)
         uu, vv = np.meshgrid(u, v)
         # TODO: u, v are coordinates, rename self.u/v to self.kern_u/v!
-        self.u = np.empty(self.dim_kern, dtype=dtype)
-        self.v = np.empty(self.dim_kern, dtype=dtype)
+        self.u = np.empty(self.dim_kern, dtype=dtype)  # TODO: Is this necessary?
+        self.v = np.empty(self.dim_kern, dtype=dtype)  # TODO: Move to _get_elementary_phase?
         self.u[...] = coeff * self._get_elementary_phase(geometry, uu, vv, a)
         # TODO: The minus sign belongs into the phasemapper (debatable)!
         self.v[...] = coeff * -self._get_elementary_phase(geometry, vv, uu, a)
diff --git a/pyramid/plottools.py b/pyramid/plottools.py
index e76ad26..635af1c 100644
--- a/pyramid/plottools.py
+++ b/pyramid/plottools.py
@@ -169,7 +169,7 @@ def add_colorwheel(axis):
     inset_axes = inset_axes(axis, width=0.75, height=0.75, loc=1)
     inset_axes.axis('off')
     cmap = colors.CMAP_CIRCULAR_DEFAULT
-    bgcolor = axis.get_facecolor()
+    bgcolor = None#axis.get_facecolor() TODO: Activate for matplotlib 2.0!
     return cmap.plot_colorwheel(size=100, axis=inset_axes, alpha=0, bgcolor=bgcolor, arrows=True)
 
 
diff --git a/pyramid/reconstruction.py b/pyramid/reconstruction.py
index cc31372..9ebec34 100644
--- a/pyramid/reconstruction.py
+++ b/pyramid/reconstruction.py
@@ -77,6 +77,61 @@ def optimize_linear(costfunction, mag_0=None, ramp_0=None, max_iter=None, verbos
     return mag_opt
 
 
+def optimize_linear_charge(costfunction, charge_0=None, ramp_0=None, max_iter=None, verbose=False):
+    """Reconstruct a three-dimensional magnetic distribution from given phase maps via the
+    conjugate gradient optimizaion method :func:`~.scipy.sparse.linalg.cg`.
+    Blazingly fast for l2-based cost functions.
+
+    Parameters
+    ----------
+    costfunction : :class:`~.Costfunction`
+        A :class:`~.Costfunction` object which implements a specified forward model and
+        regularisator which is minimized in the optimization process.
+    mag_0: :class:`~.VectorData`
+        The starting magnetisation distribution used for the reconstruction. A zero vector will be
+        used if no VectorData object is specified.
+    mag_0: :class:`~.Ramp`
+        The starting ramp for the reconstruction. A zero vector will be
+        used if no Ramp object is specified.
+    max_iter : int, optional
+        The maximum number of iterations for the opimization.
+    verbose: bool, optional
+        If set to True, information like a progressbar is displayed during reconstruction.
+        The default is False.
+
+    Returns
+    -------
+    magdata : :class:`~pyramid.fielddata.VectorData`
+        The reconstructed magnetic distribution as a :class:`~.VectorData` object.
+
+    """
+    import jutil.cg as jcg
+    from jutil.taketime import TakeTime
+    _log.debug('Calling optimize_linear')
+    _log.info('Cost before optimization: {:.3e}'.format(costfunction(np.zeros(costfunction.n))))
+    data_set = costfunction.fwd_model.data_set
+    # Get starting distribution vector x_0:
+    x_0 = np.empty(costfunction.n)
+    if charge_0 is not None:
+        costfunction.fwd_model.magdata = charge_0
+    x_0[:data_set.n] = costfunction.fwd_model.magdata.get_vector(mask=data_set.mask)
+    if ramp_0 is not None:
+        ramp_vec = ramp_0.param_cache.ravel()
+    else:
+        ramp_vec = np.zeros_like(costfunction.fwd_model.ramp.n)
+    x_0[data_set.n:] = ramp_vec
+    # Minimize:
+    with TakeTime('reconstruction time'):
+        x_opt = jcg.conj_grad_minimize(costfunction, x_0=x_0, max_iter=max_iter, verbose=verbose).x
+    _log.info('Cost after optimization: {:.3e}'.format(costfunction(x_opt)))
+    # Cut ramp parameters if necessary (this also saves the final parameters in the ramp class!):
+    x_opt = costfunction.fwd_model.ramp.extract_ramp_params(x_opt)
+    # Create and return fitting VectorData object:
+    mag_opt = VectorData(data_set.a, np.zeros((3,) + data_set.dim))
+    mag_opt.set_vector(x_opt, data_set.mask)
+    return mag_opt
+
+
 def optimize_nonlin(costfunction, first_guess=None):
     """Reconstruct a three-dimensional magnetic distribution from given phase maps via
     steepest descent method. This is slow, but works best for non l2-regularisators.
diff --git a/pyramid/utils/reconstruction_2d_from_phasemap.py b/pyramid/utils/reconstruction_2d_from_phasemap.py
index 14ad686..667717b 100644
--- a/pyramid/utils/reconstruction_2d_from_phasemap.py
+++ b/pyramid/utils/reconstruction_2d_from_phasemap.py
@@ -11,7 +11,9 @@ import numpy as np
 from .. import reconstruction
 from ..dataset import DataSet
 from ..projector import SimpleProjector
-from ..regularisator import FirstOrderRegularisator
+from ..regularisator import FirstOrderRegularisator, NoneRegularisator
+from ..kernel import KernelCharge
+from ..phasemapper import PhaseMapperCharge
 from ..forwardmodel import ForwardModel
 from ..costfunction import Costfunction
 from .pm import pm
@@ -105,3 +107,94 @@ def reconstruction_2d_from_phasemap(phasemap, b_0=1, lam=1E-3, max_iter=100, ram
             ramp.plot_phase(note='Fitted Ramp')
     # Return reconstructed magnetisation distribution and cost function:
     return magdata_rec, cost
+
+
+
+def reconstruction_2d_charge_from_phasemap(phasemap, lam=1E-3, max_iter=100, ramp_order=1,
+                                           electrode_vec=(1E6, 1E6), v_acc=300E3,
+                                           plot_results=False, verbose=True):
+    """Convenience function for reconstructing a projected distribution from a single phasemap.
+
+    Parameters
+    ----------
+    phasemap: :class:`~PhaseMap`
+        The phasemap which is used for the reconstruction.
+    b_0 : float, optional
+        The magnetic induction corresponding to a magnetization `M`\ :sub:`0` in T.
+        The default is 1.
+    lam : float
+        Regularisation parameter determining the weighting between measurements and regularisation.
+    max_iter : int, optional
+        The maximum number of iterations for the opimization.
+    ramp_order : int or None (default)
+        Polynomial order of the additional phase ramp which will be added to the phase maps.
+        All ramp parameters have to be at the end of the input vector and are split automatically.
+        Default is None (no ramps are added).
+    plot_results: boolean, optional
+        If True, the results are plotted after reconstruction.
+    ar_dens: int, optional
+        Number defining the arrow density which is plotted. A higher ar_dens number skips more
+        arrows (a number of 2 plots every second arrow). Default is 1.
+    verbose: bool, optional
+        If set to True, information like a progressbar is displayed during reconstruction.
+        The default is False.
+
+    Returns
+    -------
+    magdata_rec, cost: :class:`~.VectorData`, :class:`~.Costfunction`
+        The reconstructed magnetisation distribution and the used costfunction.
+
+    """
+    _log.debug('Calling reconstruction_2d_from_phasemap')
+    # Construct DataSet, Regularisator, ForwardModel and Costfunction:
+    dim = (1,) + phasemap.dim_uv
+    data = DataSet(phasemap.a, dim, b_0=1)
+    kernel = KernelCharge(phasemap.a, phasemap.dim_uv, electrode_vec=electrode_vec, v_acc=v_acc)
+    data.append(phasemap, SimpleProjector(dim), PhaseMapperCharge(kernel))
+    data.set_3d_mask()
+    # TODO: Rework classes below (ForwardModel, Costfunction)!
+    fwd_model = ForwardModel(data, ramp_order)
+    reg = NoneRegularisator()#FirstOrderRegularisator(data.mask, lam, add_params=fwd_model.ramp.n)
+    cost = Costfunction(fwd_model, reg)
+    # Reconstruct:
+    magdata_rec = reconstruction.optimize_linear(cost, max_iter=max_iter, verbose=verbose)
+    param_cache = cost.fwd_model.ramp.param_cache
+    if ramp_order is None:
+        offset, ramp = 0, (0, 0)
+    elif ramp_order >= 1:
+        offset, ramp = param_cache[0][0], (param_cache[1][0], param_cache[2][0])
+    elif ramp_order == 0:
+        offset, ramp = param_cache[0][0], (0, 0)
+    else:
+        raise ValueError('ramp_order has to be a positive integer or None!')
+    # Plot stuff:
+    if plot_results:
+        if ar_dens is None:
+            ar_dens = np.max([1, np.max(dim) // 64])
+        magdata_rec.plot_quiver_field(note='Reconstructed Distribution',
+                                      ar_dens=ar_dens, figsize=(16, 16))
+        phasemap_rec = pm(magdata_rec)
+        gain = 4 * 2 * np.pi / (np.abs(phasemap_rec.phase).max() + 1E-30)
+        gain = round(gain, -int(np.floor(np.log10(abs(gain)))))
+        vmin = phasemap_rec.phase.min()
+        vmax = phasemap_rec.phase.max()
+        phasemap.plot_combined(note='Input Phase', gain=gain)
+        phasemap -= fwd_model.ramp(index=0)
+        phasemap.plot_combined(note='Input Phase (ramp corrected)', gain=gain, vmin=vmin, vmax=vmax)
+        title = 'Reconstructed Phase'
+        if ramp_order is not None:
+            if ramp_order >= 0:
+                print('offset:', offset)
+                # title += ', fitted Offset: {:.2g} [rad]'.format(offset)
+            if ramp_order >= 1:
+                print('ramp:', ramp)
+                # title += ', (Fitted Ramp: (u:{:.2g}, v:{:.2g}) [rad/nm]'.format(*ramp)
+        phasemap_rec.plot_combined(note=title, gain=gain, vmin=vmin, vmax=vmax)
+        diff = (phasemap_rec - phasemap)
+        diff_name = 'Difference (RMS: {:.2g} rad)'.format(np.sqrt(np.mean(diff.phase) ** 2))
+        diff.plot_phase_with_hist(note=diff_name, sigma_clip=3)
+        if ramp_order is not None:
+            ramp = fwd_model.ramp(0)
+            ramp.plot_phase(note='Fitted Ramp')
+    # Return reconstructed magnetisation distribution and cost function:
+    return magdata_rec, cost
diff --git a/pyramid/utils/reconstruction_3d_from_magdata.py b/pyramid/utils/reconstruction_3d_from_magdata.py
index fcb649d..cd930a4 100644
--- a/pyramid/utils/reconstruction_3d_from_magdata.py
+++ b/pyramid/utils/reconstruction_3d_from_magdata.py
@@ -117,7 +117,7 @@ def reconstruction_3d_from_magdata(magdata, b_0=1, lam=1E-3, max_iter=100, ramp_
         phasemap += Ramp.create_ramp(phasemap.a, phasemap.dim_uv, (offset, ramp_u, ramp_v))
         data.phasemaps[i] = phasemap
     # Add noise if necessary:
-    if noise != 0:
+    if noise != 0:  # TODO: write function to add noise after APERTURE!! (ask Florian again)
         for i, phasemap in enumerate(data.phasemaps):
             phasemap.phase += np.random.normal(0, noise, phasemap.dim_uv)
             data.phasemaps[i] = phasemap
-- 
GitLab