/**
 * \file
 * \copyright
 * Copyright (c) 2012-2021, OpenGeoSys Community (http://www.opengeosys.org)
 *            Distributed under a Modified BSD License.
 *              See accompanying file LICENSE.txt or
 *              http://www.opengeosys.org/project/license
 *
 */

#include "BHE_1P.h"

#include <boost/math/constants/constants.hpp>

#include "FlowAndTemperatureControl.h"
#include "Physics.h"
#include "ThermoMechanicalFlowProperties.h"

namespace ProcessLib
{
namespace HeatTransportBHE
{
namespace BHE
{
BHE_1P::BHE_1P(BoreholeGeometry const& borehole,
               RefrigerantProperties const& refrigerant,
               GroutParameters const& grout,
               FlowAndTemperatureControl const& flowAndTemperatureControl,
               PipeConfiguration1PType const& pipes,
               bool const use_python_bcs)
    : BHECommon{borehole, refrigerant, grout, flowAndTemperatureControl,
                use_python_bcs},
      _pipe(pipes)
{
    _thermal_resistances.fill(std::numeric_limits<double>::quiet_NaN());

    // Initialize thermal resistances.
    auto values = visit(
        [&](auto const& control) {
            return control(refrigerant.reference_temperature,
                           0. /* initial time */);
        },
        flowAndTemperatureControl);
    updateHeatTransferCoefficients(values.flow_rate);
}

std::array<double, BHE_1P::number_of_unknowns> BHE_1P::pipeHeatCapacities()
    const
{
    double const rho_r = refrigerant.density;
    double const specific_heat_capacity = refrigerant.specific_heat_capacity;
    double const rho_g = grout.rho_g;
    double const porosity_g = grout.porosity_g;
    double const heat_cap_g = grout.heat_cap_g;

    return {{
        /*pipe*/ rho_r * specific_heat_capacity,
        /*grout*/ (1.0 - porosity_g) * rho_g * heat_cap_g,
    }};
}

std::array<double, BHE_1P::number_of_unknowns> BHE_1P::pipeHeatConductions()
    const
{
    double const lambda_r = refrigerant.thermal_conductivity;
    double const rho_r = refrigerant.density;
    double const Cp_r = refrigerant.specific_heat_capacity;
    double const alpha_L = _pipe.longitudinal_dispersion_length;
    double const porosity_g = grout.porosity_g;
    double const lambda_g = grout.lambda_g;

    // Here we calculate the laplace coefficients in the governing
    // equations of the BHE.
    return {{
        // pipe, Eq. 19
        (lambda_r + rho_r * Cp_r * alpha_L * _flow_velocity),
        // grout, Eq. 21
        (1.0 - porosity_g) * lambda_g,
    }};
}

std::array<Eigen::Vector3d, BHE_1P::number_of_unknowns>
BHE_1P::pipeAdvectionVectors(Eigen::Vector3d const& elem_direction) const
{
    double const& rho_r = refrigerant.density;
    double const& Cp_r = refrigerant.specific_heat_capacity;
    Eigen::Vector3d adv_vector = rho_r * Cp_r * _flow_velocity * elem_direction;

    return {// pipe, Eq. 19
            adv_vector,
            // grout, Eq. 21
            {0, 0, 0}};
}

double BHE_1P::compute_R_gs(double const chi, double const R_g)
{
    return (1 - chi) * R_g;
}

void BHE_1P::updateHeatTransferCoefficients(double const flow_rate)

{
    auto const tm_flow_properties = calculateThermoMechanicalFlowPropertiesPipe(
        _pipe.single_pipe, borehole_geometry.length, refrigerant, flow_rate);

    _flow_velocity = tm_flow_properties.velocity;
    _thermal_resistances =
        calcThermalResistances(tm_flow_properties.nusselt_number);
}

// Nu is the Nusselt number.
std::array<double, BHE_1P::number_of_unknowns> BHE_1P::calcThermalResistances(
    double const Nu)
{
    constexpr double pi = boost::math::constants::pi<double>();

    double const lambda_r = refrigerant.thermal_conductivity;
    double const lambda_g = grout.lambda_g;
    double const lambda_p = _pipe.single_pipe.wall_thermal_conductivity;

    // thermal resistances due to advective flow of refrigerant in the pipe
    double const R_adv_i1 = 1.0 / (Nu * lambda_r * pi);

    // thermal resistance due to thermal conductivity of the pipe wall material
    double const R_con_a = std::log(_pipe.single_pipe.outsideDiameter() /
                                    _pipe.single_pipe.diameter) /
                           (2.0 * pi * lambda_p);

    // thermal resistances of the grout
    double const D = borehole_geometry.diameter;
    double const pipe_outside_diameter = _pipe.single_pipe.outsideDiameter();

    double const chi = std::log(std::sqrt(D * D + pipe_outside_diameter *
                                                      pipe_outside_diameter) /
                                std::sqrt(2) / pipe_outside_diameter) /
                       std::log(D / pipe_outside_diameter);
    double const R_g =
        std::log(D / pipe_outside_diameter) / 2 / (pi * lambda_g);

    double const R_con_b = chi * R_g;

    // thermal resistances due to grout-soil exchange
    double const R_gs = compute_R_gs(chi, R_g);

    // Eq. 29 and 30
    double const R_fg = R_adv_i1 + R_con_a + R_con_b;

    return {{R_fg, R_gs}};
}

std::array<std::pair<std::size_t /*node_id*/, int /*component*/>, 2>
BHE_1P::getBHEInflowDirichletBCNodesAndComponents(
    std::size_t const top_node_id,
    std::size_t const bottom_node_id,
    int const in_component_id)
{
    return {std::make_pair(top_node_id, in_component_id),
            std::make_pair(bottom_node_id, in_component_id)};
}

std::optional<
    std::array<std::pair<std::size_t /*node_id*/, int /*component*/>, 2>>
BHE_1P::getBHEBottomDirichletBCNodesAndComponents(
    std::size_t const /*bottom_node_id*/,
    int const /*in_component_id*/,
    int const /*out_component_id*/)
{
    return {};
}

std::array<double, BHE_1P::number_of_unknowns> BHE_1P::crossSectionAreas() const
{
    return {{_pipe.single_pipe.area(),
             borehole_geometry.area() - _pipe.single_pipe.outsideArea()}};
}

double BHE_1P::updateFlowRateAndTemperature(double const T_out,
                                            double const current_time)
{
    auto values =
        visit([&](auto const& control) { return control(T_out, current_time); },
              flowAndTemperatureControl);
    updateHeatTransferCoefficients(values.flow_rate);
    return values.temperature;
}
}  // namespace BHE
}  // namespace HeatTransportBHE
}  // namespace ProcessLib


/*
 * Copyright 2020 Robert Bosch GmbH
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
/**
 * \file cloe/component/object.hpp
 */

#pragma once
#ifndef CLOE_COMPONENT_OBJECT_HPP_
#define CLOE_COMPONENT_OBJECT_HPP_

#include <memory>  // for shared_ptr<>
#include <vector>  // for vector<>

#include <Eigen/Geometry>             // for Isometry3d, Vector3d
#include <fable/json/with_eigen.hpp>  // for to_json

#include <cloe/core.hpp>  // for Json

namespace cloe {

/**
 * Object represents an object in 3D space.
 *
 * An object is a 3D bounding box with velocity, angular velocity, position,
 * orientation, and acceleration.
 *
 * Note: It is a POD and therefore nothing should inherit from it.
 *
 * Approximate size of Object is: 168 bytes
 *
 * TODO(ben): In one documentation from Eigen that I read, Eigen objects should not be copied, but
 * passed by reference. If there is substance to this claim, then that would mean that an Object
 * should also only be created on the heap. I have a hard time believing this, but maybe it's worth
 * investigating.
 */
struct Object {
  enum class Type { Unknown, Static, Dynamic };

  enum class Class { Unknown, Pedestrian, Bike, Motorbike, Car, Truck, Trailer };

  /// ID of object, should be unique.
  int id{-1};

  /// Object existence probability.
  double exist_prob{1.0};

  /// Type of object.
  Type type{Type::Unknown};

  /// Classification of object.
  Class classification{Class::Unknown};

  /// Pose in [m] and [rad].
  Eigen::Isometry3d pose{Eigen::Isometry3d()};

  /// Dimensions in [m].
  Eigen::Vector3d dimensions{Eigen::Vector3d::Zero()};

  /// Center of geometry offset in [m].
  Eigen::Vector3d cog_offset{Eigen::Vector3d::Zero()};

  /// Absolute velocity in [m/s].
  Eigen::Vector3d velocity{Eigen::Vector3d::Zero()};

  /// Absolute acceleration in [m/(s*s)].
  Eigen::Vector3d acceleration{Eigen::Vector3d::Zero()};

  /// Angular velocity in [rad/s].
  Eigen::Vector3d angular_velocity{Eigen::Vector3d::Zero()};

  Object() = default;

  friend void to_json(Json& j, const Object& o) {
    j = Json{
        {"id", o.id},
        {"exist_prob", o.exist_prob},
        {"type", o.type},
        {"class", o.classification},
        {"pose", o.pose},
        {"dimensions", o.dimensions},
        {"cog_offset", o.cog_offset},
        {"velocity", o.velocity},
        {"velocity_norm", static_cast<double>(o.velocity.norm())},
        {"acceleration", o.acceleration},
        {"angular_velocity", o.angular_velocity},
    };
  }
};

/**
 * Objects is an alias for a vector of objects.
 *
 * This is defined as many components in the simulation take or return
 * a collection of Objects.
 */
using Objects = std::vector<std::shared_ptr<Object>>;

inline void to_json(Json& j, const Objects& os) {
  j = Json::array();
  for (const auto& o : os) {
    assert(o != nullptr);
    j.push_back(*o);
  }
}

// clang-format off
ENUM_SERIALIZATION(Object::Type, ({
      {Object::Type::Static, "static"},
      {Object::Type::Dynamic, "dynamic"},
      {Object::Type::Unknown, "unknown"},
}))

ENUM_SERIALIZATION(Object::Class, ({
      {Object::Class::Unknown, "unknown"},
      {Object::Class::Pedestrian, "pedestrian"},
      {Object::Class::Bike, "bicycle"},
      {Object::Class::Motorbike, "motorcycle"},
      {Object::Class::Car, "car"},
      {Object::Class::Truck, "truck"},
      {Object::Class::Trailer, "trailer"},
}))
// clang-format on

}  // namespace cloe

#endif  // CLOE_COMPONENT_OBJECT_HPP_


//----------------------------------------------------------------------------
// Copyright (C) 2018-2020 Mikhail Komarov <nemo@nil.foundation>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Server Side Public License, version 1,
// as published by the author.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// Server Side Public License for more details.
//
// You should have received a copy of the Server Side Public License
// along with this program. If not, see
// <https://github.com/NilFoundation/plugin/blob/master/LICENSE_1_0.txt>.
//----------------------------------------------------------------------------

#define BOOST_TEST_MODULE post_election_circuit_test

