# encoding: utf-8
#
# Copyright (C) 2017-2024, P. R. Wiecha
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#
"""
Collection of structure generators and geometry related tools

"""
from __future__ import print_function
from __future__ import absolute_import

import warnings

import numpy as np
import copy

from pyGDM2.core import simulation
from pyGDM2 import tools
from pyGDM2 import materials


# =============================================================================
# Helper
# =============================================================================
## set ref. index values or material class of environment layers
def _n_env123_to_materials(n1, n2, n3=None):
    """convert n1,n2,n3 to proper materials-class instances"""
    if isinstance(n1, (int, float, complex)) and not isinstance(n1, bool):
        n1_material = materials.dummy(n1)
    else:
        n1_material = n1

    if isinstance(n2, (int, float, complex)) and not isinstance(n2, bool):
        n2_material = materials.dummy(n2)
    else:
        n2_material = n2

    n3 = n3 or n2  # if None, use `n2`
    if isinstance(n3, (int, float, complex)) and not isinstance(n3, bool):
        n3_material = materials.dummy(n3)
    else:
        n3_material = n3

    return n1_material, n2_material, n3_material


# ==============================================================================
# Structure container class
# ==============================================================================
class struct(object):
    """structure container

    Defines a nanostructure to perform a GDM simulation on. This includes the
    geometry and the material-specific dielectric function(s) for each meshpoint

    Note: This class works only with the new pure-python interface!


    Parameters
    ----------
    step : float
        stepsize used for discretization

    geometry : list of tuples
        list of coordinates of the meshpoints of the geometry.
        Several structure generators are provided in the module `structures`

    material : instance of `materials` class or list of such
        use material classes provided by the `materials` module.
        If a single instance is provided, the according material will be
        used for the entire structure

    normalization : str, default: 'auto'
        set mesh-type. Either 'auto' (default), 'cube', or 'hex', or alternatively
        the numeric mesh normalization factor (float).

    auto_shift_structure : bool, default: False
        whether to shift the structure to positive z values if structure or
        parts of it are below 0

    check_geometry_consistency : bool, default: True
        whether to test geometry for consistency. If yes, inconsistent
        meshpoints will be removed and a warning will be produced

    warn: bool, default: True
        show warnings if detection fails

    verbose : int, default: 1
        0: print nothing, 1: print some runtime info


    """

    def __init__(
        self,
        step,
        geometry,
        material,
        n1=None,
        n2=None,
        normalization="auto",
        n3=None,
        spacing=5000.0,
        auto_shift_structure=False,
        check_geometry_consistency=True,
        warn=True,
        verbose=True,
    ):
        """Initialization"""
        ## set precision to single by default
        self.setDtype(np.float32, np.complex64)

        ## --- mesh normalization
        self.step = step
        if type(normalization) == str:
            if normalization.lower() == "auto":
                normalization = tools.get_mesh_from_geometry(geometry, warn=warn)
                if verbose:
                    print(
                        "structure initialization - automatic mesh detection: {}".format(
                            normalization
                        )
                    )

            self.normalization = self.dtypef(get_normalization(normalization))
        elif type(normalization) in (int, float, np.float32, np.float64):
            self.normalization = self.dtypef(normalization)
        else:
            raise Exception(
                "Unknown datatype for `normalization`. "
                + "It is recommended to provide a str to set the mesh-type."
            )

        if self.normalization in [1, 1.0]:
            self.meshtype = "cube"
        else:
            self.meshtype = "hex"

        ## --- legacy compatibility: environment definition
        if n1 is not None and n2 is not None:
            self.n1_material, self.n2_material, self.n3_material = (
                _n_env123_to_materials(n1, n2, n3)
            )
            self.spacing = spacing
            if warn:
                warnings.warn(
                    "Deprecation: environment should be defined thorugh the `dyads` interface. "
                    + "Using the `struct` class is not recommended and will raise an error in future versions."
                )

        ## --- geometry
        if type(geometry) == list:
            geometry = np.array(geometry, dtype=self.dtypef)

        ## structure consistenct check:
        if check_geometry_consistency:
            self.geometry, _wrong_dipoles, index_correct = tools.test_geometry(
                geometry, step, return_index=True, plotting=False
            )
            if len(_wrong_dipoles) != 0:
                if warn:
                    warnings.warn(
                        "using only consistent part of structure. "
                        + "Please verify the geometry."
                    )

            if verbose:
                print(
                    "structure initialization - consistency check: {}/{} dipoles valid".format(
                        len(self.geometry), len(geometry)
                    )
                )
        else:
            self.geometry = geometry
            index_correct = range(len(self.geometry))

        self.n_dipoles = len(self.geometry)

        if len(self.geometry) > 0:
            ## --- optionally elevate to half-space Z>=0
            if auto_shift_structure:
                if self.geometry.T[2].min() - self.step / 2.0 < 0:
                    warnings.warn(
                        "Minimum structure Z-value lies below substrate level!"
                        + " Shifting structure bottom to Z=step/2."
                    )
                    self.geometry.T[2] += self.geometry.T[2].min() + self.step / 2.0

            ## --- define material at each meshpoint
            if isinstance(material, (list, tuple, np.ndarray)):
                if len(material) != len(geometry):  # cf wirh original geometry here!
                    raise ValueError(
                        "Error in structure initialization: "
                        + "Number of material definitions must "
                        + "equal number of dipoles."
                    )
                self.material = np.array(material)[index_correct]  # maintain order
            else:
                self.material = [material for i in range(len(self.geometry))]

        else:
            if warn:
                warnings.warn("Emtpy structure geometry.")
            self.material = material

        ## --- center of gravity
        self.r0 = np.average(self.geometry, axis=0)

    def __repr__(self, verbose=False):
        out_str = ""
        out_str += " ------ nano-object -------"
        if len(set(self.material)) <= 1:
            out_str += "\n" + "   Homogeneous object. "
            out_str += "\n" + '   material:             "{}"'.format(
                self.material[0].__name__
            )
        if len(set(self.material)) > 1:
            out_str += (
                "\n"
                + "   Inhomogeneous object, consisting of {} materials".format(
                    len(set(self.material))
                )
            )
            if (
                verbose or len(set(self.material)) < 6
            ):  # print details for up to 5 sub-constituents
                diff_mat = np.unique([s.__name__ for s in self.material])
                for i, mat in enumerate(set(diff_mat)):
                    out_str += "\n" + '      - {}: "{}"'.format(i, mat)
        out_str += "\n" + "   mesh type:            {}".format(self.meshtype)
        out_str += "\n" + "   nominal stepsize:     {}nm".format(self.step)
        out_str += "\n" + "   nr. of meshpoints:    {}".format(self.n_dipoles)
        bnds = [
            self.geometry.T[0].min() - self.step / 2,
            self.geometry.T[0].max() + self.step / 2,
            self.geometry.T[1].min() - self.step / 2,
            self.geometry.T[1].max() + self.step / 2,
            self.geometry.T[2].min() - self.step / 2,
            self.geometry.T[2].max() + self.step / 2,
        ]
        if verbose:
            out_str += "\n" + "     X-extension    :    {:.1f} - {:.1f} (nm)".format(
                bnds[0], bnds[1]
            )
            out_str += "\n" + "     Y-extension    :    {:.1f} - {:.1f} (nm)".format(
                bnds[2], bnds[3]
            )
            out_str += "\n" + "     Z-extension    :    {:.1f} - {:.1f} (nm)".format(
                bnds[4], bnds[5]
            )
        else:
            out_str += (
                "\n"
                + "   bounding box:         [{:.1f},{:.1f}], [{:.1f},{:.1f}], [{:.1f},{:.1f}] (nm)".format(
                    *bnds
                )
            )
        return out_str

    def __add__(self, other):
        if type(other) in [struct]:
            ## add a structure: Try combining both
            return combine_geometries([self.copy(), other.copy()], step=self.step)
        elif len(other) == 3:
            ## add a 3-tuple: Try shift
            return shift(self.copy(), other)
        else:
            raise ValueError("Unknown addition.")

    def copy(self):
        import copy

        return copy.deepcopy(self)

    def setDtype(self, dtypef, dtypec):
        """set dtype of arrays"""
        self.dtypef = dtypef
        self.dtypec = dtypec

    def setWavelength(self, wavelength):
        """set wavelength and evaluate environment / structure properties"""
        self.wavelength = wavelength
        self.getEpsilon(wavelength)

    def getEpsilon(self, wavelength):
        epsilon = []
        for mat in self.material:
            if hasattr(mat, "epsilonTensor"):
                epsilon.append(mat.epsilonTensor(wavelength))
            else:
                ## material with scalar permittivity: multiply by identity tensor
                epsilon.append(
                    mat.epsilon(wavelength) * np.identity(3, dtype=self.dtypec)
                )

        epsilon = np.array(epsilon, dtype=self.dtypec)
        self.epsilon_tensor = epsilon

    def getCenterOfMass(self):
        return np.mean(self.geometry, axis=0)


