"""

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
from alive_progress import alive_bar

# import matplotlib.pyplot as plt
# import cv2
import traceback

# import matplotlib.pyplot as plt
# import cv2

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",
        inter_order=1,
        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",
            "size",
            "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,
            inter_order=inter_order,
            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 _element_area(self, space="I"):
        """
        Private method to calculate the element areas based on corner nodes.

        """
        M = np.ones((len(self._elements), 3, 3))
        if space == "I":
            M[:, 1] = self._nodes[self._elements[:, :3]][:, :, 0]
            M[:, 2] = self._nodes[self._elements[:, :3]][:, :, 1]
            self._areas = 0.5 * np.linalg.det(M)
        else:
            M[:, 1] = self._Nodes[self._elements[:, :3]][:, :, 0]
            M[:, 2] = self._Nodes[self._elements[:, :3]][:, :, 1]
            self._Areas = 0.5 * np.linalg.det(M)

    def _local_coordinates(self, space="I"):
        """
        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.

        Return
        ------
        A : `numpy.ndarray` (N,3,4)
            Element centroid local coordinates.
        """

        A = np.ones((np.shape(self._elements)[0], 3, 4))
        if space == "I":
            A[:, 1:, 0] = np.mean(self._nodes[self._elements], axis=1)
            A[:, 1:, 1:] = self._nodes[self._elements][:, :3, :2].transpose(0, 2, 1)
        elif space == "O":
            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):
        """Private method for defining the shape functions at the
        element centroid.

        Return
        ------
        N : `numpy.ndarray`
            Shape function.
        dN : `numpy.ndarray`
            Shape function 1st order derivative.
        d2N : `numpy.ndarray`
            Shape function 2nd order derivative.
        """

        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, space="I"):
        """

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

        """

        # Setup.
        warps = np.zeros((np.shape(self._elements)[0], 12))
        if space == "I":
            x = self._nodes[self._elements]
            u = self._displacements[self._elements]
        else:
            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.
        warps[:, :2] = N @ u

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

        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

            warps[:, 6:] = (K_x_inv @ K_u).reshape(np.shape(self._elements)[0], -1)
        if space == "I":
            self._warps = warps
        else:
            self._Warps = warps

    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_obj : `geopyv.geometry.region.Circle` or `geopyv.geometry.region.Path`
        geopyv.geometry.region.Region class defining the boundary geometry and
        behaviours.
    _exclusion_objs : list
        List of `geopyv.geometry.region.Region` class objects defining the
        exclusion geometry and behaviours.
    _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_obj=None,
        exclusion_objs=[],
        size_lower_bound=1.0,
        size_upper_bound=1000.0,
        mesh_order=2,
        hp=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_obj : geopyv.geometry.region.Region
            Boundary of geopyv.geometry.region.Region class, instantiated
            by :mod: `~geopyv.geometry.region.Region` to define the boundary
            of the region of interest (RoI).
        exclusion_objs : list, optional
            List of geopyv.geometry.region.Region class objects, instantiated
            by :mod: `~geopyv.geometry.region.Region` to define exclusion regions
            within the region of interest (RoI).
        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.

        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_obj,
                "boundary_obj",
                [gp.geometry.region.Circle, gp.geometry.region.Path],
            ),
            "TypeError",
        )
        check = gp.check._check_type(exclusion_objs, "exclusion_objs", [list])
        if check:
            try:
                exclusion_objs = list(exclusion_objs)
                self._report(
                    gp.check._conversion(exclusion_objs, "exclusion_objs", list, False),
                    "Warning",
                )
            except Exception:
                self._report(check, "TypeError")
        for exclusion_obj in exclusion_objs:
            self._report(
                gp.check._check_type(
                    exclusion_obj,
                    "exclusion_obj",
                    [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",
        )
        check = gp.check._check_type(mesh_order, "mesh_order", [int])
        if check:
            try:
                mesh_order = int(mesh_order)
                self._report(
                    gp.check._conversion(mesh_order, "mesh_order", int), "Warning"
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(
            gp.check._check_value(mesh_order, "mesh_order", [1, 2]), "ValueError"
        )
        self._report(
            gp.check._check_type(hp, "hp", [bool]),
            "TypeError",
        )

        # Store.
        self._initialised = False
        self._f_img = f_img
        self._g_img = g_img
        self._target_nodes = target_nodes
        self._boundary_obj = boundary_obj
        if self._boundary_obj._option == "R":
            self._boundary_obj._option = "F"
        self._exclusion_objs = exclusion_objs
        self._size_lower_bound = size_lower_bound
        self._size_upper_bound = size_upper_bound
        self._mesh_order = mesh_order
        self._hard_boundary = boundary_obj._hard
        self._hp = hp
        self.solved = False
        self._unsolvable = False
        self._calibrated = False

        # Checks.
        self._target_checks()  # Size bounds and target nodes.
        if self._hp is True:
            self._update_region()  # Boundary and exclusion objects.

        # Creating the initial mesh.
        # Define region of interest.
        (
            self._borders,
            self._segments,
            self._curves,
            self._mask,
        ) = gp.geometry.meshing._define_RoI(
            f_img=self._f_img,
            boundary_obj=self._boundary_obj,
            exclusion_objs=self._exclusion_objs,
        )
        # 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.
        log.info(
            "Generating mesh using gmsh with approximately {n} nodes.".format(
                n=self._target_nodes
            )
        )
        self._initial_mesh()
        self._update_mesh()
        log.info(
            "Mesh generated with {n} nodes and {e} elements.".format(
                n=len(self._nodes), e=len(self._elements)
            )
        )
        gmsh.finalize()

        # Data.
        self.data = {
            "type": "Mesh",
            "solved": self.solved,
            "unsolvable": self._unsolvable,
            "calibrated": self._calibrated,
            "images": {
                "f_img": self._f_img.filepath,
                "g_img": self._g_img.filepath,
            },
            "target_nodes": self._target_nodes,
            "mesh_order": self._mesh_order,
            "boundary_obj": self._boundary_obj,
            "exclusion_objs": self._exclusion_objs,
            "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 _target_checks(self):
        """Private method to limit the mesh size upper bound by the boundary geometry
        and the target nodes by the number included in the boundary and exclusion
        geometries."""

        self._size_upper_bound = min(
            self._size_upper_bound,
            np.max(
                np.sqrt(
                    np.sum(
                        np.square(np.diff(self._boundary_obj._nodes, axis=0)), axis=1
                    )
                )
            ),
        )
        minimum_nodes = np.shape(self._boundary_obj._nodes)[0]
        for exclusion_obj in self._exclusion_objs:
            minimum_nodes += np.shape(exclusion_obj._nodes)[0]
        self._target_nodes = max(self._target_nodes, minimum_nodes)

    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

        # Checks.
        self._target_checks()  # Size bounds and 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()
        self._update_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,
        *,
        template=None,
        seed_coord=None,
        seed_warp=None,
        max_norm=1e-5,
        max_iterations=50,
        subset_order=1,
        tolerance=0.75,
        seed_tolerance=0.9,
        method="ICGN",
        adaptive_iterations=0,
        correction=True,
        alpha=0.5,
        history=None,
        subset_size_limits=None,
        override=False,
    ):
        r"""

        Method to solve for the mesh.

        Parameters
        ----------
        template : `geopyv.templates.Template`
            Subset template object.
        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.
        seed_warp : `numpy.ndarray` (6*subset_order,)
            Deformation preconditioning vector for the seed subset.
        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.
        correction : bool, optional
            Boolean indicator as to whether to correct poor
            correlation subsets.
            Defaults to a value of True.
        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)
        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)
        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(correction, "correction", [bool])
        if check:
            try:
                correction = bool(correction)
                self._report(
                    gp.check._conversion(correction, "correction", int),
                    "Warning",
                )
            except Exception:
                self._report(check, "TypeError")
        self._report(
            gp.check._check_range(correction, "correction", 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")

        # 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._correction = correction
        self._method = method
        self._subset_order = subset_order
        self._tolerance = tolerance
        self._seed_tolerance = seed_tolerance
        self._alpha = alpha
        self._update = False
        self._seed_warp = seed_warp
        self._history = history
        self._override = override
        self._status = 0
        if subset_size_limits is not None:
            self._ssa = True
            self._lbss = subset_size_limits[0]
            self._ubss = subset_size_limits[1]
        else:
            self._ssa = False

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

        # Create initial mesh.
        self._initial_mesh()
        self._update_mesh()

        # Solve initial mesh.
        self._message = "Solving initial mesh"
        self._find_seed_node()
        try:
            self._reliability_guided(init=True)
            if self._unsolvable and not (self._status == 3 and self._override):
                self._check_status()
                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 and not (self._status == 3 and self._override):
                    self._check_status()
                    return self.solved
            self._store_region()
            if self._unsolvable and not (self._status == 3 and self._override):
                self._check_status()
                return self.solved
            if self._status == 0:
                self._status = 1
            self.solved = True
            # 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,
            }
            # 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,
                "seed_tolerance": self._seed_tolerance,
                "correction": self._correction,
                "alpha": self._alpha,
                "override": self._override,
            }
            # Pack data.
            self.data.update(
                {
                    "nodes": self._nodes,
                    "elements": self._elements,
                    "areas": self._areas,
                    "solved": self.solved,
                    "unsolvable": self._unsolvable,
                    "centroids": np.mean(self._nodes[self._elements], axis=-2),
                    "settings": self._settings,
                    "results": self._results,
                }
            )
            del self._history
        except Exception:
            self._status = 7
            self._unsolvable = True
            self._check_status()
            print(traceback.format_exc())
        gmsh.finalize()
        return self.solved

    def _check_status(self):
        """Private method for issue catergorisation and error message delivery."""
        if self._status == 0:  # Solvable unsolved.
            return
        elif self._status == 1:  # Solvable 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),
                )
            )
        elif self._status == 2:  # Unsolvable: seed decorrelation.
            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,
                )
            )
        elif self._status == 3:  # Unsolvable: subset decorrelation.
            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,
                )
            )
        elif self._status == 4:  # Unsolvable: rigid exclusions decorrelation.s
            log.error("Rigid exclusion tracking failure.")
        elif self._status == 5:  # Unsolvable: unsolvable subset.
            log.error("Unsolvable subset.")
        elif self._status == 6:  # Unsolvable: subset compatability failure.
            log.error("Local mesh compatibility failure.")
        elif self._status == 7:  # Unsolvable: unkown issue.
            log.error("Could not solve mesh. Unrecognised problem.")
            input()
        self._update = True

    def _store_region(self):
        """Private method to record the deformation of the boundary and exclusions."""
        exclusion_obj_warp = []
        for i in range(np.shape(self._exclusion_objs)[0]):
            if self._exclusion_objs[i]._option == "R":
                subset = gp.subset.Subset(
                    f_img=self._f_img,
                    g_img=self._g_img,
                    f_coord=self._exclusion_objs[i]._centre,
                    template=gp.templates.Circle(
                        int(self._exclusion_objs[i]._specifics["radius"])
                    ),
                )
                warp_0 = np.zeros(6 * self._subset_order)
                warp_0[:2] = np.mean(self._displacements[self._exclusions[i]], axis=0)
                subset.solve(tolerance=0.9, warp_0=warp_0, order=self._subset_order)
                if subset.data["solved"] is not True:
                    self._status = 4
                    del subset
                    return
                else:
                    exclusion_obj_warp.append(subset._p.flatten())
                    del subset
            elif self._exclusion_objs[i]._option == "F":
                exclusion_obj_warp.append(self._displacements[self._exclusions[i]])
            else:
                exclusion_obj_warp.append(None)
        for i in range(np.shape(self._exclusion_objs)[0]):
            self._exclusion_objs[i]._store(exclusion_obj_warp[i])
        self._boundary_obj._store(self._displacements[self._boundary])

    def _update_region(self):
        """

        Private method to trigger region objects (boundary and exclusions) to update.

        """

        self._boundary_obj._update(self._f_img.filepath)
        for exclusion_obj in self._exclusion_objs:
            exclusion_obj._update(self._f_img.filepath)

    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.
        self._boundary = gmsh.model.occ.getCurveLoops(0)[1][0]
        self._boundary_edges = list(
            gmsh.model.mesh.getNodesForPhysicalGroup(1, 0)[0] - 1
        )
        self._exclusions = []
        self._exclusion_edges = []
        for i in range(len(self._exclusion_objs)):
            self._exclusions.append(np.sort(gmsh.model.occ.getCurveLoops(0)[1][i + 1]))
            self._exclusion_edges.append(
                list(gmsh.model.mesh.getNodesForPhysicalGroup(1, i + 1)[0] - 1)
            )

    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 _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,
            bounds=(self._size_lower_bound, self._size_upper_bound),
            method="bounded",
        )

    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]) * abs(
                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 = order * 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 _seed_solve(self):
        """

        Private method for the solving of the seed node.

        """

        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.
        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"]:  # If seed unsolved.
                self._unsolvable = True
                self._status = 2
                return
        # Store if solved.
        self._store_variables(idx=self._seed_node, flag=-1)

    def _output_preparation(self):
        # Set up.
        m = np.shape(self._nodes)[0]
        n = np.shape(self._seed_warp)[0]
        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.

    def _reliability_guided(self, init=False):
        """

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

        """

        self._output_preparation()

        # Subset instantiation with template masking.
        self._subset_instantiation(init)

        # Solve subsets in mesh.
        with alive_bar(
            np.shape(self._nodes)[0],
            dual_line=True,
            bar="blocks",
            title=self._message,
        ) as self._bar:
            # Solve for seed.
            self._bar.text = "-> Solving seed subset..."
            self._seed_solve()
            if self._unsolvable and not (self._status == 3 and self._override):
                return
            self._bar()

            # Solve for seed neighbours.
            self._neighbours(
                self._seed_node, self._subsets[self._seed_node].data["results"]["p"]
            )
            if self._unsolvable and not (self._status == 3 and self._override):
                return
            # Solve through sorted queue.
            self._bar.text = (
                "-> Solving remaining subsets using reliability guided approach..."
            )
            while np.max(self._subset_solved) > -1:
                # Highest correlation subset selected.
                cur_idx = np.argmax(self._subset_solved * self._C_ZNCC)
                self._subset_solved[cur_idx] = -1  # Set as solved.
                p_0 = self._subsets[cur_idx].data["results"]["p"]  # Precondition.
                self._neighbours(cur_idx, p_0)  # Solve for neighbours.
                self._bar()
                if self._bar.current == np.shape(self._nodes)[0]:
                    break
                if self._unsolvable and not (self._status == 3 and self._override):
                    return
        # Corrections, calculations and checks.
        if self._correction:
            self._corrections()
        self._element_area()
        self._element_strains()
        self._compatibility()
        if self._unsolvable and not (self._status == 3 and self._override):
            return
        if any(self._subset_solved != -1):
            self._unsolvable = True
            self._status = 5
        elif any(self._C_ZNCC < self._tolerance):
            self._unsolvable = True
            self._status = 3

    def _resize(self, i):
        adj_nodes_idxs = self._connectivity(i)
        dist = np.sqrt(np.sum((self._nodes[i] - self._nodes[adj_nodes_idxs]) ** 2)) / 2
        dist = np.clip(dist, self._lbss, self._ubss)
        return int(dist)

    def _subset_instantiation(self, init):
        """Private method that instantiates the mesh subsets with masking."""

        if self._ssa is True and init is False:
            sizes = []
            for i in range(np.shape(self._nodes)[0]):
                size = self._resize(i)
                sizes.append(size)
                if self._template.shape == "circle":
                    template = gp.templates.Circle(radius=size)
                elif self._template.shape == "square":
                    template = gp.templates.Square(length=size)
                _template_npx = template.n_px
                template.mask(self._nodes[i], self._mask)
                check = False
                for e in range(len(self._exclusions)):
                    if (i in self._exclusion_edges[e]) and self._exclusion_objs[
                        e
                    ]._compensate:
                        check = True
                if (
                    (i in self._boundary_edges) and self._boundary_obj._compensate
                ) or check is True:
                    if template.n_px < _template_npx:
                        size = int(size / np.sqrt(template.n_px / _template_npx))
                        if self._template.shape == "circle":
                            template = gp.templates.Circle(radius=size)
                        elif self._template.shape == "square":
                            template = gp.templates.Square(length=size)
                        template.mask(self._nodes[i], self._mask)
                self._subsets[i] = gp.subset.Subset(
                    f_coord=self._nodes[i],
                    f_img=self._f_img,
                    g_img=self._g_img,
                    template=template,
                )  # Create masked subset.
        else:
            for i in range(np.shape(self._nodes)[0]):
                template = deepcopy(self._template)
                template.mask(self._nodes[i], self._mask)
                check = False
                for e in range(len(self._exclusions)):
                    if (i in self._exclusion_edges[e]) and self._exclusion_objs[
                        e
                    ]._compensate:
                        check = True
                if (
                    (i in self._boundary_edges) and self._boundary_obj._compensate
                ) or check is True:
                    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(self._nodes[i], self._mask)
                self._subsets[i] = gp.subset.Subset(
                    f_coord=self._nodes[i],
                    f_img=self._f_img,
                    g_img=self._g_img,
                    template=template,
                )  # Create masked subset.

    def _compatibility(self):
        """

        Private method to ensure compatibility (that the displacement of a subset
        nowhere exceeds the displacement bounds of neighbouring subsets).

        """
        for i in range(len(self._elements)):
            if self._warps[i, 2] < -1 or self._warps[i, 5] < -1:
                # n1 = self._nodes[self._elements[i]]
                # n2 = (
                #     self._nodes[self._elements[i]]
                #     + self._displacements[self._elements[i]]
                # )
                # fig,ax = plt.subplots()
                # image = cv2.imread(
                #     self.data["images"]["f_img"], cv2.IMREAD_COLOR
                # )
                # image_gs = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
                # image_gs = cv2.GaussianBlur(
                #     image_gs, ksize=(5, 5), sigmaX=1.1, sigmaY=1.1
                # )
                # plt.imshow(image_gs, cmap="gray")
                # ax.plot(
                #     n1[[0,1,2,0],0],n1[[0,1,2,0],1], color = "purple"
                # )
                # ax.plot(
                #     n2[[0,1,2,0],0],n2[[0,1,2,0],1],color = "orange"
                # )
                # ax.scatter(n1[0,0],n1[0,1],color = "r")
                # ax.scatter(n2[0,0],n2[0,1],color = "r")
                # ax.scatter(n1[1,0],n1[1,1],color = "b")
                # ax.scatter(n2[1,0],n2[1,1],color = "b")
                # ax.scatter(n1[2,0],n1[2,1],color = "g")
                # ax.scatter(n2[2,0],n2[2,1],color = "g")
                # plt.show()
                self._unsolvable = True
                self._status = 6
                break

            if self._mesh_order == 2:
                if gp.geometry.utilities.polysect(
                    self._nodes[self._elements[i]][[0, 3, 1, 4, 2, 5]]
                ):
                    # n1 = self._nodes[self._elements[i]]
                    # n2 = (
                    #     self._nodes[self._elements[i]]
                    #     + self._displacements[self._elements[i]]
                    # )
                    # fig,ax = plt.subplots()
                    # image = cv2.imread(
                    #     self.data["images"]["f_img"], cv2.IMREAD_COLOR
                    # )
                    # image_gs = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
                    # image_gs = cv2.GaussianBlur(
                    #     image_gs, ksize=(5, 5), sigmaX=1.1, sigmaY=1.1
                    # )
                    # plt.imshow(image_gs, cmap="gray")
                    # ax.plot(
                    #     n1[[0,3,1,4,2,5,0],0],n1[[0,3,1,4,2,5,0],1], color = "purple"
                    # )
                    # ax.plot(
                    #     n2[[0,3,1,4,2,5,0],0],n2[[0,3,1,4,2,5,0],1],color = "orange"
                    # )
                    # ax.scatter(n1[0,0],n1[0,1],color = "r")
                    # ax.scatter(n2[0,0],n2[0,1],color = "r")
                    # ax.scatter(n1[1,0],n1[1,1],color = "b")
                    # ax.scatter(n2[1,0],n2[1,1],color = "b")
                    # ax.scatter(n1[2,0],n1[2,1],color = "g")
                    # ax.scatter(n2[2,0],n2[2,1],color = "g")
                    # ax.scatter(n1[3,0],n1[3,1],color = "c")
                    # ax.scatter(n2[3,0],n2[3,1],color = "c")
                    # ax.scatter(n1[4,0],n1[4,1],color = "m")
                    # ax.scatter(n2[4,0],n2[4,1],color = "m")
                    # ax.scatter(n1[5,0],n1[5,1],color = "k")
                    # ax.scatter(n2[5,0],n2[5,1],color = "k")
                    # plt.show()
                    self._unsolvable = True
                    self._status = 6
                    break

    def _corrections(self):
        """

        Private method to improve subset correlation through neighbour
        preconditioning.

        """

        arg = np.argsort(self._C_ZNCC)
        corarg = arg[self._C_ZNCC[arg] < self._seed_tolerance]
        j = 0
        k = 0
        benefit = np.zeros(len(self._C_ZNCC))
        if np.shape(corarg)[0] >= 1:
            with alive_bar(
                np.shape(corarg)[0],
                dual_line=True,
                bar="blocks",
                title="Corrections",
            ) as bar:
                for idx in corarg:
                    k += 1
                    subset = gp.subset.Subset(
                        f_coord=self._nodes[idx],
                        f_img=self._f_img,
                        g_img=self._g_img,
                        template=self._subsets[idx]._template,
                    )
                    neighbours = self._connectivity(idx)
                    warp = np.mean(
                        self._p[neighbours[self._C_ZNCC[neighbours] > self._tolerance]],
                        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
                            benefit[idx] = subset._C_ZNCC - self._subsets[idx]._C_ZNCC
                            self._subsets[idx] = subset
                            self._store_variables(idx=idx, flag=-1)
                    except Exception:
                        pass
                    bar()
                    bar.text = "{} corrections accepted".format(round(j / k, 3))
            log.info(
                (
                    "{ca} corrections accepted;"
                    "{max} maximum improvement;"
                    "{mean} mean improvement"
                ).format(
                    ca=round(j / k, 3),
                    max=round(np.max(benefit), 3),
                    mean=round(np.mean(benefit), 3),
                )
            )

    def _connectivity(self, idx, full=False):
        """

        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.
        full : bool, optional
            False - return immediate nodes to the index.
            True - return all nodes sharing an element with the index.


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

        """

        if self._mesh_order == 1 or full is True:
            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
        ----------
        cur_idx : int
            The current susbet index.
        p_0 : `numpy.ndarray` (N)
            Preconditioning warp function.


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

        """

        # Identify neighbours.
        neighbours = self._connectivity(cur_idx)

        # Iterate through list of neighbours.
        for idx in neighbours:
            if self._subset_solved[idx] == 0:  # If not previously solved.
                # if self._history:
                #     # 0. Use history pre-conditioning
                #     particle = gp.particle.Particle(
                #             series = self._history,
                #             coordinate_0 = self._nodes[idx]
                #         )
                #     try:
                #         particle.solve()
                #         p_h = particle._warps[-1]
                #         self._subsets[idx].solve(
                #             max_norm=self._max_norm,
                #             max_iterations=self._max_iterations,
                #             order=self._subset_order,
                #             warp_0=p_h,
                #             method=self._method,
                #             tolerance=self._tolerance,
                #         )
                #     except Exception:
                #         pass
                #     del particle
                # if self._subsets[idx].data["solved"]:  # Check against tolerance.
                #    self._store_variables(idx=idx, flag=1)
                # elif self._subsets[idx].data["unsolvable"]:
                #    self._status = 5
                #    self._unsolvable = True
                # else:
                # 1. Use nearest-neighbour 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=idx, flag=1)
                elif self._subsets[idx].data["unsolvable"]:
                    # print(idx)
                    # fig,ax=plt.subplots()
                    # ax.imshow(self._subsets[idx]._template._local_masked)
                    # plt.show()
                    # self._subsets[idx].inspect()
                    # self._subsets[idx].convergence()
                    self._status = 5
                    self._unsolvable = True
                else:
                    # 2. 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 against tolerance.
                        self._store_variables(idx=idx, flag=1)
                    elif self._subsets[idx].data["unsolvable"]:
                        # print(idx)
                        # fig,ax=plt.subplots()
                        # ax.imshow(self._subsets[idx]._template._local_masked)
                        # plt.show()
                        # self._subsets[idx].inspect()
                        # self._subsets[idx].convergence()
                        self._status = 5
                        self._unsolvable = True
                    else:
                        # 3. Use 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["unsolvable"] or np.isnan(
                            self._subsets[idx].data["results"]["C_ZNCC"]
                        ):
                            # print(idx)
                            # fig,ax=plt.subplots()
                            # ax.imshow(self._subsets[idx]._template._local_masked)
                            # plt.show()
                            # self._subsets[idx].inspect()
                            # self._subsets[idx].convergence()
                            self._status = 5
                            self._unsolvable = True
                        else:
                            self._store_variables(idx=idx, flag=1)

    def _store_variables(self, idx, flag):
        """

        Private method to store variables.

        Parameters
        ----------
        idx : int
            Index of current subset.
        flag : int
            -1 : solved and stored
            0  : unsolved
            1  : solved and unstored

        """

        self._subset_solved[idx] = flag
        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

        self._nodes = self.data["nodes"]
        self._elements = self.data["elements"]
        self._areas = self.data["areas"]
        self._mesh_order = self.data["mesh_order"]
        self._warps = self.data["results"]["warps"]
        self._displacements = self.data["results"]["displacements"]