#include <boost/test/data/monomorphic.hpp>
#include <boost/test/data/test_case.hpp>
#include <boost/test/unit_test.hpp>

#include <nil/filecoin/storage/proofs/post/election/circuit.hpp>

#include "../../core/merkle/generate_tree.hpp"

using namespace nil::filecoin;

BOOST_AUTO_TEST_SUITE(post_election_circuit_test_suite)

template<typename MerkleTreeType>
void test_election_post_circuit(std::size_t expected_constraints) {
    const auto rng = XorShiftRng::from_seed(crate::TEST_SEED);

    std::size_t leaves = 64 * get_base_tree_count<MerkleTreeType>();
    std::size_t sector_size = leaves * NODE_SIZE;

    const auto randomness = typename MerkleTreeType::hash_type::digest_type::random(rng);
    const auto prover_id = typename MerkleTreeType::hash_type::digest_type::random(rng);

    election::PublicParams pub_params = {sector_size, 20, 1};

    std::vector<sector_id_type> sectors;
    auto trees = BTreeMap();

    // Construct and store an MT using a named store.
    const auto temp_dir = tempfile::tempdir();
    const auto temp_path = temp_dir.path();

    for (std::size_t i = 0; i < 5; i++) {
        sectors.push(i.into());
        auto data, tree;
        const std::tie(_data, tree) = merkletree::generate_tree<MerkleTreeType>(rng, leaves, Some(temp_path.to_path_buf()));
        trees.insert(i.into(), tree);
    }

    const auto candidates =
        election::generate_candidates<MerkleTreeType>(&pub_params, &sectors, &trees, prover_id, randomness, );

    const auto candidate = &candidates[0];
    const auto tree = trees.remove(&candidate.sector_id);
    const auto comm_r_last = tree.root();
    const auto comm_c = typename MerkleTreeType::hash_type::digest_type::random(rng);
    const auto comm_r = <typename MerkleTreeType::hash_type>::Function::hash2(&comm_c, &comm_r_last);

    const auto pub_inputs = election::PublicInputs {
        randomness,
        sector_id : candidate.sector_id,
        prover_id,
        comm_r,
        partial_ticket : candidate.partial_ticket,
        sector_challenge_index : 0,
    };

    election::PrivateInputs<MerkleTreeType> priv_inputs {tree, comm_c, comm_r_last};

    const auto proof = ElectionPoSt<MerkleTreeType>::prove(&pub_params, &pub_inputs, &priv_inputs).expect("proving failed");

    const auto is_valid = ElectionPoSt<MerkleTreeType>::verify(&pub_params, &pub_inputs, &proof).expect("verification failed");
    BOOST_CHECK(is_valid);

    // actual circuit test

    const auto paths = proof.paths()
                    .iter()
                    .map(| p |
                         {p.iter()
                              .map(| v | {(v .0.iter().copied().map(Into::into).map(Some).collect(), Some(v .1), )})
                              .collect::<Vec<_>>()})
                    .collect();
    std::vector<_> leafs = proof.leafs().iter().map(| l | Some((*l).into())).collect();

    auto cs = TestConstraintSystem<algebra::curves::bls12<381>>();

    const auto instance = ElectionPoStCircuit<MerkleTreeType> {
        leafs,
        paths,
        comm_r : Some(comm_r.into()),
        comm_c : Some(comm_c.into()),
        comm_r_last : Some(comm_r_last.into()),
        partial_ticket : Some(candidate.partial_ticket),
        randomness : Some(randomness.into()),
        prover_id : Some(prover_id.into()),
        sector_id : Some(candidate.sector_id.into())
    };

    instance.synthesize(cs).expect("failed to synthesize circuit");

    BOOST_CHECK(cs.is_satisfied(), "constraints not satisfied");

    BOOST_CHECK_EQUAL(cs.num_inputs(), 23, "wrong number of inputs");
    BOOST_CHECK_EQUAL(cs.num_constraints(), expected_constraints, "wrong number of constraints");
    BOOST_CHECK_EQUAL(cs.get_input(0, "ONE"), Fr::one());

    const auto generated_inputs =
        ElectionPoStCompound<MerkleTreeType>::generate_public_inputs(&pub_inputs, &pub_params, None);
    const auto expected_inputs = cs.get_inputs();

    for (((input, label), generated_input) : expected_inputs.iter().skip(1).zip(generated_inputs.iter())) {
        BOOST_ASSERT_MSG(input == generated_input, std::string(label));
    }

    BOOST_CHECK_EQUAL(generated_inputs.len(), expected_inputs.len() - 1, "inputs are not the same length");
}

BOOST_AUTO_TEST_CASE(test_election_post_circuit_pedersen) {
    test_election_post_circuit<LCTree<PedersenHasher, U8, U0, U0>>(388520);
}

BOOST_AUTO_TEST_CASE(test_election_post_circuit_poseidon) {
    test_election_post_circuit<LCTree<PoseidonHasher, U8, U0, U0>>(22940);
}

BOOST_AUTO_TEST_SUITE_END()


// Copyright 2005 The Trustees of Indiana University.

// Use, modification and distribution is subject to the Boost Software
// License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

//  Authors: Douglas Gregor
//           Andrew Lumsdaine

#include <boost/graph/king_ordering.hpp>
#include "graph_types.hpp"
#include <boost/python.hpp>
#include <boost/graph/python/list_append_iterator.hpp>

namespace boost { namespace graph { namespace python {

template<typename Graph>
boost::python::list
king_ordering(const Graph& g)
{
  boost::python::list result;
  boost::king_ordering(g, list_append_iterator(result));
  return result;
}

void export_king_ordering()
{
  using boost::python::arg;
  using boost::python::def;
  def("king_ordering", &king_ordering<Graph>, arg("graph"));
}

} } } // end namespace boost::graph::python