# ==============================================================================
# STRUCTURE GENERATORS
# ==============================================================================
def rect_wire(step, L, H, W, mesh="cube", ORIENTATION=1):
    """
    Generate rectangular wire with long axis along X on X/Y plane

    Parameters
    ----------
    step : float
        stepsize in nw
    L, H, W : int
        Length, Height and Width as Nr. of steps
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    def conditionRect(xi, yi, zi):
        return (
            -L / 2.0 < xi <= L / 2.0
            and -W / 2.0 < yi <= W / 2.0
            and 0 <= zi < H - 1 / np.sqrt(2)
        )

    if mesh == "cube":
        sp = _meshCubic(
            (-int(L), int(L)), (-int(W), int(W) + 1), (-1, int(H) + 1), conditionRect
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-(int(L)), int(L) + 1),
            (-int(W), int(W) + 1),
            (-1, 2 * int(H) + 1),
            conditionRect,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T
    sp *= step
    dipoles = sp.T

    return dipoles


def rect_dimer(step, L, H, W, G, mesh="cube", ORIENTATION=1):
    """
    Generate rectangular wire dimer with long axis along X on X/Y plane

    Using rectwire model for generation of the two dimer particles.

    Note : Using an even number gap will result in non-symmetric position with respect to (X=0,Y=0)


    Parameters
    ----------
    step : float
        stepsize in nw
    L, H, W : int
        Length, Height and Width as Nr. of steps
    G : int (zero or positive)
        Gap between dimers, in Nr. of steps
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    G = float(G)
    if G < 0:
        raise ValueError("Gap must be >= 0!")

    ## wire 1
    STRUCT0 = rect_wire(step, L=L, H=H, W=W, mesh=mesh, ORIENTATION=ORIENTATION).T
    ## wire 2
    STRUCT1 = rect_wire(step, L=L, H=H, W=W, mesh=mesh, ORIENTATION=ORIENTATION).T

    ## shift and concatenate wires
    STRUCT0[0] -= (L / 2.0 + G / 2.0) * step
    STRUCT0 = STRUCT0.T

    STRUCT1[0] += (L / 2.0 + G / 2.0) * step
    STRUCT1 = STRUCT1.T

    dipoles = np.concatenate([STRUCT0, STRUCT1])

    return dipoles


def prism(step, NSIDE, H, truncate_ratio=0.0, mesh="cube", ORIENTATION=1):
    """
    Generate prism on X/Y plane

    Parameters
    ----------
    step : float
        stepsize in nw
    NSIDE : int
        sidelength of regular prism (in nrs of dipoles)
    H : int
        height of prism (in nrs of dipoles)
    truncate_ratio : float
        truncate edges. give the ratio of total sidelength to remove.
        value between 0 [no truncation] and 1 [truncate at sidelength center]
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    NL = NSIDE / 2.0
    Hs = truncate_ratio * NL * np.sqrt(3) / 2.0
    Habs = (np.sqrt(3) / 2.0) * NL

    def conditionPrism(xi, yi, zi):
        inPrism = (
            (abs(xi) <= (Habs - (yi + 0.5)) / np.sqrt(3))
            and (
                abs(xi)
                <= (
                    ((3 * Habs - np.sqrt(3) * NL * truncate_ratio) + (yi + 0.5))
                    / np.sqrt(3)
                )
            )
            and (yi <= Habs - Hs)
            and (yi >= -Habs)
            and (0 <= zi < H - 1 / np.sqrt(2))
        )
        return inPrism

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(NL) - 10, int(NL) + 10),
            (-1 * int(np.ceil(NL)) - 10, int(np.ceil(NL)) + 10),
            (-1 * int(np.ceil(H)) - 10, int(np.ceil(H)) + 10),
            conditionPrism,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(NL) - 10, int(NL) + 10),
            (-1 * int(np.ceil(NL)) - 10, int(np.ceil(NL)) + 10),
            (-1 * int(np.ceil(H)) - 10, int(np.ceil(H)) + 10),
            conditionPrism,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift Z so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2
    dipoles = sp.T

    return dipoles


def rhombus(step, L, H, alpha=90.0, mesh="cube", ORIENTATION=1):
    """
    Generate planar rhombus structure on X/Y plane

    Parameters
    ----------
    step : float
        stepsize in nw

    L : int
        sidelength of regular prism (in nrs of dipoles)

    H : int
        height of prism (in nrs of dipoles)

    alpha : float, default: 90
        top/bottom angle of rhombus (in degrees)

    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'

    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    NL = L / 2.0
    alpha = np.pi * alpha / 180.0
    alpha05 = alpha / 2.0

    def conditionRhombus(xi, yi, zi):
        inRhombus = (
            (abs(xi) <= ((L * np.cos(alpha05) - abs(yi - 0.5)) * np.tan(alpha05)))
            and (abs(yi) <= (L * np.cos(alpha05) + 0.5))
            and (0 <= zi < H - 1 / np.sqrt(2))
        )
        return inRhombus

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(L) - 5, int(L) + 5),
            (-1 * int(L) - 5, int(L) + 5),
            (-1, int(H) + 1),
            conditionRhombus,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(L) - 1, int(L) + 1),
            (-1 * int(L) - 5, int(L) + 5),
            (-1, int(H) + 1),
            conditionRhombus,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift Z so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2
    dipoles = sp.T

    return dipoles


def hexagon(step, NSIDE, H, rotate=0.0, mesh="cube", ORIENTATION=1):
    """
    Generate regular hexagon on X/Y plane

    Parameters
    ----------
    step : float
        stepsize in nw
    NSIDE : int
        sidelength of regular hexagon (in nrs of dipoles)
    H : int
        height of hexagon (in nrs of dipoles)
    rotate : float, default: 0
        rotation angle (in degrees)
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    NL = NSIDE
    A = NSIDE
    if mesh == "hex":
        A -= 0.3

    def conditionHexagon(xi, yi, zi):
        Habs = (np.sqrt(3) / 2.0) * A

        inHex = (
            (abs(yi) - A / 2 < (Habs - (abs(xi) + 0.5)) / np.sqrt(3))
            and (abs(xi) < Habs)
            and (0 < zi < H - 1 / np.sqrt(2))
        )

        return inHex

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(NL) - 10, int(NL) + 10),
            (-1 * int(np.ceil(NL)) - 10, int(np.ceil(NL)) + 10),
            (-1 * int(np.ceil(H)) - 10, int(np.ceil(H)) + 10),
            conditionHexagon,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(NL) - 10, int(NL) + 10),
            (-1 * int(np.ceil(NL)) - 10, int(np.ceil(NL)) + 10),
            (-1 * int(np.ceil(H)) - 10, int(np.ceil(H)) + 10),
            conditionHexagon,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift Z so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2

    rotate *= np.pi / 180.0
    rotX = sp[0] * np.cos(rotate) - sp[1] * np.sin(rotate)
    rotY = sp[0] * np.sin(rotate) + sp[1] * np.cos(rotate)
    sp[0] = rotX
    sp[1] = rotY
    dipoles = sp.T

    return dipoles


def double_hexagon(
    step, NSIDE1, NSIDE2, DX, DY, H, rotate=0.0, mesh="cube", ORIENTATION=1
):
    """
    Generate regular hexagon on X/Y plane

    Parameters
    ----------
    step : float
        stepsize in nw
    NSIDE1 : int
        sidelength of hexagon1 (in nrs of dipoles)
    NSIDE2 : int
        sidelength of hexagon2 (in nrs of dipoles)
    DX : int
         X-offset of hexagon2 (in nrs of dipoles)
    DY : int
         Y-offset of hexagon2 (in nrs of dipoles)
    H : int
        height of hexagon (in nrs of dipoles)
    rotate : float, default: 0
        rotation angle (in degrees)
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    NL1 = NSIDE1
    A1 = NSIDE1
    Habs1 = (np.sqrt(3) / 2.0) * A1
    NL2 = NSIDE2
    A2 = NSIDE2
    Habs2 = (np.sqrt(3) / 2.0) * A2
    if mesh == "hex":
        A1 -= 0.3
        A2 -= 0.3

    def conditionHexagon(xi, yi, zi):

        inHex1 = (
            (
                (abs(yi) - A1 / 2.0 < (Habs1 - (abs(xi) + 1.0)) / np.sqrt(3) and yi > 0)
                or (
                    abs(yi) - A1 / 2.0 < (Habs1 - (abs(xi) - 1.0)) / np.sqrt(3)
                    and yi <= 0
                )
            )
            and (abs(xi) <= Habs1)
            and (0 <= zi < H - 1 / np.sqrt(2))
        )
        xi2 = xi - DX
        inHex2 = (
            (
                (
                    abs(yi) - A2 / 2.0 < (Habs2 - (abs(xi2) + 1.0)) / np.sqrt(3)
                    and yi > 0
                )
                or (
                    abs(yi) - A2 / 2.0 < (Habs2 - (abs(xi2) - 1.0)) / np.sqrt(3)
                    and yi <= 0
                )
            )
            and (Habs2 >= xi2 > -1 * (Habs2))
            and (0 <= zi < H - 1 / np.sqrt(2))
        )

        return inHex1 or inHex2

    NL = max([NL1, NL2])
    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(NL) + min([-2, DX - 5]), int(NL) + max([2, DX + 5])),
            (
                -1 * int(np.ceil(NL)) + min([-2, DY - 5]),
                int(np.ceil(NL)) + max([2, DY + 5]),
            ),
            (-1 * int(np.ceil(H)) - 2, int(np.ceil(H)) + 2),
            conditionHexagon,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(NL) + min([-2, DX - 5]), int(NL) + max([2, DX + 5])),
            (
                -1 * int(np.ceil(NL)) + min([-2, DY - 5]),
                int(np.ceil(NL)) + max([2, DY + 5]),
            ),
            (-1 * int(np.ceil(H)) - 2, int(np.ceil(H)) + 2),
            conditionHexagon,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift Z so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2

    rotate *= np.pi / 180.0
    rotX = sp[0] * np.cos(rotate) - sp[1] * np.sin(rotate)
    rotY = sp[0] * np.sin(rotate) + sp[1] * np.cos(rotate)
    sp[0] = rotX
    sp[1] = rotY
    dipoles = sp.T

    return dipoles


