"""

Mesh module for geopyv.

"""
import logging
import numpy as np
import geopyv as gp
from geopyv.object import Object
import gmsh
from copy import deepcopy
from scipy.optimize import minimize_scalar
import PIL.Image as ImagePIL
import PIL.ImageDraw as ImageDrawPIL
from alive_progress import alive_bar
import traceback


log = logging.getLogger(__name__)


class MeshBase(Object):
    """

    Mesh base class to be used as a mixin. Contains plot functionality.

    """

    def __init__(self):
        super().__init__(object_type="Mesh")
        """

        Mesh base class initialiser.

        """

    def inspect(self, *, subset_index=None, show=True, block=True, save=None):
        """

        Method to show the mesh and associated subset quality metrics using
        :mod: `~geopyv.plots.inspect_subset` for subsets and
        :mod: `~geopyv.plots.inspect_mesh` for meshes.

        Parameters
        ----------
        subset_index : int, optional
            Index of the subset to inspect. If `None', the mesh is inspected instead.
        show : bool, optional
            Control whether the plot is displayed.
        block : bool, optional
            Control whether the plot blocks execution until closed.
        save : str, optional
            Name to use to save plot. Uses default extension of `.png`.

        Returns
        -------
        fig :  `matplotlib.pyplot.figure`
            Figure object.
        ax : `matplotlib.pyplot.axes`
            Axes object.

        .. note::
            * The figure and axes objects can be returned allowing standard
              matplotlib functionality to be used to augment the plot generated.
              See the :ref:`plots tutorial <Plots Tutorial>` for guidance.

        .. seealso::
            :meth:`~geopyv.plots.inspect_subset`
            :meth:`~geopyv.plots.inspect_mesh`

        """

        # Check input.
        self._report(
            gp.check._check_type(subset_index, "subset_index", [int, type(None)]),
            "TypeError",
        )
        if subset_index:
            self._report(
                gp.check._check_index(
                    subset_index, "subset_index", 0, self.data["nodes"]
                ),
                "IndexError",
            )
        self._report(gp.check._check_type(show, "show", [bool]), "TypeError")
        self._report(gp.check._check_type(block, "block", [bool]), "TypeError")
        self._report(gp.check._check_type(save, "save", [str, type(None)]), "TypeError")

        # Inspect.
        if subset_index is not None:
            if self.data["solved"]:
                subset_data = self.data["results"]["subsets"][subset_index]
            else:
                subset_data = gp.subset.Subset(
                    f_coord=self._nodes[subset_index],
                    f_img=self._f_img,
                    g_img=self._g_img,
                    template=self._template,
                ).data
            mask = self.data["mask"]
            log.info("Inspecting subset {subset}...".format(subset=subset_index))
            fig, ax = gp.plots.inspect_subset(
                data=subset_data,
                mask=mask,
                show=show,
                block=block,
                save=save,
            )
            return fig, ax
        else:
            log.info("Inspecting mesh...")
            fig, ax = gp.plots.inspect_mesh(
                data=self.data,
                show=show,
                block=block,
                save=save,
            )
            return fig, ax

    def convergence(
        self,
        *,
        subset_index=None,
        quantity=None,
        show=True,
        block=True,
        save=None,
    ):
        """

        Method to plot the rate of convergence using
        :mod: `~geopyv.plots.convergence_subset` for subsets and
        :mod: `~geopyv.plots.convergence_mesh` for meshes.

        Parameters
        ----------
        subset_index : int, optional
            Index of the subset to inspect.
            If `None', the convergence plot is for the mesh instead.
        quantity : str, optional
            Selector for histogram convergence property if the convergence
            plot is for mesh. Defaults to `C_ZNCC` if left as default None.
        show : bool, optional
            Control whether the plot is displayed.
        block : bool, optional
            Control whether the plot blocks execution until closed.
        save : str, optional
            Name to use to save plot. Uses default extension of `.png`.


        Returns
        -------
        fig :  matplotlib.pyplot.figure
            Figure object.
        ax : `matplotlib.pyplot.axes`
            Axes object.


        .. note::
            * The figure and axes objects can be returned allowing standard
              matplotlib functionality to be used to augment the plot generated.
              See the :ref:`plots tutorial <Plots Tutorial>` for guidance.

        .. warning::
            * Can only be used once the mesh has been solved using the
              :meth:`~geopyv.mesh.Mesh.solve` method.

        .. seealso::
            :mod: `~geopyv.plots.convergence_subset`
            :meth:`~geopyv.plots.convergence_mesh`

        """

        # Check if solved.
        if self.data["solved"] is not True:
            log.error(
                "Mesh not yet solved therefore no convergence data to plot. "
                "First, run :meth:`~geopyv.mesh.Mesh.solve()` to solve."
            )
            raise ValueError(
                "Mesh not yet solved therefore no convergence data to plot. "
                "First, run :meth:`~geopyv.mesh.Mesh.solve()` to solve."
            )

        # Check input.
        self._report(
            gp.check._check_type(subset_index, "subset_index", [int, type(None)]),
            "TypeError",
        )
        if subset_index:
            self._report(
                gp.check._check_index(
                    subset_index, "subset_index", 0, self.data["nodes"]
                ),
                "IndexError",
            )
        self._report(
            gp.check._check_type(quantity, "quantity", [str, type(None)]), "TypeError"
        )
        if quantity:
            self._report(
                gp.check._check_value(
                    quantity, "quantity", ["C_ZNCC", "iterations", "norm"]
                ),
                "ValueError",
            )
        self._report(gp.check._check_type(show, "show", [bool]), "TypeError")
        self._report(gp.check._check_type(block, "block", [bool]), "TypeError")
        self._report(gp.check._check_type(save, "save", [str, type(None)]), "TypeError")

        # Plot convergence.
        if subset_index is not None:
            log.info(
                "Generating convergence plots for subset {subset}...".format(
                    subset=subset_index
                )
            )
            fig, ax = gp.plots.convergence_subset(
                self.data["results"]["subsets"][subset_index],
                show=show,
                block=block,
                save=save,
            )
            return fig, ax
        else:
            if quantity is None:
                quantity = "C_ZNCC"
            log.info(
                "Generating {quantity} convergence histogram for mesh...".format(
                    quantity=quantity
                )
            )
            fig, ax = gp.plots.convergence_mesh(
                data=self.data,
                quantity=quantity,
                show=show,
                block=block,
                save=save,
            )
            return fig, ax

    def contour(
        self,
        *,
        quantity="C_ZNCC",
        imshow=True,
        colorbar=True,
        ticks=None,
        mesh=False,
        alpha=0.75,
        levels=None,
        axis=True,
        xlim=None,
        ylim=None,
        show=True,
        block=True,
        save=None,
    ):
        """

        Method to plot the contours of a given quantity.

        Parameters
        ----------
        quantity : str, optional
            Selector for contour parameter. Must be in:
            [`C_ZNCC`, `iterations`, `norm`, `u`, `v`, `u_x`, `v_x`,`u_y`,`v_y`, `R`]
            Defaults to `C_ZNCC`.
        imshow : bool, optional
            Control whether the reference image is plotted.
            Defaults to True.
        colorbar : bool, optional
            Control whether the colour bar is plotted.
            Defaults to True.
        ticks : list, optional
            Overwrite default colourbar ticks.
            Defaults to None.
        mesh : bool, optional
            Control whether the mesh is plotted.
            Defaults to False.
        alpha : float, optional
            Control contour opacity. Must be between 0.0-1.0.
            Defaults to 0.75.
        levels : int or array-like, optional
            Control number and position of contour lines.
            Defaults to None.
        axis : bool, optional
            Control whether the axes are plotted.
            Defaults to True.
        xlim : array-like, optional
            Set the plot x-limits (lower_limit,upper_limit).
            Defaults to None.
        ylim : array-like, optional
            Set the plot y-limits (lower_limit,upper_limit).
            Defaults to None.
        show : bool, optional
            Control whether the plot is displayed.
            Defaults to True.
        block : bool, optional
            Control whether the plot blocks execution until closed.
            Defaults to False.
        save : str, optional
            Name to use to save plot. Uses default extension of `.png`.


        Returns
        -------
        fig :  matplotlib.pyplot.figure
            Figure object.
        ax : `matplotlib.pyplot.axes`
            Axes object.


        .. note::
            * The figure and axes objects can be returned allowing standard
              matplotlib functionality to be used to augment the plot generated.
              See the :ref:`plots tutorial <Plots Tutorial>` for guidance.

        .. warning::
            * Can only be used once the mesh has been solved using the
              :meth:`~geopyv.mesh.Mesh.solve` method.

        .. seealso::
            :meth:`~geopyv.plots.contour_mesh`

        """
        # Check if solved.
        if self.data["solved"] is not True:
            log.error(
                "Mesh not yet solved therefore no convergence data to plot. "
                "First, run :meth:`~geopyv.mesh.Mesh.solve()` to solve."
            )
            raise ValueError(
                "Mesh not yet solved therefore no convergence data to plot. "
                "First, run :meth:`~geopyv.mesh.Mesh.solve()` to solve."
            )

        # Check input.
        self._report(
            gp.check._check_type(quantity, "quantity", [str, type(None)]), "TypeError"
        )
        types = [
            "C_ZNCC",
            "iterations",
            "norm",
            "u",
            "v",
            "u_x",
            "v_x",
            "u_y",
            "v_y",
            "R",
        ]
        if quantity:
            self._report(
                gp.check._check_value(quantity, "quantity", types), "ValueError"
            )
        self._report(gp.check._check_type(imshow, "imshow", [bool]), "TypeError")
        self._report(gp.check._check_type(colorbar, "colorbar", [bool]), "TypeError")
        types = [tuple, list, np.ndarray, type(None)]
        self._report(gp.check._check_type(ticks, "ticks", types), "TypeError")
        self._report(gp.check._check_type(mesh, "mesh", [bool]), "TypeError")
        check = gp.check._check_type(alpha, "alpha", [float])
        if check:
            try:
                alpha = float(alpha)
                self._report(gp.check._conversion(alpha, "alpha", float), "Warning")
            except Exception:
                self._report(check, "TypeError")
        self._report(gp.check._check_range(alpha, "alpha", 0.0, 1.0), "ValueError")
        types = [int, tuple, list, np.ndarray, type(None)]
        self._report(gp.check._check_type(levels, "levels", types), "TypeError")
        self._report(gp.check._check_type(axis, "axis", [bool]), "TypeError")
        types = [tuple, list, np.ndarray, type(None)]
        self._report(gp.check._check_type(xlim, "xlim", types), "TypeError")
        if xlim is not None:
            self._report(gp.check._check_dim(xlim, "xlim", 1), "ValueError")
            self._report(gp.check._check_axis(xlim, "xlim", 0, [2]), "ValueError")
        self._report(gp.check._check_type(ylim, "ylim", types), "TypeError")
        if ylim is not None:
            self._report(gp.check._check_dim(ylim, "ylim", 1), "ValueError")
            self._report(gp.check._check_axis(ylim, "ylim", 0, [2]), "ValueError")
        self._report(gp.check._check_type(show, "show", [bool]), "TypeError")
        self._report(gp.check._check_type(block, "block", [bool]), "TypeError")
        self._report(gp.check._check_type(save, "save", [str, type(None)]), "TypeError")

        # Plot contours.
        log.info(
            "Generating {quantity} contour plot for mesh...".format(quantity=quantity)
        )
        fig, ax = gp.plots.contour_mesh(
            data=self.data,
            imshow=imshow,
            quantity=quantity,
            colorbar=colorbar,
            ticks=ticks,
            mesh=mesh,
            alpha=alpha,
            levels=levels,
            axis=axis,
            xlim=xlim,
            ylim=ylim,
            show=show,
            block=block,
            save=save,
        )
        return fig, ax

    def quiver(
        self,
        *,
        scale=1.0,
        imshow=True,
        mesh=False,
        axis=True,
        xlim=None,
        ylim=None,
        show=True,
        block=True,
        save=None,
    ):
        """

        Method to plot a quiver plot of the displacements.

        Parameters
        ----------
        scale : float, optional
            Control size of quiver arrows.
        imshow : bool, optional
            Control whether the reference image is plotted.
            Defaults to True.
        mesh : bool, optional
            Control whether the mesh is plotted.
            Defaults to False.
        axis : bool, optional
            Control whether the axes are plotted.
            Defaults to True.
        xlim : array-like, optional
            Set the plot x-limits (lower_limit,upper_limit).
            Defaults to None.
        ylim : array-like, optional
            Set the plot y-limits (lower_limit,upper_limit).
            Defaults to None.
        show : bool, optional
            Control whether the plot is displayed.
            Defaults to True.
        block : bool, optional
            Control whether the plot blocks execution until closed.
            Defaults to False.
        save : str, optional
            Name to use to save plot. Uses default extension of `.png`.

        Returns
        -------
        fig :  matplotlib.pyplot.figure
            Figure object.
        ax : `matplotlib.pyplot.axes`
            Axes object.


        .. note::
            * The figure and axes objects can be returned allowing standard
              matplotlib functionality to be used to augment the plot generated.
              See the :ref:`plots tutorial <Plots Tutorial>` for guidance.

        .. warning::
            * Can only be used once the mesh has been solved using the
              :meth:`~geopyv.mesh.Mesh.solve` method.

        .. seealso::
            :meth:`~geopyv.plots.quiver_mesh`
        """

        # Check if solved.
        if self.data["solved"] is not True:
            log.error(
                "Mesh not yet solved therefore no convergence data to plot. "
                "First, run :meth:`~geopyv.mesh.Mesh.solve()` to solve."
            )
            raise ValueError(
                "Mesh not yet solved therefore no convergence data to plot. "
                "First, run :meth:`~geopyv.mesh.Mesh.solve()` to solve."
            )

        # Check inputs.
        check = gp.check._check_type(scale, "scale", [float])
        if check:
            try:
                scale = float(scale)
                self._report(gp.check._conversion(scale, "scale", float), "Warning")
            except Exception:
                self._report(check, "TypeError")
        self._report(gp.check._check_range(scale, "scale", 0.0), "ValueError")
        self._report(gp.check._check_type(imshow, "imshow", [bool]), "TypeError")
        self._report(gp.check._check_type(mesh, "mesh", [bool]), "TypeError")
        self._report(gp.check._check_type(axis, "axis", [bool]), "TypeError")
        types = [tuple, list, np.ndarray, type(None)]
        self._report(gp.check._check_type(xlim, "xlim", types), "TypeError")
        if xlim is not None:
            self._report(gp.check._check_dim(xlim, "xlim", 1), "ValueError")
            self._report(gp.check._check_axis(xlim, "xlim", 0, [2]), "ValueError")
        self._report(gp.check._check_type(ylim, "ylim", types), "TypeError")
        if ylim is not None:
            self._report(gp.check._check_dim(ylim, "ylim", 1), "ValueError")
            self._report(gp.check._check_axis(ylim, "ylim", 0, [2]), "ValueError")
        self._report(gp.check._check_type(show, "show", [bool]), "TypeError")
        self._report(gp.check._check_type(block, "block", [bool]), "TypeError")
        self._report(gp.check._check_type(save, "save", [str, type(None)]), "TypeError")

        # Plot quiver.
        log.info("Generating quiver plot for mesh...")
        fig, ax = gp.plots.quiver_mesh(
            data=self.data,
            scale=scale,
            imshow=imshow,
            mesh=mesh,
            axis=axis,
            xlim=xlim,
            ylim=ylim,
            show=show,
            block=block,
            save=save,
        )
        return fig, ax

    def _report(self, msg, error_type):
        if msg and error_type != "Warning":
            log.error(msg)
        elif msg and error_type == "Warning":
            log.warning(msg)
            return True
        if error_type == "ValueError" and msg:
            raise ValueError(msg)
        elif error_type == "TypeError" and msg:
            raise TypeError(msg)
        elif error_type == "IndexError" and msg:
            raise IndexError(msg)