/**
BSD 3-Clause License

This file is part of the RootBA project.
https://github.com/NikolausDemmel/rootba

Copyright (c) 2021, Nikolaus Demmel.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/
#pragma once

#include <unordered_map>

#include <Eigen/Dense>
#include <Eigen/Sparse>
#include <basalt/utils/hash.h>
#include <tbb/blocked_range.h>
#include <tbb/parallel_reduce.h>

#include "rootba/cg/utils.hpp"
#include "rootba/util/assert.hpp"
#include "rootba/util/cast.hpp"

namespace rootba {

struct HashPair {
  template <class T1, class T2>
  inline size_t operator()(const std::pair<T1, T2>& v) const {
    size_t seed = 0;
    basalt::hash_combine(seed, v.first);
    basalt::hash_combine(seed, v.second);
    return seed;
  }
};

// map of camera index to block; used to represent sparse block diagonal matrix
template <typename Scalar>
using IndexedBlocks =
    std::unordered_map<std::pair<size_t, size_t>,
                       Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>,
                       HashPair>;

// add diagonal to block-diagonal matrix; missing blocks are assumed to be 0
template <typename Scalar>
void add_diagonal(IndexedBlocks<Scalar>& block_diagonal, size_t num_blocks,
                  size_t block_size,
                  const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>& diagonal) {
  ROOTBA_ASSERT(num_blocks * block_size == unsigned_cast(diagonal.size()));
  for (size_t idx = 0; idx < num_blocks; ++idx) {
    auto [it, is_new] = block_diagonal.try_emplace(std::make_pair(idx, idx));
    if (is_new) {
      it->second = diagonal.segment(block_size * idx, block_size).asDiagonal();
    } else {
      it->second += diagonal.segment(block_size * idx, block_size).asDiagonal();
    }
  }
}

// scale dimensions of JTJ as you would do for jacobian scaling of J beforehand,
// with diagonal scaling matrix D: For jacobain we would use JD, so for JTJ we
// use DJTJD.
template <typename Scalar>
void scale_jacobians(
    IndexedBlocks<Scalar>& block_diagonal, size_t num_blocks, size_t block_size,
    const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>& scaling_vector) {
  ROOTBA_ASSERT(num_blocks * block_size ==
                unsigned_cast(scaling_vector.size()));
  for (auto& [cam_idx, block] : block_diagonal) {
    auto d = scaling_vector.segment(block_size * cam_idx.first, block_size)
                 .asDiagonal();
    block = d * block * d;
  }
}

// sum up diagonal blocks in hash map for parallel reduction
template <typename Scalar>
class BlockDiagonalAccumulator {
 public:
  using VecX = Eigen::Matrix<Scalar, Eigen::Dynamic, 1>;
  using MatX = Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>;

  inline void add(size_t idx, MatX&& block) {
    auto [it, is_new] = block_diagonal.try_emplace(std::make_pair(idx, idx));
    if (is_new) {
      it->second = std::move(block);
    } else {
      it->second += block;
    }
  }

  inline void add_diag(size_t num_blocks, size_t block_size,
                       const VecX& diagonal) {
    add_diagonal(block_diagonal, num_blocks, block_size, diagonal);
  }

  inline void join(BlockDiagonalAccumulator& b) {
    for (auto& [k, v] : b.block_diagonal) {
      auto [it, is_new] = block_diagonal.try_emplace(k);
      if (is_new) {
        it->second = std::move(v);
      } else {
        it->second += v;
      }
    }
  }

  IndexedBlocks<Scalar> block_diagonal;
};

// sum up diagonal blocks in hash map for parallel reduction
template <typename Scalar>
class BlockSparseMatrix : public LinearOperator<Scalar> {
 public:
  constexpr static int POSE_SIZE = 9;

  using VecX = Eigen::Matrix<Scalar, Eigen::Dynamic, 1>;
  using MatX = Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>;

  BlockSparseMatrix(size_t rows, size_t cols) : rows(rows), cols(cols) {}

  ~BlockSparseMatrix() override = default;

  inline void add(size_t x, size_t y, MatX&& block) {
    auto [it, is_new] = block_storage.try_emplace(std::make_pair(x, y));
    if (is_new) {
      it->second = std::move(block);
    } else {
      it->second += block;
    }
  }

  inline void add_diag(size_t num_blocks, size_t block_size,
                       const VecX& diagonal) {
    add_diagonal(block_storage, num_blocks, block_size, diagonal);
  }

  inline void join(BlockSparseMatrix& b) {
    for (auto& [k, v] : b.block_storage) {
      auto [it, is_new] = block_storage.try_emplace(k);
      if (is_new) {
        it->second = std::move(v);
      } else {
        it->second += v;
      }
    }
  }

  VecX right_multiply(const VecX& x) const override {
    ROOTBA_ASSERT(!cached_keys.empty());

    auto body = [&](const tbb::blocked_range<size_t>& range, VecX res) {
      for (size_t r = range.begin(); r != range.end(); ++r) {
        const auto& k = cached_keys[r];
        const auto& v = block_storage.at(k);

        size_t i = k.first;
        size_t j = k.second;

        res.template segment<POSE_SIZE>(i * POSE_SIZE) +=
            v * x.template segment<POSE_SIZE>(j * POSE_SIZE);
      }
      return res;
    };

    tbb::blocked_range<size_t> range(0, cached_keys.size());

    // TODO: add proper size and pose size handling
    VecX identity = VecX::Zero(x.rows());

    VecX res = tbb::parallel_reduce(range, identity, body, std::plus<VecX>());
    return res;
  }

  size_t num_cols() const override { return cols; }

  inline void recompute_keys() {
    cached_keys.clear();
    for (const auto& [k, v] : block_storage) {
      cached_keys.emplace_back(k);
    }
  }

  Eigen::SparseMatrix<Scalar, Eigen::RowMajor> get_sparse_matrix() const {
    // Since we know exactly the sparsity, we can do better than triplets:
    // https://stackoverflow.com/a/18160211/1813258
    // https://eigen.tuxfamily.org/dox/group__SparseQuickRefPage.html
    // But since it's for debugging, it probably doesn't matter...

    // prepare triplet vector
    size_t num_triplets = 0;
    for (const auto& [idx, mat] : block_storage) {
      num_triplets += mat.cols() * mat.rows();
    }
    std::vector<Eigen::Triplet<Scalar>> triplets;
    triplets.reserve(num_triplets);

    // generate all triplets
    for (const auto& [idx, mat] : block_storage) {
      const size_t row_offset = idx.first * POSE_SIZE;
      const size_t col_offset = idx.second * POSE_SIZE;

      for (Eigen::Index row = 0; row < mat.rows(); ++row) {
        for (Eigen::Index col = 0; col < mat.cols(); ++col) {
          triplets.emplace_back(row + row_offset, col + col_offset,
                                mat(row, col));
        }
      }
    }

    // build sparse matrix
    Eigen::SparseMatrix<Scalar, Eigen::RowMajor> res(rows, cols);
    if (!triplets.empty()) {
      res.setFromTriplets(triplets.begin(), triplets.end());
    }

    return res;
  }

  size_t rows, cols;

  IndexedBlocks<Scalar> block_storage;

  std::vector<std::pair<size_t, size_t>> cached_keys;
};

}  // namespace rootba


#include <algorithm>
#include <boost/optional.hpp>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <functional>
#include <iomanip>
#include <iostream>
#include <limits>
#include <list>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#define REP(i, n) for(int i = 0, i##_MACRO = (n); i < i##_MACRO; i++)
#define RANGE(i, a, b) for(int i = (a), i##_MACRO = (b); i < i##_MACRO; i++)
#define EACH(e, a) for(auto&& e : a)
#define ALL(a) std::begin(a), std::end(a)
#define RALL(a) std::rbegin(a), std::rend(a)
#define FILL(a, n) memset((a), n, sizeof(a))
#define FILLZ(a) FILL(a, 0)
#define INT(x) (static_cast<int>(x))
#define PRECISION(x) std::fixed << std::setprecision(x)

using namespace std;

using ll = long long;
using VI = std::vector<int>;
using VI2D = std::vector<vector<int>>;
using VLL = std::vector<long long>;
using VLL2D = std::vector<vector<long long>>;

constexpr int INF = 2e9;
constexpr double EPS = 1e-10;
constexpr double PI = acos(-1.0);

constexpr int dx[] = {-1, 0, 1, 0};
constexpr int dy[] = {0, -1, 0, 1};

template <typename T, std::size_t N>
struct make_vector_type {
	using type =
		typename std::vector<typename make_vector_type<T, (N - 1)>::type>;
};

template <typename T>
struct make_vector_type<T, 0> {
	using type = typename std::vector<T>;
};

template <typename T, size_t N>
auto make_vector_impl(const std::vector<std::size_t>& ls, T init_value) {
	if constexpr(N == 0) {
		return std::vector<T>(ls[N], init_value);
	} else {
		return typename make_vector_type<T, N>::type(
			ls[N], make_vector_impl<T, (N - 1)>(ls, init_value));
	}
}

template <typename T, std::size_t N>
auto make_vector(const std::size_t (&ls)[N], T init_value) {
	std::vector<std::size_t> dimensions(N);
	for(int i = 0; i < N; i++) {
		dimensions[N - i - 1] = ls[i];
	}
	return make_vector_impl<T, N - 1>(dimensions, init_value);
}

template <typename T>
std::vector<T> make_vector(std::size_t size, T init_value) {
	return std::vector<T>(size, init_value);
}

template <typename T>
constexpr int sign(T x) {
	return x < 0 ? -1 : x > 0 ? 1 : 0;
}

template <>
constexpr int sign(double x) {
	return x < -EPS ? -1 : x > EPS ? 1 : 0;
}

template <typename T, typename U>
constexpr void chmax(T& m, U x) {
	m = max<T>(m, x);
}

template <typename T, typename U>
constexpr void chmin(T& m, U x) {
	m = min<T>(m, x);
}

template <typename T>
constexpr T square(T x) {
	return x * x;
}

template <long long MOD = 1000000007>
class ModInt {
	public:
	long long n;

	constexpr ModInt() : n(0) {}
	constexpr ModInt(long long n) : n(n < 0 ? n + MOD : n % MOD) {}

	constexpr long long get() const { return this->n; }
	constexpr long long get_mod() const { return MOD; }

	constexpr ModInt inv() const { return pow<ModInt<MOD>>(*this, MOD - 2); }

	constexpr ModInt& operator=(const long long rhs) {
		return *this = ModInt(rhs);
	}
	constexpr ModInt& operator+=(const ModInt rhs) {
		return *this = ModInt(this->n + rhs.n);
	}
	constexpr ModInt& operator-=(const ModInt rhs) {
		return *this = ModInt(this->n - rhs.n);
	}
	constexpr ModInt& operator*=(const ModInt rhs) {
		return *this = ModInt(this->n * rhs.n);
	}
	constexpr ModInt& operator/=(const ModInt rhs) {
		return *this *= rhs.inv();
	}
	constexpr bool operator==(const ModInt rhs) const {
		return this->n == rhs.n;
	}
};

template <long long MOD>
constexpr ModInt<MOD> operator+(const ModInt<MOD>& lhs,
								const ModInt<MOD>& rhs) {
	return ModInt<MOD>(lhs) += rhs;
}
template <long long MOD>
constexpr ModInt<MOD> operator+(const ModInt<MOD>& lhs, const long long& rhs) {
	return ModInt<MOD>(lhs) += rhs;
}
template <long long MOD>
constexpr ModInt<MOD> operator+(const long long& lhs, const ModInt<MOD>& rhs) {
	return ModInt<MOD>(lhs) += rhs;
}

template <long long MOD>
constexpr ModInt<MOD> operator-(const ModInt<MOD>& lhs,
								const ModInt<MOD>& rhs) {
	return ModInt<MOD>(lhs) -= rhs;
}
template <long long MOD>
constexpr ModInt<MOD> operator-(const ModInt<MOD>& lhs, const long long& rhs) {
	return ModInt<MOD>(lhs) -= rhs;
}
template <long long MOD>
constexpr ModInt<MOD> operator-(const long long& lhs, const ModInt<MOD>& rhs) {
	return ModInt<MOD>(lhs) -= rhs;
}

template <long long MOD>
constexpr ModInt<MOD> operator*(const ModInt<MOD>& lhs,
								const ModInt<MOD>& rhs) {
	return ModInt<MOD>(lhs) *= rhs;
}
template <long long MOD>
constexpr ModInt<MOD> operator*(const ModInt<MOD>& lhs, const long long& rhs) {
	return ModInt<MOD>(lhs) *= rhs;
}
template <long long MOD>
constexpr ModInt<MOD> operator*(const long long& lhs, const ModInt<MOD>& rhs) {
	return ModInt<MOD>(lhs) *= rhs;
}

template <long long MOD>
constexpr ModInt<MOD> operator/(const ModInt<MOD>& lhs,
								const ModInt<MOD>& rhs) {
	return ModInt<MOD>(lhs) /= rhs;
}
template <long long MOD>
constexpr ModInt<MOD> operator/(const ModInt<MOD>& lhs, const long long& rhs) {
	return ModInt<MOD>(lhs) /= rhs;
}
template <long long MOD>
constexpr ModInt<MOD> operator/(const long long& lhs, const ModInt<MOD>& rhs) {
	return ModInt<MOD>(lhs) /= rhs;
}

template <long long MOD>
std::ostream& operator<<(std::ostream& os, const ModInt<MOD>& x) {
	return os << x.n;
}

template <long long MOD>
std::istream& operator>>(std::istream& is, const ModInt<MOD>& x) {
	return is >> x.n;
}

int roundup_pow2(int n) {
	if(!(n & (n - 1))) {
		return n;
	}

	int ret = 1;
	while(n > ret) {
		ret <<= 1;
	}
	return ret;
}

template <typename T, typename U>
class LazySegmentTree {
	using F = std::function<T(T, T)>;
	using G = std::function<T(T, U)>;
	using H = std::function<U(U, U)>;

	std::vector<T> tree;
	std::vector<U> lazy;
	F merge;
	G mapping;
	H composition;
	T id1;
	U id2;
	std::size_t size;

	public:
	LazySegmentTree(const vector<T>& a,
					const F f,
					const T id1,
					const G g,
					const H h,
					const U id2)
		: tree(roundup_pow2(a.size()) * 2 - 1, id1),
		  lazy(roundup_pow2(a.size()) * 2 - 1, id2), merge(f), id1(id1),
		  mapping(g), composition(h), id2(id2), size(roundup_pow2(a.size())) {
		int offset = this->size - 1;
		for(int i = 0; i < a.size(); i++) {
			this->tree[i + offset] = a[i];
		}
		for(int i = offset - 1; i >= 0; i--) {
			this->tree[i] =
				this->merge(this->tree[i * 2 + 1], this->tree[i * 2 + 2]);
		}
	}

	void debug_print() {
		cout << "tree: ";
		REP(i, this->size * 2 - 1) {
			if(i == this->size - 1) {
				cout << "| ";
			}
			cout << "(" << this->tree[i].first << "," << this->tree[i].second
				 << ") ";
		}
		cout << "\nlazy: ";
		REP(i, this->size * 2 - 1) {
			if(i == this->size - 1) {
				cout << "| ";
			}
			cout << this->lazy[i] << " ";
		}
		cout << endl;
	}

	void
	update(const std::size_t left, const std::size_t right, const U value) {
		this->update_impl(left, right, 0, 0, this->size, value);
	}

	T find(const std::size_t left, const std::size_t right) {
		return this->find_impl(left, right, 0, 0, this->size);
	}

	private:
	void update_impl(std::size_t query_left,
					 std::size_t query_right,
					 std::size_t node,
					 std::size_t node_left,
					 std::size_t node_right,
					 U value) {
		this->force(node);
		if(node_right <= query_left || query_right <= node_left) {
			return;
		}
		if(query_left <= node_left && node_right <= query_right) {
			this->lazy[node] = this->composition(this->lazy[node], value);
			this->force(node);
			return;
		}
		this->update_impl(query_left,
						  query_right,
						  node * 2 + 1,
						  node_left,
						  node_left + (node_right - node_left) / 2,
						  value);
		this->update_impl(query_left,
						  query_right,
						  node * 2 + 2,
						  node_left + (node_right - node_left) / 2,
						  node_right,
						  value);
		this->tree[node] =
			this->merge(this->tree[node * 2 + 1], this->tree[node * 2 + 2]);
	}

	T find_impl(size_t query_left,
				size_t query_right,
				size_t node,
				size_t node_left,
				size_t node_right) {
		this->force(node);
		if(node_right <= query_left || query_right <= node_left) {
			return this->id1;
		}
		if(query_left <= node_left && node_right <= query_right) {
			return this->tree[node];
		}

		return this->merge(find_impl(query_left,
									 query_right,
									 node * 2 + 1,
									 node_left,
									 node_left + (node_right - node_left) / 2),
						   find_impl(query_left,
									 query_right,
									 node * 2 + 2,
									 node_left + (node_right - node_left) / 2,
									 node_right));
	}

	void force(std::size_t node) {
		if(this->lazy[node] == this->id2) {
			return;
		}
		if(node * 2 + 1 < this->size * 2 - 1) {
			this->lazy[node * 2 + 1] =
				this->composition(lazy[node * 2 + 1], lazy[node]);
			this->lazy[node * 2 + 2] =
				this->composition(lazy[node * 2 + 2], lazy[node]);
		}
		this->tree[node] = this->mapping(this->tree[node], lazy[node]);
		this->lazy[node] = this->id2;
	}
};

using mint = ModInt<998244353>;

int main() {
	int n, q;
	cin >> n >> q;
	VI l(q), r(q);
	VI d(q);
	REP(i, q) {
		cin >> l[i] >> r[i] >> d[i];
		l[i] = n - l[i] + 1;
		r[i] = n - r[i];
		swap(l[i], r[i]);
	}

	vector<mint> keta(n + 1, 0);
	keta[0] = 1;
	REP(i, n) { keta[i + 1] = keta[i] * 10; }

	vector<mint> repunit(n + 1, 0);
	repunit[0] = 1;
	REP(i, n) { repunit[i + 1] = repunit[i] * 10 + 1; }

	vector<pair<mint, int>> s(n, {1, 1});

	LazySegmentTree<pair<mint, int>, int> lst(
		s,
		[&](pair<mint, int> a, pair<mint, int> b) {
			return make_pair(a.first + b.first * keta[a.second],
							 a.second + b.second);
		},
		{0, 1},
		[&](pair<mint, int> a, int b) {
			return make_pair(b ? b * repunit[a.second - 1] : a.first, a.second);
		},
		[](int a, int b) { return b ? b : a; },
		0);
	// lst.debug_print();
	REP(i, q) {
		lst.update(l[i], r[i], d[i]);
		cout << lst.find(0, n).first << endl;
		// lst.debug_print();
	}
	return 0;
}


#include "gtest/gtest.h"

#include <Eigen/Sparse>

#include "GeometricMultigridOperators.h"
#include "GeometricMultigridPoissonSolver.h"
#include "InitialMultigridTestDomains.h"
#include "Renderer.h"
#include "ScalarGrid.h"
#include "SparseUniformGrid.h"
#include "Transform.h"
#include "UniformGrid.h"
#include "Utilities.h"

using namespace FluidSim3D;
using namespace FluidSim3D::GeometricMultigridOperators;

static void testBoundaryCells(const UniformGrid<CellLabels>& cellLabels, const VectorGrid<double>* boundaryWeights)
{
    if (boundaryWeights != nullptr)
    {
        ASSERT_TRUE(boundaryWeights->sampleType() == VectorGridSettings::SampleType::STAGGERED);
        ASSERT_TRUE(boundaryWeights->size(0)[0] == cellLabels.size()[0] + 1);
        ASSERT_TRUE(boundaryWeights->size(0)[1] == cellLabels.size()[1]);
        ASSERT_TRUE(boundaryWeights->size(0)[2] == cellLabels.size()[2]);

        ASSERT_TRUE(boundaryWeights->size(1)[0] == cellLabels.size()[0]);
        ASSERT_TRUE(boundaryWeights->size(1)[1] == cellLabels.size()[1] + 1);
        ASSERT_TRUE(boundaryWeights->size(1)[2] == cellLabels.size()[2]);

        ASSERT_TRUE(boundaryWeights->size(2)[0] == cellLabels.size()[0]);
        ASSERT_TRUE(boundaryWeights->size(2)[1] == cellLabels.size()[1]);
        ASSERT_TRUE(boundaryWeights->size(2)[2] == cellLabels.size()[2] + 1);
    }

    // Verfify that cells along the boundary are not active
    for (int axis : {0, 1, 2})
    {
        for (int direction : {0, 1})
        {
            Vec3i startCell = Vec3i::Zero();
            Vec3i endCell = cellLabels.size();

            if (direction == 0)
            {
                endCell[axis] = 1;
            }
            else
            {
                startCell[axis] = cellLabels.size()[axis] - 1;
            }

            forEachVoxelRange(startCell, endCell, [&](const Vec3i& cell)
            {
                ASSERT_TRUE(cellLabels(cell) == CellLabels::EXTERIOR_CELL || cellLabels(cell) == CellLabels::DIRICHLET_CELL);
            });
        }
    }

    forEachVoxelRange(Vec3i::Zero(), cellLabels.size(), [&](const Vec3i& cell)
    {
        // Verify that an interior cell only has interior and boundary cell neighbours
        if (cellLabels(cell) == CellLabels::INTERIOR_CELL)
        {
            ASSERT_TRUE(cell[0] > 0);
            ASSERT_TRUE(cell[1] > 0);
            ASSERT_TRUE(cell[2] > 0);

            ASSERT_TRUE(cell[0] < cellLabels.size()[0] - 1);
            ASSERT_TRUE(cell[1] < cellLabels.size()[1] - 1);
            ASSERT_TRUE(cell[2] < cellLabels.size()[2] - 1);

            for (int axis : {0, 1, 2})
            {
                for (int direction : {0, 1})
                {
                    Vec3i adjacentCell = cellToCell(cell, axis, direction);

                    EXPECT_TRUE(cellLabels(adjacentCell) == CellLabels::INTERIOR_CELL || cellLabels(adjacentCell) == CellLabels::BOUNDARY_CELL);

                    if (boundaryWeights != nullptr)
                    {
                        Vec3i face = cellToFace(cell, axis, direction);
                        EXPECT_TRUE((*boundaryWeights)(face, axis) == 1);
                    }
                }
            }
        }
        else if (cellLabels(cell) == CellLabels::BOUNDARY_CELL)
        {
            ASSERT_TRUE(cell[0] > 0);
            ASSERT_TRUE(cell[1] > 0);
            ASSERT_TRUE(cell[2] > 0);

            ASSERT_TRUE(cell[0] < cellLabels.size()[0] - 1);
            ASSERT_TRUE(cell[1] < cellLabels.size()[1] - 1);
            ASSERT_TRUE(cell[2] < cellLabels.size()[2] - 1);

            bool hasValidBoundary = false;

            for (int axis : {0, 1, 2})
            {
                for (int direction : {0, 1})
                {
                    Vec3i adjacentCell = cellToCell(cell, axis, direction);

                    // A boundary cell should have at least one neighbour that is not active in the solver OR a boundary weight
                    // between an adjacent boundary cell that is less that one
                    if (!(cellLabels(adjacentCell) == CellLabels::INTERIOR_CELL || cellLabels(adjacentCell) == CellLabels::BOUNDARY_CELL))
                    {
                        hasValidBoundary = true;
                    }
                    else if (boundaryWeights != nullptr)
                    {
                        Vec3i face = cellToFace(cell, axis, direction);
                        if ((*boundaryWeights)(face, axis) < 1 && cellLabels(adjacentCell) == CellLabels::BOUNDARY_CELL)
                        {
                            hasValidBoundary = true;
                        }
                    }
                }
            }

            EXPECT_TRUE(hasValidBoundary);
        }
    });

    EXPECT_TRUE(unitTestBoundaryCells(cellLabels, boundaryWeights));
}

std::pair<UniformGrid<CellLabels>, VectorGrid<double>> buildTestDomain(bool useComplexDomain, bool useSolidSphere, int gridSize)
{
    UniformGrid<CellLabels> baseDomainCellLabels;
    VectorGrid<double> baseBoundaryWeights;
    // Complex domain set up
    if (useComplexDomain)
    {
        buildComplexDomain(baseDomainCellLabels, baseBoundaryWeights, gridSize, useSolidSphere);
    }
    // Simple domain set up
    else
    {
        buildSimpleDomain(baseDomainCellLabels, baseBoundaryWeights, gridSize, 1 /*dirichlet band*/);
    }

    std::pair<UniformGrid<CellLabels>, VectorGrid<double>> grids;

    std::pair<Vec3i, int> mgSettings = buildExpandedDomain(grids.first, grids.second, baseDomainCellLabels, baseBoundaryWeights);

    return grids;
}