def diabolo(
    step,
    nside_upper,
    nside_lower,
    length,
    width,
    H,
    dir_prisms="in",
    mesh="cube",
    ORIENTATION=1,
):
    """generate diabolo structure on X/Y plane

    Parameters
    ----------
    step : float
        stepsize in nw

    nside_upper : int
        sidelength of upper regular prism (in nrs of dipoles)

    nside_lower : int
        sidelength of upper regular prism (in nrs of dipoles)

    length : int
        length of bridge between prisms (in nrs of dipoles)

    width : int
        width of bridge between prisms (in nrs of dipoles)

    H : int
        height of prism (in nrs of dipoles)

    dir_prisms : str, default="in"
        "in" or "out". Direction of prism tips, inwards or outwards.

    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'

    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    nu = nside_upper / 2.0
    nl = nside_lower / 2.0
    L = length
    W = width

    def conditionPrism(xi, yi, zi):
        Habs1 = (np.sqrt(3) / 2.0) * nu
        Habs2 = (np.sqrt(3) / 2.0) * nl

        if dir_prisms.lower() == "in":
            in_up_Prism = (
                (abs(xi) <= (Habs1 + (yi - L / 2 + 0.5)) / np.sqrt(3))
                and (abs(yi - L / 2) <= Habs1)
                and (0 <= zi < H - 1 / np.sqrt(2))
            )

            in_low_Prism = (
                (abs(-xi) <= (Habs2 + (-yi - L / 2 + 0.5)) / np.sqrt(3))
                and (abs(-yi - L / 2) <= Habs2)
                and (0 <= zi < H - 1 / np.sqrt(2))
            )
        else:
            in_up_Prism = (
                (abs(xi) <= (Habs1 - (yi - L / 2 + 0.5)) / np.sqrt(3))
                and (abs(yi - L / 2) <= Habs1)
                and (0 <= zi < H - 1 / np.sqrt(2))
            )

            in_low_Prism = (
                (abs(xi) <= (Habs2 - (-yi - L / 2 + 0.5)) / np.sqrt(3))
                and (abs(-yi - L / 2) <= Habs2)
                and (0 <= zi < H - 1 / np.sqrt(2))
            )

        in_bridge = (
            (abs(xi) <= W / 2) and (abs(yi) <= L / 2) and (0 <= zi < H - 1 / np.sqrt(2))
        )

        inDiabolo = in_up_Prism or in_low_Prism or in_bridge

        return inDiabolo

    NL = nu + nl + L
    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(NL) - 1, int(NL) + 1),
            (-1 * int(np.ceil(NL)) - 1, int(np.ceil(NL)) + 1),
            (-1 * int(np.ceil(H)) - 1, int(np.ceil(H)) + 1),
            conditionPrism,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(NL) - 1, int(NL) + 1),
            (-1 * int(np.ceil(NL)) - 1, int(np.ceil(NL)) + 1),
            (-1 * int(np.ceil(H)) - 1, int(np.ceil(H)) + 1),
            conditionPrism,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift Z so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2
    dipoles = sp.T

    return dipoles


def polygon(
    step, config_dict, H, mesh="cube", ORIENTATION=1, plot_testing=False, verbose=False
):
    """Generate polygonal, planar structure on X/Y plane

    Can take a list of polygons, they can be either added or removed.
    The order of adding / removing polygons is important!

    Requires module `shapely`.

    Parameters
    ----------
    step : float
        stepsize in nm

    config_dict : dict or list of dict
        every dictionary describes one polygon and contains the keywords:
            - `N` (mandatory): number of eges
            - `S` (mandatory): side length in units of `step`
            - `offset`: optional offset. tuple (X, Y). default: (0,0)
            - `aspect`: optional aspect ratio (X/Y). default: 1
            - `alpha`: optional rotation angle (in degrees). default: 0
            - `mode`: optional. either 'add' or 'remove'. Will either add the
                    polygon to the collection or remove it. default: 'add'

    H : int
        height of planar structure in units of steps

    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'

    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    plot_testing : bool, default: False
        plot polygon outlines using `matplotlib`'s pyplot

    verbose : bool, default: False
        print timing info

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    import time
    from shapely.geometry import Point
    from shapely.geometry.polygon import Polygon

    if type(config_dict) == dict:
        config_dict = [config_dict]

    ## --- generate polygons
    polygon_list = []
    allX = []
    allY = []
    for di in config_dict:
        if "N" not in di.keys() or "S" not in di.keys():
            raise ValueError(
                "polygon dictionary must contain at least keys 'N' and 'S'."
            )
        if "aspect" not in di.keys():
            di["aspect"] = 1
        if "alpha" not in di.keys():
            di["alpha"] = 0
        if "offset" not in di.keys():
            di["offset"] = [0, 0]
        if "mode" not in di.keys():
            di["mode"] = "add"
        else:
            if di["mode"].lower() not in ["add", "remove"]:
                raise ValueError(
                    "polygon dictionary entry 'mode' must be one of ['add', 'remove']"
                )

        k_pts = np.arange(di["N"])

        X, Y = (
            1.01
            * di["S"]
            * (
                np.array(
                    [
                        np.cos(2 * np.pi * k_pts / di["N"]),
                        np.sin(2 * np.pi * k_pts / di["N"]),
                    ]
                )
            )
        )
        X = np.concatenate([X, [X[0]]]) * di["aspect"]
        Y = np.concatenate([Y, [Y[0]]])

        alpha = -1 * di["alpha"] * np.pi / 180.0
        Xr = X * np.cos(alpha) - Y * np.sin(alpha) + di["offset"][0]
        Yr = X * np.sin(alpha) + Y * np.cos(alpha) + di["offset"][1]
        allX.append(Xr)
        allY.append(Yr)

        if plot_testing:
            import matplotlib.pyplot as plt

            plt.plot(Xr * step, Yr * step)

        poly_pts = np.transpose([Xr[:-1], Yr[:-1]])
        polygon_list.append(Polygon(poly_pts))

    def cond_poly(xi, yi, zi):
        if zi >= H:
            return False
        else:
            testpt = [xi, yi]
            testpoint = Point(testpt[0], testpt[1])
            poly_condition = None
            for i_poly, polygon in enumerate(polygon_list):
                mode = config_dict[i_poly]["mode"].lower()
                if mode == "remove":
                    if poly_condition is None:
                        poly_condition = not polygon.contains(testpoint)
                    else:
                        poly_condition = (
                            not polygon.contains(testpoint) and poly_condition
                        )
                elif mode == "add":
                    if poly_condition is None:
                        poly_condition = polygon.contains(testpoint)
                    else:
                        poly_condition = polygon.contains(testpoint) or poly_condition
            return poly_condition

    t0 = time.time()
    limits_X = [
        int(np.concatenate(allX).min() - 4),
        int(np.concatenate(allX).max() + 4),
    ]
    limits_Y = [
        int(np.concatenate(allY).min() - 4),
        int(np.concatenate(allY).max() + 4),
    ]
    limits_Z = [0, H]
    if mesh == "cube":
        sp = _meshCubic(limits_X, limits_Y, limits_Z, cond_poly)
    elif mesh == "hex":
        sp = _meshHexagonalCompact(limits_X, limits_Y, limits_Z, cond_poly, ORIENTATION)

    if verbose:
        print("polygon structure - elapsed time: {:.4f}s".format(time.time() - t0))

    sp = np.array(sp, dtype=np.float32).T
    sp *= step
    dipoles = sp.T

    return dipoles