class Mesh(MeshBase):
    """
    Mesh class for geopyv.

    Private Attributes
    ------------------
    _f_img : `geopyv.image.Image`
        Reference image of geopyv.image.Image class, instantiated by
        :mod:`~image.Image`.
    _g_img : `geopyv.image.Image`
        Target image of geopyv.image.Image class, instantiated by
        :mod:`~image.Image`.
    _target_nodes : int
        Target number of nodes. Defaults to a value of 1000.
    _boundary : `numpy.ndarray` (Nx, 2)
        Array of coordinates that define the mesh boundary.
    _exclusions : list
        List of `numpy.ndarray` (Nx,2) that define the exclusion boundaries.
    _size_lower_bound : float
        Lower bound on element size.
    _size_upper_bound : float
        Lower bound on element size.
    _mesh_order : int
        Mesh element order. Either 1 or 2.
    _unsolvable : bool
        Boolean to indicate if the mesh cannot be solved.
    _nodes : `numpy.ndarray` (Nx,2)
        Array of subset coordinates.
    _elements : `numpy.ndarray(Nx,3*_mesh_order)
        Node connectivity matrix.

    """

    def __init__(
        self,
        *,
        f_img=None,
        g_img=None,
        target_nodes=1000,
        boundary=None,
        exclusions=[],
        size_lower_bound=1.0,
        size_upper_bound=1000.0,
        mesh_order=2,
        subset_size_compensation=False,
    ):
        """

        Initialisation of geopyv mesh object.

        Parameters
        ----------
        f_img : geopyv.image.Image, optional
            Reference image of geopyv.image.Image class, instantiated
            by :mod:`~geopyv.image.Image`.
        g_img : geopyv.image.Image, optional
            Target image of geopyv.imageImage class, instantiated by
            :mod:`~geopyv.image.Image`.
        target_nodes : int, optional
            Target number of nodes. Defaults to a value of 1000.
        boundary : `numpy.ndarray` (N,2)
            Array of coordinates to define the mesh boundary.
        exclusions : list, optional
            List of `numpy.ndarray` to define the mesh exclusions.
        size_lower_bound : float, optional
            Lower bound on element size. Defaults to a value of 1.0.
        size_upper_bound : float, optional
            Lower bound on element size. Defaults to a value of 1000.0.
        mesh_order : int, optional
            Mesh element order. Options are 1 and 2. Defaults to 2.
        subset_size_compensation: bool, optional
            Boolean to control whether masked subsets are enlarged to
            maintain area (and thus better correlation).
            Defaults to False.

        Attributes
        ----------
        data : dict
            Data object containing all settings and results.
            See the data structure :ref:`here <mesh_data_structure>`.
        solved : bool
            Boolean to indicate if the mesh has been solved.

        """
        # Set initialised boolean.
        self.initialised = False

        # Check types.
        if self._report(
            gp.check._check_type(f_img, "f_img", [gp.image.Image]), "Warning"
        ):
            f_img = gp.io._load_f_img()
        if self._report(
            gp.check._check_type(g_img, "g_img", [gp.image.Image]), "Warning"
        ):
            g_img = gp.io._load_g_img()
        check = gp.check._check_type(target_nodes, "target_nodes", [int])
        if check:
            try:
                target_nodes = int(target_nodes)
                self._report(
                    gp.check._conversion(target_nodes, "target_nodes", int), "Warning"
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(
            gp.check._check_range(target_nodes, "target_nodes", 1), "ValueError"
        )
        self._report(
            gp.check._check_type(
                boundary,
                "boundary",
                [gp.geometry.region.Circle, gp.geometry.region.Path],
            ),
            "TypeError",
        )
        check = gp.check._check_type(exclusions, "exclusions", [list])
        if check:
            try:
                exclusions = list(exclusions)
                self._report(
                    gp.check._conversion(exclusions, "exclusions", list, False),
                    "Warning",
                )
            except Exception:
                self._report(check, "TypeError")
        for exclusion in exclusions:
            self._report(
                gp.check._check_type(
                    exclusion,
                    "exclusion",
                    [gp.geometry.region.Circle, gp.geometry.region.Path],
                ),
                "TypeError",
            )
        self._report(
            gp.check._check_type(size_lower_bound, "size_lower_bound", [int, float]),
            "TypeError",
        )
        self._report(
            gp.check._check_type(size_upper_bound, "size_upper_bound", [int, float]),
            "TypeError",
        )
        self._report(
            gp.check._check_comp(
                size_lower_bound,
                "size_lower_bound",
                size_upper_bound,
                "size_upper_bound",
            ),
            "ValueError",
        )
        self._report(
            gp.check._check_type(
                subset_size_compensation, "subset_size_compensation", [bool]
            ),
            "TypeError",
        )

        # Pre-processing.
        size_upper_bound = min(
            size_upper_bound,
            np.max(
                np.sqrt(np.sum(np.square(np.diff(boundary._boundary, axis=0)), axis=1))
            ),
        )
        minimum_nodes = np.shape(boundary._boundary)[0]
        for exclusion in exclusions:
            minimum_nodes += np.shape(exclusion._boundary)[0]
        target_nodes = max(target_nodes, minimum_nodes)

        # Store.
        self._initialised = False
        self._f_img = f_img
        self._g_img = g_img
        self._target_nodes = target_nodes
        self._boundary = boundary
        self._boundary._rigid = False
        self._exclusions = exclusions
        self._size_lower_bound = size_lower_bound
        self._size_upper_bound = size_upper_bound
        self._mesh_order = mesh_order
        self._hard_boundary = boundary._hard
        self._subset_size_compensation = subset_size_compensation
        self.solved = False
        self._unsolvable = False

        # Define region of interest.
        (
            self._borders,
            self._segments,
            self._curves,
            self._mask,
        ) = gp.geometry.meshing._define_RoI(
            self._f_img, self._boundary, self._exclusions
        )
        # self._define_RoI()

        # Initialize gmsh if not already initialized.
        if gmsh.isInitialized() == 0:
            gmsh.initialize()
        gmsh.option.setNumber("General.Verbosity", 2)
        # 0: silent except for fatal errors, 1: +errors, 2: +warnings,
        # 3: +direct, 4: +information, 5: +status, 99: +debug.

        # Create initial mesh.
        log.info(
            "Generating mesh using gmsh with approximately {n} nodes.".format(
                n=self._target_nodes
            )
        )
        self._initial_mesh()
        log.info(
            "Mesh generated with {n} nodes and {e} elements.".format(
                n=len(self._nodes), e=len(self._elements)
            )
        )
        self._border_tags()
        gmsh.finalize()

        # Data.
        self.data = {
            "type": "Mesh",
            "solved": self.solved,
            "unsolvable": self._unsolvable,
            "images": {
                "f_img": self._f_img.filepath,
                "g_img": self._g_img.filepath,
            },
            "target_nodes": self._target_nodes,
            "mesh_order": self._mesh_order,
            "boundary": self._boundary,
            "exclusions": self._exclusions,
            "size_lower_bound": self._size_lower_bound,
            "size_upper_bound": self._size_upper_bound,
            "nodes": self._nodes,
            "elements": self._elements,
            "mask": self._mask,
        }

        self.initialised = True

    def set_target_nodes(self, target_nodes):
        """

        Method to create a mesh with a target number of nodes.

        Parameters
        ----------
        target_nodes : int
            Target number of nodes.


        .. note::
            * This method can be used to update the number of target nodes.
            * It will generate a new initial mesh with the specified target
              number of nodes.

        """

        # Check inputs.
        check = gp.check._check_type(target_nodes, "target_nodes", [int])
        if check:
            try:
                target_nodes = int(target_nodes)
                self._report(
                    gp.check._conversion(target_nodes, "target_nodes", int), "Warning"
                )
            except Exception:
                self._report(check, "TypeError")

        # Store.
        self._target_nodes = target_nodes

        # Initialize gmsh if not already initialized.
        if gmsh.isInitialized() == 0:
            gmsh.initialize()
        gmsh.option.setNumber("General.Verbosity", 2)

        # Create mesh.
        log.info(
            "Generating mesh using gmsh with approximately {n} nodes.".format(
                n=self._target_nodes
            )
        )
        self._initial_mesh()
        log.info(
            "Mesh generated with {n} nodes and {e} elements.".format(
                n=len(self._nodes), e=len(self._elements)
            )
        )
        gmsh.finalize()

    def solve(
        self,
        *,
        seed_coord=None,
        seed_warp=None,
        template=None,
        max_norm=1e-5,
        max_iterations=50,
        subset_order=1,
        tolerance=0.75,
        seed_tolerance=0.9,
        method="ICGN",
        adaptive_iterations=0,
        corrective_iterations=0,
        alpha=0.5,
    ):
        r"""

        Method to solve for the mesh.

        Parameters
        ----------
        seed_coord : `numpy.ndarray` (2,)
            An image coordinate selected in a region of low deformation. The
            reliability-guided approach is initiated from the nearest subset.
        template : `geopyv.templates.Template`
            Subset template object.
        max_norm : float, optional
            Exit criterion for norm of increment in warp function.
            Defaults to value of
            :math:`1 \cdot 10^{-5}`.
        max_iterations : int, optional
            Exit criterion for number of Gauss-Newton iterations.
            Defaults to value
            of 50.
        subset_order : int
            Warp function order. Options are 1 and 2.
            Defaults to a value of 1.
        tolerance: float, optional
            Correlation coefficient tolerance.
            Defaults to a value of 0.75.
        seed_tolerance: float, optional
            Correlation coefficient tolerance for the seed subset.
            Defaults to a value of 0.9.
        method : str
            Solution method. Options are FAGN and ICGN.
            Default is ICGN since it is faster.
        adaptive_iterations : int, optional
            Number of mesh adaptivity iterations to perform.
            Defaults to a value of 0.
        alpha : float, optional
            Mesh adaptivity control parameter.
            Defaults to a value of 0.5.


        Returns
        -------
        solved : bool
            Boolean to indicate if the mesh instance has been solved.

        .. note::
            * For guidance on how to use this class see the
              :ref:`mesh tutorial <Mesh Tutorial>`.

        """
        # Check if solved.
        if self.data["solved"] is True:
            log.error("Mesh has already been solved. Cannot be solved again.")
            return

        # Check inputs.
        if template is None:
            template = gp.templates.Circle(50)
        types = [gp.templates.Circle, gp.templates.Square]
        self._report(gp.check._check_type(template, "template", types), "TypeError")
        if self._report(
            gp.check._check_type(seed_coord, "seed_coord", [np.ndarray]), "Warning"
        ):
            selector = gp.gui.selectors.coordinate.CoordinateSelector()
            seed_coord = selector.select(self._f_img, template)
        elif self._report(gp.check._check_dim(seed_coord, "seed_coord", 1), "Warning"):
            selector = gp.gui.selectors.coordinate.CoordinateSelector()
            seed_coord = selector.select(self._f_img, template)
        elif self._report(
            gp.check._check_axis(seed_coord, "seed_coord", 0, [2]), "Warning"
        ):
            selector = gp.gui.selectors.coordinate.CoordinateSelector()
            seed_coord = selector.select(self._f_img, template)

        check = gp.check._check_type(max_norm, "max_norm", [float])
        if check:
            try:
                max_norm = float(max_norm)
                self._report(
                    gp.check._conversion(max_norm, "max_norm", float), "Warning"
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(gp.check._check_range(max_norm, "max_norm", 1e-20), "ValueError")
        check = gp.check._check_type(max_iterations, "max_iterations", [int])
        if check:
            try:
                max_iterations = int(max_iterations)
                self._report(
                    gp.check._conversion(max_iterations, "max_iterations", int),
                    "Warning",
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(
            gp.check._check_range(max_iterations, "max_iterations", 0.5), "ValueError"
        )
        check = gp.check._check_type(subset_order, "subset_order", [int])
        if check:
            try:
                subset_order = int(subset_order)
                self._report(
                    gp.check._conversion(subset_order, "subset_order", int), "Warning"
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(
            gp.check._check_value(subset_order, "subset_order", [1, 2]), "ValueError"
        )
        check = gp.check._check_type(adaptive_iterations, "adaptive_iterations", [int])
        if check:
            try:
                adaptive_iterations = int(adaptive_iterations)
                self._report(
                    gp.check._conversion(
                        adaptive_iterations, "adaptive_iterations", int
                    ),
                    "Warning",
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(
            gp.check._check_range(adaptive_iterations, "adaptive_iterations", 0),
            "ValueError",
        )
        check = gp.check._check_type(tolerance, "tolerance", [float])
        if check:
            try:
                tolerance = float(tolerance)
                self._report(
                    gp.check._conversion(tolerance, "tolerance", float), "Warning"
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(
            gp.check._check_range(tolerance, "tolerance", 0.0, 1.0), "ValueError"
        )
        check = gp.check._check_type(seed_tolerance, "seed_tolerance", [float])
        if check:
            try:
                seed_tolerance = float(seed_tolerance)
                self._report(
                    gp.check._conversion(seed_tolerance, "seed_tolerance", float),
                    "Warning",
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(
            gp.check._check_range(seed_tolerance, "seed_tolerance", 0.0, 1.0),
            "ValueError",
        )
        self._report(
            gp.check._check_comp(
                tolerance,
                "tolerance",
                seed_tolerance,
                "seed_tolerance",
            ),
            "ValueError",
        )
        self._report(gp.check._check_type(method, "method", [str]), "TypeError")
        self._report(
            gp.check._check_value(method, "method", ["FAGN", "ICGN"]), "ValueError"
        )
        check = gp.check._check_type(alpha, "alpha", [float])
        if check:
            try:
                alpha = float(alpha)
                self._report(gp.check._conversion(alpha, "alpha", float), "Warning")
            except Exception:
                self._report(check, "TypeError")
        self._report(gp.check._check_range(alpha, "alpha", 0.0, 1.0), "ValueError")
        if seed_warp is not None:
            check = gp.check._check_type(seed_warp, "seed_warp", [np.ndarray])
            if check:
                try:
                    seed_warp = np.asarray(seed_warp)
                    self._report(
                        gp.check._conversion(seed_warp, "seed_warp", np.ndarray),
                        "Warning",
                    )
                except Exception:
                    self._report(check, "TypeError")
            self._report(gp.check._check_dim(seed_warp, "seed_warp", 1), "ValueError")
            self._report(
                gp.check._check_axis(seed_warp, "seed_warp", 0, [6 * subset_order]),
                "ValueError",
            )
        else:
            seed_warp = np.zeros(6 * subset_order)

        # Store variables.
        self._seed_coord = seed_coord
        self._template = template
        self._max_iterations = max_iterations
        self._max_norm = max_norm
        self._adaptive_iterations = adaptive_iterations
        self._corrective_iterations = corrective_iterations
        self._method = method
        self._subset_order = subset_order
        self._tolerance = tolerance
        self._seed_tolerance = seed_tolerance
        self._alpha = alpha
        self._subset_bgf_nodes = None
        self._subset_bgf_values = None
        self._update = False
        self._seed_warp = seed_warp

        # Initialize gmsh if not already initialized.
        if gmsh.isInitialized() == 0:
            gmsh.initialize()
        gmsh.option.setNumber("General.Verbosity", 2)

        # Create initial mesh.
        self._initial_mesh()

        # Solve initial mesh.
        self._message = "Solving initial mesh"
        self._find_seed_node()
        try:
            self._reliability_guided()
            if self._unsolvable:
                if (
                    np.amin(self._C_ZNCC[np.where(self._C_ZNCC > 0.0)])
                    < self._tolerance
                ):
                    log.error(
                        "Specified correlation coefficient tolerance not met. "
                        "Minimum correlation coefficient: "
                        "{min_C:.2f}; tolerance: {tolerance:.2f}.".format(
                            min_C=np.amin(self._C_ZNCC[np.where(self._C_ZNCC > 0.0)]),
                            tolerance=self._tolerance,
                        )
                    )
                else:
                    log.error("Mesh unsolvable.")
                return self.solved
            # Solve adaptive iterations.
            for iteration in range(1, adaptive_iterations + 1):
                self._message = "Adaptive iteration {}".format(iteration)
                self._adaptive_mesh()
                self._update_mesh()
                self._find_seed_node()
                self._reliability_guided()
                if self._unsolvable:
                    log.error(
                        "Specified correlation coefficient tolerance not met. "
                        "Minimum correlation coefficient: "
                        "{min_C:.2f}; tolerance: {tolerance:.2f}.".format(
                            min_C=np.amin(self._C_ZNCC[np.where(self._C_ZNCC > 0.0)]),
                            tolerance=self._tolerance,
                        )
                    )
                    return self.solved
            self._region_tracing()
            if self._unsolvable:
                log.error("Rigid exclusion tracking failure.")
                return self.solved

            log.info(
                "Solved mesh. Minimum correlation coefficient: {min_C:.2f}; "
                "maximum correlation coefficient: {max_C:.2f}.".format(
                    min_C=np.amin(self._C_ZNCC),
                    max_C=np.amax(self._C_ZNCC),
                )
            )

            # Pack data.
            self.solved = True
            self.data["nodes"] = self._nodes
            self.data["elements"] = self._elements
            self.data["areas"] = self._areas
            self.data["solved"] = self.solved
            self.data["unsolvable"] = self._unsolvable
            self.data.update(
                {"centroids": np.mean(self._nodes[self._elements], axis=-2)}
            )

            # Pack settings.
            self._settings = {
                "max_iterations": self._max_iterations,
                "max_norm": self._max_norm,
                "adaptive_iterations": self._adaptive_iterations,
                "method": self._method,
                "tolerance": self._tolerance,
            }
            self.data.update({"settings": self._settings})

            # Extract data from subsets.
            subset_data = []
            for subset in self._subsets:
                subset_data.append(subset.data)

            # Pack results.
            self._results = {
                "subsets": subset_data,
                "displacements": self._displacements,
                "warps": self._warps,
                "C_ZNCC": self._C_ZNCC,
                "seed": self._seed_node,
            }
            self.data.update({"results": self._results})

        except Exception:
            log.error("Could not solve mesh. Not a correlation issue.")
            print(traceback.format_exc())
            self._update = True
            self.solved = False
            self._unsolvable = True
        gmsh.finalize()
        return self.solved

    def _region_tracing(self):
        exclusion_warp = []
        for i in range(np.shape(self._exclusions)[0]):
            if self._exclusions[i]._rigid:
                subset = gp.subset.Subset(
                    f_img=self._f_img,
                    g_img=self._g_img,
                    f_coord=self._exclusions[i]._coord,
                    template=gp.templates.Circle(
                        int(self._exclusions[i]._specifics["radius"])
                    ),
                )
                subset.solve(
                    tolerance=0.9,
                )
                if subset.data["solved"] is not True:
                    self._update = True
                    self.solved = False
                    self._unsolvable = True
                    del subset
                    return False
                else:
                    exclusion_warp.append(subset._p.flatten())
                    del subset
            else:
                exclusion_warp.append(self._displacements[self._exclusions_tags[i]])
        self._boundary._update(
            self.data["images"]["f_img"], self._displacements[self._boundary_tags]
        )
        for i in range(np.shape(self._exclusions)[0]):
            self._exclusions[i]._update(self.data["images"]["f_img"], exclusion_warp[i])

    def _update_mesh(self):
        """

        Private method to update the mesh variables.

        """
        (
            _,
            nc,
            _,
        ) = gmsh.model.mesh.getNodes()  # Extracts: node coordinates.
        _, _, ent = gmsh.model.mesh.getElements(dim=2)  # Extracts: element node tags.
        self._nodes = np.column_stack(
            (nc[0::3], nc[1::3])
        )  # Nodal coordinate array (x,y).
        self._elements = np.reshape(
            (np.asarray(ent) - 1).flatten(), (-1, 3 * self._mesh_order)
        )  # Element connectivity array.

    def _find_seed_node(self):
        """

        Private method to find seed node given seed coordinate.

        """
        dist = np.sqrt(
            (self._nodes[:, 0] - self._seed_coord[0]) ** 2
            + (self._nodes[:, 1] - self._seed_coord[1]) ** 2
        )
        self._seed_node = np.argmin(dist)

    def _define_RoI(self):
        """

        Private method to define the RoI.

        """
        # Create binary mask RoI.
        binary_img = ImagePIL.new(
            "L",
            (
                np.shape(self._f_img.image_gs)[1],
                np.shape(self._f_img.image_gs)[0],
            ),
            0,
        )
        if self._hard_boundary:
            ImageDrawPIL.Draw(binary_img).polygon(
                self._boundary._boundary.flatten().tolist(), outline=1, fill=1
            )
        else:
            image_edge = np.asarray(
                [
                    [0.0, 0.0],
                    [0.0, np.shape(self._f_img.image_gs)[0]],
                    [
                        np.shape(self._f_img.image_gs)[1],
                        np.shape(self._f_img.image_gs)[0],
                    ],
                    [np.shape(self._f_img.image_gs)[1], 0.0],
                ]
            )
            ImageDrawPIL.Draw(binary_img).polygon(
                image_edge.flatten().tolist(), outline=1, fill=1
            )

        # Create objects for mesh generation.
        self._segments = np.empty(
            (np.shape(self._boundary._boundary)[0], 2), dtype=np.int32
        )  # Initiate segment array.
        self._segments[:, 0] = np.arange(
            np.shape(self._boundary._boundary)[0], dtype=np.int32
        )  # Fill segment array.
        self._segments[:, 1] = np.roll(self._segments[:, 0], -1)  # Fill segment array.
        self._curves = [list(self._segments[:, 0])]  # Create curve list.

        # Add exclusions.
        self._borders = self._boundary._boundary
        for exclusion in self._exclusions:
            ImageDrawPIL.Draw(binary_img).polygon(
                exclusion._boundary.flatten().tolist(), outline=1, fill=0
            )  # Add exclusion to binary mask.
            cur_max_idx = np.amax(
                self._segments
            )  # Highest index used by current segments.
            exclusion_segment = np.empty(
                np.shape(exclusion._boundary)
            )  # Initiate exclusion segment array.
            exclusion_segment[:, 0] = np.arange(
                cur_max_idx + 1,
                cur_max_idx + 1 + np.shape(exclusion._boundary)[0],
            )  # Fill exclusion segment array.
            exclusion_segment[:, 1] = np.roll(
                exclusion_segment[:, 0], -1
            )  # Fill exclusion segment array.
            self._borders = np.append(
                self._borders, exclusion._boundary, axis=0
            )  # Append exclusion to boundary array.
            self._segments = np.append(
                self._segments, exclusion_segment, axis=0
            ).astype(
                "int32"
            )  # Append exclusion segments to segment array.
            self._curves.append(
                list(exclusion_segment[:, 0].astype("int32"))
            )  # Append exclusion curve to curve list.

        # Finalise mask.
        self._mask = np.array(binary_img)

    def _border_tags(self):
        """
        Private method to extract the border and exclusions tags from gmsh.
        """
        self._boundary_tags = gmsh.model.occ.getCurveLoops(0)[1][0]
        self._exclusions_tags = []
        for i in range(len(self._exclusions)):
            self._exclusions_tags.append(gmsh.model.occ.getCurveLoops(0)[1][i + 1])

    def _initial_mesh(self):
        """

        Private method to optimize the element size to generate
        approximately the desired number of elements.

        """

        def f(size):
            return self._uniform_remesh(
                size,
                self._borders,
                self._segments,
                self._curves,
                self._target_nodes,
                self._size_lower_bound,
                self._size_upper_bound,
                self._mesh_order,
            )

        # minimize_scalar(f)
        minimize_scalar(
            f,
            bounds=(self._size_lower_bound, self._size_upper_bound),
            method="bounded",
        )
        self._update_mesh()

    def _adaptive_mesh(self):
        """

        Private method to perform adaptive remeshing.

        """
        message = "Adaptively remeshing..."
        with alive_bar(dual_line=True, bar=None, title=message) as bar:
            D = (
                abs(self._warps[:, 3] + self._warps[:, 4]) * self._areas
            )  # Elemental shear strain-area products.
            D_b = np.mean(D)  # Mean elemental shear strain-area product.
            self._areas *= (
                np.clip(D / D_b, self._alpha, 1 / self._alpha)
            ) ** -2  # Target element areas calculated.

            def f(scale):
                return self._adaptive_remesh(
                    scale,
                    self._target_nodes,
                    self._nodes,
                    self._elements,
                    self._areas,
                    self._mesh_order,
                )

            minimize_scalar(f, bounds=(0.1, 5))
            bar()

    @staticmethod
    def _uniform_remesh(
        size,
        boundary,
        segments,
        curves,
        target_nodes,
        size_lower_bound,
        size_upper_bound,
        order,
    ):
        """

        Private method to create the initial mesh.

        Parameters
        ----------
        size : int
            Target size of elements.
        boundary : `numpy.ndarray` (Nx,Ny)
            Array of coordinates to define the mesh boundary.
        segments : `numpy.ndarray` (Nx,Ny)
            Array of segments for gmsh mesh generation.
        curves : `numpy.ndarray` (Nx,Ny)
            Array of curves for gmsh mesh generation.
        target_nodes : int
            Target number of nodes.
        size_lower_bound : int
            Lower bound on element size.


        Returns
        -------
        error : int
            Error between target and actual number of nodes.

        """

        # Make mesh.
        gmsh.model.add("base")  # Create model.

        # Add points.
        for i in range(np.shape(boundary)[0]):
            gmsh.model.occ.addPoint(boundary[i, 0], boundary[i, 1], 0, size, i)

        # Add line segments.
        for i in range(np.shape(segments)[0]):
            gmsh.model.occ.addLine(segments[i, 0], segments[i, 1], i)

        # Add curves.
        for i in range(len(curves)):
            gmsh.model.occ.addCurveLoop(curves[i], i)
        curve_indices = list(np.arange(len(curves), dtype=np.int32))

        # Create surface.
        gmsh.model.occ.addPlaneSurface(curve_indices, 0)

        # Generate mesh.
        gmsh.option.setNumber("Mesh.MeshSizeMin", size_lower_bound)
        gmsh.option.setNumber("Mesh.MeshSizeMax", size_upper_bound)
        gmsh.model.occ.synchronize()

        # Add physical groups.
        for i in range(len(curves)):
            gmsh.model.addPhysicalGroup(1, curves[i], i)

        # Mesh.
        gmsh.model.mesh.generate(2)
        gmsh.model.mesh.optimize()
        gmsh.model.mesh.setOrder(order)

        # Get mesh topology.
        (
            _,
            nc,
            _,
        ) = (
            gmsh.model.mesh.getNodes()
        )  # Extracts: node tags, node coordinates, parametric coordinates.
        nodes = np.column_stack((nc[0::3], nc[1::3]))  # Nodal coordinate array (x,y).
        error = (np.shape(nodes)[0] - target_nodes) ** 2

        return error

    @staticmethod
    def _adaptive_remesh(scale, target, nodes, elements, areas, order):
        """

        Private method to perform adaptive mesh generation.

        Parameters
        ----------
        scale : float
            Scale factor on element size.
        target : int
            Target number of nodes.
        nodes : `numpy.ndarray`
            Nodes for background mesh.
        elements : `numpy.ndarray`
            Elements for background mesh.
        areas : float
            Target element areas.


        Returns
        -------
        error : float
            Error between target and actual number of nodes.

        """
        lengths = gp.geometry.utilities.area_to_length(
            areas * scale
        )  # Convert target areas to target characteristic lengths.
        bg = gmsh.view.add("bg", 1)  # Create background view.
        data = np.pad(
            nodes[elements[:, :3]],
            ((0, 0), (0, 0), (0, 2)),
            mode="constant",
        )  # Prepare data input (coordinates and buffer).
        data[:, :, 3] = np.reshape(
            np.repeat(lengths, 3), (-1, 3)
        )  # Fill data input buffer with target weights.
        data = np.transpose(data, (0, 2, 1)).flatten()  # Reshape for input.
        gmsh.view.addListData(bg, "ST", len(elements), data)  # Add data to view.
        bgf = gmsh.model.mesh.field.add("PostView")  # Add view to field.
        gmsh.model.mesh.field.setNumber(
            bgf, "ViewTag", bg
        )  # Establish field reference (important for function reuse).
        gmsh.model.mesh.field.setAsBackgroundMesh(bgf)  # Set field as background.
        gmsh.option.setNumber(
            "Mesh.MeshSizeExtendFromBoundary", 0
        )  # Prevent boundary influence on mesh.
        gmsh.option.setNumber(
            "Mesh.MeshSizeFromPoints", 0
        )  # Prevent point influence on mesh.
        gmsh.option.setNumber(
            "Mesh.MeshSizeFromCurvature", 0
        )  # Prevent curve influence on mesh.
        gmsh.option.setNumber("Mesh.Algorithm", 5)
        gmsh.model.mesh.clear()  # Tidy.
        gmsh.model.mesh.generate(2)  # Generate mesh.
        gmsh.model.mesh.optimize()
        gmsh.model.mesh.setOrder(order)

        (
            nt,
            nc,
            npar,
        ) = (
            gmsh.model.mesh.getNodes()
        )  # Extracts: node tags, node coordinates, parametric coordinates.
        nodes = np.column_stack((nc[0::3], nc[1::3]))  # Nodal coordinate array (x,y).
        error = (np.shape(nodes)[0] - target) ** 2
        return error

    def _element_area(self):
        """
        Private method to calculate the element areas.

        """
        M = np.ones((len(self._elements), 3, 3))
        M[:, 1] = self._nodes[self._elements[:, :3]][
            :, :, 0
        ]  # [:3] will provide corner nodes in both 1st and 2nd order element case.
        M[:, 2] = self._nodes[self._elements[:, :3]][:, :, 1]
        self._areas = 0.5 * np.linalg.det(M)

    def _local_coordinates(self):
        """
        Private method to define the element centroid in terms of the local
        coordinate system. Thereotically, this is constant but is calculated in
        case of rounding error.
        """

        A = np.ones((np.shape(self._elements)[0], 3, 4))
        A[:, 1:, 0] = np.mean(self._nodes[self._elements], axis=1)
        A[:, 1:, 1:] = self._nodes[self._elements][:, :3, :2].transpose(0, 2, 1)

        return A

    def _shape_function(self):
        if self._mesh_order == 1:
            N = np.asarray([1 / 3, 1 / 3, 1 / 3])
            dN = np.asarray([[1, 0, -1], [0, 1, -1]])
            d2N = None
        elif self._mesh_order == 2:
            N = np.asarray([-1 / 9, -1 / 9, -1 / 9, 4 / 9, 4 / 9, 4 / 9])
            dN = np.asarray(
                [
                    [1 / 3, 0, -1 / 3, 4 / 3, -4 / 3, 0],
                    [0, 1 / 3, -1 / 3, 4 / 3, 0, -4 / 3],
                ]
            )
            d2N = np.asarray(
                [
                    [4, 0, 4, 0, 0, -8],
                    [0, 0, 4, 4, -4, -4],
                    [0, 4, 4, 0, -8, 0],
                ]
            )
        return N, dN, d2N

    def _element_strains(self):
        """

        Private method to calculate the elemental strain the "B" matrix
        relating element node displacements to elemental strain.

        """

        self._warps = np.zeros((np.shape(self._elements)[0], 12))
        x = self._nodes[self._elements]
        u = self._displacements[self._elements]
        # Coordinate matrix.
        A = self._local_coordinates()

        # Shape function and derivatives.
        N, dN, d2N = self._shape_function()

        # Displacements.
        self._warps[:, :2] = N @ u

        # 1st Order Strains
        J_x_T = dN @ x
        J_u_T = dN @ u

        self._warps[:, 2:6] = (np.linalg.inv(J_x_T) @ J_u_T).reshape(
            np.shape(self._elements)[0], -1
        )

        # 2nd Order Strains
        if self._mesh_order == 2:
            K_u = d2N @ u
            dz = np.zeros((np.shape(self._elements)[0], 2, 2))
            dz[:, 0, 0] = x[:, 1, 1] - x[:, 2, 1]
            dz[:, 0, 1] = x[:, 2, 1] - x[:, 0, 1]
            dz[:, 1, 0] = x[:, 2, 0] - x[:, 1, 0]
            dz[:, 1, 1] = x[:, 0, 0] - x[:, 2, 0]
            dz /= np.linalg.det(A[:, :, [1, 2, 3]])[:, None, None]

            K_x_inv = np.zeros((np.shape(self._elements)[0], 3, 3))
            K_x_inv[:, 0, 0] = dz[:, 0, 0] ** 2
            K_x_inv[:, 0, 1] = 2 * dz[:, 0, 0] * dz[:, 0, 1]
            K_x_inv[:, 0, 2] = dz[:, 0, 1] ** 2
            K_x_inv[:, 1, 0] = dz[:, 0, 0] * dz[:, 1, 0]
            K_x_inv[:, 1, 1] = dz[:, 0, 0] * dz[:, 1, 1] + dz[:, 0, 1] * dz[:, 1, 0]
            K_x_inv[:, 1, 2] = dz[:, 0, 1] * dz[:, 1, 1]
            K_x_inv[:, 2, 0] = dz[:, 1, 0] ** 2
            K_x_inv[:, 2, 1] = 2 * dz[:, 1, 0] * dz[:, 1, 1]
            K_x_inv[:, 2, 2] = dz[:, 1, 1] ** 2

            self._warps[:, 6:] = (K_x_inv @ K_u).reshape(
                np.shape(self._elements)[0], -1
            )

    def _reliability_guided(self):
        """

        Private method to perform reliability-guided (RG) PIV analysis.

        """
        # Set up.
        m = np.shape(self._nodes)[0]
        n = np.shape(self._seed_warp)[0]
        self._solvables = []
        self._subset_solved = np.zeros(
            m, dtype=int
        )  # Solved/unsolved reference array (1 if unsolved, -1 if solved).
        self._C_ZNCC = np.zeros(m, dtype=np.float64)  # Correlation coefficient array.
        self._subsets = np.empty(m, dtype=object)  # Initiate subset array.
        self._p = np.zeros((m, n), dtype=np.float64)  # Warp function array.
        self._displacements = np.zeros(
            (m, 2), dtype=np.float64
        )  # Displacement output array.

        # All nodes.
        entities = gmsh.model.getEntities()
        self._node_tags = []
        for e in entities:
            tags, _, _ = gmsh.model.mesh.getNodes(e[0], e[1])
            self._node_tags = np.append(self._node_tags, tags.flatten()).astype(int)
        # Interior and boundary nodes.
        entities = gmsh.model.getEntities(2)
        self._interior_node_tags = []
        for e in entities:
            tags, _, _ = gmsh.model.mesh.getNodes(e[0], e[1])
            self._interior_node_tags = np.append(
                self._interior_node_tags, tags.flatten()
            ).astype(int)
        self._borders_node_tags = (
            np.setdiff1d(self._node_tags, self._interior_node_tags).astype(int) - 1
        )
        # Template masking using binary mask.
        for tag in range(len(self._node_tags)):
            if tag in self._borders_node_tags:
                centre = self._nodes[tag]
                template = deepcopy(self._template)
                template.mask(centre, self._mask)
                if self._subset_size_compensation:
                    if template.n_px < self._template.n_px:
                        size = int(
                            self._template.size
                            / np.sqrt(template.n_px / self._template.n_px)
                        )
                        if self._template.shape == "circle":
                            template = gp.templates.Circle(radius=size)
                        elif self._template.shape == "square":
                            template = gp.templates.Square(length=size)
                        template.mask(centre, self._mask)
                self._subsets[tag] = gp.subset.Subset(
                    f_coord=self._nodes[tag],
                    f_img=self._f_img,
                    g_img=self._g_img,
                    template=template,
                )  # Create masked boundary subset.
            else:
                self._subsets[tag] = gp.subset.Subset(
                    f_coord=self._nodes[tag],
                    f_img=self._f_img,
                    g_img=self._g_img,
                    template=self._template,
                )  # Create full subset.
        # Solve subsets in mesh.
        number_nodes = np.shape(self._nodes)[0]
        with alive_bar(
            number_nodes,
            dual_line=True,
            bar="blocks",
            title=self._message,
        ) as self._bar:
            # Solve for seed.
            self._bar.text = "-> Solving seed subset..."
            self._subsets[self._seed_node].solve(
                max_norm=self._max_norm,
                max_iterations=self._max_iterations,
                warp_0=self._seed_warp,
                order=self._subset_order,
                method=self._method,
                tolerance=self._seed_tolerance,
            )  # Solve for seed subset.
            self._bar()
            if not self._subsets[self._seed_node].data["solved"]:  # If seed unsolved.
                self._subsets[self._seed_node].solve(
                    max_norm=self._max_norm,
                    max_iterations=self._max_iterations,
                    warp_0=np.zeros(np.shape(self._seed_warp)),
                    order=self._subset_order,
                    method=self._method,
                    tolerance=self._seed_tolerance,
                )
                if not self._subsets[self._seed_node].data["solved"]:
                    self._update = True
                    log.error(
                        "Specified seed correlation coefficient tolerance not met."
                        "Minimum seed correlation coefficient:"
                        "{seed_C:.2f}; tolerance: {seed_tolerance:.2f}.".format(
                            seed_C=self._subsets[self._seed_node].data["results"][
                                "C_ZNCC"
                            ],
                            seed_tolerance=self._seed_tolerance,
                        )
                    )
                    self._C_ZNCC[self._seed_node] = self._subsets[self._seed_node].data[
                        "results"
                    ]["C_ZNCC"]
            if self._subsets[self._seed_node].data["solved"]:
                # Store if solved.
                self._store_variables(self._seed_node, seed=True)
                # Solve for neighbours of the seed subset.
                p_0 = self._subsets[self._seed_node].data["results"][
                    "p"
                ]  # Set seed subset warp function as the preconditioning.
                self._neighbours(
                    self._seed_node, p_0
                )  # Solve for neighbouring subsets.
                # Solve through sorted queue.
                self._bar.text = (
                    "-> Solving remaining subsets using reliability guided approach..."
                )
                count = 1
                while np.max(self._subset_solved) > -1:
                    # Identify next subset.
                    cur_idx = np.argmax(
                        self._subset_solved * self._C_ZNCC
                    )  # Subset with highest correlation coefficient selected.
                    try:
                        p_0 = self._subsets[cur_idx].data["results"][
                            "p"
                        ]  # Precondition based on selected subset.
                    except Exception:
                        p_0 = self._subsets[self._seed_node].data["results"]["p"]
                    self._subset_solved[cur_idx] = -1  # Set as solved.
                    solvable = self._neighbours(
                        cur_idx, p_0
                    )  # Calculate for neighbouring subsets.
                    count += 1
                    self._bar()
                    if count == number_nodes:
                        break
                    elif solvable is False:
                        break
        if self._subsets[self._seed_node].data["solved"]:
            self._corrections()
        # Update check.
        if any(self._subset_solved != -1) or any(self._C_ZNCC < self._tolerance):
            self._update = True
            self.solved = False
            self._unsolvable = True
        else:
            # self._corrections()
            # Compute element areas and strains.
            self._update = False
            self.solved = True
            self._unsolvable = False
            self._element_area()
            self._element_strains()

    def _corrections(self):
        for corrective_iteration in range(1, self._corrective_iterations + 1):
            sorted_correlation = np.argsort(self._C_ZNCC)
            j = 0
            with alive_bar(
                np.shape(self._subsets)[0],
                dual_line=True,
                bar="blocks",
                title="Performing corrections...",
            ) as bar:
                for idx in range(np.shape(sorted_correlation)[0]):
                    subset = gp.subset.Subset(
                        f_coord=self._nodes[idx],
                        f_img=self._f_img,
                        g_img=self._g_img,
                        template=self._subsets[idx]._template,
                    )
                    warp = np.mean(self._p[self._connectivity(idx)], axis=0)
                    subset.solve(
                        max_norm=self._max_norm,
                        max_iterations=self._max_iterations,
                        warp_0=warp,
                        order=self._subset_order,
                        method=self._method,
                        tolerance=self._seed_tolerance,
                    )
                    try:
                        if (
                            subset._C_ZNCC > self._subsets[idx]._C_ZNCC
                            and subset.solved
                        ):
                            j += 1
                            self._subsets[idx] = subset
                            self._store_variables(idx, seed=True)
                    except Exception:
                        pass
                    bar()
                    bar.text = "{} corrections accepted".format(round(j / (idx + 1), 3))
            log.info("{ca} corrections accepted".format(ca=round(j / (idx + 1), 3)))

    def _connectivity(self, idx):
        """

        A private method that returns the indices of nodes connected
        to the index node according to the input array.

        Parameters
        ----------
        idx : int
            Index of node.


        Returns
        -------
        pts_idx : `numpy.ndarray` (N)
            Mesh array.

        """

        if self._mesh_order == 1:
            element_idxs = np.argwhere(np.any(self._elements == idx, axis=1)).flatten()
            pts_idxs = np.unique(self._elements[element_idxs])
            pts_idxs = np.delete(pts_idxs, np.argwhere(pts_idxs == idx))
        elif self._mesh_order == 2:
            element_idxs = np.argwhere(self._elements == idx)
            pts_idxs = []
            for i in range(len(element_idxs)):
                if element_idxs[i, 1] == 0:  # If 1
                    pts_idxs.append(self._elements[element_idxs[i, 0], 3::2])  # Add 4,6
                elif element_idxs[i, 1] == 1:  # If 2
                    pts_idxs.append(self._elements[element_idxs[i, 0], 3:5])  # Add 4,5
                elif element_idxs[i, 1] == 2:  # If 3
                    pts_idxs.append(self._elements[element_idxs[i, 0], 4:])  # Add 5,6
                elif element_idxs[i, 1] == 3:  # If 4
                    pts_idxs.append(self._elements[element_idxs[i, 0], :2])  # Add 1,2
                elif element_idxs[i, 1] == 4:  # If 5
                    pts_idxs.append(self._elements[element_idxs[i, 0], 1:3])  # Add 2,3
                elif element_idxs[i, 1] == 5:  # If 6
                    pts_idxs.append(self._elements[element_idxs[i, 0], :3:2])  # Add 1,3
            pts_idxs = np.unique(pts_idxs)

        return pts_idxs

    def _neighbours(self, cur_idx, p_0):
        """

        Private method to calculate the correlation coefficients and
        warp functions of the neighbouring nodes.

        Parameters
        ----------
        p_0 : `numpy.ndarray` (N)
            Preconditioning warp function.


        Returns
        -------
        solved : bool
            Boolean to indicate whether the neighbouring subsets have been solved.

        """
        neighbours = self._connectivity(cur_idx)
        for idx in neighbours:
            if self._subset_solved[idx] == 0:  # If not previously solved.
                # Use nearest-neighbout pre-conditioning.
                self._subsets[idx].solve(
                    max_norm=self._max_norm,
                    max_iterations=self._max_iterations,
                    order=self._subset_order,
                    warp_0=p_0,
                    method=self._method,
                    tolerance=self._tolerance,
                )
                if self._subsets[idx].data["solved"]:  # Check against tolerance.
                    self._store_variables(idx)
                elif self._subsets[idx].data["unsolvable"]:
                    return False
                else:
                    # Use projected pre-conditioning.
                    diff = self._nodes[idx] - self._nodes[cur_idx]
                    p = self._subsets[cur_idx].data["results"]["p"]
                    if np.shape(p)[0] == 6:
                        p_0[0] = p[0] + p[2] * diff[0] + p[3] * diff[1]
                        p_0[1] = p[1] + p[4] * diff[0] + p[5] * diff[1]
                    elif np.shape(p)[0] == 12:
                        p_0[0] = (
                            p[0]
                            + p[2] * diff[0]
                            + p[4] * diff[1]
                            + 0.5 * p[6] * diff[0] ** 2
                            + p[8] * diff[0] * diff[1]
                            + 0.5 * p[10] * diff[1] ** 2
                        )
                        p_0[1] = (
                            p[1]
                            + p[3] * diff[0]
                            + p[5] * diff[1]
                            + 0.5 * p[7] * diff[0] ** 2
                            + p[9] * diff[0] * diff[1]
                            + 0.5 * p[11] * diff[1] ** 2
                        )
                        p_0[2] = p[2] + p[6] * diff[0] + p[8] * diff[1]
                        p_0[3] = p[3] + p[7] * diff[0] + p[9] * diff[1]
                        p_0[4] = p[4] + p[8] * diff[1] + p[10] * diff[1]
                        p_0[5] = p[5] + p[9] * diff[1] + p[11] * diff[1]
                    self._subsets[idx].solve(
                        max_norm=self._max_norm,
                        max_iterations=self._max_iterations,
                        warp_0=p_0,
                        method=self._method,
                        tolerance=self._tolerance,
                        order=self._subset_order,
                    )
                    if self._subsets[idx].data["solved"]:  # Check.
                        self._store_variables(idx)
                    else:
                        # Finally, try the NCC initial guess.
                        self._subsets[idx].solve(
                            max_norm=self._max_norm,
                            max_iterations=self._max_iterations,
                            warp_0=np.zeros(np.shape(p_0)),
                            method=self._method,
                            tolerance=self._tolerance,
                            order=self._subset_order,
                        )
                        if self._subsets[idx].data["solved"]:
                            self._store_variables(idx)
                        else:
                            self._C_ZNCC[idx] = np.max(
                                (self._subsets[idx].data["results"]["C_ZNCC"], 0)
                            )
        return True

    def _store_variables(self, idx, seed=False):
        """

        Private method to store variables.

        Parameters
        ----------
        idx : int
            Index of current subset.
        seed : bool
            Boolean to indicate whether this is the seed subset.

        """
        if seed is True:
            self._subset_solved[idx] = -1
        elif self._subsets[idx].data["solved"] is False:
            self._subset_solved[idx] = 0
        else:
            self._subset_solved[idx] = 1
        self._C_ZNCC[idx] = np.max(
            (self._subsets[idx].data["results"]["C_ZNCC"], 0)
        )  # Clip correlation coefficient to positive values.
        self._p[idx] = self._subsets[idx].data["results"]["p"].flatten()
        self._displacements[idx, 0] = self._subsets[idx].data["results"]["u"]
        self._displacements[idx, 1] = self._subsets[idx].data["results"]["v"]


class MeshResults(MeshBase):
    """

    MeshResults class for geopyv.

    Parameters
    ----------
    data : dict
        geopyv data dict from Mesh object.


    Attributes
    ----------
    data : dict
        geopyv data dict from Mesh object.

    """

    def __init__(self, data):
        """Initialisation of geopyv MeshResults class."""
        self.data = data