TEST(GEOMETRIC_MULTIGRID_TESTS, BOUNDARY_CELLS_TEST)
{
    std::vector<bool> complexDomainSettings = {false, true};
    std::vector<bool> solidSphereSettings = {false, true};

    const int startGrid = 16;
    const int endGrid = startGrid * int(pow(2, 5));

    for (int gridSize = startGrid; gridSize < endGrid; gridSize *= 2)
    {
        for (bool useComplexDomain : complexDomainSettings)
        {
            std::pair<UniformGrid<CellLabels>, VectorGrid<double>> grids;
            if (useComplexDomain)
            {
                for (bool useSolidSphere : solidSphereSettings)
                {
                    grids = buildTestDomain(useComplexDomain, useSolidSphere, gridSize);
                }
            }
            else
            {
                grids = buildTestDomain(useComplexDomain, false, gridSize);
            }

            testBoundaryCells(grids.first, &grids.second);
        }
    }
}

static void testExteriorCells(const UniformGrid<CellLabels>& cellLabels)
{
    for (int axis : {0, 1, 2})
    {
        for (int direction : {0, 1})
        {
            Vec3i startCell = Vec3i::Zero();
            Vec3i endCell = cellLabels.size();

            if (direction == 0)
            {
                endCell[axis] = 1;
            }
            else
            {
                startCell[axis] = endCell[axis] - 1;
            }

            forEachVoxelRange(startCell, endCell, [&](const Vec3i& cell)
            {
                EXPECT_TRUE(cellLabels(cell) == CellLabels::EXTERIOR_CELL);
            });
        }
    }

    EXPECT_TRUE(unitTestExteriorCells(cellLabels));
}

TEST(GEOMETRIC_MULTIGRID_TESTS, EXTERIOR_CELLS_TEST)
{
    std::vector<bool> complexDomainSettings = {false, true};
    std::vector<bool> solidSphereSettings = {false, true};

    const int startGrid = 16;
    const int endGrid = startGrid * int(pow(2, 5));

    for (int gridSize = startGrid; gridSize < endGrid; gridSize *= 2)
    {
        for (bool useComplexDomain : complexDomainSettings)
        {
            std::pair<UniformGrid<CellLabels>, VectorGrid<double>> grids;
            if (useComplexDomain)
            {
                for (bool useSolidSphere : solidSphereSettings)
                {
                    grids = buildTestDomain(useComplexDomain, useSolidSphere, gridSize);
                }
            }
            else
            {
                grids = buildTestDomain(useComplexDomain, false, gridSize);
            }

            testExteriorCells(grids.first);
        }
    }
}