def sphere(step, R, mesh="cube", ORIENTATION=1):
    """
    Generate sphere with radius R on X/Y plane

    Parameters
    ----------
    step : float
        stepsize in nw
    R : float
        radius of sphere (in nrs of dipoles). Not limited to integer numbers.
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    def conditionSphere(xi, yi, zi):
        return xi**2 + yi**2 + zi**2 <= (R + 0.2) ** 2

    if mesh == "cube":
        sp = _meshCubic(
            (-2 * int(R), 2 * int(R) + 1),
            (-2 * int(R), 2 * int(R) + 1),
            (-2 * int(R), 2 * int(R) + 1),
            conditionSphere,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-2 * int(R), 2 * int(R) + 1),
            (-2 * int(R), 2 * int(R) + 1),
            (-2 * int(R), 2 * int(R) + 1),
            conditionSphere,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2
    dipoles = sp.T

    return dipoles


def spheroid(step, R1, R2, R3, mesh="cube", ORIENTATION=1):
    """Generate spheroid with radii R1, R2, R3 along X, Y, Z axis

      - if R3>R2=R1 --> prolate spheroid
      - if R3<R2=R1 --> oblate spheroid
      - if R3=R2=R1 --> sphere

    contributed by C. Majorel.

    Parameters
    ----------
    step : float
        stepsize in nm

    R1 : float
        radius along X axis (in nrs of steps). Not limited to integer numbers.

    R2 : float
        radius along Y axis (in nrs of steps). Not limited to integer numbers.

    R3 : float
        radiu along Z axis  (in nrs of steps). Not limited to integer numbers.

    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'

    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    def conditionSpheroid(xi, yi, zi):
        return (xi / (R1 + 0.2)) ** 2 + (yi / (R2 + 0.2)) ** 2 + (
            zi / (R3 + 0.2)
        ) ** 2 <= 1

    if mesh == "cube":
        sp = _meshCubic(
            (-2 * int(R1), 2 * int(R1) + 1),
            (-2 * int(R2), 2 * int(R2) + 1),
            (-2 * int(R3), 2 * int(R3) + 1),
            conditionSpheroid,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-2 * int(R1), 2 * int(R1) + 1),
            (-2 * int(R2), 2 * int(R2) + 1),
            (-2 * int(R3), 2 * int(R3) + 1),
            conditionSpheroid,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2
    dipoles = sp.T

    return dipoles


def nanodisc(step, R, H, ELONGATED=0, mesh="cube", ORIENTATION=1):
    """
    Generate round nanodisc in X/Y plane. Height H, Radius R.

    Parameters
    ----------
    step : float
        stepsize in nw
    R : float
        radius of circular crosssection (in nrs of dipoles). Not limited to integer numbers.
    H : int
       Height of Structure (in nrs of dipoles).
    ELONGATED : int, default: 0
       add optional elongation "bridge" between half-circles (in nrs of steps)
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    L = ELONGATED

    def conditionRod(xi, yi, zi):
        #        InRod = ((abs(zi) < H) and (np.sqrt(xi**2+yi**2)<(R+0.2)))
        InRod = (
            (abs(zi) < H - 1 / np.sqrt(2))
            and (
                (np.sqrt((xi - int(L / 2)) ** 2 + yi**2) < (R + 0.2))
                or (np.sqrt((xi + np.ceil(L / 2.0)) ** 2 + yi**2) < (R + 0.2))
            )
            or ((np.sqrt(yi**2) < (R + 0.2)) and (abs(xi) < L / 2))
        )
        return InRod

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(R) - L / 2 - 2, 1 * int(R) + L / 2 + 2),
            (-1 * int(R) - 2, 1 * int(R) + 2),
            (0, int(np.ceil(H))),
            conditionRod,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(R) - 5 - L / 2, 1 * int(R) + 5 + L / 2),
            (-1 * int(R) - 5, 1 * int(R) + 5),
            (0, int(np.ceil(H))),
            conditionRod,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2
    dipoles = sp.T

    return dipoles


def ellipse(step, R1, R2, H, mesh="cube", ORIENTATION=1):
    """Generate ellipse shape with constant height. radii R1, R2 along X, Y axis

    Parameters
    ----------
    step : float
        stepsize in nm

    R1 : float
        radius along X axis (in nrs of steps). Not limited to integer numbers.

    R2 : float
        radius along Y axis (in nrs of steps). Not limited to integer numbers.

    H : int
        height (Z axis)  (in nrs of steps)

    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'

    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    def conditionEllipse(xi, yi, zi):
        return ((xi / (R1 + 0.2)) ** 2 + (yi / (R2 + 0.2)) ** 2 <= 1) and (
            0 <= zi < H - 1 / np.sqrt(2)
        )

    if mesh == "cube":
        sp = _meshCubic(
            (-2 * int(R1), 2 * int(R1) + 1),
            (-2 * int(R2), 2 * int(R2) + 1),
            (-1, 2 * int(H) + 1),
            conditionEllipse,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-2 * int(R1), 2 * int(R1) + 1),
            (-2 * int(R2), 2 * int(R2) + 1),
            (-1, 2 * int(H) + 1),
            conditionEllipse,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T
    sp *= step
    dipoles = sp.T

    return dipoles


def nanorod(step, L, R, caps="flat", mesh="cube", ORIENTATION=1):
    """
    Generate round nanorod with axis along X on X/Y plane

    Parameters
    ----------
    step : float
        stepsize in nw
    L : int
       length of rod (in nrs of dipoles).
    R : float
        radius of circular crosssection (in nrs of dipoles). Not limited to integer numbers.
    caps : str, default: 'flat'
        'flat': flat caps, 'round': semispherical caps
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    def conditionRod(xi, yi, zi):
        InRod = (abs(xi) <= L / 2.0) and (np.sqrt(yi**2 + zi**2) <= (R + 0.2))

        if caps in ["round", "true", True]:
            InCaps = np.sqrt((abs(xi) - L / 2.0) ** 2 + yi**2 + zi**2) <= (R + 0.2)
        else:
            InCaps = False
        return InRod or InCaps

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(L), 1 * int(L) + 1),
            (-2 * int(R), 2 * int(R) + 1),
            (-2 * int(R), 2 * int(R) + 1),
            conditionRod,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(L), 1 * int(L) + 1),
            (-2 * int(R), 2 * int(R) + 1),
            (-2 * int(R), 2 * int(R) + 1),
            conditionRod,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2
    dipoles = sp.T

    return dipoles


def nanocone(step, R, H, cutoff=1.0, mesh="cube", ORIENTATION=1):
    """
    Generate a right circular cone on X/Y plane. Height H, Radius R.

    Parameters
    ----------
    step : float
        stepsize in nw

    R : float
        radius of circular base (in nrs of dipoles). Not limited to integer numbers.

    H : int
        Height of cone (in nrs of dipoles).

    cutoff : float, deafult: 1.0
        percentage of cone to construct (1.0=100%, 0.5=stop at 50% of height)

    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'

    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    H = int(H)

    def conditionCone(xi, yi, zi):
        InRod = (abs(zi) < H * cutoff) and (
            np.sqrt(xi**2 + yi**2) < (H - zi) / (float(H)) * (R + 0.2)
        )
        return InRod

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(R) - 2, 1 * int(R) + 2),
            (-1 * int(R) - 2, 1 * int(R) + 2),
            (0, H + 1),
            conditionCone,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(R) - 5, 1 * int(R) + 5),
            (-1 * int(R) - 5, 1 * int(R) + 5),
            (0, H + 3),
            conditionCone,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T * step
    sp[2] -= np.min(sp[2])  # shift so that bottom at z=0
    sp[2] += step / 2  # shift so that bottom at z=step/2
    dipoles = sp.T

    return dipoles


