text: """Boundary conditions."""
  
  from __future__ import absolute_import
  from __future__ import division
  from __future__ import print_function
  
  import numbers
  from abc import ABC, abstractmethod
  
  import numpy as np
  
  from .. import backend as bkd
  from .. import config
  from .. import gradients as grad
  
  
  # TODO: Performance issue of backend pytorch.
  # For some BCs, we need to call self.func(X[beg:end]) in BC.error(). For backend
  # tensorflow.compat.v1/tensorflow, self.func() is only called once in graph mode, but
  # for backend pytorch, it will be recomputed in each iteration. To reduce the
  # computation, one solution is that we cache the results by using @functools.cache, but
  # numpy.ndarray is unhashable. So we need to implement a hash function and a cache
  # function for numpy.ndarray.
  # References:
  # - https://docs.python.org/3/library/functools.html
  # - https://stackoverflow.com/questions/52331944/cache-decorator-for-numpy-arrays
  # - https://forum.kavli.tudelft.nl/t/caching-of-python-functions-with-array-input/59/6
  # - https://stackoverflow.com/questions/16589791/most-efficient-property-to-hash-for-numpy-array/16592241#16592241
  # - https://stackoverflow.com/questions/39674863/python-alternative-for-using-numpy-array-as-key-in-dictionary/47922199
  # Similarly, self.geom.boundary_normal() in BC.normal_derivative()
  
  
  class BC(ABC):
      """Boundary condition base class.
  
      Args:
          on_boundary: (x, Geometry.on_boundary(x)) -> True/False.
          component: The output component satisfying this BC.
      """
  
      def __init__(self, geom, on_boundary, component):
          self.geom = geom
          self.on_boundary = lambda x, on: np.array(
              [on_boundary(x[i], on[i]) for i in range(len(x))]
          )
          self.component = component
  
      def filter(self, X):
          return X[self.on_boundary(X, self.geom.on_boundary(X))]
  
      def collocation_points(self, X):
          return self.filter(X)
  
      def normal_derivative(self, X, inputs, outputs, beg, end):
          dydx = grad.jacobian(outputs, inputs, i=self.component, j=None)[beg:end]
          n = bkd.from_numpy(self.geom.boundary_normal(X[beg:end]))
          return bkd.sum(dydx * n, 1, keepdims=True)
  
      @abstractmethod
      def error(self, X, inputs, outputs, beg, end):
          """Returns the error."""
  
  
  class DirichletBC(BC):
      """Dirichlet boundary conditions: y(x) = func(x)."""
  
      def __init__(self, geom, func, on_boundary, component=0):
          super(DirichletBC, self).__init__(geom, on_boundary, component)
          self.func = func
  
      def error(self, X, inputs, outputs, beg, end):
          values = self.func(X[beg:end])
          if not isinstance(values, numbers.Number) and values.shape[1] != 1:
              raise RuntimeError(
                  "DirichletBC func should return an array of shape N by 1 for a single component."
                  "Use argument 'component' for different components."
              )
          values = bkd.as_tensor(values, dtype=config.real(bkd.lib))
          return outputs[beg:end, self.component : self.component + 1] - values
  
  
  class NeumannBC(BC):
      """Neumann boundary conditions: dy/dn(x) = func(x)."""
  
      def __init__(self, geom, func, on_boundary, component=0):
          super(NeumannBC, self).__init__(geom, on_boundary, component)
          self.func = func
  
      def error(self, X, inputs, outputs, beg, end):
          values = bkd.as_tensor(self.func(X[beg:end]), dtype=config.real(bkd.lib))
          return self.normal_derivative(X, inputs, outputs, beg, end) - values
  
  
  class RobinBC(BC):
      """Robin boundary conditions: dy/dn(x) = func(x, y)."""
  
      def __init__(self, geom, func, on_boundary, component=0):
          super(RobinBC, self).__init__(geom, on_boundary, component)
          self.func = func
  
      def error(self, X, inputs, outputs, beg, end):
          return self.normal_derivative(X, inputs, outputs, beg, end) - self.func(
              X[beg:end], outputs[beg:end]
          )
  
  
  class PeriodicBC(BC):
      """Periodic boundary conditions on component_x."""
  
      def __init__(self, geom, component_x, on_boundary, derivative_order=0, component=0):
          super(PeriodicBC, self).__init__(geom, on_boundary, component)
          self.component_x = component_x
          self.derivative_order = derivative_order
          if derivative_order > 1:
              raise NotImplementedError(
                  "PeriodicBC only supports derivative_order 0 or 1."
              )
  
      def collocation_points(self, X):
          X1 = self.filter(X)
          X2 = self.geom.periodic_point(X1, self.component_x)
          return np.vstack((X1, X2))
  
      def error(self, X, inputs, outputs, beg, end):
          mid = beg + (end - beg) // 2
          if self.derivative_order == 0:
              yleft = outputs[beg:mid, self.component : self.component + 1]
              yright = outputs[mid:end, self.component : self.component + 1]
          else:
              dydx = grad.jacobian(outputs, inputs, i=self.component, j=self.component_x)
              yleft = dydx[beg:mid]
              yright = dydx[mid:end]
          return yleft - yright
  
  
  class OperatorBC(BC):
      """General operator boundary conditions: func(inputs, outputs, X) = 0.
  
      Args:
          geom: ``Geometry``.
          func: A function takes arguments (`inputs`, `outputs`, `X`)
              and outputs a tensor of size `N x 1`, where `N` is the length of `inputs`.
              `inputs` and `outputs` are the network input and output tensors, respectively;
              `X` are the values of the `inputs`.
          on_boundary: (x, Geometry.on_boundary(x)) -> True/False.
      """
  
      def __init__(self, geom, func, on_boundary):
          super(OperatorBC, self).__init__(geom, on_boundary, 0)
          self.func = func
  
      def error(self, X, inputs, outputs, beg, end):
          return self.func(inputs, outputs, X)[beg:end]
  
  
  class PointSetBC(object):
      """Dirichlet boundary condition for a set of points.
      Compare the output (that associates with `points`) with `values` (target data).
  
      Args:
          points: An array of points where the corresponding target values are known and used for training.
          values: An array of values that gives the exact solution of the problem.
          component: The output component satisfying this BC.
      """
  
      def __init__(self, points, values, component=0):
          self.points = np.array(points, dtype=config.real(np))
          if not isinstance(values, numbers.Number) and values.shape[1] != 1:
              raise RuntimeError(
                  "PointSetBC should output 1D values. Use argument 'component' for different components."
              )
          self.values = bkd.as_tensor(values, dtype=config.real(bkd.lib))
          self.component = component
  
      def collocation_points(self, X):
          return self.points
  
      def error(self, X, inputs, outputs, beg, end):
          return outputs[beg:end, self.component : self.component + 1] - self.values
  