static void testCoarsening(const UniformGrid<CellLabels>& coarseCellLabels, const UniformGrid<CellLabels>& fineCellLabels)
{
    // The coarse cell grid must be exactly have the size of the fine cell grid.
    ASSERT_TRUE((2 * coarseCellLabels.size()).eval() == fineCellLabels.size());

    ASSERT_TRUE(coarseCellLabels.size()[0] % 2 == 0);
    ASSERT_TRUE(coarseCellLabels.size()[1] % 2 == 0);
    ASSERT_TRUE(coarseCellLabels.size()[2] % 2 == 0);

    ASSERT_TRUE(fineCellLabels.size()[0] % 2 == 0);
    ASSERT_TRUE(fineCellLabels.size()[1] % 2 == 0);
    ASSERT_TRUE(fineCellLabels.size()[2] % 2 == 0);

    forEachVoxelRange(Vec3i::Zero(), fineCellLabels.size(), [&](const Vec3i& fineCell)
    {
        Vec3i coarseCell = fineCell / 2;

        // If the fine cell is Dirichlet, it's coarse cell equivalent has to also be Dirichlet
        if (fineCellLabels(fineCell) == CellLabels::DIRICHLET_CELL)
        {
            EXPECT_TRUE(coarseCellLabels(coarseCell) == CellLabels::DIRICHLET_CELL);
        }
        else if (fineCellLabels(fineCell) == CellLabels::INTERIOR_CELL || fineCellLabels(fineCell) == CellLabels::BOUNDARY_CELL)
        {
            // If the fine cell is active, the coarse cell cannot be exterior
            EXPECT_TRUE(coarseCellLabels(coarseCell) != CellLabels::EXTERIOR_CELL);
        }
    });

    forEachVoxelRange(Vec3i::Zero(), coarseCellLabels.size(), [&](const Vec3i& coarseCell)
    {
        bool foundDirichletChild = false;
        bool foundActiveChild = false;
        bool foundExteriorChild = false;

        for (int childIndex = 0; childIndex < 8; ++childIndex)
        {
            Vec3i fineCell = getChildCell(coarseCell, childIndex);

            CellLabels fineLabel = fineCellLabels(fineCell);

            if (fineLabel == CellLabels::DIRICHLET_CELL)
            {
                foundDirichletChild = true;
            }
            else if (fineLabel == CellLabels::INTERIOR_CELL || fineLabel == CellLabels::BOUNDARY_CELL)
            {
                foundActiveChild = true;
            }
            else if (fineLabel == CellLabels::EXTERIOR_CELL)
            {
                foundExteriorChild = true;
            }
        }

        CellLabels coarseLabel = coarseCellLabels(coarseCell);
        if (coarseLabel == CellLabels::DIRICHLET_CELL)
        {
            EXPECT_TRUE(foundDirichletChild);
        }
        else if (coarseLabel == CellLabels::INTERIOR_CELL || coarseLabel == CellLabels::BOUNDARY_CELL)
        {
            EXPECT_FALSE(foundDirichletChild);
            EXPECT_TRUE(foundActiveChild);
        }
        else if (coarseLabel == CellLabels::EXTERIOR_CELL)
        {
            EXPECT_FALSE(foundDirichletChild);
            EXPECT_FALSE(foundActiveChild);
            EXPECT_TRUE(foundExteriorChild);
        }
    });

    EXPECT_TRUE(unitTestCoarsening(coarseCellLabels, fineCellLabels));
}

TEST(GEOMETRIC_MULTIGRID_TESTS, COARSENING_TEST)
{
    std::vector<bool> complexDomainSettings = {false, true};
    std::vector<bool> solidSphereSettings = {false, true};

    const int startGrid = 32;
    const int endGrid = 8 * startGrid;

    for (int gridSize = startGrid; gridSize < endGrid; gridSize *= 2)
    {
        for (bool useComplexDomain : complexDomainSettings)
        {
            std::pair<UniformGrid<CellLabels>, VectorGrid<double>> grids;
            if (useComplexDomain)
            {
                for (bool useSolidSphere : solidSphereSettings)
                {
                    grids = buildTestDomain(useComplexDomain, useSolidSphere, gridSize);
                }
            }
            else
            {
                grids = buildTestDomain(useComplexDomain, false, gridSize);
            }

            UniformGrid<CellLabels> coarseCell = buildCoarseCellLabels(grids.first);

            testCoarsening(coarseCell, grids.first);
        }
    }
}

static void geometricVectorTest(const UniformGrid<CellLabels>& cellLabels)
{
    VectorXd vecA = VectorXd::Random(cellLabels.voxelCount());
    VectorXd vecB = VectorXd::Random(cellLabels.voxelCount());

    UniformGrid<double> gridA(cellLabels.size(), 0);
    UniformGrid<double> gridB(cellLabels.size(), 0);

    tbb::parallel_for(tbb::blocked_range<int>(0, cellLabels.voxelCount(), tbbLightGrainSize), [&](const tbb::blocked_range<int>& range)
    {
        for (int cellIndex = range.begin(); cellIndex != range.end(); ++cellIndex)
        {
            Vec3i cell = cellLabels.unflatten(cellIndex);

            if (cellLabels(cell) == CellLabels::INTERIOR_CELL || cellLabels(cell) == CellLabels::BOUNDARY_CELL)
            {
                gridA(cell) = vecA(cellIndex);
                gridB(cell) = vecB(cellIndex);
            }
            else
            {
                vecA(cellIndex) = 0;
                vecB(cellIndex) = 0;
            }
        }
    });

    // Test dot product
    {
        double vecDot = vecA.dot(vecB);
        double gridDot = dotProduct(gridA, gridB, cellLabels);

        EXPECT_TRUE(isNearlyEqual(vecDot, gridDot, 1e-10, true));
    }

    // Test L2 norm
    {
        double vecAL2 = vecA.squaredNorm();
        double gridAL2 = squaredl2Norm(gridA, cellLabels);

        EXPECT_TRUE(isNearlyEqual(vecAL2, gridAL2, 1e-10, true));
    }

    {
        double vecBL2 = vecB.squaredNorm();
        double gridBL2 = squaredl2Norm(gridB, cellLabels);

        EXPECT_TRUE(isNearlyEqual(vecBL2, gridBL2, 1e-10, true));
    }

    // Test L-infinity norm
    {
        double vecALinf = vecA.lpNorm<Eigen::Infinity>();
        double gridALinf = lInfinityNorm(gridA, cellLabels);

        EXPECT_EQ(vecALinf, gridALinf);
    }

    {
        double vecBLinf = vecB.lpNorm<Eigen::Infinity>();
        double gridBLinf = lInfinityNorm(gridB, cellLabels);

        EXPECT_EQ(vecBLinf, gridBLinf);
    }

    // Add vectors
    {
        double scale = 10;
        VectorXd vecC = vecA + scale * vecB;
        UniformGrid<double> gridC(cellLabels.size(), 0);

        addVectors(gridC, gridA, gridB, cellLabels, scale);

        forEachVoxelRange(Vec3i::Zero(), cellLabels.size(), [&](const Vec3i& cell)
        {
            int cellIndex = cellLabels.flatten(cell);
            EXPECT_EQ(vecC(cellIndex), gridC(cell));
        });
    }

    // Add to vector
    {
        double scale = 20;
        vecA += scale * vecB;

        addToVector(gridA, gridB, cellLabels, scale);

        forEachVoxelRange(Vec3i::Zero(), cellLabels.size(), [&](const Vec3i& cell)
        {
            int cellIndex = cellLabels.flatten(cell);
            EXPECT_EQ(vecA(cellIndex), gridA(cell));
        });
    }
}

TEST(GEOMETRIC_MULTIGRID_TESTS, GEOMETRIC_VECTOR_TEST)
{
    std::vector<bool> complexDomainSettings = {false, true};
    std::vector<bool> solidSphereSettings = {false, true};

    const int startGrid = 32;
    const int endGrid = 8 * startGrid;

    for (int gridSize = startGrid; gridSize < endGrid; gridSize *= 2)
    {
        for (bool useComplexDomain : complexDomainSettings)
        {
            std::pair<UniformGrid<CellLabels>, VectorGrid<double>> grids;
            if (useComplexDomain)
            {
                for (bool useSolidSphere : solidSphereSettings)
                {
                    grids = buildTestDomain(useComplexDomain, useSolidSphere, gridSize);
                }
            }
            else
            {
                grids = buildTestDomain(useComplexDomain, false, gridSize);
            }

            geometricVectorTest(grids.first);
        }
    }
}