def lshape_rect(step, L, W, H, DELTA, mesh="cube", ORIENTATION=1):
    """
    Generate symmetric L-shaped nanoantenna in X/Y plane formed by
    two rectangular elements.

    Parameters
    ----------
    step : float
        stepsize in nw
    L,W,H : int
        length,width,height of each rod (in nrs of dipoles)
    DELTA : int (odd number)
        Gap width - only odd numbers work for symmetry reasons! (in nr of dipoles)
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    _DELTA = DELTA / 2

    def conditionRod(xi, yi, zi):
        InRod1 = (
            (_DELTA < xi <= _DELTA + L)
            and (-1 * _DELTA > yi >= -1 * _DELTA - W)
            and (0 <= zi < H - 1 / np.sqrt(2))
        )
        InRod2 = (
            (-1 * _DELTA > xi >= -1 * _DELTA - W)
            and (_DELTA < yi <= _DELTA + L)
            and (0 <= zi < H - 1 / np.sqrt(2))
        )

        return InRod1 or InRod2

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * abs(DELTA) - W - 1, L + abs(DELTA) + 2),
            (-1 * abs(DELTA) - W - 1, L + abs(DELTA) + 2),
            (0, H + 2),
            conditionRod,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * DELTA - W - 1, L + DELTA + 2),
            (-1 * DELTA - W - 1, L + DELTA + 2),
            (0, H + 2),
            conditionRod,
            ORIENTATION,
        )

    Rect1 = np.array(sp, dtype=np.float32).T * step
    dipoles = Rect1.T

    return dipoles


def lshape_rect_nonsym(step, L1, W1, L2, W2, H, DELTA, mesh="cube", ORIENTATION=1):
    """
    Generate symmetric L-shaped nanoantenna in X/Y plane formed by
    two rectangular elements.

    Parameters
    ----------
    step : float
        stepsize in nw
    L,W,H : int
        length,width,height of each rod (in nrs of dipoles)
    DELTA : int (odd number)
        Gap width - only odd numbers work for symmetry reasons! (in nr of dipoles)
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    _DELTA = int(DELTA / 2)
    L1 = int(L1)
    L2 = int(L2)
    W1 = int(W1)
    W2 = int(W2)
    H = int(H)

    def conditionRod(xi, yi, zi):
        InRod1 = (
            (_DELTA < xi <= _DELTA + L1)
            and (-1 * _DELTA > yi >= -1 * _DELTA - W1)
            and (0 <= zi < H - 1 / np.sqrt(2))
        )
        InRod2 = (
            (-1 * _DELTA > xi >= -1 * _DELTA - W2)
            and (_DELTA < yi <= _DELTA + L2)
            and (0 <= zi < H - 1 / np.sqrt(2))
        )

        return InRod1 or InRod2

    W = W1 + W2
    L = L1 + L2
    if mesh == "cube":
        sp = _meshCubic(
            (-1 * abs(DELTA) - W - 1, L + abs(DELTA) + 2),
            (-1 * abs(DELTA) - W - 1, L + abs(DELTA) + 2),
            (0, H + 2),
            conditionRod,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * DELTA - W - 1, L + DELTA + 2),
            (-1 * DELTA - W - 1, L + DELTA + 2),
            (0, H + 2),
            conditionRod,
            ORIENTATION,
        )

    Rect1 = np.array(sp, dtype=np.float32).T * step
    dipoles = Rect1.T

    return dipoles


def lshape_round(step, L, W, H, DELTA, RAD, mesh="cube", ORIENTATION=1):
    """
    Generate symmetric L-shaped nanoantenna in X/Y plane formed by
    two rectangular elements.

    Parameters
    ----------
    step : float
        stepsize in nm
    L,W,H : int
        length,width,height of each rod (in nrs of dipoles)
    DELTA : int (odd number)
        Gap width - only odd numbers work for symmetry reasons! (in nr of dipoles)
    RAD : float
        radius of curvature in steps (nr of dipoles)
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    D = DELTA / 2
    RAD += 0.2

    def conditionRod(xi, yi, zi):
        InRod1 = (
            (D < xi <= D + L)
            and (-1 * D > yi >= -1 * D - W)
            and (0 <= zi < H - 1 / np.sqrt(2))
        )
        ## Roundings Rod 1
        R1R1 = (not ((xi < int(D + RAD)) and (yi > -1.0 * int(D + RAD)))) or (
            (xi - int(RAD + D + 1)) ** 2 + (yi + int(RAD + D + 1)) ** 2 <= RAD**2
        )
        R1R2 = (not ((xi < int(D + RAD)) and (yi < -1.0 * int(D + W - RAD)))) or (
            (xi - int(RAD + D + 1)) ** 2 + (yi + int(D + W - RAD + 1)) ** 2 <= RAD**2
        )
        R1R3 = (not ((xi > int(D + L - RAD)) and (yi > -1.0 * int(D + RAD)))) or (
            (xi - int(D + L - RAD + 1)) ** 2 + (yi + int(RAD + D + 1)) ** 2 <= RAD**2
        )
        R1R4 = (not ((xi > int(D + L - RAD)) and (yi < -1.0 * int(D + W - RAD)))) or (
            (xi - int(D + L - RAD + 1)) ** 2 + (yi + int(D + W - RAD + 1)) ** 2
            <= RAD**2
        )
        Round1 = R1R1 and R1R2 and R1R3 and R1R4

        InRod2 = (
            (-1 * D > xi >= -1 * D - W)
            and (D < yi <= D + L)
            and (0 <= zi < H - 1 / np.sqrt(2))
        )
        ## Roundings Rod 2
        R2R1 = (not ((yi < int(D + RAD)) and (xi > -1.0 * int(D + RAD)))) or (
            (yi - int(RAD + D + 1)) ** 2 + (xi + int(RAD + D + 1)) ** 2 <= RAD**2
        )
        R2R2 = (not ((yi < int(D + RAD)) and (xi < -1.0 * int(D + W - RAD)))) or (
            (yi - int(RAD + D + 1)) ** 2 + (xi + int(D + W - RAD + 1)) ** 2 <= RAD**2
        )
        R2R3 = (not ((yi > int(D + L - RAD)) and (xi > -1.0 * int(D + RAD)))) or (
            (yi - int(D + L - RAD + 1)) ** 2 + (xi + int(RAD + D + 1)) ** 2 <= RAD**2
        )
        R2R4 = (not ((yi > int(D + L - RAD)) and (xi < -1.0 * int(D + W - RAD)))) or (
            (yi - int(D + L - RAD + 1)) ** 2 + (xi + int(D + W - RAD + 1)) ** 2
            <= RAD**2
        )
        Round2 = R2R1 and R2R2 and R2R3 and R2R4

        return (InRod1 and Round1) or (InRod2 and Round2)

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * abs(DELTA) - W - 1, L + abs(DELTA) + 2),
            (-1 * abs(DELTA) - W - 1, L + abs(DELTA) + 2),
            (0, H + 2),
            conditionRod,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * DELTA - W - 10, L + DELTA + 10),
            (-1 * DELTA - W - 10, L + DELTA + 10),
            (0, H + 2),
            conditionRod,
            ORIENTATION,
        )

    Rect1 = np.array(sp, dtype=np.float32).T * step
    dipoles = Rect1.T

    return dipoles


def split_ring(step, R, W, H, G=-1, alphaG=0.0, mesh="cube", ORIENTATION=1):
    """
    Generate splitring structure on X/Y plane


    If G (gap) is set: close the resonator up to "gap" (G=)

    Parameters
    ----------
    step : float
        stepsize in nw
    R,W,H : int,int,int
        Dimensions: Radius, linewidth and height of structure (in nr. of dipoles)
    G : (optional) int, default: -1
        Gap width in numbers of steps.
        If G == -1, alphaG is taken instead
    alphaG : (optional) float, default: 0
        Gap width as angle in degrees.
        Allowed angles: Between 0 and 180.
        '0': entirely closed
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    if G != -1:
        alphaG = 2.0 * np.tan(G / (2.0 * R)) * 180.0 / np.pi

    def conditionSR(xi, yi, zi):
        A1 = R**2 >= (xi**2 + yi**2) >= (R - W) ** 2
        if yi > 0 and alphaG != 0:
            A2 = not (np.arctan2(abs(xi), abs(yi)) * 180 / np.pi <= alphaG / 2.0)
        else:
            A2 = True

        CZ = 0 <= zi < H - 1 / np.sqrt(2)

        return (A1 and A2) and CZ

    if mesh == "cube":
        sp = _meshCubic(
            (-1 * int(R) - 5, int(R) + 5),
            (-1 * int(R) - 5, int(R) + 5),
            (-1, int(H) + 1),
            conditionSR,
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1 * int(R) - 5, int(R) + 5),
            (-1 * int(R) - 5, int(R) + 5),
            (-1, int(H) + 1),
            conditionSR,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T
    sp *= step
    dipoles = sp.T

    return dipoles


