genice_core.dipole API documentation
Description: Optimizes the orientations of directed paths to reduce the net dipole moment.

# Module `genice_core.dipole`

Optimizes the orientations of directed paths to reduce the net dipole moment.

Expand source code

    """
    Optimizes the orientations of directed paths to reduce the net dipole moment.
    """
    from logging import getLogger, DEBUG
    from typing import Union

    import numpy as np
    import networkx as nx

    def vector_sum(
        dg: nx.DiGraph, vertexPositions: np.ndarray, isPeriodicBoundary: bool = False
    ) -> np.ndarray:
        """Net polarization (actually a vector sum) of a digraph

        Args:
            dg (nx.DiGraph): The digraph.
            vertexPositions (np.ndarray): Positions of the vertices.
            isPeriodicBoundary (bool, optional): If true, the vertex positions must be in fractional coordinate. Defaults to False.

        Returns:
            np.ndarray: net polarization
        """
        pol = np.zeros(3)
        for i, j in dg.edges():
            d = vertexPositions[j] - vertexPositions[i]
            if isPeriodicBoundary:
                d -= np.floor(d + 0.5)
            pol += d
        return pol

    def optimize(
        paths: list[list],
        vertexPositions: np.ndarray,
        dipoleOptimizationCycles: int = 2000,
        isPeriodicBoundary: bool = False,
        targetPol: Union[np.ndarray, None] = None,
    ) -> list[list]:
        """Minimize the net polarization by flipping several paths.

        It is assumed that every vector has an identical dipole moment.

        Args:
            paths (list of list): List of directed paths. A path is a list of integer. A path with identical labels at first and last items are considered to be cyclic.
            pos (nx.ndarray[*,3]): Positions of the nodes.
            maxiter (int, optional): Number of random orientations for the paths. Defaults to 1000.
            pbc (bool, optional): If `True`, the positions of the nodes must be in the fractional coordinate system.
            target (np.ndarray, optional): Target value for the dipole-moment optimization.
        Returns:
            list of paths: Optimized paths.
        """
        logger = getLogger()

        if dipoleOptimizationCycles < 1:
            return paths

        if targetPol is None:
            targetPol = np.zeros_like(vertexPositions[0])

        # polarized chains and cycles. Small cycle of dipoles are eliminated.
        polarizedEdges = []

        dipoles = []
        for i, path in enumerate(paths):
            if isPeriodicBoundary:
                # vectors between adjacent vertices.
                relativeVector = vertexPositions[path[1:]] - vertexPositions[path[:-1]]
                # PBC wrap
                relativeVector -= np.floor(relativeVector + 0.5)
                # total dipole along the chain (or a cycle)
                chainPol = np.sum(relativeVector, axis=0)
                # if it is large enough, i.e. if it is a spanning cycle,
                if chainPol @ chainPol > 1e-6:
                    logger.debug(path)
                    dipoles.append(chainPol)
                    polarizedEdges.append(i)
            else:
                # dipole moment of a path; NOTE: No PBC.
                if path[0] != path[-1]:
                    # If no PBC, a chain pol is simply an end-to-end pol.
                    chainPol = vertexPositions[path[-1]] - vertexPositions[path[0]]
                    dipoles.append(chainPol)
                    polarizedEdges.append(i)
        dipoles = np.array(dipoles)
        # logger.debug(dipoles)

        optimalParities = np.ones(len(dipoles))
        optimalPol = optimalParities @ dipoles - targetPol
        logger.info(f"init {optimalPol} dipole {targetPol}")

        for loop in range(dipoleOptimizationCycles):
            # random sequence of +1/-1
            parities = np.random.randint(2, size=len(dipoles)) * 2 - 1

            # Set directions to chains by parity.
            pol = parities @ dipoles - targetPol

            # If the new directions give better (smaller) net dipole moment,
            if pol @ pol < optimalPol @ optimalPol:
                # that is the optimal
                optimalPol = pol
                optimalParities = parities
                logger.info(f"{loop} {optimalPol} dipole")

                # if well-converged,
                if optimalPol @ optimalPol < 1e-10:
                    logger.debug("Optimized.")
                    break

        # invert some chains according to parity_optimal
        for i, parity in zip(polarizedEdges, optimalParities):
            if parity < 0:
                # invert the chain
                paths[i] = paths[i][::-1]

        return paths

## Functions