static void symmetryTest(bool useComplexDomain, bool useSolidSphere, int gridSize)
{
    UniformGrid<CellLabels> domainCellLabels;
    VectorGrid<double> boundaryWeights;
    int mgLevels;
    {
        UniformGrid<CellLabels> baseDomainCellLabels;
        VectorGrid<double> baseBoundaryWeights;

        // Complex domain set up
        if (useComplexDomain)
        {
            buildComplexDomain(baseDomainCellLabels, baseBoundaryWeights, gridSize, useSolidSphere);
        }
        // Simple domain set up
        else
        {
            buildSimpleDomain(baseDomainCellLabels, baseBoundaryWeights, gridSize, 1 /*dirichlet band*/);
        }

        // Build expanded domain
        std::pair<Vec3i, int> mgSettings = buildExpandedDomain(domainCellLabels, boundaryWeights, baseDomainCellLabels, baseBoundaryWeights);

        mgLevels = mgSettings.second;
    }

    double dx = boundaryWeights.dx();

    UniformGrid<double> rhsA(domainCellLabels.size(), 0);
    UniformGrid<double> rhsB(domainCellLabels.size(), 0);

    {
        VectorXd randVecA = VectorXd::Random(domainCellLabels.voxelCount());
        VectorXd randVecB = VectorXd::Random(domainCellLabels.voxelCount());

        ASSERT_GT((randVecA - randVecB).norm(), 0.);

        tbb::parallel_for(tbb::blocked_range<int>(0, domainCellLabels.voxelCount(), tbbLightGrainSize), [&](const tbb::blocked_range<int>& range) {

            for (int cellIndex = range.begin(); cellIndex != range.end(); ++cellIndex)
            {
                Vec3i cell = domainCellLabels.unflatten(cellIndex);

                if (domainCellLabels(cell) == CellLabels::INTERIOR_CELL || domainCellLabels(cell) == CellLabels::BOUNDARY_CELL)
                {
                    rhsA(cell) = randVecA(cellIndex);
                    rhsB(cell) = randVecB(cellIndex);
                }
            }
        });
    }

    Transform xform(dx, Vec3d::Zero());

    // Jacobi smoother symmetry test
    {
        UniformGrid<double> solutionA(domainCellLabels.size(), 0);
        UniformGrid<double> solutionB(domainCellLabels.size(), 0);

        VecVec3i boundaryCells = buildBoundaryCells(domainCellLabels, 3);

        boundaryJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, boundaryCells, dx, &boundaryWeights);
        boundaryJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, boundaryCells, dx, &boundaryWeights);

        interiorJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, dx, &boundaryWeights);
        interiorJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, dx, &boundaryWeights);

        boundaryJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, boundaryCells, dx, &boundaryWeights);
        boundaryJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, boundaryCells, dx, &boundaryWeights);

        double dotA = dotProduct(solutionA, rhsB, domainCellLabels);
        double dotB = dotProduct(solutionB, rhsA, domainCellLabels);

        EXPECT_TRUE(isNearlyEqual(dotA, dotB, 1e-10, true));
    }

    // Test direct solve symmetry
    {
        Eigen::SimplicialCholesky<Eigen::SparseMatrix<double>> solver;
        Eigen::SparseMatrix<double> sparseMatrix;

        int interiorCellCount = 0;
        UniformGrid<int> directSolverIndices(domainCellLabels.size(), -1);
        {
            forEachVoxelRange(Vec3i::Zero(), domainCellLabels.size(), [&](const Vec3i& cell)
            {
                if (domainCellLabels(cell) == CellLabels::INTERIOR_CELL || domainCellLabels(cell) == CellLabels::BOUNDARY_CELL)
                {
                    directSolverIndices(cell) = interiorCellCount++;
                }
            });

            // Build rows
            std::vector<Eigen::Triplet<double>> sparseElements;

            double gridScale = 1. / std::pow(dx, 2);
            forEachVoxelRange(Vec3i::Zero(), domainCellLabels.size(), [&](const Vec3i& cell)
            {
                if (domainCellLabels(cell) == CellLabels::INTERIOR_CELL)
                {
                    int index = directSolverIndices(cell);
                    ASSERT_TRUE(index >= 0);

                    for (int axis : {0, 1, 2})
                    {
                        for (int direction : {0, 1})
                        {
                            Vec3i adjacentCell = cellToCell(cell, axis, direction);

                            ASSERT_TRUE(domainCellLabels(adjacentCell) == CellLabels::INTERIOR_CELL || domainCellLabels(adjacentCell) == CellLabels::BOUNDARY_CELL);

                            Vec3i face = cellToFace(cell, axis, direction);
                            ASSERT_TRUE(boundaryWeights(face, axis) == 1);

                            int adjacentIndex = directSolverIndices(adjacentCell);
                            ASSERT_TRUE(adjacentIndex >= 0);

                            sparseElements.emplace_back(index, adjacentIndex, -gridScale);
                        }
                    }

                    sparseElements.emplace_back(index, index, 6. * gridScale);
                }
                else if (domainCellLabels(cell) == CellLabels::BOUNDARY_CELL)
                {
                    int index = directSolverIndices(cell);
                    ASSERT_TRUE(index >= 0);

                    double diagonal = 0;

                    for (int axis : {0, 1, 2})
                    {
                        for (int direction : {0, 1})
                        {
                            Vec3i adjacentCell = cellToCell(cell, axis, direction);

                            if (domainCellLabels(adjacentCell) == CellLabels::INTERIOR_CELL)
                            {
                                int adjacentIndex = directSolverIndices(adjacentCell);
                                ASSERT_TRUE(adjacentIndex >= 0);

                                Vec3i face = cellToFace(cell, axis, direction);
                                ASSERT_TRUE(boundaryWeights(face, axis) == 1);

                                sparseElements.emplace_back(index, adjacentIndex, -gridScale);
                                ++diagonal;
                            }
                            else if (domainCellLabels(adjacentCell) == CellLabels::BOUNDARY_CELL)
                            {
                                int adjacentIndex = directSolverIndices(adjacentCell);
                                ASSERT_TRUE(adjacentIndex >= 0);

                                Vec3i face = cellToFace(cell, axis, direction);
                                double weight = boundaryWeights(face, axis);

                                sparseElements.emplace_back(index, adjacentIndex, -gridScale * weight);
                                diagonal += weight;
                            }
                            else if (domainCellLabels(adjacentCell) == CellLabels::DIRICHLET_CELL)
                            {
                                int adjacentIndex = directSolverIndices(adjacentCell);
                                ASSERT_TRUE(adjacentIndex == -1);

                                Vec3i face = cellToFace(cell, axis, direction);
                                double weight = boundaryWeights(face, axis);

                                diagonal += weight;
                            }
                            else
                            {
                                ASSERT_TRUE(domainCellLabels(adjacentCell) == CellLabels::EXTERIOR_CELL);
                                int adjacentIndex = directSolverIndices(adjacentCell);
                                ASSERT_TRUE(adjacentIndex == -1);

                                Vec3i face = cellToFace(cell, axis, direction);
                                ASSERT_TRUE(boundaryWeights(face, axis) == 0);
                            }
                        }
                    }

                    sparseElements.emplace_back(index, index, gridScale * diagonal);
                }
            });

            // Solve system
            sparseMatrix = Eigen::SparseMatrix<double>(interiorCellCount, interiorCellCount);
            sparseMatrix.setFromTriplets(sparseElements.begin(), sparseElements.end());

            solver.compute(sparseMatrix);

            ASSERT_TRUE(solver.info() == Eigen::Success);
        }

        UniformGrid<double> solutionA(domainCellLabels.size(), 0);
        UniformGrid<double> solutionB(domainCellLabels.size(), 0);

        // Copy from grid to Eigen, solve, and copy back to grid
        {
            VectorXd rhsVectorA = VectorXd::Zero(interiorCellCount);
            VectorXd rhsVectorB = VectorXd::Zero(interiorCellCount);

            tbb::parallel_for(tbb::blocked_range<int>(0, domainCellLabels.voxelCount(), tbbLightGrainSize), [&](const tbb::blocked_range<int>& range)
            {
                for (int cellIndex = range.begin(); cellIndex != range.end(); ++cellIndex)
                {
                    Vec3i cell = domainCellLabels.unflatten(cellIndex);

                    if (domainCellLabels(cell) == CellLabels::INTERIOR_CELL || domainCellLabels(cell) == CellLabels::BOUNDARY_CELL)
                    {
                        int index = directSolverIndices(cell);
                        ASSERT_TRUE(index >= 0);

                        rhsVectorA(index) = rhsA(cell);
                        rhsVectorB(index) = rhsB(cell);
                    }
                }
            });

            VectorXd directSolutionA = solver.solve(rhsVectorA);
            VectorXd directSolutionB = solver.solve(rhsVectorB);

            // Copy solution back
            tbb::parallel_for(tbb::blocked_range<int>(0, domainCellLabels.voxelCount(), tbbLightGrainSize), [&](const tbb::blocked_range<int>& range)
            {
                for (int cellIndex = range.begin(); cellIndex != range.end(); ++cellIndex)
                {
                    Vec3i cell = domainCellLabels.unflatten(cellIndex);

                    if (domainCellLabels(cell) == CellLabels::INTERIOR_CELL || domainCellLabels(cell) == CellLabels::BOUNDARY_CELL)
                    {
                        int index = directSolverIndices(cell);
                        ASSERT_TRUE(index >= 0);

                        solutionA(cell) = directSolutionA(index);
                        solutionB(cell) = directSolutionB(index);
                    }
                }
            });
        }

        // Compute dot products
        double dotA = dotProduct(solutionA, rhsB, domainCellLabels);
        double dotB = dotProduct(solutionB, rhsA, domainCellLabels);

        EXPECT_TRUE(isNearlyEqual(dotA, dotB, 1e-10, true));
    }
    {
        // Test down and up sampling
        UniformGrid<CellLabels> coarseDomainLabels = buildCoarseCellLabels(domainCellLabels);

        ASSERT_TRUE(unitTestBoundaryCells(coarseDomainLabels) && unitTestBoundaryCells(domainCellLabels, &boundaryWeights));
        ASSERT_TRUE(unitTestExteriorCells(coarseDomainLabels) && unitTestExteriorCells(domainCellLabels));
        ASSERT_TRUE(unitTestCoarsening(coarseDomainLabels, domainCellLabels));

        UniformGrid<double> coarseRhs(coarseDomainLabels.size(), 0);

        UniformGrid<double> solutionA(domainCellLabels.size(), 0);

        {
            downsample(coarseRhs, rhsA, coarseDomainLabels, domainCellLabels);
            upsampleAndAdd(solutionA, coarseRhs, domainCellLabels, coarseDomainLabels);
        }

        UniformGrid<double> solutionB(domainCellLabels.size(), 0);

        {
            downsample(coarseRhs, rhsB, coarseDomainLabels, domainCellLabels);
            upsampleAndAdd(solutionB, coarseRhs, domainCellLabels, coarseDomainLabels);
        }

        // Compute dot products
        double dotA = dotProduct(solutionA, rhsB, domainCellLabels);
        double dotB = dotProduct(solutionB, rhsA, domainCellLabels);

        EXPECT_TRUE(isNearlyEqual(dotA, dotB, 1e-10, true));
    }

    // Test single level correction
    {
        UniformGrid<CellLabels> coarseDomainLabels = buildCoarseCellLabels(domainCellLabels);

        ASSERT_TRUE(unitTestBoundaryCells(coarseDomainLabels) && unitTestBoundaryCells(domainCellLabels, &boundaryWeights));
        ASSERT_TRUE(unitTestExteriorCells(coarseDomainLabels) && unitTestExteriorCells(domainCellLabels));
        ASSERT_TRUE(unitTestCoarsening(coarseDomainLabels, domainCellLabels));

        Eigen::SimplicialCholesky<Eigen::SparseMatrix<double>> coarseSolver;
        Eigen::SparseMatrix<double> sparseMatrix;

        // Pre-build matrix at the coarsest level
        int interiorCellCount = 0;
        UniformGrid<int> directSolverIndices(coarseDomainLabels.size(), -1);
        {
            forEachVoxelRange(Vec3i::Zero(), coarseDomainLabels.size(), [&](const Vec3i& cell)
            {
                if (coarseDomainLabels(cell) == CellLabels::INTERIOR_CELL || coarseDomainLabels(cell) == CellLabels::BOUNDARY_CELL)
                    directSolverIndices(cell) = interiorCellCount++;
            });

            // Build rows
            std::vector<Eigen::Triplet<double>> sparseElements;

            double gridScale = 1. / std::pow(2. * dx, 2);
            forEachVoxelRange(Vec3i::Zero(), coarseDomainLabels.size(), [&](const Vec3i& cell)
            {
                if (coarseDomainLabels(cell) == CellLabels::INTERIOR_CELL)
                {
                    int index = directSolverIndices(cell);
                    ASSERT_TRUE(index >= 0);
                    for (int axis : {0, 1, 2})
                    {
                        for (int direction : {0, 1})
                        {
                            Vec3i adjacentCell = cellToCell(cell, axis, direction);

                            auto adjacentLabels = coarseDomainLabels(adjacentCell);
                            ASSERT_TRUE(adjacentLabels == CellLabels::INTERIOR_CELL || adjacentLabels == CellLabels::BOUNDARY_CELL);

                            int adjacentIndex = directSolverIndices(adjacentCell);
                            ASSERT_TRUE(adjacentIndex >= 0);

                            sparseElements.emplace_back(index, adjacentIndex, -gridScale);
                        }
                    }

                    sparseElements.emplace_back(index, index, 4. * gridScale);
                }
                else if (coarseDomainLabels(cell) == CellLabels::BOUNDARY_CELL)
                {
                    double diagonal = 0;
                    int index = directSolverIndices(cell);
                    ASSERT_TRUE(index >= 0);
                    for (int axis : {0, 1, 2})
                    {
                        for (int direction : {0, 1})
                        {
                            Vec3i adjacentCell = cellToCell(cell, axis, direction);

                            auto cellLabels = coarseDomainLabels(adjacentCell);
                            if (cellLabels == CellLabels::INTERIOR_CELL || cellLabels == CellLabels::BOUNDARY_CELL)
                            {
                                int adjacentIndex = directSolverIndices(adjacentCell);
                                ASSERT_TRUE(adjacentIndex >= 0);

                                sparseElements.emplace_back(index, adjacentIndex, -gridScale);
                                ++diagonal;
                            }
                            else if (cellLabels == CellLabels::DIRICHLET_CELL)
                                ++diagonal;
                        }
                    }

                    sparseElements.emplace_back(index, index, diagonal * gridScale);
                }
            });

            sparseMatrix = Eigen::SparseMatrix<double>(interiorCellCount, interiorCellCount);
            sparseMatrix.setFromTriplets(sparseElements.begin(), sparseElements.end());

            coarseSolver.compute(sparseMatrix);

            ASSERT_TRUE(coarseSolver.info() == Eigen::Success);
        }

        // Transfer fine rhs to coarse rhs as if it was a residual with a zero initial guess
        UniformGrid<double> solutionA(domainCellLabels.size(), 0);
        UniformGrid<double> solutionB(domainCellLabels.size(), 0);

        {
            // Pre-smooth to get an initial guess
            VecVec3i boundaryCells = buildBoundaryCells(domainCellLabels, 3);

            // Test Jacobi symmetry
            for (int iteration = 0; iteration < 3; ++iteration)
            {
                boundaryJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, boundaryCells, dx, &boundaryWeights);
                boundaryJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, boundaryCells, dx, &boundaryWeights);
            }

            interiorJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, dx, &boundaryWeights);
            interiorJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, dx, &boundaryWeights);

            for (int iteration = 0; iteration < 3; ++iteration)
            {
                boundaryJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, boundaryCells, dx, &boundaryWeights);
                boundaryJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, boundaryCells, dx, &boundaryWeights);
            }

            // Compute new residual
            UniformGrid<double> residualA(domainCellLabels.size(), 0);
            UniformGrid<double> residualB(domainCellLabels.size(), 0);

            computePoissonResidual(residualA, solutionA, rhsA, domainCellLabels, dx, &boundaryWeights);
            computePoissonResidual(residualB, solutionB, rhsB, domainCellLabels, dx, &boundaryWeights);

            UniformGrid<double> coarseRhsA(coarseDomainLabels.size(), 0);
            UniformGrid<double> coarseRhsB(coarseDomainLabels.size(), 0);

            downsample(coarseRhsA, residualA, coarseDomainLabels, domainCellLabels);
            downsample(coarseRhsB, residualB, coarseDomainLabels, domainCellLabels);

            VectorXd coarseRHSVectorA = VectorXd::Zero(interiorCellCount);
            VectorXd coarseRHSVectorB = VectorXd::Zero(interiorCellCount);

            // Copy to Eigen and direct solve
            tbb::parallel_for(tbb::blocked_range<int>(0, coarseDomainLabels.voxelCount(), tbbLightGrainSize), [&](const tbb::blocked_range<int>& range) {
                for (int cellIndex = range.begin(); cellIndex != range.end(); ++cellIndex)
                {
                    Vec3i cell = coarseDomainLabels.unflatten(cellIndex);

                    if (coarseDomainLabels(cell) == CellLabels::INTERIOR_CELL || coarseDomainLabels(cell) == CellLabels::BOUNDARY_CELL)
                    {
                        int index = directSolverIndices(cell);
                        ASSERT_TRUE(index >= 0);

                        coarseRHSVectorA(index) = coarseRhsA(cell);
                        coarseRHSVectorB(index) = coarseRhsB(cell);
                    }
                }
            });

            UniformGrid<double> coarseSolutionA(coarseDomainLabels.size(), 0);
            UniformGrid<double> coarseSolutionB(coarseDomainLabels.size(), 0);

            VectorXd directSolutionA = coarseSolver.solve(coarseRHSVectorA);
            VectorXd directSolutionB = coarseSolver.solve(coarseRHSVectorB);

            // Copy solution back
            tbb::parallel_for(tbb::blocked_range<int>(0, coarseDomainLabels.voxelCount(), tbbLightGrainSize), [&](const tbb::blocked_range<int>& range) {
                for (int cellIndex = range.begin(); cellIndex != range.end(); ++cellIndex)
                {
                    Vec3i cell = coarseDomainLabels.unflatten(cellIndex);

                    if (coarseDomainLabels(cell) == CellLabels::INTERIOR_CELL || coarseDomainLabels(cell) == CellLabels::BOUNDARY_CELL)
                    {
                        int index = directSolverIndices(cell);
                        ASSERT_TRUE(index >= 0);

                        coarseSolutionA(cell) = directSolutionA(index);
                        coarseSolutionB(cell) = directSolutionB(index);
                    }
                }
            });

            upsampleAndAdd(solutionA, coarseSolutionA, domainCellLabels, coarseDomainLabels);
            upsampleAndAdd(solutionB, coarseSolutionB, domainCellLabels, coarseDomainLabels);

            for (int iteration = 0; iteration < 3; ++iteration)
            {
                boundaryJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, boundaryCells, dx, &boundaryWeights);
                boundaryJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, boundaryCells, dx, &boundaryWeights);
            }

            interiorJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, dx, &boundaryWeights);
            interiorJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, dx, &boundaryWeights);

            for (int iteration = 0; iteration < 3; ++iteration)
            {
                boundaryJacobiPoissonSmoother(solutionA, rhsA, domainCellLabels, boundaryCells, dx, &boundaryWeights);
                boundaryJacobiPoissonSmoother(solutionB, rhsB, domainCellLabels, boundaryCells, dx, &boundaryWeights);
            }
        }

        double dotA = dotProduct(solutionA, rhsB, domainCellLabels);
        double dotB = dotProduct(solutionB, rhsA, domainCellLabels);

        EXPECT_TRUE(isNearlyEqual(dotA, dotB, 1e-10, true));
    }

    // Test multigrid preconditioner symmetry
    {
        // Pre-build multigrid preconditioner
        GeometricMultigridPoissonSolver mgSolver(domainCellLabels, boundaryWeights, mgLevels, dx);

        UniformGrid<double> solutionA(domainCellLabels.size(), 0);
        mgSolver.applyMGVCycle(solutionA, rhsA);
        mgSolver.applyMGVCycle(solutionA, rhsA, true);
        mgSolver.applyMGVCycle(solutionA, rhsA, true);
        mgSolver.applyMGVCycle(solutionA, rhsA, true);

        UniformGrid<double> solutionB(domainCellLabels.size(), 0);
        mgSolver.applyMGVCycle(solutionB, rhsB);
        mgSolver.applyMGVCycle(solutionB, rhsB, true);
        mgSolver.applyMGVCycle(solutionB, rhsB, true);
        mgSolver.applyMGVCycle(solutionB, rhsB, true);

        double dotA = dotProduct(solutionA, rhsB, domainCellLabels);
        double dotB = dotProduct(solutionB, rhsA, domainCellLabels);

        EXPECT_TRUE(isNearlyEqual(dotA, dotB, 1e-10, true));
    }
}