def rect_split_ring(step, L1, L2, H, W, G=False, mesh="cube", ORIENTATION=1):
    """
    Generate rectangular splitring structure on X/Y plane


    If G (gap) is set: close the resonator up to "gap" (G=)

    Parameters
    ----------
    step : float
        stepsize in nw
    L1,L2, H, W : int
        Length (L1:X,L2:Y), Height and Width in Nr. of steps
    G : (optional) int, default: False
        Gap of a closed splitring.
            If False: totally open splitring,
            If '0': entirely closed
    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """

    def conditionSR(xi, yi, zi):
        A1 = 0 <= xi < L1 and 0 <= yi < W and (0 <= zi < H - 1 / np.sqrt(2))
        A2 = 0 <= xi < W and 0 <= yi < L2 and (0 <= zi < H - 1 / np.sqrt(2))
        A3 = L1 - W <= xi < L1 and 0 <= yi < L2 and (0 <= zi < H - 1 / np.sqrt(2))

        if G is not False:
            A4 = (
                (0 <= xi < (L1 - G) / 2 or (L1 + G) / 2 <= xi < L1)
                and L2 - W <= yi < L2
                and (0 <= zi < H - 1 / np.sqrt(2))
            )
        else:
            A4 = False

        return A1 or A2 or A3 or A4

    if mesh == "cube":
        sp = _meshCubic(
            (-1, int(L1 + W) + 1), (-1, int(L2 + W) + 1), (-1, int(H) + 1), conditionSR
        )
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            (-1, int(L1 + W) + 1),
            (-1, int(L2 + W) + 1),
            (-1, int(H) + 1),
            conditionSR,
            ORIENTATION,
        )

    sp = np.array(sp, dtype=np.float32).T
    sp[0] -= L1 / 2
    sp[1] -= L2 / 2
    sp *= step
    dipoles = sp.T

    return dipoles


# ==============================================================================
# MESHERS
# ==============================================================================
def _meshCubic(XRANGE, YRANGE, ZRANGE, condition, Z_offset=0.5):
    """mesh on a cubic grid

    Mesh a 3d structure on a cubic grid within given spacial limits
    using a boolean selection function

    Parameters
    ----------
    XRANGE : tuple
        tuple of 2  ints (x0, x1): Indices of x-coordinates on mesh
    YRANGE : tuple
        tuple of 2  ints (y0, y1): Indices of y-coordinates on mesh
    ZRANGE : tuple
        tuple of 2  ints (z0, z1): Indices of z-coordinates on mesh
    condition : function
        function of mesh-coord.-indices, whether to place a meshpoint or not:
        'func(xi,yi,zi)', must returns a Boolean

    Z_offset : float, default: 0.5
        additional z-offset, to avoid placing dipoles at z=0


    Returns
    -------
    list of tuples
         list of 3d coordinate-index tuples
    """
    sp = []
    for xi in range(int(XRANGE[0]), int(XRANGE[1])):
        for yi in range(int(YRANGE[0]), int(YRANGE[1])):
            for zi in range(int(ZRANGE[0]), int(ZRANGE[1])):

                if condition(xi, yi, zi):
                    sp.append([xi, yi, zi + Z_offset])

    return sp


def _meshHexagonalCompact(
    XRANGE,
    YRANGE,
    ZRANGE,
    condition,
    ORIENTATION=1,
    Z_offset=0.5,
    auto_expand_range=True,
):
    """mesh on a hexagonal compact grid

    Parameters
    ----------
    XRANGE : tuple
        tuple of 2  ints (x0, x1): Indices of x-coordinates on mesh
    YRANGE : tuple
        tuple of 2  ints (y0, y1): Indices of y-coordinates on mesh
    ZRANGE : tuple
        tuple of 2  ints (z0, z1): Indices of z-coordinates on mesh
    condition : function
        function of mesh-coord.-indices, whether to place a meshpoint or not:
        'func(xi,yi,zi)', must returns a Boolean
    ORIENTATION : int, default: 1
        For hex. meshing only. '1' or '2'. Orientation of Hexagons.

    Z_offset : float, default: 0.5
        additional z-offset, to avoid placing dipoles at z=0

    auto_expand_range : bool, default: True
        automatically expand meshing range in order to account for denser than cubic meshing

    Returns
    -------
    list of tuples
         list of 3d coordinate-index tuples
    """
    sp = []
    XRANGE = list(XRANGE)
    YRANGE = list(YRANGE)
    ZRANGE = list(ZRANGE)

    ## Auto-expand range for inhomogeneous hex-meshcell extensions
    if auto_expand_range:
        XRANGE[0] = XRANGE[0] - np.abs(XRANGE[0]) * 2 / np.sqrt(3)
        XRANGE[1] = XRANGE[1] + np.abs(XRANGE[1]) * 2 / np.sqrt(3)

        YRANGE[0] = YRANGE[0] - np.abs(YRANGE[0]) * 2 / np.sqrt(3)
        YRANGE[1] = YRANGE[1] + np.abs(YRANGE[1]) * 2 / np.sqrt(3)

        ZRANGE[0] = ZRANGE[0] - np.abs(ZRANGE[0]) * 2 / np.sqrt(3)
        ZRANGE[1] = ZRANGE[1] + np.abs(ZRANGE[1]) * 2 / np.sqrt(3)

    if ORIENTATION == 1:
        for zi in range(int(ZRANGE[0]), int(ZRANGE[1])):
            Z = zi * np.sqrt(2.0 / 3.0)
            for yi in range(int(YRANGE[0]), int(YRANGE[1])):
                Y = yi * np.sqrt(3.0) / 2.0 + 2.0 * abs(
                    zi / 2.0 - int(zi / 2.0)
                ) / np.sqrt(3.0)
                for xi in range(int(XRANGE[0]), int(XRANGE[1])):
                    X = xi + abs(yi / 2.0 - int(yi / 2.0))

                    if condition(X, Y, Z):
                        sp.append([X, Y, Z + Z_offset])

    elif ORIENTATION == 2:
        for xi in range(int(XRANGE[0]), int(XRANGE[1])):
            X = xi * np.sqrt(2.0 / 3.0)
            for yi in range(int(YRANGE[0]), int(YRANGE[1])):
                Y = yi * np.sqrt(3.0) / 2.0 + 2.0 * abs(
                    xi / 2.0 - int(xi / 2.0)
                ) / np.sqrt(3.0)
                for zi in range(int(ZRANGE[0]), int(ZRANGE[1])):
                    Z = zi + abs(yi / 2.0 - int(yi / 2.0))

                    if condition(X, Y, Z):
                        sp.append([X, Y, Z + Z_offset])

    return sp


# ==============================================================================
# Other Functions
# ==============================================================================
def get_normalization(mesh):
    """Provide normalization factor for mesh type

    Parameters
    ----------
    mesh : string
        mesh used for structure generator

    Returns
    -------
    normalization : float
        normalization factor corresponding to `mesh`
    """
    if mesh == "cube":
        normalization = 1.0
    elif mesh == "hex":
        normalization = np.sqrt(2.0)
    else:
        raise ValueError('Meshing definition: "mesh" must be either "cube" or "hex"!')

    return normalization


