# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for jax_cfd.boundaries."""

# TODO(jamieas): Consider updating these tests using the `hypothesis` framework.
from absl.testing import absltest
from absl.testing import parameterized
import jax.numpy as jnp
from jax_cfd.base import boundaries
from jax_cfd.base import finite_differences as fd
from jax_cfd.base import grids
from jax_cfd.base import test_util
import numpy as np

BCType = boundaries.BCType


class ConstantBoundaryConditionsTest(test_util.TestCase):

  def test_init_usage(self):

    with self.subTest('init 1d'):
      bc = boundaries.HomogeneousBoundaryConditions(
          ((BCType.PERIODIC, BCType.PERIODIC)))
      self.assertEqual(bc.types, (('periodic', 'periodic')))

    with self.subTest('init 2d'):
      bc = boundaries.HomogeneousBoundaryConditions([
          (BCType.PERIODIC, BCType.PERIODIC),
          (BCType.DIRICHLET, BCType.DIRICHLET)
      ])
      self.assertEqual(bc.types, (
          ('periodic', 'periodic'),
          ('dirichlet', 'dirichlet'),
      ))

    with self.subTest('periodic bc utility'):
      bc = boundaries.periodic_boundary_conditions(ndim=3)
      self.assertEqual(bc.types, (
          ('periodic', 'periodic'),
          ('periodic', 'periodic'),
          ('periodic', 'periodic'),
      ))

    with self.subTest('dirichlet bc utility'):
      bc = boundaries.dirichlet_boundary_conditions(ndim=3)
      self.assertEqual(bc.types, (
          ('dirichlet', 'dirichlet'),
          ('dirichlet', 'dirichlet'),
          ('dirichlet', 'dirichlet'),
      ))

    with self.subTest('neumann bc utility'):
      bc = boundaries.neumann_boundary_conditions(ndim=3)
      self.assertEqual(bc.types, (
          ('neumann', 'neumann'),
          ('neumann', 'neumann'),
          ('neumann', 'neumann'),
      ))

    with self.subTest('channel flow 2d bc utility'):
      bc = boundaries.channel_flow_boundary_conditions(ndim=2)
      self.assertEqual(bc.types, (
          ('periodic', 'periodic'),
          ('dirichlet', 'dirichlet'),
      ))

    with self.subTest('channel flow 3d bc utility'):
      bc = boundaries.channel_flow_boundary_conditions(ndim=3)
      self.assertEqual(bc.types, (
          ('periodic', 'periodic'),
          ('dirichlet', 'dirichlet'),
          ('periodic', 'periodic'),
      ))

    with self.subTest('periodic and neumann bc utility'):
      bc = boundaries.periodic_and_neumann_boundary_conditions()
      self.assertEqual(bc.types, (
          ('periodic', 'periodic'),
          ('neumann', 'neumann'),
      ))

  @parameterized.parameters(
      dict(
          shape=(11,),
          initial_offset=(0.0,),
          step=1,
          offset=(0,),
      ),
      dict(
          shape=(11,),
          initial_offset=(0.0,),
          step=1,
          offset=(1,),
      ),
      dict(
          shape=(11,),
          initial_offset=(0.0,),
          step=1,
          offset=(-1,),
      ),
      dict(
          shape=(11,),
          initial_offset=(0.0,),
          step=1,
          offset=(5,),
      ),
      dict(
          shape=(11,),
          initial_offset=(0.0,),
          step=1,
          offset=(13,),
      ),
      dict(
          shape=(11,),
          initial_offset=(0.0,),
          step=1,
          offset=(31,),
      ),
      dict(
          shape=(11, 12, 17),
          initial_offset=(-0.5, -1.0, 1.0),
          step=0.1,
          offset=(-236, 10001, 3),
      ),
      dict(
          shape=(121,),
          initial_offset=(-0.5,),
          step=1,
          offset=(31,),
      ),
      dict(
          shape=(11, 12, 17),
          initial_offset=(0.5, 0.0, 1.0),
          step=0.1,
          offset=(-236, 10001, 3),
      ),
  )
  def test_shift_periodic(self, shape, initial_offset, step, offset):
    """Test that `shift` returns the expected values for periodic BC."""
    grid = grids.Grid(shape, step)
    data = np.arange(np.prod(shape)).reshape(shape)
    array = grids.GridArray(data, initial_offset, grid)
    bc = boundaries.periodic_boundary_conditions(grid.ndim)

    shifted_array = array
    for axis, o in enumerate(offset):
      shifted_array = bc.shift(shifted_array, o, axis)

    shifted_indices = [(jnp.arange(s) + o) % s for s, o in zip(shape, offset)]
    shifted_mesh = jnp.meshgrid(*shifted_indices, indexing='ij')
    expected_offset = tuple(i + o for i, o in zip(initial_offset, offset))
    expected = grids.GridArray(data[tuple(shifted_mesh)], expected_offset, grid)

    self.assertArrayEqual(shifted_array, expected)

  @parameterized.parameters(
      # Periodic BC
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          shift_offset=-2,
          expected_data=np.array([13, 14, 11, 12]),
          expected_offset=(-2,),
      ),
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          shift_offset=-1,
          expected_data=np.array([14, 11, 12, 13]),
          expected_offset=(-1,),
      ),
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          shift_offset=0,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          shift_offset=1,
          expected_data=np.array([12, 13, 14, 11]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          shift_offset=2,
          expected_data=np.array([13, 14, 11, 12]),
          expected_offset=(2,),
      ),
      # Dirichlet BC
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([11, 12, 13, 0]),
          input_offset=(1,),
          shift_offset=-1,
          expected_data=np.array([0, 11, 12, 13]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([0, 12, 13, 14]),
          input_offset=(0,),
          shift_offset=0,
          expected_data=np.array([0, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([0, 12, 13, 14]),
          input_offset=(0,),
          shift_offset=1,
          expected_data=np.array([12, 13, 14, 0]),
          expected_offset=(1,),
      ),
      # Neumann BC
      dict(
          bc_types=((BCType.NEUMANN, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          shift_offset=-1,
          expected_data=np.array([11, 11, 12, 13]),
          expected_offset=(-.5,),
      ),
      dict(
          bc_types=((BCType.NEUMANN, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          shift_offset=0,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0.5,),
      ),
      dict(
          bc_types=((BCType.NEUMANN, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          shift_offset=1,
          expected_data=np.array([12, 13, 14, 14]),
          expected_offset=(1.5,),
      ),
      # Dirichlet / Neumann BC
      dict(
          bc_types=((BCType.DIRICHLET, BCType.NEUMANN),),
          input_data=np.array([0, 12, 13, 14, 15]),
          input_offset=(0.5,),
          shift_offset=-1,
          expected_data=np.array([0, 0, 12, 13, 14]),
          expected_offset=(-.5,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.NEUMANN),),
          input_data=np.array([0, 12, 13, 14, 15]),
          input_offset=(0.5,),
          shift_offset=1,
          expected_data=np.array([12, 13, 14, 15, 15]),
          expected_offset=(1.5,),
      ),
  )
  def test_shift_1d(self, bc_types, input_data, input_offset, shift_offset,
                    expected_data, expected_offset):
    grid = grids.Grid(input_data.shape)
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.HomogeneousBoundaryConditions(bc_types)
    actual = bc.shift(array, shift_offset, axis=0)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      # Periodic BC
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=-2,
          expected_data=np.array([13, 14, 11, 12, 13, 14]),
          expected_offset=(-2,),
      ),
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=-1,
          expected_data=np.array([14, 11, 12, 13, 14]),
          expected_offset=(-1,),
      ),
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=0,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=1,
          expected_data=np.array([11, 12, 13, 14, 11]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.PERIODIC, BCType.PERIODIC),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=2,
          expected_data=np.array([11, 12, 13, 14, 11, 12]),
          expected_offset=(0,),
      ),
      # Dirichlet BC
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([0, 12, 13, 14]),
          input_offset=(0,),
          width=0,
          expected_data=np.array([0, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([0, 12, 13, 14]),
          input_offset=(0,),
          width=1,
          expected_data=np.array([0, 12, 13, 14, 0]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([12, 13, 14, 0]),
          input_offset=(1,),
          width=-1,
          expected_data=np.array([0, 12, 13, 14, 0]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([0, 12, 13, 14]),
          input_offset=(0,),
          width=1,
          expected_data=np.array([0, 12, 13, 14, 0]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=1,
          expected_data=np.array([11, 12, 13, 14, -14]),
          expected_offset=(0.5,),
      ),
      # Neumann BC
      dict(
          bc_types=((BCType.NEUMANN, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(.5,),
          width=-1,
          expected_data=np.array([11, 11, 12, 13, 14]),
          expected_offset=(-.5,),
      ),
      dict(
          bc_types=((BCType.NEUMANN, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=0,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0.5,),
      ),
      dict(
          bc_types=((BCType.NEUMANN, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(.5,),
          width=-1,
          expected_data=np.array([11, 11, 12, 13, 14]),
          expected_offset=(-.5,),
      ),
      dict(
          bc_types=((BCType.NEUMANN, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(.5,),
          width=1,
          expected_data=np.array([11, 12, 13, 14, 14]),
          expected_offset=(.5,),
      ),
      # Dirichlet / Neumann BC
      dict(
          bc_types=((BCType.DIRICHLET, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=-1,
          expected_data=np.array([-11, 11, 12, 13, 14]),
          expected_offset=(-.5,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=1,
          expected_data=np.array([11, 12, 13, 14, 14]),
          expected_offset=(.5,),
      ),
  )
  def test_pad_1d_no_mode(self, bc_types, input_data, input_offset, width,
                          expected_data, expected_offset):
    grid = grids.Grid(input_data.shape)
    input_data = input_data.astype('float')
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.HomogeneousBoundaryConditions(bc_types)
    actual = bc.pad(array, width, axis=0)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([0, 12, 13, 14]),
          input_offset=(0,),
          width=-3,
          expected_data=np.array([-14, -13, -12, 0, 12, 13, 14]),
          expected_offset=(-3,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([0, 12, 13, 14]),
          input_offset=(0,),
          width=3,
          expected_data=np.array([0, 12, 13, 14, 0, -14, -13]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=3,
          expected_data=np.array([11, 12, 13, 14, -14, -13, -12]),
          expected_offset=(0.5,),
      ),
      dict(
          bc_types=((BCType.DIRICHLET, BCType.DIRICHLET),),
          input_data=np.array([11, 12, 13, 0]),
          input_offset=(1.,),
          width=3,
          expected_data=np.array([11, 12, 13, 0, -13, -12, -11]),
          expected_offset=(1.,),
      ),
      dict(
          bc_types=((BCType.NEUMANN, BCType.NEUMANN),),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(.5,),
          width=2,
          expected_data=np.array([11, 12, 13, 14, 14, 13]),
          expected_offset=(.5,),
      ),
  )
  def test_pad_1d_mirror(self, bc_types, input_data, input_offset, width,
                         expected_data, expected_offset):
    grid = grids.Grid(input_data.shape)
    input_data = input_data.astype('float')
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.HomogeneousBoundaryConditions(bc_types)
    actual = bc.pad(array, width, axis=0, mode=boundaries.Padding.MIRROR)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      # Dirichlet BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          grid_shape=4,
          input_data=np.array([-13, -12, -11, 1, 12, 13, 14]),
          input_offset=(-3,),
          expected_data=np.array([12, 13, 14]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          grid_shape=4,
          input_data=np.array([1, 12, 13, 14]),
          input_offset=(0,),
          expected_data=np.array([12, 13, 14]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          grid_shape=4,
          input_data=np.array([1, 12, 13, 14, 2, -12, -11]),
          input_offset=(0,),
          expected_data=np.array([12, 13, 14]),
          expected_offset=(1,),
      ),
      # Neumann BC
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          grid_shape=4,
          input_data=np.array([10, 11, 12, 13, 14]),
          input_offset=(-.5,),
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(.5,),
      ),
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          grid_shape=4,
          input_data=np.array([12, 13, 14, 15, 13]),
          input_offset=(.5,),
          expected_data=np.array([12, 13, 14, 15]),
          expected_offset=(.5,),
      ),
      # Dirichlet / Neumann BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          grid_shape=4,
          input_data=np.array([-11, 12, 13, 14, 12]),
          input_offset=(-.5,),
          expected_data=np.array([12, 13, 14, 12]),
          expected_offset=(.5,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          grid_shape=4,
          input_data=np.array([12, 13, 14, 12]),
          input_offset=(.5,),
          expected_data=np.array([12, 13, 14, 12]),
          expected_offset=(.5,),
      ),
      # Periodic BC
      dict(
          bc_types=(((BCType.PERIODIC, BCType.PERIODIC),), (None,)),
          grid_shape=4,
          input_data=np.array([-12, 11, 12, 13, 14]),
          input_offset=(-1,),
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=(((BCType.PERIODIC, BCType.PERIODIC),), (None,)),
          grid_shape=4,
          input_data=np.array([11, 12, 13, 14, 12]),
          input_offset=(0,),
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
  )
  def test_trim_padding_1d(
      self,
      grid_shape,
      input_data,
      input_offset,
      bc_types,
      expected_data,
      expected_offset,
  ):
    grid = grids.Grid((grid_shape,))
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.ConstantBoundaryConditions(bc_types[0], bc_types[1])
    actual, _ = bc._trim_padding(array, axis=0)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      # Dirichlet BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([-14, -13, -12, 11, 12, 13, 14]),
          input_offset=(-3,),
          grid_size=4,
          expected_data=np.array([12, 13, 14]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          grid_size=4,
          expected_data=np.array([12, 13, 14]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14, 2, -12, -11]),
          input_offset=(0,),
          grid_size=4,
          expected_data=np.array([12, 13, 14]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14, 2, -12, -11]),
          input_offset=(1,),
          grid_size=5,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14, -12, -11]),
          input_offset=(.5,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(.5,),
      ),
      # Neumann BC
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(.5,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(.5,),
      ),
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14, 12]),
          input_offset=(-.5,),
          grid_size=4,
          expected_data=np.array([12, 13, 14, 12]),
          expected_offset=(.5,),
      ),
      # Periodic BC
      dict(
          bc_types=(((BCType.PERIODIC, BCType.PERIODIC),), (None,)),
          input_data=np.array([-12, 11, 12, 13, 14]),
          input_offset=(-1,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=(((BCType.PERIODIC, BCType.PERIODIC),), (None,)),
          input_data=np.array([11, 12, 13, 14, 12]),
          input_offset=(0,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
  )
  def test_trim_boundary_1d(
      self,
      input_data,
      input_offset,
      grid_size,
      bc_types,
      expected_data,
      expected_offset,
  ):
    grid = grids.Grid((grid_size,))
    input_data = input_data.astype(
        'float')  # tests fail with integer input_data
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.ConstantBoundaryConditions(bc_types[0], bc_types[1])
    actual = bc.trim_boundary(array)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      # Dirichlet BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([12, 13, 14]),
          input_offset=(1,),
          grid_size=4,
          expected_data=np.array([1, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(1,),
          grid_size=5,
          expected_data=np.array([11, 12, 13, 14, 2]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(.5,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(.5,),
      ),
      # Neumann BC
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0.5,),
      ),
      # Periodic BC
      dict(
          bc_types=(((BCType.PERIODIC, BCType.PERIODIC),), (None,)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
  )
  def test_pad_and_impose_bc_1d(
      self,
      input_data,
      input_offset,
      grid_size,
      bc_types,
      expected_data,
      expected_offset,
  ):
    grid = grids.Grid((grid_size,))
    input_data = input_data.astype(
        'float')  # tests fail with integer input_data
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.ConstantBoundaryConditions(bc_types[0], bc_types[1])
    actual = bc.pad_and_impose_bc(array, expected_offset).array
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      # Dirichlet BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([0, 12, 13, 14]),
          input_offset=(0,),
          grid_size=4,
          expected_data=np.array([1, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14, 11]),
          input_offset=(1,),
          grid_size=5,
          expected_data=np.array([11, 12, 13, 14, 2]),
          expected_offset=(1,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(.5,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(.5,),
      ),
      # Neumann BC
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0.5,),
      ),
      # Periodic BC
      dict(
          bc_types=(((BCType.PERIODIC, BCType.PERIODIC),), (None,)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          grid_size=4,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
  )
  def test_impose_bc_1d(
      self,
      input_data,
      input_offset,
      grid_size,
      bc_types,
      expected_data,
      expected_offset,
  ):
    grid = grids.Grid((grid_size,))
    input_data = input_data.astype(
        'float')  # tests fail with integer input_data
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.ConstantBoundaryConditions(bc_types[0], bc_types[1])
    actual = bc.impose_bc(array).array
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      # Dirichlet BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([1, 12, 13, 14]),
          input_offset=(0,),
          width=-3,
          expected_data=np.array([-13, -12, -11, 1, 12, 13, 14]),
          expected_offset=(-3,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([1, 12, 13, 14]),
          input_offset=(0,),
          width=0,
          expected_data=np.array([1, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([1, 12, 13, 14]),
          input_offset=(0,),
          width=3,
          expected_data=np.array([1, 12, 13, 14, 2, -12, -11]),
          expected_offset=(0,),
      ),
      # Neumann BC
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=-2,
          expected_data=np.array([11, 10, 11, 12, 13, 14]),
          expected_offset=(-1.5,),
      ),
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=0,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0.5,),
      ),
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=2,
          expected_data=np.array([11, 12, 13, 14, 16, 15]),
          expected_offset=(0.5,),
      ),
      # Dirichlet / Neumann BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=-1,
          expected_data=np.array([-9, 11, 12, 13, 14]),
          expected_offset=(-0.5,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=1,
          expected_data=np.array([11, 12, 13, 14, 16]),
          expected_offset=(0.5,),
      ),
      # Dirichlet / Neumann BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=(2, 1),
          expected_data=np.array([-10, -9, 11, 12, 13, 14, 16]),
          expected_offset=(-1.5,),
      ),
  )
  def test_pad_1d_inhomogeneous(self, bc_types, input_data, input_offset, width,
                                expected_data, expected_offset):
    grid = grids.Grid((4,))
    input_data = input_data.astype(
        'float')  # tests fail with integer input_data
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.ConstantBoundaryConditions(bc_types[0], bc_types[1])
    actual = bc.pad(array, width, axis=0, mode=boundaries.Padding.MIRROR)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      # Dirichlet BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([1, 12, 13, 14]),
          input_offset=(0,),
          width=-3,
          expected_data=np.array([1, 1, 1, 1, 12, 13, 14]),
          expected_offset=(-3,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([1, 12, 13, 14]),
          input_offset=(0,),
          width=0,
          expected_data=np.array([1, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.DIRICHLET),), ((1.0, 2.0),)),
          input_data=np.array([1, 12, 13, 14]),
          input_offset=(0,),
          width=3,
          expected_data=np.array([1, 12, 13, 14, 2, 2, 2]),
          expected_offset=(0,),
      ),
      # Neumann BC
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=-2,
          expected_data=np.array([10, 10, 11, 12, 13, 14]),
          expected_offset=(-1.5,),
      ),
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=0,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0.5,),
      ),
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=2,
          expected_data=np.array([11, 12, 13, 14, 16, 16]),
          expected_offset=(0.5,),
      ),
      # Dirichlet / Neumann BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=-1,
          expected_data=np.array([-9, 11, 12, 13, 14]),
          expected_offset=(-0.5,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=1,
          expected_data=np.array([11, 12, 13, 14, 16]),
          expected_offset=(0.5,),
      ),
      # Dirichlet / Neumann BC
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=(2, 1),
          expected_data=np.array([-9, -9, 11, 12, 13, 14, 16]),
          expected_offset=(-1.5,),
      ),
  )
  def test_pad_1d_inhomogeneous_extend(self, bc_types, input_data, input_offset,
                                       width, expected_data, expected_offset):
    grid = grids.Grid((4,))
    input_data = input_data.astype('float')
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.ConstantBoundaryConditions(bc_types[0], bc_types[1])
    actual = bc.pad(array, width, 0, mode=boundaries.Padding.EXTEND)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=((2, 1),),
          expected_data=np.array([-10, -9, 11, 12, 13, 14, 16]),
          expected_offset=(-1.5,),
      ),
      dict(
          bc_types=(((BCType.DIRICHLET, BCType.NEUMANN),), ((1.0, 2.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          width=((2, 0),),
          expected_data=np.array([-10, -9, 11, 12, 13, 14]),
          expected_offset=(-1.5,),
      ),
      dict(
          bc_types=(((BCType.PERIODIC, BCType.PERIODIC),
                     (BCType.DIRICHLET, BCType.DIRICHLET)), ((0.0, 0.0),
                                                             (0.0, 0.0))),
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.5, 1),
          width=((1, 1), (1, 1)),
          expected_data=np.array([
              [0, 31, 32, 33, 0, -33],
              [0, 11, 12, 13, 0, -13],
              [0, 21, 22, 23, 0, -23],
              [0, 31, 32, 33, 0, -33],
              [0, 11, 12, 13, 0, -13],
          ]),
          expected_offset=(-0.5, 0.),
      ),
  )
  def test_pad_all(self, bc_types, input_data, input_offset, width,
                   expected_data, expected_offset):
    grid = grids.Grid(input_data.shape)
    input_data = input_data.astype(
        'float')  # tests fail with integer input_data
    expected_data = expected_data.astype(
        'float')  # tests fail with integer input_data
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.ConstantBoundaryConditions(bc_types[0], bc_types[1])
    actual = bc.pad_all(array, width, mode=boundaries.Padding.MIRROR)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((10.0, 20.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          expected_offset=(0.0,),
          step=1.,
          expected_data=np.array([10., 1., 1., 1.])),
      dict(
          bc_types=(((BCType.NEUMANN, BCType.NEUMANN),), ((10.0, 20.0),)),
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0.5,),
          expected_offset=(0.0,),
          step=0.5,
          expected_data=np.array([10., 2., 2., 2.])),
  )
  def test_neumann_boundary(self, bc_types, input_data, input_offset,
                            expected_offset, step, expected_data):
    grid = grids.Grid((4,), step=step)
    input_data = input_data.astype('float')
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.ConstantBoundaryConditions(bc_types[0], bc_types[1])
    grid_var = bc.impose_bc(array)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(fd.backward_difference(grid_var, 0), expected)

  @parameterized.parameters(
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.5, 0.5),
          width=-2,
          axis=0,
          expected_data=np.array([
              [-21, -22, -23, -24],
              [-11, -12, -13, -14],
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          expected_offset=(-1.5, 0.5),
      ),
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.5, 0.5),
          width=2,
          axis=1,
          expected_data=np.array([
              [11, 12, 13, 14, -14, -13],
              [21, 22, 23, 24, -24, -23],
              [31, 32, 33, 34, -34, -33],
          ]),
          expected_offset=(0.5, 0.5),
      ),
      dict(
          input_data=np.array([
              [11, 12, 13, 0],
              [21, 22, 23, 0],
              [31, 32, 33, 0],
          ]),
          input_offset=(0.5, 1),  # edge aligned offset
          width=-2,
          axis=1,
          expected_data=np.array([
              [-11, 0, 11, 12, 13, 0],
              [-21, 0, 21, 22, 23, 0],
              [-31, 0, 31, 32, 33, 0],
          ]),
          expected_offset=(0.5, -1),
      ),
  )
  def test_pad_dirichlet_cell_center(self, input_data, input_offset, width,
                                     axis, expected_data, expected_offset):
    grid = grids.Grid(input_data.shape)
    input_data = input_data.astype(
        'float')  # tests fail with integer input_data
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.dirichlet_boundary_conditions(grid.ndim)
    actual = bc.pad(array, width, axis, mode=boundaries.Padding.MIRROR)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.5, 0.5),
          width=-2,
          axis=0,
      ),
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.0, 0.5),
          width=-2,
          axis=0,
      ),
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(1.0, 0.5),
          width=-2,
          axis=0,
      ))
  def test_pad_periodic_raises(self, input_data, input_offset, width, axis):
    grid = grids.Grid((4, 4))
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.periodic_boundary_conditions(grid.ndim)
    error_msg = 'the GridArray does not contain all interior grid values.'
    with self.assertRaisesRegex(ValueError, error_msg):
      _ = bc.pad(array, width, axis, mode=boundaries.Padding.MIRROR)

  @parameterized.parameters(
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.5, 0.5),
          width=-1,
          axis=0,
      ),)
  def test_pad_neumann_raises(self, input_data, input_offset, width, axis):
    grid = grids.Grid((4, 4))
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.neumann_boundary_conditions(grid.ndim)
    error_msg = 'the GridArray does not contain all interior grid values.'
    with self.assertRaisesRegex(ValueError, error_msg):
      _ = bc.pad(array, width, axis, mode=boundaries.Padding.MIRROR)

  @parameterized.parameters(
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.5, 0.5),
          width=-2,
          axis=0,
      ),
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.0, 0.5),
          width=-2,
          axis=0,
      ),
  )
  def test_pad_dirichlet_raises(self, input_data, input_offset, width, axis):
    grid = grids.Grid((4, 4))
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.dirichlet_boundary_conditions(grid.ndim)
    error_msg = 'the GridArray does not contain all interior grid values.'
    with self.assertRaisesRegex(ValueError, error_msg):
      _ = bc.pad(array, width, axis, mode=boundaries.Padding.MIRROR)

  @parameterized.parameters(
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.5, 0.5),
          values=((1.0, 2.0), (3.0, 4.0)),
          width=-2,
          axis=0,
          expected_data=np.array([
              [-19, -20, -21, -22],
              [-9, -10, -11, -12],
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          expected_offset=(-1.5, 0.5),
      ),
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          input_offset=(0.5, 0.5),
          values=((1.0, 2.0), (3.0, 4.0)),
          width=2,
          axis=1,
          expected_data=np.array([
              [11, 12, 13, 14, -6, -5],
              [21, 22, 23, 24, -16, -15],
              [31, 32, 33, 34, -26, -25],
          ]),
          expected_offset=(0.5, 0.5),
      ),
      dict(
          input_data=np.array([
              [11, 12, 13, 4],
              [21, 22, 23, 4],
              [31, 32, 33, 4],
          ]),
          input_offset=(0.5, 1),  # edge aligned offset
          values=((1.0, 2.0), (3.0, 4.0)),
          width=-2,
          axis=1,
          expected_data=np.array([
              [-8, 3, 11, 12, 13, 4],
              [-18, 3, 21, 22, 23, 4],
              [-28, 3, 31, 32, 33, 4],
          ]),
          expected_offset=(0.5, -1),
      ),
  )
  def test_pad_dirichlet_cell_center_inhomogeneous(self, input_data,
                                                   input_offset, values, width,
                                                   axis, expected_data,
                                                   expected_offset):
    input_data = input_data.astype(
        'float')  # tests fail with integer input_data
    expected_data = expected_data.astype(
        'float')  # tests fail with integer input_data
    grid = grids.Grid(input_data.shape)
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.dirichlet_boundary_conditions(grid.ndim, values)
    actual = bc.pad(array, width, axis, mode=boundaries.Padding.MIRROR)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      dict(
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=-1,
          expected_data=np.array([12, 13, 14]),
          expected_offset=(1,),
      ),
      dict(
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=0,
          expected_data=np.array([11, 12, 13, 14]),
          expected_offset=(0,),
      ),
      dict(
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=1,
          expected_data=np.array([11, 12, 13]),
          expected_offset=(0,),
      ),
      dict(
          input_data=np.array([11, 12, 13, 14]),
          input_offset=(0,),
          width=2,
          expected_data=np.array([11, 12]),
          expected_offset=(0,),
      ),
  )
  def test_trim_1d(self, input_data, input_offset, width, expected_data,
                   expected_offset):
    grid = grids.Grid(input_data.shape)
    array = grids.GridArray(input_data, input_offset, grid)
    bc = boundaries.periodic_boundary_conditions(grid.ndim)
    # Note: trim behavior does not depend on bc type
    actual = bc._trim(array, width, axis=0)
    expected = grids.GridArray(expected_data, expected_offset, grid)
    self.assertArrayEqual(actual, expected)

  @parameterized.parameters(
      dict(
          values=((1.0, 2.0),), axis=0, shape=(3,), expected_values=(1.0, 2.0)),
      dict(
          values=((1.0, 2.0), (3.0, 4.0)),
          axis=0,
          shape=(3, 4),
          expected_values=((1.0, 1.0, 1.0, 1.0), (2.0, 2.0, 2.0, 2.0))),
      dict(
          values=((1.0, 2.0), (3.0, 4.0)),
          axis=1,
          shape=(3, 4),
          expected_values=((3.0, 3.0, 3.0), (4.0, 4.0, 4.0))),
  )
  def test_values_constant_boundary(self, values, axis, shape, expected_values):
    grid = grids.Grid(shape)
    bc = boundaries.dirichlet_boundary_conditions(grid.ndim, values)
    actual = bc.values(axis, grid)
    self.assertArrayEqual(actual, expected_values)
    self.assertIsInstance(actual, tuple)
    for x in actual:
      self.assertIsInstance(x, jnp.ndarray)

  @parameterized.parameters(
      dict(axis=0, shape=(3,), expected_values=(0.0, 0.0)),
      dict(
          axis=0,
          shape=(3, 4),
          expected_values=((0.0, 0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 0.0))),
      dict(
          axis=1,
          shape=(3, 4),
          expected_values=((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))),
  )
  def test_values_homogeneous_boundary(self, axis, shape, expected_values):
    grid = grids.Grid(shape)
    bc = boundaries.dirichlet_boundary_conditions(grid.ndim)
    actual = bc.values(axis, grid)
    self.assertArrayEqual(actual, expected_values)
    self.assertIsInstance(actual, tuple)
    for x in actual:
      self.assertIsInstance(x, jnp.ndarray)

  @parameterized.parameters(
      dict(
          input_data=np.array([11, 12, 13, 14]),
          offset=(0.0,),
          values=((1.0, 2.0),),
          expected_data=np.array([1, 12, 13, 14])),
      dict(
          input_data=np.array([11, 12, 13, 14]),
          offset=(1.0,),
          values=((1.0, 2.0),),
          expected_data=np.array([11, 12, 13, 2])),
      dict(
          input_data=np.array([11, 12, 13, 14]),
          offset=(0.5,),
          values=((1.0, 2.0),),
          expected_data=np.array([11, 12, 13, 14])),
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          offset=(1.0, 0.5),
          values=((1.0, 2.0), (3.0, 4.0)),
          expected_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [2, 2, 2, 2],
          ])),
      dict(
          input_data=np.array([
              [11, 12, 13, 14],
              [21, 22, 23, 24],
              [31, 32, 33, 34],
          ]),
          offset=(0.5, 0.0),
          values=((1.0, 2.0), (3.0, 4.0)),
          expected_data=np.array([
              [3, 12, 13, 14],
              [3, 22, 23, 24],
              [3, 32, 33, 34],
          ])),
  )
  def test_impose_bc_constant_boundary(
      self, input_data, offset, values, expected_data):
    grid = grids.Grid(input_data.shape)
    array = grids.GridArray(input_data, offset, grid)
    bc = boundaries.dirichlet_boundary_conditions(grid.ndim, values)
    variable = grids.GridVariable(array, bc)
    variable = variable.impose_bc()
    expected = grids.GridArray(expected_data, offset, grid)
    self.assertArrayEqual(variable.array, expected)

  def test_has_all_periodic_boundary_conditions(self):
    grid = grids.Grid((10, 10))
    array = grids.GridArray(np.zeros((10, 10)), (0.5, 0.5), grid)
    periodic_bc = boundaries.periodic_boundary_conditions(ndim=2)
    nonperiodic_bc = boundaries.periodic_and_neumann_boundary_conditions()

    with self.subTest('returns True'):
      c = grids.GridVariable(array, periodic_bc)
      v = (grids.GridVariable(array, periodic_bc),
           grids.GridVariable(array, periodic_bc))
      self.assertTrue(boundaries.has_all_periodic_boundary_conditions(c, *v))

    with self.subTest('returns False'):
      c = grids.GridVariable(array, periodic_bc)
      v = (grids.GridVariable(array, periodic_bc),
           grids.GridVariable(array, nonperiodic_bc))
      self.assertFalse(boundaries.has_all_periodic_boundary_conditions(c, *v))

  def test_get_pressure_bc_from_velocity_2d(self):
    grid = grids.Grid((10, 10))
    u_array = grids.GridArray(jnp.zeros(grid.shape), (1, 0.5), grid)
    v_array = grids.GridArray(jnp.zeros(grid.shape), (0.5, 1), grid)
    velocity_bc = boundaries.channel_flow_boundary_conditions(ndim=2)
    v = (grids.GridVariable(u_array, velocity_bc),
         grids.GridVariable(v_array, velocity_bc))
    pressure_bc = boundaries.get_pressure_bc_from_velocity(v)
    self.assertEqual(pressure_bc.types, ((BCType.PERIODIC, BCType.PERIODIC),
                                         (BCType.NEUMANN, BCType.NEUMANN)))

  def test_get_pressure_bc_from_velocity_3d(self):
    grid = grids.Grid((10, 10, 10))
    u_array = grids.GridArray(jnp.zeros(grid.shape), (1, 0.5, 0.5), grid)
    v_array = grids.GridArray(jnp.zeros(grid.shape), (0.5, 1, 0.5), grid)
    w_array = grids.GridArray(jnp.zeros(grid.shape), (0.5, 0.5, 1), grid)
    velocity_bc = boundaries.channel_flow_boundary_conditions(ndim=3)
    v = (grids.GridVariable(u_array, velocity_bc),
         grids.GridVariable(v_array, velocity_bc),
         grids.GridVariable(w_array, velocity_bc))
    pressure_bc = boundaries.get_pressure_bc_from_velocity(v)
    self.assertEqual(pressure_bc.types, ((BCType.PERIODIC, BCType.PERIODIC),
                                         (BCType.NEUMANN, BCType.NEUMANN),
                                         (BCType.PERIODIC, BCType.PERIODIC)))

if __name__ == '__main__':
  absltest.main()