`def optimize(paths: list[list], vertexPositions: numpy.ndarray, dipoleOptimizationCycles: int = 2000, isPeriodicBoundary: bool = False, targetPol: Optional[numpy.ndarray] = None) ‑> list[list]`

  Minimize the net polarization by flipping several paths.

  It is assumed that every vector has an identical dipole moment.

  ## Args

  `paths` :`list` of `list`
    List of directed paths. A path is a list of integer. A path with identical labels at first and last items are considered to be cyclic.
  pos (nx.ndarray[*,3]): Positions of the nodes.
  `maxiter` :`int`, optional
    Number of random orientations for the paths. Defaults to 1000.
  `pbc` :`bool`, optional
    If `True`, the positions of the nodes must be in the fractional coordinate system.
  `target` :`np.ndarray`, optional
    Target value for the dipole-moment optimization.

  ## Returns

  `list` of `paths`
    Optimized paths.
  Expand source code

      def optimize(
          paths: list[list],
          vertexPositions: np.ndarray,
          dipoleOptimizationCycles: int = 2000,
          isPeriodicBoundary: bool = False,
          targetPol: Union[np.ndarray, None] = None,
      ) -> list[list]:
          """Minimize the net polarization by flipping several paths.

          It is assumed that every vector has an identical dipole moment.

          Args:
              paths (list of list): List of directed paths. A path is a list of integer. A path with identical labels at first and last items are considered to be cyclic.
              pos (nx.ndarray[*,3]): Positions of the nodes.
              maxiter (int, optional): Number of random orientations for the paths. Defaults to 1000.
              pbc (bool, optional): If `True`, the positions of the nodes must be in the fractional coordinate system.
              target (np.ndarray, optional): Target value for the dipole-moment optimization.
          Returns:
              list of paths: Optimized paths.
          """
          logger = getLogger()

          if dipoleOptimizationCycles < 1:
              return paths

          if targetPol is None:
              targetPol = np.zeros_like(vertexPositions[0])

          # polarized chains and cycles. Small cycle of dipoles are eliminated.
          polarizedEdges = []

          dipoles = []
          for i, path in enumerate(paths):
              if isPeriodicBoundary:
                  # vectors between adjacent vertices.
                  relativeVector = vertexPositions[path[1:]] - vertexPositions[path[:-1]]
                  # PBC wrap
                  relativeVector -= np.floor(relativeVector + 0.5)
                  # total dipole along the chain (or a cycle)
                  chainPol = np.sum(relativeVector, axis=0)
                  # if it is large enough, i.e. if it is a spanning cycle,
                  if chainPol @ chainPol > 1e-6:
                      logger.debug(path)
                      dipoles.append(chainPol)
                      polarizedEdges.append(i)
              else:
                  # dipole moment of a path; NOTE: No PBC.
                  if path[0] != path[-1]:
                      # If no PBC, a chain pol is simply an end-to-end pol.
                      chainPol = vertexPositions[path[-1]] - vertexPositions[path[0]]
                      dipoles.append(chainPol)
                      polarizedEdges.append(i)
          dipoles = np.array(dipoles)
          # logger.debug(dipoles)

          optimalParities = np.ones(len(dipoles))
          optimalPol = optimalParities @ dipoles - targetPol
          logger.info(f"init {optimalPol} dipole {targetPol}")

          for loop in range(dipoleOptimizationCycles):
              # random sequence of +1/-1
              parities = np.random.randint(2, size=len(dipoles)) * 2 - 1

              # Set directions to chains by parity.
              pol = parities @ dipoles - targetPol

              # If the new directions give better (smaller) net dipole moment,
              if pol @ pol < optimalPol @ optimalPol:
                  # that is the optimal
                  optimalPol = pol
                  optimalParities = parities
                  logger.info(f"{loop} {optimalPol} dipole")

                  # if well-converged,
                  if optimalPol @ optimalPol < 1e-10:
                      logger.debug("Optimized.")
                      break

          # invert some chains according to parity_optimal
          for i, parity in zip(polarizedEdges, optimalParities):
              if parity < 0:
                  # invert the chain
                  paths[i] = paths[i][::-1]

          return paths

`def vector_sum(dg: networkx.classes.digraph.DiGraph, vertexPositions: numpy.ndarray, isPeriodicBoundary: bool = False) ‑> numpy.ndarray`

  Net polarization (actually a vector sum) of a digraph

  ## Args

  `dg` :`nx.DiGraph`
    The digraph.
  `vertexPositions` :`np.ndarray`
    Positions of the vertices.
  `isPeriodicBoundary` :`bool`, optional
    If true, the vertex positions must be in fractional coordinate. Defaults to False.

  ## Returns

  `np.ndarray`
    net polarization
  Expand source code

      def vector_sum(
          dg: nx.DiGraph, vertexPositions: np.ndarray, isPeriodicBoundary: bool = False
      ) -> np.ndarray:
          """Net polarization (actually a vector sum) of a digraph

          Args:
              dg (nx.DiGraph): The digraph.
              vertexPositions (np.ndarray): Positions of the vertices.
              isPeriodicBoundary (bool, optional): If true, the vertex positions must be in fractional coordinate. Defaults to False.

          Returns:
              np.ndarray: net polarization
          """
          pol = np.zeros(3)
          for i, j in dg.edges():
              d = vertexPositions[j] - vertexPositions[i]
              if isPeriodicBoundary:
                  d -= np.floor(d + 0.5)
              pol += d
          return pol