def image_to_struct(
    img_name,
    nm_per_pixel,
    stepsize,
    H,
    threshold=100,
    useDarkPixel=True,
    returnImage=False,
    center_structure=True,
    mesh="cube",
    ORIENTATION=1,
):
    """Convert an image to a planar structure

    Might be useful with SEM images

    Parameters
    ----------
    img_name : string or numpy array
        path to image-file or numpy array containing image. Array values should range from 0 to 255.

    nm_per_pixel : float
        number of nanometers corresponding to one pixel in image

    stepsize : float
        target stepsize

    H : int
        Height of final structure in numbers of stepsize

    threshold : float, (default: 100)
        threshold value between [0, 255] to declare a pixel as *structure*
        all brighter pixels will be used

    useDarkPixel : bool, default: True
        if False, use bright pixels as structure (below threshold), instead
        of the dark ones

    returnImage : bool, default: False
        if True, returns a 2D numpy array corresponding to the image
        AND the structure

    center_structure : bool, default: True
        whether to automatically center structure

    mesh : string, default: 'cube'
        meshing type. 'cube' or 'hex'

    ORIENTATION : int, default: 1
        For hexagonal meshing only. '1' or '2'. Orientation of Hexagons.

    Returns
    -------
    struct : list of tuples
         list of 3d coordinates in tuples (x,y,z)

    if returnImage==True:
        returns tuple: (img_array, struct)

    """
    from PIL import Image

    ## --- load image
    if type(img_name) == str:
        img = Image.open(img_name)
        img.load()  # required for png.split()
    else:
        img = Image.fromarray(img_name)

    ## --- remove alpha channel, if exists
    if len(img.split()) == 4:
        ## RGBA data --> remove alpha channel
        background = Image.new("RGB", img.size, (255, 255, 255))
        background.paste(img, mask=img.split()[3])  # 3 is the alpha channel
    else:
        ## RGB data --> no alpha channel
        background = img

    ## --- hex meshing: add white border to account for denser mesh than cubic
    if mesh.lower() == "hex":
        old_size = img.size
        new_size = (int(old_size[0] * 1.41), int(old_size[1] * 1.41))
        new_im = Image.new("RGB", new_size, color=(255, 255, 255))
        # new_im = Image.new("RGB", new_size, color=0)
        new_im.paste(
            background,
            (
                int((new_size[0] - old_size[0]) * 0),
                int((new_size[1] - old_size[1]) / 1),
            ),
        )
        # new_im.paste(background, ((new_size[0]-old_size[0])//2,
        #                           (new_size[1]-old_size[1])//2))
        background = new_im

    ## original image to return if required
    img0 = background.convert("L")
    data0 = np.asarray(img0.getdata()).reshape(img0.size[::-1])

    ## --- rescale image to 1 stepsize per pixel
    X0 = background.size[0]
    basewidth = int(round(X0 * nm_per_pixel / float(stepsize)))

    hsize = int(
        (float(background.size[1]) * float((basewidth / float(background.size[0]))))
    )
    img = background.resize((basewidth, hsize), Image.LANCZOS)
    img = img.convert("L")

    data = np.asarray(img.getdata()).reshape(img.size[::-1])
    data = np.rot90(data, 3)  # rotate by 270 degrees

    ## --- generate structure using all pixel above threshold
    if useDarkPixel:
        data[data <= threshold] = threshold
        data[data > threshold] = 0
    else:
        data[data >= threshold] = threshold
        data[data < threshold] = 0

    ## --- mesh the structure
    def condition(xi, yi, zi):
        return data[int(xi)][int(yi)] != 0

    if mesh == "cube":
        sp = _meshCubic([0, data.shape[0]], [0, data.shape[1]], [0, H], condition)
    elif mesh == "hex":
        sp = _meshHexagonalCompact(
            [0, data.shape[0]],
            [0, data.shape[1]],
            [0, H],
            condition,
            ORIENTATION,
            auto_expand_range=False,
        )
    sp = np.array(sp, dtype=np.float32).T * stepsize
    sp[2] -= np.min(sp[2])  # shift Z so that bottom at z=0
    sp[2] += stepsize / 2  # shift Z so that bottom at z=step/2
    struct = sp.T

    if center_structure:
        struct = center_struct(struct)

    if returnImage:
        return data0, struct
    else:
        return struct


def struct_to_image(
    STRUCT, RGB=True, minimum_size=-1, center_struct="half", projection="XY"
):
    """generate image-compatible array from structure geometry

    Parameters
    ----------
    struct : list of tuple *or* :class:`.core.simulation`
        list of coordinate tuples or instance of :class:`.core.simulation`

    RGB : bool, default: True
        If True, generates an RGB array. If False, generate a 2D array with
        simply 0 and 1 as entries (0: no material, 1: material)

    minimum_size : int, default: -1
        minimum canvas size in pixels.
        -1: use structure extensions. absolute position will be lost!
        If >0: enlarge canvas and center structure if structure extensions
        smaller than image.

    center_struct : str, default: 'half'
        Has no effect if minimum_size==-1.
        how to place structure in canvas. 'half': origin (0,0) is at half x/y.
        'auto': center structure relative to image. absolute position will be lost!

    projection : str, default: "XY"
        2D plane for projection. One of ['XY', 'XZ', 'YZ']

    Returns
    -------
    np.array for image conisting of rgb tuples for each pixel.
    Plot e.g. using `matplotlib`'s `imshow`.
    Save to bitmap or png file e.g. using `scipy.misc.imsave`.

    """
    step = tools.get_step_from_geometry(STRUCT)
    _X, _Y, _Z = tools.get_geometry_2d_projection(STRUCT, projection).T
    if projection.lower() == "xy":
        X, Y = _X, _Y
    elif projection.lower() == "yz":
        X, Y = _Y, _Z
    elif projection.lower() == "xz":
        X, Y = _X, _Z

    X /= float(step)
    Y /= float(step)
    if center_struct.lower() == "auto" or minimum_size == -1:
        X -= X.min()
        Y -= Y.min()
    else:
        X += minimum_size // 2
        Y += minimum_size // 2

    ## -- optionally enlarge canvas and center struct
    if minimum_size <= X.max():
        DX = int(X.max() + 1)
    else:
        if center_struct.lower() == "auto":
            X += int((minimum_size - X.max()) / 2.0)
        DX = minimum_size

    if minimum_size <= Y.max():
        DY = int(Y.max() + 1)
    else:
        if center_struct.lower() == "auto":
            Y += int((minimum_size - Y.max()) / 2.0)
        DY = minimum_size

    struct = np.transpose([X, Y])

    ## -- generate 2D image array
    if RGB:
        arr = np.full((DX, DY, 3), 255, dtype=np.uint8)
        for i in struct:
            arr[int(i[0])][int(i[1])] = np.array([0, 0, 0])
    else:
        arr = np.full((DX, DY), 1, dtype=np.uint8)
        for i in struct:
            arr[int(i[0])][int(i[1])] = 0

    return np.rot90(arr)


def rotate_XY(STRUCT, ALPHA):
    """Rotate a structure around the z axis

    see :func:`.rotate` for full documentation
    """
    return rotate(STRUCT, ALPHA, axis="z")


