text: # coding: utf-8
  # Copyright (c) Pymatgen Development Team.
  # Distributed under the terms of the MIT License.
  
  
  """
  This module contains the object used to describe the possible bonded atoms based on a Voronoi analysis.
  """
  
  __author__ = "David Waroquiers"
  __copyright__ = "Copyright 2012, The Materials Project"
  __credits__ = "Geoffroy Hautier"
  __version__ = "2.0"
  __maintainer__ = "David Waroquiers"
  __email__ = "david.waroquiers@gmail.com"
  __date__ = "Feb 20, 2016"
  
  import logging
  import time
  
  import numpy as np
  from monty.json import MSONable
  from scipy.spatial import Voronoi
  
  from pymatgen.analysis.chemenv.utils.coordination_geometry_utils import (
      get_lower_and_upper_f,
      my_solid_angle,
      rectangle_surface_intersection,
  )
  from pymatgen.analysis.chemenv.utils.defs_utils import AdditionalConditions
  from pymatgen.analysis.chemenv.utils.math_utils import normal_cdf_step
  from pymatgen.core.sites import PeriodicSite
  from pymatgen.core.structure import Structure
  
  
  def from_bson_voronoi_list2(bson_nb_voro_list2, structure):
      """
      Returns the voronoi_list needed for the VoronoiContainer object from a bson-encoded voronoi_list.
  
      Args:
          bson_nb_voro_list2: List of periodic sites involved in the Voronoi.
          structure: Structure object.
  
      Returns:
          The voronoi_list needed for the VoronoiContainer (with PeriodicSites as keys of the dictionary - not
          allowed in the BSON format).
      """
      voronoi_list = [None] * len(bson_nb_voro_list2)
      for isite, voro in enumerate(bson_nb_voro_list2):
          if voro is None or voro == "None":
              continue
          voronoi_list[isite] = []
          for psd, dd in voro:
              struct_site = structure[dd["index"]]
              periodic_site = PeriodicSite(
                  struct_site._species,
                  struct_site.frac_coords + psd[1],
                  struct_site._lattice,
                  properties=struct_site.properties,
              )
              dd["site"] = periodic_site
              voronoi_list[isite].append(dd)
      return voronoi_list
  
  
  class DetailedVoronoiContainer(MSONable):
      """
      Class used to store the full Voronoi of a given structure.
      """
  
      AC = AdditionalConditions()
      default_voronoi_cutoff = 10.0
      default_normalized_distance_tolerance = 1e-5
      default_normalized_angle_tolerance = 1e-3
  
      def __init__(
          self,
          structure=None,
          voronoi_list2=None,
          voronoi_cutoff=default_voronoi_cutoff,
          isites=None,
          normalized_distance_tolerance=default_normalized_distance_tolerance,
          normalized_angle_tolerance=default_normalized_angle_tolerance,
          additional_conditions=None,
          valences=None,
          maximum_distance_factor=None,
          minimum_angle_factor=None,
      ):
          """
          Constructor for the VoronoiContainer object. Either a structure is given, in which case the Voronoi is
          computed, or the different components of the VoronoiContainer are given (used in the from_dict method).
  
          Args:
              structure: Structure for which the Voronoi is computed.
              voronoi_list2: List of voronoi polyhedrons for each site.
              voronoi_cutoff: cutoff used for the voronoi.
              isites: indices of sites for which the Voronoi has to be computed.
              normalized_distance_tolerance: Tolerance for two normalized distances to be considered equal.
              normalized_angle_tolerance:Tolerance for two normalized angles to be considered equal.
              additional_conditions: Additional conditions to be used.
              valences: Valences of all the sites in the structure (used when additional conditions require it).
              maximum_distance_factor: The maximum distance factor to be considered.
              minimum_angle_factor: The minimum angle factor to be considered.
  
          Raises:
              RuntimeError if the Voronoi cannot be constructed.
          """
          self.normalized_distance_tolerance = normalized_distance_tolerance
          self.normalized_angle_tolerance = normalized_angle_tolerance
          if additional_conditions is None:
              self.additional_conditions = [self.AC.NONE, self.AC.ONLY_ACB]
          else:
              self.additional_conditions = additional_conditions
          self.valences = valences
          self.maximum_distance_factor = maximum_distance_factor
          self.minimum_angle_factor = minimum_angle_factor
          if isites is None:
              indices = list(range(len(structure)))
          else:
              indices = isites
          self.structure = structure
          logging.debug("Setting Voronoi list")
          if voronoi_list2 is not None:
              self.voronoi_list2 = voronoi_list2
          else:
              self.setup_voronoi_list(indices=indices, voronoi_cutoff=voronoi_cutoff)
          logging.debug("Setting neighbors distances and angles")
          t1 = time.process_time()
          self.setup_neighbors_distances_and_angles(indices=indices)
          t2 = time.process_time()
          logging.debug("Neighbors distances and angles set up in {:.2f} seconds".format(t2 - t1))
  
      def setup_voronoi_list(self, indices, voronoi_cutoff):
          """
          Set up of the voronoi list of neighbours by calling qhull.
  
          Args:
              indices: indices of the sites for which the Voronoi is needed.
              voronoi_cutoff: Voronoi cutoff for the search of neighbours.
  
          Raises:
              RuntimeError: If an infinite vertex is found in the voronoi construction.
          """
          self.voronoi_list2 = [None] * len(self.structure)
          self.voronoi_list_coords = [None] * len(self.structure)
          logging.debug("Getting all neighbors in structure")
          struct_neighbors = self.structure.get_all_neighbors(voronoi_cutoff, include_index=True)
          t1 = time.process_time()
          logging.debug("Setting up Voronoi list :")
  
          for jj, isite in enumerate(indices):
              logging.debug("  - Voronoi analysis for site #{:d} ({:d}/{:d})".format(isite, jj + 1, len(indices)))
              site = self.structure[isite]
              neighbors1 = [(site, 0.0, isite)]
              neighbors1.extend(struct_neighbors[isite])
              distances = [i[1] for i in sorted(neighbors1, key=lambda s: s[1])]
              neighbors = [i[0] for i in sorted(neighbors1, key=lambda s: s[1])]
              qvoronoi_input = [s.coords for s in neighbors]
              voro = Voronoi(points=qvoronoi_input, qhull_options="o Fv")
              all_vertices = voro.vertices
  
              results2 = []
              maxangle = 0.0
              mindist = 10000.0
              for iridge, ridge_points in enumerate(voro.ridge_points):
                  if 0 in ridge_points:
                      ridge_vertices_indices = voro.ridge_vertices[iridge]
                      if -1 in ridge_vertices_indices:
                          raise RuntimeError(
                              "This structure is pathological," " infinite vertex in the voronoi " "construction"
                          )
  
                      ridge_point2 = max(ridge_points)
                      facets = [all_vertices[i] for i in ridge_vertices_indices]
                      sa = my_solid_angle(site.coords, facets)
                      maxangle = max([sa, maxangle])
  
                      mindist = min([mindist, distances[ridge_point2]])
                      for iii, sss in enumerate(self.structure):
                          if neighbors[ridge_point2].is_periodic_image(sss, tolerance=1.0e-6):
                              myindex = iii
                              break
                      results2.append(
                          {
                              "site": neighbors[ridge_point2],
                              "angle": sa,
                              "distance": distances[ridge_point2],
                              "index": myindex,
                          }
                      )
              for dd in results2:
                  dd["normalized_angle"] = dd["angle"] / maxangle
                  dd["normalized_distance"] = dd["distance"] / mindist
              self.voronoi_list2[isite] = results2
              self.voronoi_list_coords[isite] = np.array([dd["site"].coords for dd in results2])
          t2 = time.process_time()
          logging.debug("Voronoi list set up in {:.2f} seconds".format(t2 - t1))
  
      def setup_neighbors_distances_and_angles(self, indices):
          """
          Initializes the angle and distance separations.
  
          Args:
              indices: Indices of the sites for which the Voronoi is needed.
          """
          self.neighbors_distances = [None] * len(self.structure)
          self.neighbors_normalized_distances = [None] * len(self.structure)
          self.neighbors_angles = [None] * len(self.structure)
          self.neighbors_normalized_angles = [None] * len(self.structure)
          for isite in indices:
              results = self.voronoi_list2[isite]
              if results is None:
                  continue
              # Initializes neighbors distances and normalized distances groups
              self.neighbors_distances[isite] = []
              self.neighbors_normalized_distances[isite] = []
              normalized_distances = [nb_dict["normalized_distance"] for nb_dict in results]
              isorted_distances = np.argsort(normalized_distances)
              self.neighbors_normalized_distances[isite].append(
                  {
                      "min": normalized_distances[isorted_distances[0]],
                      "max": normalized_distances[isorted_distances[0]],
                  }
              )
              self.neighbors_distances[isite].append(
                  {
                      "min": results[isorted_distances[0]]["distance"],
                      "max": results[isorted_distances[0]]["distance"],
                  }
              )
              icurrent = 0
              nb_indices = {int(isorted_distances[0])}
              dnb_indices = {int(isorted_distances[0])}
              for idist in iter(isorted_distances):
                  wd = normalized_distances[idist]
                  if self.maximum_distance_factor is not None:
                      if wd > self.maximum_distance_factor:
                          self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                          self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                          self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                          self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                          break
                  if np.isclose(
                      wd,
                      self.neighbors_normalized_distances[isite][icurrent]["max"],
                      rtol=0.0,
                      atol=self.normalized_distance_tolerance,
                  ):
                      self.neighbors_normalized_distances[isite][icurrent]["max"] = wd
                      self.neighbors_distances[isite][icurrent]["max"] = results[idist]["distance"]
                      dnb_indices.add(int(idist))
                  else:
                      self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                      self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                      self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                      self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                      dnb_indices = {int(idist)}
                      self.neighbors_normalized_distances[isite].append({"min": wd, "max": wd})
                      self.neighbors_distances[isite].append(
                          {
                              "min": results[idist]["distance"],
                              "max": results[idist]["distance"],
                          }
                      )
                      icurrent += 1
                  nb_indices.add(int(idist))
              else:
                  self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                  self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
                  self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                  self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
              for idist in range(len(self.neighbors_distances[isite]) - 1):
                  dist_dict = self.neighbors_distances[isite][idist]
                  dist_dict_next = self.neighbors_distances[isite][idist + 1]
                  dist_dict["next"] = dist_dict_next["min"]
                  ndist_dict = self.neighbors_normalized_distances[isite][idist]
                  ndist_dict_next = self.neighbors_normalized_distances[isite][idist + 1]
                  ndist_dict["next"] = ndist_dict_next["min"]
              if self.maximum_distance_factor is not None:
                  dfact = self.maximum_distance_factor
              else:
                  dfact = self.default_voronoi_cutoff / self.neighbors_distances[isite][0]["min"]
              self.neighbors_normalized_distances[isite][-1]["next"] = dfact
              self.neighbors_distances[isite][-1]["next"] = dfact * self.neighbors_distances[isite][0]["min"]
              # Initializes neighbors angles and normalized angles groups
              self.neighbors_angles[isite] = []
              self.neighbors_normalized_angles[isite] = []
              normalized_angles = [nb_dict["normalized_angle"] for nb_dict in results]
              isorted_angles = np.argsort(normalized_angles)[::-1]
              self.neighbors_normalized_angles[isite].append(
                  {
                      "max": normalized_angles[isorted_angles[0]],
                      "min": normalized_angles[isorted_angles[0]],
                  }
              )
              self.neighbors_angles[isite].append(
                  {
                      "max": results[isorted_angles[0]]["angle"],
                      "min": results[isorted_angles[0]]["angle"],
                  }
              )
              icurrent = 0
              nb_indices = {int(isorted_angles[0])}
              dnb_indices = {int(isorted_angles[0])}
              for iang in iter(isorted_angles):
                  wa = normalized_angles[iang]
                  if self.minimum_angle_factor is not None:
                      if wa < self.minimum_angle_factor:
                          self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                          self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                          self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                          self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                          break
                  if np.isclose(
                      wa,
                      self.neighbors_normalized_angles[isite][icurrent]["min"],
                      rtol=0.0,
                      atol=self.normalized_angle_tolerance,
                  ):
                      self.neighbors_normalized_angles[isite][icurrent]["min"] = wa
                      self.neighbors_angles[isite][icurrent]["min"] = results[iang]["angle"]
                      dnb_indices.add(int(iang))
                  else:
                      self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                      self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                      self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                      self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                      dnb_indices = {int(iang)}
                      self.neighbors_normalized_angles[isite].append({"max": wa, "min": wa})
                      self.neighbors_angles[isite].append({"max": results[iang]["angle"], "min": results[iang]["angle"]})
                      icurrent += 1
                  nb_indices.add(int(iang))
              else:
                  self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                  self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
                  self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
                  self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
              for iang in range(len(self.neighbors_angles[isite]) - 1):
                  ang_dict = self.neighbors_angles[isite][iang]
                  ang_dict_next = self.neighbors_angles[isite][iang + 1]
                  ang_dict["next"] = ang_dict_next["max"]
                  nang_dict = self.neighbors_normalized_angles[isite][iang]
                  nang_dict_next = self.neighbors_normalized_angles[isite][iang + 1]
                  nang_dict["next"] = nang_dict_next["max"]
              if self.minimum_angle_factor is not None:
                  afact = self.minimum_angle_factor
              else:
                  afact = 0.0
              self.neighbors_normalized_angles[isite][-1]["next"] = afact
              self.neighbors_angles[isite][-1]["next"] = afact * self.neighbors_angles[isite][0]["max"]
  
      def _precompute_additional_conditions(self, ivoronoi, voronoi, valences):
          additional_conditions = {ac: [] for ac in self.additional_conditions}
          for ips, (ps, vals) in enumerate(voronoi):
              for ac in self.additional_conditions:
                  additional_conditions[ac].append(
                      self.AC.check_condition(
                          condition=ac,
                          structure=self.structure,
                          parameters={
                              "valences": valences,
                              "neighbor_index": vals["index"],
                              "site_index": ivoronoi,
                          },
                      )
                  )
          return additional_conditions
  
      def _precompute_distance_conditions(self, ivoronoi, voronoi):
          distance_conditions = []
          for idp, dp_dict in enumerate(self.neighbors_normalized_distances[ivoronoi]):
              distance_conditions.append([])
              dp = dp_dict["max"]
              for ips, (ps, vals) in enumerate(voronoi):
                  distance_conditions[idp].append(
                      vals["normalized_distance"] <= dp
                      or np.isclose(
                          vals["normalized_distance"],
                          dp,
                          rtol=0.0,
                          atol=self.normalized_distance_tolerance / 2.0,
                      )
                  )
          return distance_conditions
  
      def _precompute_angle_conditions(self, ivoronoi, voronoi):
          angle_conditions = []
          for iap, ap_dict in enumerate(self.neighbors_normalized_angles[ivoronoi]):
              angle_conditions.append([])
              ap = ap_dict["max"]
              for ips, (ps, vals) in enumerate(voronoi):
                  angle_conditions[iap].append(
                      vals["normalized_angle"] >= ap
                      or np.isclose(
                          vals["normalized_angle"],
                          ap,
                          rtol=0.0,
                          atol=self.normalized_angle_tolerance / 2.0,
                      )
                  )
          return angle_conditions
  
      # def neighbors_map(self, isite, distfactor, angfactor, additional_condition):
      #     if self.neighbors_normalized_distances[isite] is None:
      #         return None
      #     dist_where = np.argwhere(
      #         np.array([wd['min'] for wd in self.neighbors_normalized_distances[isite]]) <= distfactor)
      #     if len(dist_where) == 0:
      #         return None
      #     idist = dist_where[-1][0]
      #     ang_where = np.argwhere(np.array([wa['max'] for wa in self.neighbors_normalized_angles[isite]]) >= angfactor)
      #     if len(ang_where) == 0:
      #         return None
      #     iang = ang_where[0][0]
      #     if self.additional_conditions.count(additional_condition) != 1:
      #         return None
      #     i_additional_condition = self.additional_conditions.index(additional_condition)
      #     return {'i_distfactor': idist, 'i_angfactor': iang, 'i_additional_condition': i_additional_condition}
  
      def neighbors_surfaces(self, isite, surface_calculation_type=None, max_dist=2.0):
          """
          Get the different surfaces corresponding to the different distance-angle cutoffs for a given site.
  
          Args:
              isite: Index of the site
              surface_calculation_type: How to compute the surface.
              max_dist: The maximum distance factor to be considered.
  
          Returns:
              Surfaces for each distance-angle cutoff.
          """
          if self.voronoi_list2[isite] is None:
              return None
          bounds_and_limits = self.voronoi_parameters_bounds_and_limits(isite, surface_calculation_type, max_dist)
          distance_bounds = bounds_and_limits["distance_bounds"]
          angle_bounds = bounds_and_limits["angle_bounds"]
          surfaces = np.zeros((len(distance_bounds), len(angle_bounds)), np.float_)
          for idp in range(len(distance_bounds) - 1):
              this_dist_plateau = distance_bounds[idp + 1] - distance_bounds[idp]
              for iap in range(len(angle_bounds) - 1):
                  this_ang_plateau = angle_bounds[iap + 1] - angle_bounds[iap]
                  surfaces[idp][iap] = np.absolute(this_dist_plateau * this_ang_plateau)
          return surfaces
  
      def neighbors_surfaces_bounded(self, isite, surface_calculation_options=None):
          """
          Get the different surfaces (using boundaries) corresponding to the different distance-angle cutoffs
          for a given site.
  
          Args:
              isite: Index of the site.
              surface_calculation_options: Options for the boundaries.
  
          Returns:
              Surfaces for each distance-angle cutoff.
          """
          if self.voronoi_list2[isite] is None:
              return None
          if surface_calculation_options is None:
              surface_calculation_options = {
                  "type": "standard_elliptic",
                  "distance_bounds": {"lower": 1.2, "upper": 1.8},
                  "angle_bounds": {"lower": 0.1, "upper": 0.8},
              }
          if surface_calculation_options["type"] in [
              "standard_elliptic",
              "standard_diamond",
              "standard_spline",
          ]:
              plot_type = {
                  "distance_parameter": ("initial_normalized", None),
                  "angle_parameter": ("initial_normalized", None),
              }
          else:
              raise ValueError(
                  'Type "{}" for the surface calculation in DetailedVoronoiContainer '
                  "is invalid".format(surface_calculation_options["type"])
              )
          max_dist = surface_calculation_options["distance_bounds"]["upper"] + 0.1
          bounds_and_limits = self.voronoi_parameters_bounds_and_limits(
              isite=isite, plot_type=plot_type, max_dist=max_dist
          )
  
          distance_bounds = bounds_and_limits["distance_bounds"]
          angle_bounds = bounds_and_limits["angle_bounds"]
          lower_and_upper_functions = get_lower_and_upper_f(surface_calculation_options=surface_calculation_options)
          mindist = surface_calculation_options["distance_bounds"]["lower"]
          maxdist = surface_calculation_options["distance_bounds"]["upper"]
          minang = surface_calculation_options["angle_bounds"]["lower"]
          maxang = surface_calculation_options["angle_bounds"]["upper"]
  
          f_lower = lower_and_upper_functions["lower"]
          f_upper = lower_and_upper_functions["upper"]
          surfaces = np.zeros((len(distance_bounds), len(angle_bounds)), np.float_)
          for idp in range(len(distance_bounds) - 1):
              dp1 = distance_bounds[idp]
              dp2 = distance_bounds[idp + 1]
              if dp2 < mindist or dp1 > maxdist:
                  continue
              if dp1 < mindist:
                  d1 = mindist
              else:
                  d1 = dp1
              if dp2 > maxdist:
                  d2 = maxdist
              else:
                  d2 = dp2
              for iap in range(len(angle_bounds) - 1):
                  ap1 = angle_bounds[iap]
                  ap2 = angle_bounds[iap + 1]
                  if ap1 > ap2:
                      ap1 = angle_bounds[iap + 1]
                      ap2 = angle_bounds[iap]
                  if ap2 < minang or ap1 > maxang:
                      continue
                  intersection, interror = rectangle_surface_intersection(
                      rectangle=((d1, d2), (ap1, ap2)),
                      f_lower=f_lower,
                      f_upper=f_upper,
                      bounds_lower=[mindist, maxdist],
                      bounds_upper=[mindist, maxdist],
                      check=False,
                  )
                  surfaces[idp][iap] = intersection
          return surfaces
  
      @staticmethod
      def _get_vertices_dist_ang_indices(parameter_indices_list):
          pp0 = [pp[0] for pp in parameter_indices_list]
          pp1 = [pp[1] for pp in parameter_indices_list]
          min_idist = min(pp0)
          min_iang = min(pp1)
          max_idist = max(pp0)
          max_iang = max(pp1)
          i_min_angs = np.argwhere(np.array(pp1) == min_iang)
          i_max_dists = np.argwhere(np.array(pp0) == max_idist)
          pp0_at_min_iang = [pp0[ii[0]] for ii in i_min_angs]
          pp1_at_max_idist = [pp1[ii[0]] for ii in i_max_dists]
          max_idist_at_min_iang = max(pp0_at_min_iang)
          min_iang_at_max_idist = min(pp1_at_max_idist)
  
          p1 = (min_idist, min_iang)
          p2 = (max_idist_at_min_iang, min_iang)
          p3 = (max_idist_at_min_iang, min_iang_at_max_idist)
          p4 = (max_idist, min_iang_at_max_idist)
          p5 = (max_idist, max_iang)
          p6 = (min_idist, max_iang)
  
          return [p1, p2, p3, p4, p5, p6]
  
      def maps_and_surfaces(
          self,
          isite,
          surface_calculation_type=None,
          max_dist=2.0,
          additional_conditions=None,
      ):
          """
          Get the different surfaces and their cn_map corresponding to the different distance-angle cutoffs
          for a given site.
  
          Args:
              isite: Index of the site
              surface_calculation_type: How to compute the surface.
              max_dist: The maximum distance factor to be considered.
              additional_conditions: If additional conditions have to be considered.
  
          Returns:
              Surfaces and cn_map's for each distance-angle cutoff.
          """
          if self.voronoi_list2[isite] is None:
              return None
          if additional_conditions is None:
              additional_conditions = [self.AC.ONLY_ACB]
          surfaces = self.neighbors_surfaces(
              isite=isite,
              surface_calculation_type=surface_calculation_type,
              max_dist=max_dist,
          )
          maps_and_surfaces = []
          for cn, value in self._unique_coordinated_neighbors_parameters_indices[isite].items():  # pylint: disable=E1101
              for imap, list_parameters_indices in enumerate(value):
                  thissurf = 0.0
                  for (idp, iap, iacb) in list_parameters_indices:
                      if iacb in additional_conditions:
                          thissurf += surfaces[idp, iap]
                  maps_and_surfaces.append(
                      {
                          "map": (cn, imap),
                          "surface": thissurf,
                          "parameters_indices": list_parameters_indices,
                      }
                  )
          return maps_and_surfaces
  
      def maps_and_surfaces_bounded(self, isite, surface_calculation_options=None, additional_conditions=None):
          """
          Get the different surfaces (using boundaries) and their cn_map corresponding to the different
          distance-angle cutoffs for a given site.
  
          Args:
              isite: Index of the site
              surface_calculation_options: Options for the boundaries.
              additional_conditions: If additional conditions have to be considered.
  
          Returns:
              Surfaces and cn_map's for each distance-angle cutoff.
          """
          if self.voronoi_list2[isite] is None:
              return None
          if additional_conditions is None:
              additional_conditions = [self.AC.ONLY_ACB]
          surfaces = self.neighbors_surfaces_bounded(isite=isite, surface_calculation_options=surface_calculation_options)
          maps_and_surfaces = []
          for cn, value in self._unique_coordinated_neighbors_parameters_indices[isite].items():  # pylint: disable=E1101
              for imap, list_parameters_indices in enumerate(value):
                  thissurf = 0.0
                  for (idp, iap, iacb) in list_parameters_indices:
                      if iacb in additional_conditions:
                          thissurf += surfaces[idp, iap]
                  maps_and_surfaces.append(
                      {
                          "map": (cn, imap),
                          "surface": thissurf,
                          "parameters_indices": list_parameters_indices,
                      }
                  )
          return maps_and_surfaces
  
      def neighbors(self, isite, distfactor, angfactor, additional_condition=None):
          """
          Get the neighbors of a given site corresponding to a given distance and angle factor.
  
          Args:
              isite: Index of the site.
              distfactor: Distance factor.
              angfactor: Angle factor.
              additional_condition: Additional condition to be used (currently not implemented).
  
          Returns:
              List of neighbors of the given site for the given distance and angle factors.
          """
          idist = None
          dfact = None
          for iwd, wd in enumerate(self.neighbors_normalized_distances[isite]):
              if distfactor >= wd["min"]:
                  idist = iwd
                  dfact = wd["max"]
              else:
                  break
          iang = None
          afact = None
          for iwa, wa in enumerate(self.neighbors_normalized_angles[isite]):
              if angfactor <= wa["max"]:
                  iang = iwa
                  afact = wa["min"]
              else:
                  break
          if idist is None or iang is None:
              raise ValueError("Distance or angle parameter not found ...")
  
          return [
              nb
              for nb in self.voronoi_list2[isite]
              if nb["normalized_distance"] <= dfact and nb["normalized_angle"] >= afact
          ]
  
      def voronoi_parameters_bounds_and_limits(self, isite, plot_type, max_dist):
          """
          Get the different boundaries and limits of the distance and angle factors for the given site.
  
          Args:
              isite: Index of the site.
              plot_type: Types of distance/angle parameters to get.
              max_dist: Maximum distance factor.
  
          Returns:
              Distance and angle bounds and limits.
          """
          # Initializes the distance and angle parameters
          if self.voronoi_list2[isite] is None:
              return None
          if plot_type is None:
              plot_type = {
                  "distance_parameter": ("initial_inverse_opposite", None),
                  "angle_parameter": ("initial_opposite", None),
              }
          dd = [dist["min"] for dist in self.neighbors_normalized_distances[isite]]
          dd[0] = 1.0
          if plot_type["distance_parameter"][0] == "initial_normalized":
              dd.append(max_dist)
              distance_bounds = np.array(dd)
              dist_limits = [1.0, max_dist]
          elif plot_type["distance_parameter"][0] == "initial_inverse_opposite":
              ddinv = [1.0 / dist for dist in dd]
              ddinv.append(0.0)
              distance_bounds = np.array([1.0 - invdist for invdist in ddinv])
              dist_limits = [0.0, 1.0]
          elif plot_type["distance_parameter"][0] == "initial_inverse3_opposite":
              ddinv = [1.0 / dist ** 3.0 for dist in dd]
              ddinv.append(0.0)
              distance_bounds = np.array([1.0 - invdist for invdist in ddinv])
              dist_limits = [0.0, 1.0]
          else:
              raise NotImplementedError(
                  'Plotting type "{}" ' "for the distance is not implemented".format(plot_type["distance_parameter"])
              )
          if plot_type["angle_parameter"][0] == "initial_normalized":
              aa = [0.0]
              aa.extend([ang["max"] for ang in self.neighbors_normalized_angles[isite]])
              angle_bounds = np.array(aa)
          elif plot_type["angle_parameter"][0] == "initial_opposite":
              aa = [0.0]
              aa.extend([ang["max"] for ang in self.neighbors_normalized_angles[isite]])
              aa = [1.0 - ang for ang in aa]
              angle_bounds = np.array(aa)
          else:
              raise NotImplementedError(
                  'Plotting type "{}" ' "for the angle is not implemented".format(plot_type["angle_parameter"])
              )
          ang_limits = [0.0, 1.0]
          return {
              "distance_bounds": distance_bounds,
              "distance_limits": dist_limits,
              "angle_bounds": angle_bounds,
              "angle_limits": ang_limits,
          }
  
      def is_close_to(self, other, rtol=0.0, atol=1e-8):
          """
          Whether two DetailedVoronoiContainer objects are close to each other.
  
          Args:
              other: Another DetailedVoronoiContainer to be compared with.
              rtol: Relative tolerance to compare values.
              atol: Absolute tolerance to compare values.
  
          Returns:
              True if the two DetailedVoronoiContainer are close to each other.
          """
          isclose = (
              np.isclose(
                  self.normalized_angle_tolerance,
                  other.normalized_angle_tolerance,
                  rtol=rtol,
                  atol=atol,
              )
              and np.isclose(
                  self.normalized_distance_tolerance,
                  other.normalized_distance_tolerance,
                  rtol=rtol,
                  atol=atol,
              )
              and self.additional_conditions == other.additional_conditions
              and self.valences == other.valences
          )
          if not isclose:
              return isclose
          for isite, site_voronoi in enumerate(self.voronoi_list2):
              self_to_other_nbs = {}
              for inb, nb in enumerate(site_voronoi):
                  if nb is None:
                      if other.voronoi_list2[isite] is None:
                          continue
                      return False
                  if other.voronoi_list2[isite] is None:
                      return False
                  nb_other = None
                  for inb2, nb2 in enumerate(other.voronoi_list2[isite]):
                      if nb["site"] == nb2["site"]:
                          self_to_other_nbs[inb] = inb2
                          nb_other = nb2
                          break
                  if nb_other is None:
                      return False
                  if not np.isclose(nb["distance"], nb_other["distance"], rtol=rtol, atol=atol):
                      return False
                  if not np.isclose(nb["angle"], nb_other["angle"], rtol=rtol, atol=atol):
                      return False
                  if not np.isclose(
                      nb["normalized_distance"],
                      nb_other["normalized_distance"],
                      rtol=rtol,
                      atol=atol,
                  ):
                      return False
                  if not np.isclose(
                      nb["normalized_angle"],
                      nb_other["normalized_angle"],
                      rtol=rtol,
                      atol=atol,
                  ):
                      return False
                  if nb["index"] != nb_other["index"]:
                      return False
                  if nb["site"] != nb_other["site"]:
                      return False
          return True
  
      def get_rdf_figure(self, isite, normalized=True, figsize=None, step_function=None):
          """
          Get the Radial Distribution Figure for a given site.
  
          Args:
              isite: Index of the site.
              normalized: Whether to normalize distances.
              figsize: Size of the figure.
              step_function: Type of step function to be used for the RDF.
  
          Returns:
              Matplotlib figure.
          """
  
          def dp_func(dp):
              return 1.0 - 1.0 / np.power(dp, 3.0)
  
          import matplotlib.pyplot as plt
  
          if step_function is None:
              step_function = {"type": "normal_cdf", "scale": 0.0001}
  
          # Initializes the figure
          if figsize is None:
              fig = plt.figure()
          else:
              fig = plt.figure(figsize=figsize)
          subplot = fig.add_subplot(111)
          if normalized:
              dists = self.neighbors_normalized_distances[isite]
          else:
              dists = self.neighbors_distances[isite]
  
          if step_function["type"] == "step_function":
              isorted = np.argsort([dd["min"] for dd in dists])
              sorted_dists = [dists[ii]["min"] for ii in isorted]
              dnb_dists = [len(dists[ii]["dnb_indices"]) for ii in isorted]
              xx = [0.0]
              yy = [0.0]
              for idist, dist in enumerate(sorted_dists):
                  xx.append(dist)
                  xx.append(dist)
                  yy.append(yy[-1])
                  yy.append(yy[-1] + dnb_dists[idist])
              xx.append(1.1 * xx[-1])
              yy.append(yy[-1])
          elif step_function["type"] == "normal_cdf":
              scale = step_function["scale"]
              mydists = [dp_func(dd["min"]) for dd in dists]
              mydcns = [len(dd["dnb_indices"]) for dd in dists]
              xx = np.linspace(0.0, 1.1 * max(mydists), num=500)
              yy = np.zeros_like(xx)
              for idist, dist in enumerate(mydists):
                  yy += mydcns[idist] * normal_cdf_step(xx, mean=dist, scale=scale)
          else:
              raise ValueError('Step function of type "{}" is not allowed'.format(step_function["type"]))
          subplot.plot(xx, yy)
  
          return fig
  
      def get_sadf_figure(self, isite, normalized=True, figsize=None, step_function=None):
          """
          Get the Solid Angle Distribution Figure for a given site.
          Args:
              isite: Index of the site.
              normalized: Whether to normalize angles.
              figsize: Size of the figure.
              step_function: Type of step function to be used for the SADF.
  
          Returns:
              Matplotlib figure.
          """
  
          def ap_func(ap):
              return np.power(ap, -0.1)
  
          import matplotlib.pyplot as plt
  
          if step_function is None:
              step_function = {"type": "step_function", "scale": 0.0001}
  
          # Initializes the figure
          if figsize is None:
              fig = plt.figure()
          else:
              fig = plt.figure(figsize=figsize)
          subplot = fig.add_subplot(111)
          if normalized:
              angs = self.neighbors_normalized_angles[isite]
          else:
              angs = self.neighbors_angles[isite]
  
          if step_function["type"] == "step_function":
              isorted = np.argsort([ap_func(aa["min"]) for aa in angs])
              sorted_angs = [ap_func(angs[ii]["min"]) for ii in isorted]
              dnb_angs = [len(angs[ii]["dnb_indices"]) for ii in isorted]
              xx = [0.0]
              yy = [0.0]
              for iang, ang in enumerate(sorted_angs):
                  xx.append(ang)
                  xx.append(ang)
                  yy.append(yy[-1])
                  yy.append(yy[-1] + dnb_angs[iang])
              xx.append(1.1 * xx[-1])
              yy.append(yy[-1])
          elif step_function["type"] == "normal_cdf":
              scale = step_function["scale"]
              myangs = [ap_func(aa["min"]) for aa in angs]
              mydcns = [len(dd["dnb_indices"]) for dd in angs]
              xx = np.linspace(0.0, 1.1 * max(myangs), num=500)
              yy = np.zeros_like(xx)
              for iang, ang in enumerate(myangs):
                  yy += mydcns[iang] * normal_cdf_step(xx, mean=ang, scale=scale)
          else:
              raise ValueError('Step function of type "{}" is not allowed'.format(step_function["type"]))
          subplot.plot(xx, yy)
  
          return fig
  
      def __eq__(self, other):
          return (
              self.normalized_angle_tolerance == other.normalized_angle_tolerance
              and self.normalized_distance_tolerance == other.normalized_distance_tolerance
              and self.additional_conditions == other.additional_conditions
              and self.valences == other.valences
              and self.voronoi_list2 == other.voronoi_list2
              and self.structure == other.structure
          )
  
      def __ne__(self, other):
          return not self == other
  
      def to_bson_voronoi_list2(self):
          """
          Transforms the voronoi_list into a vlist + bson_nb_voro_list, that are BSON-encodable.
  
          Returns:
              [vlist, bson_nb_voro_list], to be used in the as_dict method.
          """
          bson_nb_voro_list2 = [None] * len(self.voronoi_list2)
          for ivoro, voro in enumerate(self.voronoi_list2):
              if voro is None or voro == "None":
                  continue
              site_voro = []
              # {'site': neighbors[nn[1]],
              #  'angle': sa,
              #  'distance': distances[nn[1]],
              #  'index': myindex}
              for nb_dict in voro:
                  site = nb_dict["site"]
                  site_dict = {key: val for key, val in nb_dict.items() if key not in ["site"]}
                  # site_voro.append([ps.as_dict(), dd]) [float(c) for c in self.frac_coords]
                  diff = site.frac_coords - self.structure[nb_dict["index"]].frac_coords
                  site_voro.append([[nb_dict["index"], [float(c) for c in diff]], site_dict])
              bson_nb_voro_list2[ivoro] = site_voro
          return bson_nb_voro_list2
  
      def as_dict(self):
          """
          Bson-serializable dict representation of the VoronoiContainer.
  
          Returns:
              dictionary that is BSON-encodable.
          """
          bson_nb_voro_list2 = self.to_bson_voronoi_list2()
          return {
              "@module": self.__class__.__module__,
              "@class": self.__class__.__name__,
              "bson_nb_voro_list2": bson_nb_voro_list2,
              # "neighbors_lists": self.neighbors_lists,
              "structure": self.structure.as_dict(),
              "normalized_angle_tolerance": self.normalized_angle_tolerance,
              "normalized_distance_tolerance": self.normalized_distance_tolerance,
              "additional_conditions": self.additional_conditions,
              "valences": self.valences,
              "maximum_distance_factor": self.maximum_distance_factor,
              "minimum_angle_factor": self.minimum_angle_factor,
          }
  
      @classmethod
      def from_dict(cls, d):
          """
          Reconstructs the VoronoiContainer object from a dict representation of the VoronoiContainer created using
          the as_dict method.
  
          Args:
              d: dict representation of the VoronoiContainer object.
  
          Returns:
              VoronoiContainer object.
          """
          structure = Structure.from_dict(d["structure"])
          voronoi_list2 = from_bson_voronoi_list2(d["bson_nb_voro_list2"], structure)
          maximum_distance_factor = d["maximum_distance_factor"] if "maximum_distance_factor" in d else None
          minimum_angle_factor = d["minimum_angle_factor"] if "minimum_angle_factor" in d else None
          return cls(
              structure=structure,
              voronoi_list2=voronoi_list2,
              # neighbors_lists=neighbors_lists,
              normalized_angle_tolerance=d["normalized_angle_tolerance"],
              normalized_distance_tolerance=d["normalized_distance_tolerance"],
              additional_conditions=d["additional_conditions"],
              valences=d["valences"],
              maximum_distance_factor=maximum_distance_factor,
              minimum_angle_factor=minimum_angle_factor,
          )
  