TEST(GEOMETRIC_MULTIGRID_TESTS, SYMMETRY_TEST)
{
    std::vector<bool> complexDomainSettings = {false, true};
    std::vector<bool> solidSphereSettings = {false, true};

    const int startGrid = 16;
    const int endGrid = 4 * startGrid;

    for (int gridSize = startGrid; gridSize < endGrid; gridSize *= 2)
    {
        for (bool useComplexDomain : complexDomainSettings)
        {
            if (useComplexDomain)
            {
                for (bool useSolidSphere : solidSphereSettings)
                {
                    symmetryTest(useComplexDomain, useSolidSphere, gridSize);
                }
            }
            else
            {
                symmetryTest(useComplexDomain, false, gridSize);
            }
        }
    }
}

static void smootherTest(bool useComplexDomain, bool useSolidSphere, int gridSize)
{
    UniformGrid<CellLabels> domainCellLabels;
    VectorGrid<double> boundaryWeights;
    int mgLevels;
    Vec3i exteriorOffset;
    {
        UniformGrid<CellLabels> baseDomainCellLabels;
        VectorGrid<double> baseBoundaryWeights;

        // Complex domain set up
        if (useComplexDomain)
        {
            buildComplexDomain(baseDomainCellLabels, baseBoundaryWeights, gridSize, useSolidSphere);
        }
        // Simple domain set up
        else
        {
            buildSimpleDomain(baseDomainCellLabels, baseBoundaryWeights, gridSize, 1 /*dirichlet band*/);
        }

        // Build expanded domain
        std::pair<Vec3i, int> mgSettings = buildExpandedDomain(domainCellLabels, boundaryWeights, baseDomainCellLabels, baseBoundaryWeights);

        exteriorOffset = mgSettings.first;
        mgLevels = mgSettings.second;
    }

    double dx = boundaryWeights.dx();

    UniformGrid<double> rhsGrid(domainCellLabels.size(), 0);
    UniformGrid<double> solutionGrid(domainCellLabels.size(), 0);
    UniformGrid<double> residualGrid(domainCellLabels.size(), 0);

    {
        VectorXd randVec = VectorXd::Random(domainCellLabels.voxelCount());

        tbb::parallel_for(tbb::blocked_range<int>(0, domainCellLabels.voxelCount(), tbbLightGrainSize), [&](const tbb::blocked_range<int>& range)
        {
            for (int cellIndex = range.begin(); cellIndex != range.end(); ++cellIndex)
            {
                Vec3i cell = domainCellLabels.unflatten(cellIndex);

                if (domainCellLabels(cell) == CellLabels::INTERIOR_CELL || domainCellLabels(cell) == CellLabels::BOUNDARY_CELL)
                {
                    solutionGrid(cell) = randVec(cellIndex);
                }
            }
        });
    }

    // Set delta function
    double deltaPercent = .1;
    double deltaAmplitude = 1000;
    Vec3i deltaPoint = (deltaPercent * Vec3d::Constant(gridSize)).cast<int>() + exteriorOffset;

    forEachVoxelRange(deltaPoint - Vec3i::Ones(), deltaPoint + Vec3i::Constant(2), [&](const Vec3i& cell)
    {
        if (domainCellLabels(cell) == CellLabels::INTERIOR_CELL || domainCellLabels(cell) == CellLabels::BOUNDARY_CELL)
        {
            rhsGrid(cell) = deltaAmplitude;
        }
    });

    VecVec3i boundaryCells = buildBoundaryCells(domainCellLabels, 3);

    int maxIterations = 10;

    computePoissonResidual(residualGrid, solutionGrid, rhsGrid, domainCellLabels, dx);

    double oldL2Residual = std::sqrt(squaredl2Norm(residualGrid, domainCellLabels));

    for (int iteration = 0; iteration < maxIterations; ++iteration)
    {
        boundaryJacobiPoissonSmoother(solutionGrid, rhsGrid, domainCellLabels, boundaryCells, dx, &boundaryWeights);

        interiorJacobiPoissonSmoother(solutionGrid, rhsGrid, domainCellLabels, dx, &boundaryWeights);

        boundaryJacobiPoissonSmoother(solutionGrid, rhsGrid, domainCellLabels, boundaryCells, dx, &boundaryWeights);

        computePoissonResidual(residualGrid, solutionGrid, rhsGrid, domainCellLabels, dx);

        double l2Residual = std::sqrt(squaredl2Norm(residualGrid, domainCellLabels));

        EXPECT_LT(l2Residual, oldL2Residual);
        oldL2Residual = l2Residual;
    }
}