def rotate(geo, alpha, axis="z"):
    """Rotate a structure around the specified axis

    Parameters
    ----------
    geo : list of tuples or :class:`pyGDM2.core.simulation`
        geometry as list of (x,y,z) coordinate tuples.
        As generated by the functions of the structure module

    alpha : float
        rotation angle in degrees

    axis : str, default: 'z'
        rotation axis. one of ["x", "y", "z"]

    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    if axis.lower() not in ["x", "y", "z"]:
        raise Exception("rotation axis '{}': Unknown axis description.".format(axis))

    A = alpha * np.pi / 180.0

    from pyGDM2 import core

    if type(geo) == core.simulation:
        if axis.lower() == "x":
            yNew = (
                np.cos(A) * geo.struct.geometry.T[1]
                - np.sin(A) * geo.struct.geometry.T[2]
            )
            zNew = (
                np.sin(A) * geo.struct.geometry.T[1]
                + np.cos(A) * geo.struct.geometry.T[2]
            )
            geo.struct.geometry.T[1:3] = yNew, zNew
        elif axis.lower() == "y":
            xNew = (
                np.cos(A) * geo.struct.geometry.T[0]
                + np.sin(A) * geo.struct.geometry.T[2]
            )
            zNew = (
                -np.sin(A) * geo.struct.geometry.T[0]
                + np.cos(A) * geo.struct.geometry.T[2]
            )
            geo.struct.geometry.T[0] = xNew
            geo.struct.geometry.T[2] = zNew
        elif axis.lower() == "z":
            xNew = (
                np.cos(A) * geo.struct.geometry.T[0]
                - np.sin(A) * geo.struct.geometry.T[1]
            )
            yNew = (
                np.sin(A) * geo.struct.geometry.T[0]
                + np.cos(A) * geo.struct.geometry.T[1]
            )
            geo.struct.geometry.T[0:2] = xNew, yNew
    elif type(geo) in [struct]:
        if axis.lower() == "x":
            yNew = np.cos(A) * struct.geometry.T[1] - np.sin(A) * struct.geometry.T[2]
            zNew = np.sin(A) * struct.geometry.T[1] + np.cos(A) * struct.geometry.T[2]
            struct.geometry.T[1:3] = yNew, zNew
        elif axis.lower() == "y":
            xNew = np.cos(A) * struct.geometry.T[0] + np.sin(A) * struct.geometry.T[2]
            zNew = -np.sin(A) * struct.geometry.T[0] + np.cos(A) * struct.geometry.T[2]
            struct.geometry.T[0] = xNew
            struct.geometry.T[2] = zNew
        elif axis.lower() == "z":
            xNew = np.cos(A) * struct.geometry.T[0] - np.sin(A) * struct.geometry.T[1]
            yNew = np.sin(A) * struct.geometry.T[0] + np.cos(A) * struct.geometry.T[1]
            struct.geometry.T[0:2] = xNew, yNew
    else:
        if axis.lower() == "x":
            yNew = np.cos(A) * geo.T[1] - np.sin(A) * geo.T[2]
            zNew = np.sin(A) * geo.T[1] + np.cos(A) * geo.T[2]
            geo.T[1:3] = yNew, zNew
        elif axis.lower() == "y":
            xNew = np.cos(A) * geo.T[0] + np.sin(A) * geo.T[2]
            zNew = -np.sin(A) * geo.T[0] + np.cos(A) * geo.T[2]
            geo.T[0] = xNew
            geo.T[2] = zNew
        elif axis.lower() == "z":
            xNew = np.cos(A) * geo.T[0] - np.sin(A) * geo.T[1]
            yNew = np.sin(A) * geo.T[0] + np.cos(A) * geo.T[1]
            geo.T[0:2] = xNew, yNew

    return geo


def shift(geo, delta):
    """move a structure along a cartesian direction

    Parameters
    ----------
    geo : list of tuples or :class:`pyGDM2.core.simulation` or :class:`pyGDM2.structures.struct`
        geometry as list of (x,y,z) coordinate tuples.
        As generated by the functions of the structure module

    delta : tuple of float
        vector [Dx, Dy, Dz] to shift the structure by (in nm)

    Returns
    -------
    list of tuples or :class:`pyGDM2.core.simulation`
         list of shifted 3d coordinates in tuples (x,y,z)
    """
    if len(delta) != 3:
        raise Exception("shift delta must be a 3-vector [x,y,z].")

    from pyGDM2 import core

    if type(geo) == core.simulation:
        geo.struct.geometry.T[0] += delta[0]
        geo.struct.geometry.T[1] += delta[1]
        geo.struct.geometry.T[2] += delta[2]
    elif type(geo) in [struct]:
        geo.geometry.T[0] += delta[0]
        geo.geometry.T[1] += delta[1]
        geo.geometry.T[2] += delta[2]
    else:
        geo.T[0] += delta[0]
        geo.T[1] += delta[1]
        geo.T[2] += delta[2]

    return geo


def center_struct(STRUCT, which_axis=["x", "y"], returnOffsets=False):
    """
    Center a structure on the x/y plane around (0,0)

    Parameters
    ----------
    STRUCT : list of tuples, or `struct`-object or `simulation`-class
        list of (x,y,z) coordinate tuples, as generated by the functions
        of the structure module; or `struct` or `simulation` object (which
        includes the geometry information)

    which_axis : list of str, default: ['x', 'y']
        on which axis to center the structure.

    returnOffsets : bool, default: False
        if True, return tuple of applied (X,Y)-offset together with structure

    Returns
    -------
         - list of 3d coordinates as tuples (x,y,z); or `struct` or
           `simulation` object with adapted geometry.

         - optionally: Offset-tuple as second return value

    """
    if type(STRUCT) in [list, np.ndarray]:
        returnType = "list"
        X, Y, Z = np.transpose(STRUCT)
    elif type(STRUCT) in [struct]:
        returnType = "struct"
        try:
            X, Y, Z = STRUCT.geometry.T
        except:
            raise Exception("Not a valid structure object")
    elif type(STRUCT) == simulation:
        returnType = "simulation"
        try:
            X, Y, Z = STRUCT.struct.geometry.T
        except:
            raise Exception("Not a valid structure object")
    else:
        raise Exception("Got no valid structure data.")

    DX = (np.max(X) + np.min(X)) / 2.0
    DY = (np.max(Y) + np.min(Y)) / 2.0
    DZ = (np.max(Z) + np.min(Z)) / 2.0
    if "x" in which_axis or "X" in which_axis:
        X -= DX
    if "y" in which_axis or "Y" in which_axis:
        Y -= DY
    if "z" in which_axis or "Z" in which_axis:
        Z -= DZ

    if returnType == "list":
        STRUCT = np.transpose([X, Y, Z])
        if returnOffsets:
            return STRUCT, (DX, DY, DZ)
        else:
            return STRUCT
    elif returnType == "struct":
        STRUCT.geometry.T[0] = X
        STRUCT.geometry.T[1] = Y
        STRUCT.geometry.T[2] = Z
        if returnOffsets:
            return STRUCT, (DX, DY, DZ)
        else:
            return STRUCT
    elif returnType == "simulation":
        STRUCT.struct.geometry.T[0] = X
        STRUCT.struct.geometry.T[1] = Y
        STRUCT.struct.geometry.T[2] = Z
        if returnOffsets:
            return STRUCT, (DX, DY, DZ)
        else:
            return STRUCT


def combine_geometries(geo_list, step=None):
    """combine several coordinate-lists to one structure

    Parameters
    ----------
    geo_list : list of tuples
        list of list of (x,y,z) coordinate tuples.

    step : float, default: None
        stepsize of geometries. It is recomended to provide the stepzise.
        If given, a consistency check is performed on the fused structure
        (test whether meshpoints overlap)


    Returns
    -------
    list of tuples
         list of 3d coordinates in tuples (x,y,z)
    """
    geo0 = geo_list[0]
    ## try to get step from first geometry
    if step is None:
        try:
            step = tools.get_step_from_geometry(
                geo0
            )  # use first geometry as reference if no step given
        except:
            pass

    ## determine input type
    from pyGDM2 import core

    if type(geo0) == core.simulation:
        from pyGDM2 import tools

        return tools.combine_simulations(geo_list)
    elif type(geo0) in [struct]:
        _geo_list = [_struct.geometry for _struct in geo_list]
        _mat_list = [_struct.material for _struct in geo_list]
        mat_full = np.concatenate(_mat_list)
    else:
        _geo_list = geo_list

    ## concatenate all geometries and perform test
    geo_full = np.concatenate(_geo_list)
    if step is not None:
        from pyGDM2 import tools

        geo_full, _geo_wrong, index_correct = tools.test_geometry(
            geo_full, step, return_index=True, plotting=False
        )
    else:
        index_correct = np.arange(len(geo_full))

    if type(geo0) in [struct]:
        new_struct = geo0.copy()
        new_struct.geometry = geo_full.copy()
        new_struct.material = np.array(mat_full)[index_correct]
        new_struct.n_dipoles = len(geo_full)
        geo = new_struct
    else:
        geo = geo_full

    return geo


# def separate_constituends(struct, step=None):
#     """separate physically non-touching constituents in a structure

#     Parameters
#     ----------
#     struct : list of tuples
#         list of (x,y,z) coordinate tuples.


#     Returns
#     -------
#     list of lists
#          list of lists of (x,y,z) tuples, one for each sub-structure
#     """

#     geo_full = np.concatenate(geo_list)

#     if step is not None:
#         from pyGDM2 import tools
#         geo, geo_wrong = tools.test_geometry(geo_full, step, plotting=False)
#     else:
#         geo = geo_full

#     return geo


## -- list of all available structure generators
STRUCT_LIST = [
    sphere,
    spheroid,
    rect_wire,
    nanorod,
    nanocone,
    nanodisc,
    prism,
    rhombus,
    hexagon,
    double_hexagon,
    diabolo,
    lshape_rect,
    lshape_round,
    lshape_rect_nonsym,
    split_ring,
    rect_split_ring,
]