TEST(GEOMETRIC_MULTIGRID_TESTS, SMOOTHER_TEST)
{
    std::vector<bool> complexDomainSettings = {false, true};
    std::vector<bool> solidSphereSettings = {false, true};

    const int startGrid = 32;
    //const int endGrid = startGrid * int(pow(2, 5));
    const int endGrid = 8 * startGrid;

    for (int gridSize = startGrid; gridSize < endGrid; gridSize *= 2)
    {
        for (bool useComplexDomain : complexDomainSettings)
        {
            if (useComplexDomain)
            {
                for (bool useSolidSphere : solidSphereSettings)
                {
                    smootherTest(useComplexDomain, useSolidSphere, gridSize);
                }
            }
            else
            {
                smootherTest(useComplexDomain, false, gridSize);
            }
        }
    }
}


/*
 * Copyright Nick Thompson, 2020
 * Use, modification and distribution are subject to the
 * Boost Software License, Version 1.0. (See accompanying file
 * LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
 */

#ifndef BOOST_MATH_SPECIAL_DAUBECHIES_WAVELET_HPP
#define BOOST_MATH_SPECIAL_DAUBECHIES_WAVELET_HPP
#include <vector>
#include <array>
#include <cmath>
#include <thread>
#include <future>
#include <iostream>
#include <boost/math/constants/constants.hpp>
#include <boost/math/special_functions/detail/daubechies_scaling_integer_grid.hpp>
#include <boost/math/special_functions/daubechies_scaling.hpp>
#include <boost/math/filters/daubechies.hpp>
#include <boost/math/interpolators/detail/cubic_hermite_detail.hpp>
#include <boost/math/interpolators/detail/quintic_hermite_detail.hpp>
#include <boost/math/interpolators/detail/septic_hermite_detail.hpp>

namespace boost::math {

   template<class Real, int p, int order>
   std::vector<Real> daubechies_wavelet_dyadic_grid(int64_t j_max)
   {
      if (j_max == 0)
      {
         throw std::domain_error("The wavelet dyadic grid is refined from the scaling integer grid, so its minimum amount of data is half integer widths.");
      }
      auto phijk = daubechies_scaling_dyadic_grid<Real, p, order>(j_max - 1);
      //psi_j[l] = psi(-p+1 + l/2^j) = \sum_{k=0}^{2p-1} (-1)^k c_k \phi(1-2p+k + l/2^{j-1})
      //For derivatives just map c_k -> 2^order c_k.
      auto d = boost::math::filters::daubechies_scaling_filter<Real, p>();
      Real scale = boost::math::constants::root_two<Real>() * (1 << order);
      for (size_t i = 0; i < d.size(); ++i)
      {
         d[i] *= scale;
         if (!(i & 1))
         {
            d[i] = -d[i];
         }
      }

      std::vector<Real> v(2 * p + (2 * p - 1) * ((int64_t(1) << j_max) - 1), std::numeric_limits<Real>::quiet_NaN());
      v[0] = 0;
      v[v.size() - 1] = 0;

      for (int64_t l = 1; l < static_cast<int64_t>(v.size() - 1); ++l)
      {
         Real term = 0;
         for (int64_t k = 0; k < static_cast<int64_t>(d.size()); ++k)
         {
            int64_t idx = (int64_t(1) << (j_max - 1)) * (1 - 2 * p + k) + l;
            if (idx < 0 || idx >= static_cast<int64_t>(phijk.size()))
            {
               continue;
            }
            term += d[k] * phijk[idx];
         }
         v[l] = term;
      }

      return v;
   }

   template<class Real, int p>
   class daubechies_wavelet {
      //
      // Some type manipulation so we know the type of the interpolator, and the vector type it requires:
      //
      typedef std::vector < std::array < Real, p < 6 ? 2 : p < 10 ? 3 : 4>> vector_type;
      //
      // List our interpolators:
      //
      typedef std::tuple<
         detail::null_interpolator, detail::matched_holder_aos<vector_type>, detail::linear_interpolation_aos<vector_type>,
         interpolators::detail::cardinal_cubic_hermite_detail_aos<vector_type>, interpolators::detail::cardinal_quintic_hermite_detail_aos<vector_type>,
         interpolators::detail::cardinal_septic_hermite_detail_aos<vector_type> > interpolator_list;
      //
      // Select the one we need:
      //
      typedef std::tuple_element_t<
         p == 1 ? 0 :
         p == 2 ? 1 :
         p == 3 ? 2 :
         p <= 5 ? 3 :
         p <= 9 ? 4 : 5, interpolator_list> interpolator_type;
   public:
      daubechies_wavelet(int grid_refinements = -1)
      {
         static_assert(p < 20, "Daubechies wavelets are only implemented for p < 20.");
         static_assert(p > 0, "Daubechies wavelets must have at least 1 vanishing moment.");
         if (grid_refinements == 0)
         {
            throw std::domain_error("The wavelet requires at least 1 grid refinement.");
         }
         if constexpr (p == 1)
         {
            return;
         }
         else
         {
            if (grid_refinements < 0)
            {
               if (std::is_same_v<Real, float>)
               {
                  if (grid_refinements == -2)
                  {
                     // Control absolute error:
                     //                          p= 2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
                     std::array<int, 20> r{ -1, -1, 18, 19, 16, 11,  8,  7,  7,  7,  5,  5,  4,  4,  4,  4,  3,  3,  3,  3 };
                     grid_refinements = r[p];
                  }
                  else
                  {
                     // Control relative error:
                     //                          p= 2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
                     std::array<int, 20> r{ -1, -1, 21, 21, 21, 17, 16, 15, 14, 13, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11 };
                     grid_refinements = r[p];
                  }
               }
               else if (std::is_same_v<Real, double>)
               {
                  //                          p= 2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
                  std::array<int, 20> r{ -1, -1, 21, 21, 21, 21, 21, 21, 21, 21, 20, 20, 19, 18, 18, 18, 18, 18, 18, 18 };
                  grid_refinements = r[p];
               }
               else
               {
                  grid_refinements = 21;
               }
            }

            // Compute the refined grid:
            // In fact for float precision I know the grid must be computed in double precision and then cast back down, or else parts of the support are systematically inaccurate.
            std::future<std::vector<Real>> t0 = std::async(std::launch::async, [&grid_refinements]() {
               // Computing in higher precision and downcasting is essential for 1ULP evaluation in float precision:
               auto v = daubechies_wavelet_dyadic_grid<typename detail::daubechies_eval_type<Real>::type, p, 0>(grid_refinements);
               return detail::daubechies_eval_type<Real>::vector_cast(v);
               });
            // Compute the derivative of the refined grid:
            std::future<std::vector<Real>> t1 = std::async(std::launch::async, [&grid_refinements]() {
               auto v = daubechies_wavelet_dyadic_grid<typename detail::daubechies_eval_type<Real>::type, p, 1>(grid_refinements);
               return detail::daubechies_eval_type<Real>::vector_cast(v);
               });

            // if necessary, compute the second and third derivative:
            std::vector<Real> d2ydx2;
            std::vector<Real> d3ydx3;
            if constexpr (p >= 6) {
               std::future<std::vector<Real>> t3 = std::async(std::launch::async, [&grid_refinements]() {
                  auto v = daubechies_wavelet_dyadic_grid<typename detail::daubechies_eval_type<Real>::type, p, 2>(grid_refinements);
                  return detail::daubechies_eval_type<Real>::vector_cast(v);
                  });

               if constexpr (p >= 10) {
                  std::future<std::vector<Real>> t4 = std::async(std::launch::async, [&grid_refinements]() {
                     auto v = daubechies_wavelet_dyadic_grid<typename detail::daubechies_eval_type<Real>::type, p, 3>(grid_refinements);
                     return detail::daubechies_eval_type<Real>::vector_cast(v);
                     });
                  d3ydx3 = t4.get();
               }
               d2ydx2 = t3.get();
            }

            auto y = t0.get();
            auto dydx = t1.get();

            if constexpr (p >= 2)
            {
               vector_type data(y.size());
               for (size_t i = 0; i < y.size(); ++i)
               {
                  data[i][0] = y[i];
                  data[i][1] = dydx[i];
                  if constexpr (p >= 6)
                     data[i][2] = d2ydx2[i];
                  if constexpr (p >= 10)
                     data[i][3] = d3ydx3[i];
               }
               if constexpr (p <= 3)
                  m_interpolator = std::make_shared<interpolator_type>(std::move(data), grid_refinements, Real(-p + 1));
               else
                  m_interpolator = std::make_shared<interpolator_type>(std::move(data), Real(-p + 1), Real(1) / (1 << grid_refinements));
            }
            else
               m_interpolator = std::make_shared<detail::null_interpolator>();
         }
      }

      inline Real operator()(Real x) const
      {
         if (x <= -p + 1 || x >= p)
         {
            return 0;
         }
         if constexpr (p == 1)
         {
            if (x < Real(1) / Real(2))
            {
               return 1;
            }
            else if (x == Real(1) / Real(2))
            {
               return 0;
            }
            return -1;
         }
         return (*m_interpolator)(x);
      }

      inline Real prime(Real x) const
      {
         static_assert(p > 2, "The 3-vanishing moment Daubechies wavelet is the first which is continuously differentiable.");
         if (x <= -p + 1 || x >= p)
         {
            return 0;
         }
         return m_interpolator->prime(x);
      }

      inline Real double_prime(Real x) const
      {
         static_assert(p >= 6, "Second derivatives of Daubechies wavelets require at least 6 vanishing moments.");
         if (x <= -p + 1 || x >= p)
         {
            return Real(0);
         }
         return m_interpolator->double_prime(x);
      }

      std::pair<Real, Real> support() const
      {
         return { Real(-p + 1), Real(p) };
      }

      int64_t bytes() const
      {
         return m_interpolator->bytes() + sizeof(*this);
      }

   private:
      std::shared_ptr<interpolator_type> m_interpolator;
   };

}
#endif