/*
 * gpcxx/examples/artificial_ant/ant_simulation_plots.cpp
 * Date: 2014-09-01
 * Author: Gerard Choinka (gerard.choinka+gpcxx@gmail.com)
 * Copyright: Gerard Choinka
 *
 * Distributed under 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)
 */
#include "simulation.hpp"
#include "nodes.hpp"
#include "santa_fe_trail.hpp"

#include <gpcxx/app.hpp>
#include <gpcxx/evolve.hpp>
#include <gpcxx/generate.hpp>
#include <gpcxx/io.hpp>
#include <gpcxx/operator.hpp>
#include <gpcxx/stat.hpp>

#include <boost/lexical_cast.hpp>
#include <boost/range/numeric.hpp>

#include <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics/stats.hpp>
#include <boost/accumulators/statistics/mean.hpp>
#include <boost/accumulators/statistics/moment.hpp>
using namespace boost::accumulators;

#include <string>
#include <iostream>
#include <sstream>
#include <random>
#include <algorithm>
#include <tuple>
#include <cmath>

using rng_type = std::mt19937;

template<typename T>
class frange
{
public:
    frange()
        :frange( 0 , 0 , 1 )
    {
    }

    frange( frange const & ) = default;

    frange( T start , T end , T step )
        :m_start( start ) , m_end( end ) , m_step( step ) , m_iteration( 0 )
    {
    }

    frange( T start , T end )
        :frange( start , end , 1 )
    {
    }

    frange( T start )
        :frange( start, start, 1 )
    {
    }

    T value() const
    {
        return m_start + ( m_step * m_iteration );
    }

    bool has_next() const
    {
        return m_iteration < iteration_max();
    }

    void make_step()
    {
        if(m_iteration < iteration_max())
            ++m_iteration;
    }

private:
    int iteration_max() const
    {
        return static_cast< int >( ( m_end - m_start ) / m_step );
    }

private:
    T m_start;
    T m_end;
    T m_step;
    int m_iteration;
};

using arguments_type = std::unordered_map< std::string, frange< double > >;

struct ant_result
{
    double time;
    size_t generation;
    bool has_optimal_fitness;
    int fitness;
};

struct ant_result_stats
{
    ant_result_stats()
    : count(0),time(0.0),avg_generations(0.0),min_generations(0),max_generations(0),std_generations(0.0),avg_fitness(0.0),min_fitness(0),max_fitness(0),std_fitness(0.0),optimal_fitness_count(0)
    {
    }

    size_t count;
    double time;
    double avg_generations;
    size_t min_generations;
    size_t max_generations;
    double std_generations;
    double avg_fitness;
    double min_fitness;
    double max_fitness;
    double std_fitness;
    size_t optimal_fitness_count;
};

ant_result_stats make_stat(std::vector<ant_result> runs)
{
    ant_result_stats avg_runs;
    if( runs.empty() )
        return avg_runs;

    avg_runs.min_generations = runs.front().generation;
    avg_runs.max_generations = runs.front().generation;
    avg_runs.min_fitness = runs.front().fitness;
    avg_runs.max_fitness = runs.front().fitness;
    for(ant_result r : runs)
    {
        avg_runs.time += r.time / runs.size();
        avg_runs.avg_generations += double( r.generation ) / runs.size();
        avg_runs.avg_fitness     += double( r.fitness) / runs.size();
        avg_runs.optimal_fitness_count += r.has_optimal_fitness ? 1 : 0;
        avg_runs.min_generations = std::min( avg_runs.min_generations, r.generation );
        avg_runs.max_generations = std::max( avg_runs.max_generations, r.generation );
        avg_runs.min_fitness = std::min( avg_runs.min_fitness, double( r.fitness) );
        avg_runs.max_fitness = std::max( avg_runs.max_fitness, double( r.fitness) );
    }

    double var_generations = 0;
    double var_fitness = 0;
    for(ant_result r : runs)
    {
        var_generations += ( r.generation  - avg_runs.avg_generations ) * ( r.generation  - avg_runs.avg_generations );
        var_fitness     += ( r.fitness - avg_runs.avg_fitness )     * ( r.fitness - avg_runs.avg_fitness );
    }
    avg_runs.std_generations = std::sqrt(var_generations);
    avg_runs.std_fitness = std::sqrt(var_fitness);

    return avg_runs;
}

ant_result ant_simulation_gp(
    size_t const population_size,
    size_t const generation_max,
    double const number_elite,
    double const mutation_rate,
    double const crossover_rate,
    double const crossover_pip_rate,
    bool const   crossover_use_pip,
    double const reproduction_rate,
    size_t const min_tree_height,
    size_t const init_max_tree_height,
    size_t const max_tree_height,
    size_t const tournament_size,
    rng_type &rng,
    bool print_generations,
    size_t current_run
)
{
    using namespace ant_example;
    char const newl { '\n' };

    board const b{ santa_fe::x_size, santa_fe::y_size };
    int const max_steps { 400 };
    ant_simulation::food_trail_type const santa_fe_trail { santa_fe::make_santa_fe_trail( b ) };
    ant_simulation const                  ant_sim_santa_fe{ santa_fe_trail, b.get_size_x(), b.get_size_y(), { 0, 0 }, east, max_steps };

    gpcxx::uniform_symbol< node_type > terminal_gen { std::vector< node_type >{
        node_type { ant_move_task_terminal{} ,          "m" } ,
        node_type { ant_turn_left_task_terminal{} ,     "l" } ,
        node_type { ant_turn_right_task_terminal{} ,    "r" }
    } };

    gpcxx::uniform_symbol< node_type > binary_gen { std::vector< node_type > {
        node_type { prog2{} , "p2" } ,
        node_type { if_food_ahead{} , "if" }
    } };

    gpcxx::uniform_symbol< node_type > ternary_gen { std::vector< node_type > {
        node_type { prog3{} , "p3" }
    } };

    gpcxx::node_generator< node_type , rng_type , 3 > node_generator {
        { double( terminal_gen.num_symbols() ) , 0 , terminal_gen } ,
        { double( binary_gen.num_symbols() ) , 2 , binary_gen } ,
        { double( ternary_gen.num_symbols() ) , 3 , ternary_gen } };

    population_type population( population_size );

    auto tree_generator      = gpcxx::make_basic_generate_strategy( rng , node_generator , min_tree_height , max_tree_height );
    auto init_tree_generator = gpcxx::make_basic_generate_strategy( rng , node_generator , min_tree_height , init_max_tree_height );
    for( auto & individum :  population )
        init_tree_generator( individum );

    using evolver_type = gpcxx::static_pipeline< population_type , fitness_type , rng_type > ;
    evolver_type evolver( number_elite , mutation_rate , crossover_rate , reproduction_rate , rng );

    evolver.mutation_function() = gpcxx::make_mutation(
         gpcxx::make_point_mutation( rng , tree_generator , max_tree_height , 20 ) ,
         gpcxx::make_tournament_selector( rng , tournament_size ) );

    if(crossover_use_pip)
        evolver.crossover_function() = gpcxx::make_crossover(
                gpcxx::make_one_point_crossover_pip_strategy(rng, max_tree_height, crossover_pip_rate ),
                gpcxx::make_tournament_selector(rng, tournament_size));
    else
        evolver.crossover_function() = gpcxx::make_crossover(
                gpcxx::make_one_point_crossover_strategy(rng, max_tree_height ),
                gpcxx::make_tournament_selector(rng, tournament_size));

    evolver.reproduction_function() = gpcxx::make_reproduce( gpcxx::make_tournament_selector( rng , tournament_size ) );

    evaluator       fitness_f;
    fitness_type    fitness( population_size , 0.0 );

    gpcxx::timer overall_timer;
    bool    has_optimal_fitness { false };
    size_t  generation { 0 };
    do
    {
        gpcxx::timer iteration_timer;
        if( generation != 0 )
            evolver.next_generation( population , fitness );
        double evolve_time { iteration_timer.seconds() };
        iteration_timer.restart();
        std::transform( population.begin() , population.end() , fitness.begin() , [&]( tree_type const &t ) {
            return fitness_f( t , ant_sim_santa_fe );
        } );
        double eval_time = iteration_timer.seconds();

        if(print_generations)
        {
            std::cout << gpcxx::indent(0) << "Generation " << current_run << ":" << generation << newl;
            std::cout << gpcxx::indent(1) << "Evolve time " << evolve_time << newl;
            std::cout << gpcxx::indent(1) << "Eval time " << eval_time << newl;
            std::cout << gpcxx::indent(1) << "Best individuals\n" << gpcxx::best_individuals(population, fitness, 2, 3, false) << newl;
            std::cout << gpcxx::indent(1) << "Statistics : " << gpcxx::calc_population_statistics(population) << newl << std::endl;
        }

        generation++;
        has_optimal_fitness = ( 0 == *std::min_element( fitness.begin(), fitness.end() ) );
    } while( !has_optimal_fitness && generation < generation_max );

    return { overall_timer.seconds() , generation , has_optimal_fitness, *std::min_element( fitness.begin(), fitness.end() ) };
}

ant_result ant_simulation_gp_stats_helper(
    arguments_type arguments,
    std::ostream & out,
    size_t repeat_ntimes,
    bool print_header,
    bool print_generations
)
{
    rng_type rng;

    if(print_header) {
        out << "i " << "\t"
            << "count" << "\t"
            << "avg_time" << "\t"
            << "avg_generations" << "\t"
            << "min_generations" << "\t"
            << "max_generations" << "\t"
            << "std_generations" << "\t"
            << "avg_fitness" << "\t"
            << "min_fitness" << "\t"
            << "max_fitness" << "\t"
            << "std_fitness" << "\t"
            << "optimal_fitness_count" << "\t"
            << "population_size" << "\t"
            << "generation_max" << "\t"
            << "number_elite" << "\t"
            << "mutation_rate" << "\t"
            << "crossover_rate" << "\t"
            << "crossover_pip_rate" << "\t"
            << "crossover_use_pip" << "\t"
            << "reproduction_rate" << "\t"
            << "min_tree_height" << "\t"
            << "init_max_tree_height" << "\t"
            << "max_tree_height" << "\t"
            << "tournament_size"
            << std::endl;
    }
    size_t iteration = 0;

    bool any_has_next = false;
    do
    {
        auto population_size =      arguments["population_size"].value();
        auto generation_max =       arguments["generation_max"].value();
        auto number_elite =         arguments["number_elite"].value();
        auto mutation_rate =        arguments["mutation_rate"].value();
        auto crossover_rate =       arguments["crossover_rate"].value();
        auto crossover_pip_rate =   arguments["crossover_pip_rate"].value();
        auto crossover_use_pip =    arguments["crossover_use_pip"].value();
        auto reproduction_rate =    arguments["reproduction_rate"].value();
        auto min_tree_height =      arguments["min_tree_height"].value();
        auto init_max_tree_height = arguments["init_max_tree_height"].value();
        auto max_tree_height =      arguments["max_tree_height"].value();
        auto tournament_size =      arguments["tournament_size"].value();

        auto print_row = [&](std::ostream & os, std::vector<ant_result> const & results)
        {
            auto stats = make_stat(results);
            os << std::fixed
                << iteration << "\t"
                << stats.count << "\t"
                << stats.time << "\t"
                << stats.avg_generations << "\t"
                << stats.min_generations << "\t"
                << stats.max_generations << "\t"
                << stats.std_generations << "\t"
                << stats.avg_fitness << "\t"
                << stats.min_fitness << "\t"
                << stats.max_fitness << "\t"
                << stats.std_fitness << "\t"
                << stats.optimal_fitness_count << "\t"
                << population_size << "\t"
                << generation_max << "\t"
                << number_elite << "\t"
                << mutation_rate << "\t"
                << crossover_rate << "\t"
                << crossover_pip_rate << "\t"
                << crossover_use_pip << "\t"
                << reproduction_rate << "\t"
                << min_tree_height << "\t"
                << init_max_tree_height << "\t"
                << max_tree_height << "\t"
                << tournament_size
                << std::endl;
        };

        std::vector<ant_result> results;
        for( size_t i = 0; i < repeat_ntimes; ++i)
        {
            rng.seed(std::chrono::high_resolution_clock::now().time_since_epoch().count());
            results.emplace_back(ant_simulation_gp(
                population_size,
                generation_max,
                number_elite,
                mutation_rate,
                crossover_rate,
                crossover_pip_rate,
                crossover_use_pip,
                reproduction_rate,
                min_tree_height,
                init_max_tree_height,
                max_tree_height,
                tournament_size,
                rng,
                print_generations,
                i
            ));
            print_row( std::cout , results );
        }
        print_row( out , results );
        ++iteration;
        any_has_next = std::any_of(arguments.begin(), arguments.end(), [](arguments_type::value_type const & v){
            return v.second.has_next();
        });
        for_each(arguments.begin(), arguments.end(), [](arguments_type::value_type & v){
            if(v.second.has_next()) v.second.make_step();
        });
    } while( any_has_next );

    return ant_result {};
}

int main( int argc , char *argv[] )
{
    int mandatory_args_count = 5;
    int range_args_count = 4;
    if(((argc - mandatory_args_count) % range_args_count)  != 0 || argc < mandatory_args_count)
    {
        std::cerr << "Usage: " << argv[0] <<
            " <filename> <avg_over_n> <print_header> <print_generations> "\
            "[[<argumentname> <range_start> <range_end> <range_step>]"\
            " [<argumentname> <range_start> <range_end> <range_step>] [...]] "
                        << std::endl;
        return 1;
    }

    arguments_type  const default_arguments
    {
        { "population_size" ,   frange< double >( 500 ) },
        { "generation_max" ,    frange< double >( 500 ) },
        { "number_elite" ,      frange< double >( 0 ) },
        { "mutation_rate" ,     frange< double >( 0.0 ) },
        { "crossover_rate" ,    frange< double >( 0.9 ) },
        { "crossover_pip_rate" ,frange< double >( 0.9 ) },
        { "crossover_use_pip" , frange< double >( 1 ) }   ,
        { "reproduction_rate" , frange< double >( 0.0 ) },
        { "min_tree_height" ,   frange< double >( 1 ) },
        { "init_max_tree_height" , frange< double >( 6 ) },
        { "max_tree_height" ,   frange< double >( 17 ) },
        { "tournament_size" ,   frange< double >( 7 ) }
    };

    std::string filename { argv[1] };
    size_t repeate_ntimes    = boost::lexical_cast<size_t>( argv[2] );
    bool print_header = boost::lexical_cast<bool>( argv[3] );
    bool print_generations = boost::lexical_cast<bool>( argv[4] );
    arguments_type arguments;

    for(int i = 0; i < ((argc - mandatory_args_count) / range_args_count); ++i)
    {
        size_t offset = (i*range_args_count)+mandatory_args_count;
        std::string  arg_name { argv[offset + 0] };
        double start    = boost::lexical_cast<double>( argv[offset + 1] );
        double end      = boost::lexical_cast<double>( argv[offset + 2] );
        double step     = boost::lexical_cast<double>( argv[offset + 3] );
        arguments[arg_name] = frange<double>{ start, end, step };
    }
    std::unordered_map<std::string, arguments_type > variations{ { filename, arguments } };

    for( auto const & task : variations )
    {
        arguments_type default_arguments_copy{ task.second };
        default_arguments_copy.insert(default_arguments.begin(), default_arguments.end());

        std::ofstream outfile{ task.first };
        ant_simulation_gp_stats_helper( default_arguments_copy, outfile, repeate_ntimes, print_header, print_generations );
    }
}


/*
 * Copyright (c) 2013, Christian Gehring, Hannes Sommer, Paul Furgale, Remo Diethelm
 * 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 Autonomous Systems Lab, ETH Zurich 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 Christian Gehring, Hannes Sommer, Paul Furgale,
 * Remo Diethelm 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.
 *
*/

#ifndef KINDR_ROTATIONS_EIGEN_EULERANGLESZYX_HPP_
#define KINDR_ROTATIONS_EIGEN_EULERANGLESZYX_HPP_

#include <cmath>

#include <Eigen/Geometry>

#include "kindr/common/common.hpp"
#include "kindr/common/assert_macros_eigen.hpp"
#include "kindr/rotations/RotationBase.hpp"
#include "kindr/rotations/eigen/RotationEigenFunctions.hpp"

namespace kindr {
namespace rotations {
namespace eigen_impl {

/*! \class EulerAnglesZyx
 *  \brief Implementation of Euler angles (Z-Y'-X'' / yaw-pitch-roll) rotation based on Eigen::Matrix<Scalar, 3, 1>
 *
 *  The following typedefs are provided for convenience:
 *   - \ref eigen_impl::EulerAnglesZyxAD "EulerAnglesZyxAD" for active rotation and double primitive type
 *   - \ref eigen_impl::EulerAnglesZyxAF "EulerAnglesZyxAF" for active rotation and float primitive type
 *   - \ref eigen_impl::EulerAnglesZyxPD "EulerAnglesZyxPD" for passive rotation and double primitive type
 *   - \ref eigen_impl::EulerAnglesZyxPF "EulerAnglesZyxPF" for passive rotation and float primitive type
 *   - EulerAnglesYprAD = EulerAnglesZyxAD
 *   - EulerAnglesYprAF = EulerAnglesZyxAF
 *   - EulerAnglesYprPD = EulerAnglesZyxPD
 *   - EulerAnglesYprPF = EulerAnglesZyxPF
 *
 *  \tparam PrimType_ the primitive type of the data (double or float)
 *  \tparam Usage_ the rotation usage which is either active or passive
 *  \ingroup rotations
 */
template<typename PrimType_, enum RotationUsage Usage_>
class EulerAnglesZyx : public EulerAnglesZyxBase<EulerAnglesZyx<PrimType_, Usage_>, Usage_> {
 private:
  /*! \brief The base type.
   */
  typedef Eigen::Matrix<PrimType_, 3, 1> Base;

  /*! \brief vector of Euler angles [yaw; pitch; roll]
   */
  Base zyx_;
 public:
  /*! \brief The implementation type.
   *  The implementation type is always an Eigen object.
   */
  typedef Base Implementation;
  /*! \brief The primitive type.
   *  Float/Double
   */
  typedef PrimType_ Scalar;

  /*! \brief Euler angles as 3x1-matrix
   */
  typedef Base Vector;

  /*! \brief Default constructor using identity rotation.
   */
  EulerAnglesZyx()
    : zyx_(Base::Zero()) {
  }

  /*! \brief Constructor using three scalars.
   *  \param yaw      first rotation angle around Z axis
   *  \param pitch    second rotation angle around Y' axis
   *  \param roll     third rotation angle around X'' axis
   */
  EulerAnglesZyx(Scalar yaw, Scalar pitch, Scalar roll)
    : zyx_(yaw,pitch,roll) {
  }

  /*! \brief Constructor using Eigen::Matrix.
   *  \param other   Eigen::Matrix<PrimType_,3,1> [roll; pitch; yaw]
   */
  explicit EulerAnglesZyx(const Base& other)
    : zyx_(other) {
  }

  /*! \brief Constructor using another rotation.
   *  \param other   other rotation
   */
  template<typename OtherDerived_>
  inline explicit EulerAnglesZyx(const RotationBase<OtherDerived_, Usage_>& other)
    : zyx_(internal::ConversionTraits<EulerAnglesZyx, OtherDerived_>::convert(other.derived()).toImplementation()) {
  }

  /*! \brief Assignment operator using another rotation.
   *  \param other   other rotation
   *  \returns referece
   */
  template<typename OtherDerived_>
  EulerAnglesZyx& operator =(const RotationBase<OtherDerived_, Usage_>& other) {
    this->toImplementation() = internal::ConversionTraits<EulerAnglesZyx, OtherDerived_>::convert(other.derived()).toImplementation();
    return *this;
  }

  /*! \brief Parenthesis operator to convert from another rotation.
   *  \param other   other rotation
   *  \returns reference
   */
  template<typename OtherDerived_>
  EulerAnglesZyx& operator ()(const RotationBase<OtherDerived_, Usage_>& other) {
    this->toImplementation() = internal::ConversionTraits<EulerAnglesZyx, OtherDerived_>::convert(other.derived()).toImplementation();
    return *this;
  }

  /*! \brief Returns the inverse of the rotation.
   *  \returns the inverse of the rotation
   */
  EulerAnglesZyx inverted() const {
    return EulerAnglesZyx(eigen_internal::getInverseYpr<PrimType_, PrimType_>(this->toImplementation()));
  }

  /*! \brief Inverts the rotation.
   *  \returns reference
   */
  EulerAnglesZyx& invert() {
    *this = this->inverted();
    return *this;
  }

  /*! \brief Returns the Euler angles in a vector.
   *  \returns  vector Eigen::Matrix<Scalar,3, 1>
   */
  inline const Vector vector() const {
    return this->toImplementation();
  }

  /*! \brief Cast to the implementation type.
   *  \returns the implementation for direct manipulation (recommended only for advanced users)
   */
  inline Base& toImplementation() {
    return static_cast<Base&>(zyx_);
  }

  /*! \brief Cast to the implementation type.
   *  \returns the implementation for direct manipulation (recommended only for advanced users)
   */
  inline const Base& toImplementation() const {
    return static_cast<const Base&>(zyx_);
  }

  /*! \brief Gets yaw (Z) angle.
   *  \returns yaw angle (scalar)
   */
  inline Scalar yaw() const {
    return zyx_(0);
  }

  /*! \brief Gets pitch (Y') angle.
   *  \returns pitch angle (scalar)
   */
  inline Scalar pitch() const {
    return zyx_(1);
  }

  /*! \brief Gets roll (X'') angle.
   *  \returns roll angle (scalar)
   */
  inline Scalar roll() const {
    return zyx_(2);
  }

  /*! \brief Sets yaw (Z) angle.
   */
  inline void setYaw(Scalar yaw) {
    zyx_(0) = yaw;
  }

  /*! \brief Sets pitch (Y') angle.
   */
  inline void setPitch(Scalar pitch) {
    zyx_(1) = pitch;
  }

  /*! \brief Sets roll (X'') angle.
   */
  inline void setRoll(Scalar roll) {
    zyx_(2) = roll;
  }

  /*! \brief Reading access to yaw (Z) angle.
   *  \returns yaw angle (scalar) with reading access
   */
  inline Scalar z() const {
    return zyx_(0);
  }

  /*! \brief Reading access to pitch (Y') angle.
   *  \returns pitch angle (scalar) with reading access
   */
  inline Scalar y() const {
    return zyx_(1);
  }

  /*! \brief Reading access to roll (X'') angle.
   *  \returns roll angle (scalar) with reading access
   */
  inline Scalar x() const {
    return zyx_(2);
  }

  /*! \brief Writing access to yaw (Z) angle.
   *  \returns yaw angle (scalar) with writing access
   */
  inline void setZ(Scalar z) {
    zyx_(0) = z;
  }

  /*! \brief Writing access to pitch (Y') angle.
   *  \returns pitch angle (scalar) with writing access
   */
  inline void setY(Scalar y) {
    zyx_(1) = y;
  }

  /*! \brief Writing access to roll (X'') angle.
   *  \returns roll angle (scalar) with writing access
   */
  inline void setX(Scalar x) {
    zyx_(2) = x;
  }

  /*! \brief Sets the rotation to identity.
   *  \returns reference
   */
  EulerAnglesZyx& setIdentity() {
    zyx_.setZero();
    return *this;
  }

  /*! \brief Returns a unique Euler angles rotation with angles in [-pi,pi),[-pi/2,pi/2),[-pi,pi).
   *  This function is used to compare different rotations.
   *  \returns copy of the Euler angles rotation which is unique
   */
  EulerAnglesZyx getUnique() const {
    Base zyx(kindr::common::floatingPointModulo(z()+M_PI,2*M_PI)-M_PI,
             kindr::common::floatingPointModulo(y()+M_PI,2*M_PI)-M_PI,
             kindr::common::floatingPointModulo(x()+M_PI,2*M_PI)-M_PI); // wrap all angles into [-pi,pi)

    const double tol = 1e-3;

    // wrap angles into [-pi,pi),[-pi/2,pi/2),[-pi,pi)
    if(zyx.y() < -M_PI/2 - tol)
    {
      if(zyx.z() < 0) {
        zyx.z() = zyx.z() + M_PI;
      } else {
        zyx.z() = zyx.z() - M_PI;
      }

      zyx.y() = -(zyx.y() + M_PI);

      if(zyx.x() < 0) {
        zyx.x() = zyx.x() + M_PI;
      } else {
        zyx.x() = zyx.x() - M_PI;
      }
    }
    else if(-M_PI/2 - tol <= zyx.y() && zyx.y() <= -M_PI/2 + tol)
    {
      zyx.z() += zyx.x();
      zyx.x() = 0;
    }
    else if(-M_PI/2 + tol < zyx.y() && zyx.y() < M_PI/2 - tol)
    {
      // ok
    }
    else if(M_PI/2 - tol <= zyx.y() && zyx.y() <= M_PI/2 + tol)
    {
      // todo: M_PI/2 should not be in range, other formula?
      zyx.z() -= zyx.x();
      zyx.x() = 0;
    }
    else // M_PI/2 + tol < zyx.y()
    {
      if(zyx.z() < 0) {
        zyx.z() = zyx.z() + M_PI;
      } else {
        zyx.z() = zyx.z() - M_PI;
      }

      zyx.y() = -(zyx.y() - M_PI);

      if(zyx.x() < 0) {
        zyx.x() = zyx.x() + M_PI;
      } else {
        zyx.x() = zyx.x() - M_PI;
      }
    }

    return EulerAnglesZyx(zyx);
  }

  /*! \brief Modifies the Euler angles rotation such that the angles lie in [-pi,pi),[-pi/2,pi/2),[-pi,pi).
   *  \returns reference
   */
  EulerAnglesZyx& setUnique() {  // wraps angles into [-pi,pi),[-pi/2,pi/2),[-pi,pi)
    *this = getUnique();
    return *this;
  }

  /*! \brief Concenation operator.
   *  This is explicitly specified, because Eigen::Matrix provides also an operator*.
   *  \returns the concenation of two rotations
   */
  using EulerAnglesZyxBase<EulerAnglesZyx<PrimType_, Usage_>, Usage_>::operator*;

  /*! \brief Equivalence operator.
   *  This is explicitly specified, because Eigen::Matrix provides also an operator==.
   *  \returns true if two rotations are similar.
   */
  using EulerAnglesZyxBase<EulerAnglesZyx<PrimType_, Usage_>, Usage_>::operator==;

  /*! \brief Used for printing the object with std::cout.
   *  \returns std::stream object
   */
  friend std::ostream& operator << (std::ostream& out, const EulerAnglesZyx& zyx) {
    out << zyx.toImplementation().transpose();
    return out;
  }
};

//! \brief Active Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) with double primitive type
typedef EulerAnglesZyx<double, RotationUsage::ACTIVE>  EulerAnglesZyxAD;
//! \brief Active Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) with float primitive type
typedef EulerAnglesZyx<float,  RotationUsage::ACTIVE>  EulerAnglesZyxAF;
//! \brief Passive Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) with double primitive type
typedef EulerAnglesZyx<double, RotationUsage::PASSIVE> EulerAnglesZyxPD;
//! \brief Passive Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) with float primitive type
typedef EulerAnglesZyx<float,  RotationUsage::PASSIVE> EulerAnglesZyxPF;

//! \brief Equivalent Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) class
template <typename PrimType_, enum RotationUsage Usage_>
using EulerAnglesYpr = EulerAnglesZyx<PrimType_, Usage_>;

//! \brief Active Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) with double primitive type
typedef EulerAnglesYpr<double, RotationUsage::ACTIVE>  EulerAnglesYprAD;
//! \brief Active Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) with float primitive type
typedef EulerAnglesYpr<float,  RotationUsage::ACTIVE>  EulerAnglesYprAF;
//! \brief Passive Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) with double primitive type
typedef EulerAnglesYpr<double, RotationUsage::PASSIVE> EulerAnglesYprPD;
//! \brief Passive Euler angles rotation (Z,Y',X'' / yaw,pitch,roll) with float primitive type
typedef EulerAnglesYpr<float,  RotationUsage::PASSIVE> EulerAnglesYprPF;

} // namespace eigen_impl

namespace internal {

template<typename PrimType_, enum RotationUsage Usage_>
class get_scalar<eigen_impl::EulerAnglesZyx<PrimType_, Usage_>> {
 public:
  typedef PrimType_ Scalar;
};

template<typename PrimType_, enum RotationUsage Usage_>
class get_matrix3X<eigen_impl::EulerAnglesZyx<PrimType_, Usage_>>{
 public:
  typedef int  IndexType;

  template <IndexType Cols>
  using Matrix3X = Eigen::Matrix<PrimType_, 3, Cols>;
};

template<typename PrimType_>
class get_other_usage<eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::ACTIVE>> {
 public:
  typedef eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::PASSIVE> OtherUsage;
};

template<typename PrimType_>
class get_other_usage<eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::PASSIVE>> {
 public:
  typedef eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::ACTIVE> OtherUsage;
};

/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 * Conversion Traits
 * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
template<typename DestPrimType_, typename SourcePrimType_, enum RotationUsage Usage_>
class ConversionTraits<eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>, eigen_impl::AngleAxis<SourcePrimType_, Usage_>> {
 public:
  inline static eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_> convert(const eigen_impl::AngleAxis<SourcePrimType_, Usage_>& aa) {
//    return eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>(eigen_impl::getYprFromAngleAxis<SourcePrimType_, DestPrimType_>(aa.toImplementation()));
    return eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>(eigen_impl::RotationQuaternion<DestPrimType_, Usage_>(aa));
  }
};

template<typename DestPrimType_, typename SourcePrimType_, enum RotationUsage Usage_>
class ConversionTraits<eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>, eigen_impl::RotationVector<SourcePrimType_, Usage_>> {
 public:
  inline static eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_> convert(const eigen_impl::RotationVector<SourcePrimType_, Usage_>& rotationVector) {
    return eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>(eigen_impl::AngleAxis<SourcePrimType_, Usage_>(rotationVector));
  }
};

template<typename DestPrimType_, typename SourcePrimType_, enum RotationUsage Usage_>
class ConversionTraits<eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>, eigen_impl::RotationQuaternion<SourcePrimType_, Usage_>> {
 public:
  inline static eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_> convert(const eigen_impl::RotationQuaternion<SourcePrimType_, Usage_>& q) {
    return eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>(eigen_impl::eigen_internal::getYprFromQuaternion<SourcePrimType_, DestPrimType_>(q.toImplementation()));
  }
};

template<typename DestPrimType_, typename SourcePrimType_, enum RotationUsage Usage_>
class ConversionTraits<eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>, eigen_impl::RotationMatrix<SourcePrimType_, Usage_>> {
 public:
  inline static eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_> convert(const eigen_impl::RotationMatrix<SourcePrimType_, Usage_>& R) {
    return eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>(eigen_impl::eigen_internal::getYprFromRotationMatrix<SourcePrimType_, DestPrimType_>(R.toImplementation().transpose()));
  }
};

template<typename DestPrimType_, typename SourcePrimType_, enum RotationUsage Usage_>
class ConversionTraits<eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>, eigen_impl::EulerAnglesXyz<SourcePrimType_, Usage_>> {
 public:
  inline static eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_> convert(const eigen_impl::EulerAnglesXyz<SourcePrimType_, Usage_>& xyz) {
    return eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>(eigen_impl::eigen_internal::getYprFromRpy<SourcePrimType_, DestPrimType_>(xyz.toImplementation()));
  }
};

template<typename DestPrimType_, typename SourcePrimType_, enum RotationUsage Usage_>
class ConversionTraits<eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>, eigen_impl::EulerAnglesZyx<SourcePrimType_, Usage_>> {
 public:
  inline static eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_> convert(const eigen_impl::EulerAnglesZyx<SourcePrimType_, Usage_>& zyx) {
    return eigen_impl::EulerAnglesZyx<DestPrimType_, Usage_>(zyx.toImplementation().template cast<DestPrimType_>());
  }
};

/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 * Multiplication Traits
 * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
//template<typename PrimType_>
//class MultiplicationTraits<RotationBase<eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::ACTIVE>, RotationUsage::ACTIVE>, RotationBase<eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::ACTIVE>, RotationUsage::ACTIVE>> {
// public:
//  inline static eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::ACTIVE> mult(const eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::ACTIVE>& a, const eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::ACTIVE>& b) {
//    return eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::ACTIVE>(eigen_impl::RotationQuaternion<PrimType_, RotationUsage::ACTIVE>(
//                                                                 eigen_impl::RotationQuaternion<PrimType_, RotationUsage::ACTIVE>(a).toImplementation()*
//                                                                 eigen_impl::RotationQuaternion<PrimType_, RotationUsage::ACTIVE>(b).toImplementation()));
//  }
//};
//
//template<typename PrimType_>
//class MultiplicationTraits<RotationBase<eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::PASSIVE>, RotationUsage::PASSIVE>, RotationBase<eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::PASSIVE>, RotationUsage::PASSIVE>> {
// public:
//  inline static eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::PASSIVE> mult(const eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::PASSIVE>& a, const eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::PASSIVE>& b) {
//    return eigen_impl::EulerAnglesZyx<PrimType_, RotationUsage::PASSIVE>(eigen_impl::RotationQuaternion<PrimType_, RotationUsage::PASSIVE>(
//                                                                 eigen_impl::RotationQuaternion<PrimType_, RotationUsage::PASSIVE>(a).toImplementation()*
//                                                                 eigen_impl::RotationQuaternion<PrimType_, RotationUsage::PASSIVE>(b).toImplementation()));
//  }
//};

//template<typename PrimType_, enum RotationUsage Usage_>
//class MultiplicationTraits<RotationBase<eigen_impl::EulerAnglesZyx<PrimType_, Usage_>, Usage_>, RotationBase<eigen_impl::EulerAnglesZyx<PrimType_, Usage_>, Usage_>> {
// public:
//  inline static eigen_impl::EulerAnglesZyx<PrimType_, Usage_> mult(const eigen_impl::EulerAnglesZyx<PrimType_, Usage_>& a, const eigen_impl::EulerAnglesZyx<PrimType_, Usage_>& b) {
//    return eigen_impl::EulerAnglesZyx<PrimType_, Usage_>(eigen_impl::RotationQuaternion<PrimType_, Usage_>(
//                                                                 eigen_impl::RotationQuaternion<PrimType_, Usage_>(a).toImplementation()*
//                                                                 eigen_impl::RotationQuaternion<PrimType_, Usage_>(b).toImplementation()));
//  }
//};

/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 * Rotation Traits
 * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */

/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 * Comparison Traits
 * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */

} // namespace internal
} // namespace rotations
} // namespace kindr

#endif /* KINDR_ROTATIONS_EIGEN_EULERANGLESZYX_HPP_ */


// Copyright (c) 2016 Graphcore Ltd. All rights reserved.
#include "poplin/ConvUtil.hpp"
#include "ConvUtilInternal.hpp"

#include "CanonicalConvParams.hpp"
#include "poplibs_support/gcd.hpp"
#include <boost/optional.hpp>
#include <boost/range/irange.hpp>
#include <cassert>
#include <map>
#include <poplibs_support/logging.hpp>
#include <popops/Rearrange.hpp>
#include <poputil/Util.hpp>
#include <poputil/VertexTemplates.hpp>
#include <poputil/exceptions.hpp>

using namespace popops;
using namespace poputil;
using namespace poplibs_support;

namespace poplin {

namespace {

class ConvRange {
  unsigned begin_;
  unsigned end_;
  /// Whether the range is exact or a conservative approximation.
  bool isExact_;
  /// Whether the range may contains gaps / zeros.
  bool isDilated_;

  void canonicalize() {
    if (begin_ >= end_) {
      begin_ = end_ = 0;
      isExact_ = true;
      isDilated_ = false;
    } else if (size() < 3) {
      isDilated_ = false;
    }
  }

public:
  ConvRange(unsigned begin, unsigned end, bool isExact = true,
            bool isDilated = false)
      : begin_(begin), end_(end), isExact_(isExact), isDilated_(isDilated) {
    canonicalize();
  }
  explicit ConvRange(std::pair<unsigned, unsigned> range, bool isExact = true,
                     bool isDilated = false)
      : ConvRange(range.first, range.second, isExact, isDilated) {}
  static ConvRange getEmpty() { return ConvRange(0, 0); }
  bool empty() const { return begin_ == end_; }
  unsigned size() const { return end_ - begin_; }
  unsigned begin() const { return begin_; }
  unsigned end() const { return end_; }
  bool isExact() const { return isExact_; }
  bool isDilated() const { return isDilated_; }
  unsigned last() const {
    assert(!empty());
    return end_ - 1;
  }
  // Set lower bound of the range to the max of the current lower bound and
  // the specified value.
  void refineBegin(unsigned bound) {
    if (bound > begin_) {
      begin_ = bound;
      if (isDilated_)
        isExact_ = false;
      canonicalize();
    }
  }
  // Set upper bound of the range to the max of the current upper bound and
  // the specified value.
  void refineEnd(unsigned bound) {
    if (bound < end_) {
      end_ = bound;
      if (isDilated_)
        isExact_ = false;
      canonicalize();
    }
  }
  operator std::pair<unsigned, unsigned>() const { return {begin_, end_}; }
};

} // End anonymous namespace

unsigned getDilatedSize(unsigned size, unsigned dilation) {
  if (size == 0)
    return 0;
  return 1 + (size - 1) * dilation;
}

/// Given an index in a volume return the corresponding index after applying the
/// specified truncation, dilation, padding and flipping. Return ~0U if
/// truncation means the element is ignored.
static unsigned applyTruncateDilatePadAndFlip(
    unsigned index, unsigned inputSize, unsigned truncationLower,
    unsigned truncationUpper, unsigned dilation, unsigned paddingLower,
    unsigned paddingUpper, bool flip) {
  assert(index < inputSize);
  if (index < truncationLower || index >= inputSize - truncationUpper)
    return ~0U;
  const auto truncatedIndex = index - truncationLower;
  const auto truncatedSize = inputSize - (truncationLower + truncationUpper);
  const auto truncatedDilatedIndex = truncatedIndex * dilation;
  const auto truncatedDilatedSize = getDilatedSize(truncatedSize, dilation);
  const auto truncatedDilatedPaddedIndex = truncatedDilatedIndex + paddingLower;
  const auto truncatedDilatedPaddedSize =
      paddingLower + truncatedDilatedSize + paddingUpper;
  return flip ? truncatedDilatedPaddedSize - 1 - truncatedDilatedPaddedIndex
              : truncatedDilatedPaddedIndex;
}

/// Given a range in a volume return the corresponding range after applying the
/// specified truncation, dilation, padding and flipping.
static ConvRange applyTruncateDilatePadAndFlip(
    ConvRange range, unsigned inputSize, unsigned truncationLower,
    unsigned truncationUpper, unsigned dilation, unsigned paddingLower,
    unsigned paddingUpper, bool flip) {
  assert(range.begin() <= range.end());
  assert(range.end() <= inputSize);
  range.refineBegin(truncationLower);
  range.refineEnd(inputSize - truncationUpper);
  if (range.empty())
    return range;
  auto transformedBegin = applyTruncateDilatePadAndFlip(
      range.begin(), inputSize, truncationLower, truncationUpper, dilation,
      paddingLower, paddingUpper, flip);
  assert(transformedBegin != ~0U);
  if (range.size() == 1) {
    return {transformedBegin, transformedBegin + 1, range.isExact(), false};
  }
  auto transformedLast = applyTruncateDilatePadAndFlip(
      range.last(), inputSize, truncationLower, truncationUpper, dilation,
      paddingLower, paddingUpper, flip);
  assert(transformedLast != ~0U);
  if (flip) {
    std::swap(transformedBegin, transformedLast);
  }
  return {transformedBegin, transformedLast + 1, range.isExact(),
          range.isDilated() || dilation > 1};
}

static ConvRange applyTransform(unsigned dim, ConvRange range,
                                const std::vector<std::size_t> &inputSize,
                                const ConvParams::InputTransform &transform) {
  return applyTruncateDilatePadAndFlip(
      range, inputSize[dim], transform.truncationLower[dim],
      transform.truncationUpper[dim], transform.dilation[dim],
      transform.paddingLower[dim], transform.paddingUpper[dim],
      transform.flip[dim]);
}

/// Given a index in a dilated and padded volume return the index in the
/// original volume. Return ~0U if the index doesn't correspond to any
/// index in the original volume.
static unsigned reverseTruncateDilatePadAndFlip(
    unsigned truncatedDilatedPaddedFlippedIndex, unsigned inputSize,
    unsigned truncationLower, unsigned truncationUpper, unsigned dilation,
    unsigned paddingLower, unsigned paddingUpper, bool flip) {
  const auto truncatedSize = inputSize - (truncationLower + truncationUpper);
  const auto truncatedDilatedSize = getDilatedSize(truncatedSize, dilation);
  const auto truncatedDilatedPaddedSize =
      paddingLower + truncatedDilatedSize + paddingUpper;
  assert(truncatedDilatedPaddedFlippedIndex < truncatedDilatedPaddedSize);
  const auto truncatedDilatedPaddedIndex =
      flip ? truncatedDilatedPaddedSize - 1 - truncatedDilatedPaddedFlippedIndex
           : truncatedDilatedPaddedFlippedIndex;
  if (truncatedDilatedPaddedIndex < paddingLower ||
      truncatedDilatedPaddedIndex >= truncatedDilatedPaddedSize - paddingUpper)
    return ~0U;
  const auto truncatedDilatedIndex = truncatedDilatedPaddedIndex - paddingLower;
  if (truncatedDilatedIndex % dilation != 0)
    return ~0U;
  const auto truncatedIndex = truncatedDilatedIndex / dilation;
  return truncatedIndex + truncationLower;
}

/// Given a range in a dilated and padded volume return the range in the
/// original volume.
static ConvRange reverseTruncateDilatePadAndFlip(
    ConvRange range, unsigned inputSize, unsigned truncationLower,
    unsigned truncationUpper, unsigned dilation, unsigned paddingLower,
    unsigned paddingUpper, bool flip) {
  auto codomain = applyTruncateDilatePadAndFlip(
      {0, inputSize}, inputSize, truncationLower, truncationUpper, dilation,
      paddingLower, paddingUpper, flip);
  range.refineBegin(codomain.begin());
  range.refineEnd(codomain.end());
  if (range.empty())
    return range;
  if (dilation > 1) {
    auto roundUp = [](unsigned a, unsigned b) { return ((a + b - 1) / b) * b; };
    auto roundDown = [](unsigned a, unsigned b) { return (a / b) * b; };
    range.refineBegin(codomain.begin() +
                      roundUp(range.begin() - codomain.begin(), dilation));
    if (range.empty())
      return range;
    range.refineEnd(codomain.begin() +
                    roundDown(range.last() - codomain.begin(), dilation) + 1);
    if (range.empty())
      return range;
  }
  auto transformedBegin = reverseTruncateDilatePadAndFlip(
      range.begin(), inputSize, truncationLower, truncationUpper, dilation,
      paddingLower, paddingUpper, flip);
  assert(transformedBegin != ~0U);
  if (range.size() == 1) {
    return {transformedBegin, transformedBegin + 1, range.isExact(), false};
  }
  auto transformedLast = reverseTruncateDilatePadAndFlip(
      range.last(), inputSize, truncationLower, truncationUpper, dilation,
      paddingLower, paddingUpper, flip);
  assert(transformedLast != ~0U);
  if (flip) {
    std::swap(transformedBegin, transformedLast);
  }
  return {transformedBegin, transformedLast + 1, range.isExact(),
          range.isDilated()};
}

static ConvRange reverseTransform(unsigned dim, ConvRange range,
                                  const std::vector<std::size_t> &inputSize,
                                  const ConvParams::InputTransform &transform) {
  return reverseTruncateDilatePadAndFlip(
      range, inputSize[dim], transform.truncationLower[dim],
      transform.truncationUpper[dim], transform.dilation[dim],
      transform.paddingLower[dim], transform.paddingUpper[dim],
      transform.flip[dim]);
}

unsigned getInputIndex(unsigned dim, unsigned truncatedStridedPaddedOutputIndex,
                       unsigned kernelIndex, const ConvParams &params) {
  const auto outputSize = params.getOutputSize(dim);
  assert(truncatedStridedPaddedOutputIndex < outputSize);
  const auto paddedKernelIndex =
      applyTruncateDilatePadAndFlip(kernelIndex, params.kernelShape[dim],
                                    params.kernelTransform.truncationLower[dim],
                                    params.kernelTransform.truncationUpper[dim],
                                    params.kernelTransform.dilation[dim],
                                    params.kernelTransform.paddingLower[dim],
                                    params.kernelTransform.paddingUpper[dim],
                                    params.kernelTransform.flip[dim]);
  if (paddedKernelIndex == ~0U)
    return ~0U;
  if (truncatedStridedPaddedOutputIndex <
          params.outputTransform.paddingLower[dim] ||
      truncatedStridedPaddedOutputIndex >=
          outputSize - params.outputTransform.paddingUpper[dim])
    return ~0U;
  const auto truncatedStridedOutputIndex =
      truncatedStridedPaddedOutputIndex -
      params.outputTransform.paddingLower[dim];
  const auto truncatedOutputIndex =
      truncatedStridedOutputIndex * params.outputTransform.stride[dim];
  const auto outputIndex =
      truncatedOutputIndex + params.outputTransform.truncationLower[dim];
  int paddedInputIndex = paddedKernelIndex + outputIndex;
  return reverseTruncateDilatePadAndFlip(
      paddedInputIndex, params.inputFieldShape[dim],
      params.inputTransform.truncationLower[dim],
      params.inputTransform.truncationUpper[dim],
      params.inputTransform.dilation[dim],
      params.inputTransform.paddingLower[dim],
      params.inputTransform.paddingUpper[dim], params.inputTransform.flip[dim]);
}

unsigned getKernelIndex(unsigned dim,
                        unsigned truncatedStridedPaddedOutputIndex,
                        unsigned inputIndex, const ConvParams &params) {
  const auto outputSize = params.getOutputSize(dim);
  assert(truncatedStridedPaddedOutputIndex < outputSize);
  const auto paddedInputIndex = applyTruncateDilatePadAndFlip(
      inputIndex, params.inputFieldShape[dim],
      params.inputTransform.truncationLower[dim],
      params.inputTransform.truncationUpper[dim],
      params.inputTransform.dilation[dim],
      params.inputTransform.paddingLower[dim],
      params.inputTransform.paddingUpper[dim], params.inputTransform.flip[dim]);
  if (paddedInputIndex == ~0U)
    return ~0U;
  if (truncatedStridedPaddedOutputIndex <
          params.outputTransform.paddingLower[dim] ||
      truncatedStridedPaddedOutputIndex >=
          outputSize - params.outputTransform.paddingUpper[dim])
    return ~0U;
  const auto truncatedStridedOutputIndex =
      truncatedStridedPaddedOutputIndex -
      params.outputTransform.paddingLower[dim];
  const auto truncatedOutputIndex =
      truncatedStridedOutputIndex * params.outputTransform.stride[dim];
  const auto outputIndex =
      truncatedOutputIndex + params.outputTransform.truncationLower[dim];
  if (outputIndex > paddedInputIndex)
    return ~0U;
  const auto paddedKernelIndex = paddedInputIndex - outputIndex;
  if (paddedKernelIndex >= params.getTransformedKernelSize(dim))
    return ~0U;
  return reverseTruncateDilatePadAndFlip(
      paddedKernelIndex, params.kernelShape[dim],
      params.kernelTransform.truncationLower[dim],
      params.kernelTransform.truncationUpper[dim],
      params.kernelTransform.dilation[dim],
      params.kernelTransform.paddingLower[dim],
      params.kernelTransform.paddingUpper[dim],
      params.kernelTransform.flip[dim]);
}

// Quickly compute the output range that corresponds to the specified input and
// kernel ranges. The range returned may be larger than the true range, in which
// case the isExact field of the range will be false.
static ConvRange
getOutputRangeQuick(unsigned dim, std::pair<unsigned, unsigned> inputRange,
                    std::pair<unsigned, unsigned> kernelRange,
                    std::pair<unsigned, unsigned> outputRangeBounds,
                    const ConvParams &params) {
  auto transformedInputRange =
      applyTransform(dim, ConvRange(inputRange), params.inputFieldShape,
                     params.inputTransform);
  if (transformedInputRange.empty()) {
    return ConvRange::getEmpty();
  }
  auto transformedKernelRange = applyTransform(
      dim, ConvRange(kernelRange), params.kernelShape, params.kernelTransform);
  if (transformedKernelRange.empty()) {
    return ConvRange::getEmpty();
  }
  const auto transformedInputBegin = transformedInputRange.begin();
  const auto transformedInputLast = transformedInputRange.last();
  const auto transformedKernelBegin = transformedKernelRange.begin();
  const auto transformedKernelLast = transformedKernelRange.last();
  const auto untransformedOutputSize = params.getUntransformedOutputSize(dim);
  if (transformedKernelBegin > transformedInputLast ||
      transformedKernelLast + untransformedOutputSize <=
          transformedInputBegin) {
    return ConvRange::getEmpty();
  }
  bool isExact =
      transformedInputRange.isExact() && transformedKernelRange.isExact();
  bool isDilated =
      transformedInputRange.isDilated() || transformedKernelRange.isDilated();
  unsigned untransformedOutputBegin;
  if (transformedInputBegin >= transformedKernelLast) {
    untransformedOutputBegin = transformedInputBegin - transformedKernelLast;
  } else {
    if (isDilated)
      isExact = false;
    untransformedOutputBegin = 0;
  }
  auto untransformedOutputLast = transformedInputLast - transformedKernelBegin;
  const auto untransformedOutputRange =
      ConvRange(untransformedOutputBegin, untransformedOutputLast + 1, isExact,
                isDilated);
  auto outputRange = reverseTruncateDilatePadAndFlip(
      untransformedOutputRange, params.getOutputSize(dim),
      params.outputTransform.paddingLower[dim],
      params.outputTransform.paddingUpper[dim],
      params.outputTransform.stride[dim],
      params.outputTransform.truncationLower[dim],
      params.outputTransform.truncationUpper[dim], false);
  outputRange.refineBegin(outputRangeBounds.first);
  outputRange.refineEnd(outputRangeBounds.second);
  return outputRange;
}

// Quickly compute the input range that corresponds to the specified kernel and
// output ranges. The range returned may be larger than the true range, in which
// case the isExact field of the range will be false.
static ConvRange
getInputRangeQuick(unsigned dim, std::pair<unsigned, unsigned> inputRangeBounds,
                   std::pair<unsigned, unsigned> kernelRange,
                   std::pair<unsigned, unsigned> outputRange,
                   const ConvParams &params) {
  auto untransformedOutputRange = applyTruncateDilatePadAndFlip(
      ConvRange(outputRange), params.getOutputSize(dim),
      params.outputTransform.paddingLower[dim],
      params.outputTransform.paddingUpper[dim],
      params.outputTransform.stride[dim],
      params.outputTransform.truncationLower[dim],
      params.outputTransform.truncationUpper[dim], false);
  if (untransformedOutputRange.empty()) {
    return ConvRange::getEmpty();
  }
  auto transformedKernelRange = applyTransform(
      dim, ConvRange(kernelRange), params.kernelShape, params.kernelTransform);
  if (transformedKernelRange.empty()) {
    return ConvRange::getEmpty();
  }
  const auto untransformedOutputBegin = untransformedOutputRange.begin();
  const auto untransformedOutputLast = untransformedOutputRange.last();
  const auto transformedKernelBegin = transformedKernelRange.begin();
  const auto transformedKernelLast = transformedKernelRange.last();
  const auto transformedInputBegin =
      untransformedOutputBegin + transformedKernelBegin;
  const auto transformedInputLast =
      untransformedOutputLast + transformedKernelLast;
  bool isExact =
      untransformedOutputRange.isExact() && transformedKernelRange.isExact();
  bool isDilated = untransformedOutputRange.isDilated() ||
                   transformedKernelRange.isDilated();
  const auto transformedInputRange = ConvRange(
      transformedInputBegin, transformedInputLast + 1, isExact, isDilated);
  auto inputRange =
      reverseTransform(dim, transformedInputRange, params.inputFieldShape,
                       params.inputTransform);
  inputRange.refineBegin(inputRangeBounds.first);
  inputRange.refineEnd(inputRangeBounds.second);
  return inputRange;
}

// Quickly compute the kernel range that corresponds to the specified input and
// output ranges. The range returned may be larger than the true range, in which
// case the isExact field of the range will be false.
static ConvRange
getKernelRangeQuick(unsigned dim, std::pair<unsigned, unsigned> inputRange,
                    std::pair<unsigned, unsigned> kernelRangeBounds,
                    std::pair<unsigned, unsigned> outputRange,
                    const ConvParams &params) {
  auto untransformedOutputRange = applyTruncateDilatePadAndFlip(
      ConvRange(outputRange), params.getOutputSize(dim),
      params.outputTransform.paddingLower[dim],
      params.outputTransform.paddingUpper[dim],
      params.outputTransform.stride[dim],
      params.outputTransform.truncationLower[dim],
      params.outputTransform.truncationUpper[dim], false);
  if (untransformedOutputRange.empty()) {
    return ConvRange::getEmpty();
  }
  auto transformedInputRange =
      applyTransform(dim, ConvRange(inputRange), params.inputFieldShape,
                     params.inputTransform);
  if (transformedInputRange.empty()) {
    return ConvRange::getEmpty();
  }
  const auto untransformedOutputBegin = untransformedOutputRange.begin();
  const auto untransformedOutputLast = untransformedOutputRange.last();
  const auto transformedInputBegin = transformedInputRange.begin();
  const auto transformedInputLast = transformedInputRange.last();
  const auto transformedKernelSize = params.getTransformedKernelSize(dim);
  if (transformedInputLast < untransformedOutputBegin ||
      transformedInputBegin >=
          untransformedOutputLast + transformedKernelSize) {
    return ConvRange::getEmpty();
  }
  bool isExact =
      untransformedOutputRange.isExact() && transformedInputRange.isExact();
  bool isDilated =
      untransformedOutputRange.isDilated() || transformedInputRange.isDilated();
  unsigned transformedKernelBegin, transformedKernelLast;
  if (transformedInputBegin >= untransformedOutputLast) {
    transformedKernelBegin = transformedInputBegin - untransformedOutputLast;
  } else {
    if (isDilated)
      isExact = false;
    transformedKernelBegin = 0;
  }
  if (transformedInputLast - untransformedOutputBegin <=
      transformedKernelSize - 1) {
    transformedKernelLast = transformedInputLast - untransformedOutputBegin;
  } else {
    if (isDilated)
      isExact = true;
    transformedKernelLast = transformedKernelSize - 1;
  }
  const auto transformedKernelRange = ConvRange(
      transformedKernelBegin, transformedKernelLast + 1, isExact, isDilated);
  auto kernelRange = reverseTransform(
      dim, transformedKernelRange, params.kernelShape, params.kernelTransform);
  kernelRange.refineBegin(kernelRangeBounds.first);
  kernelRange.refineEnd(kernelRangeBounds.second);
  return kernelRange;
}

std::pair<unsigned, unsigned>
getOutputRangeForKernelIndex(unsigned dim,
                             std::pair<unsigned, unsigned> outputRangeBounds,
                             unsigned kernelIndex, const ConvParams &params) {
  assert(outputRangeBounds.first <= outputRangeBounds.second);
  auto refinedOutputRangeBounds = getOutputRangeQuick(
      dim, {0, params.inputFieldShape[dim]}, {kernelIndex, kernelIndex + 1},
      outputRangeBounds, params);
  if (refinedOutputRangeBounds.isExact()) {
    return refinedOutputRangeBounds;
  }
  unsigned outputBegin = 0, outputEnd = 0;
  for (unsigned i = refinedOutputRangeBounds.begin();
       i != refinedOutputRangeBounds.end(); ++i) {
    if (getInputIndex(dim, i, kernelIndex, params) == ~0U) {
      continue;
    }
    outputBegin = i;
    break;
  }
  for (unsigned i = refinedOutputRangeBounds.end();
       i != refinedOutputRangeBounds.begin(); --i) {
    if (getInputIndex(dim, i - 1, kernelIndex, params) == ~0U) {
      continue;
    }
    outputEnd = i;
    break;
  }
  return {outputBegin, outputEnd};
}

std::pair<unsigned, unsigned>
getOutputRangeForInputIndex(unsigned dim,
                            std::pair<unsigned, unsigned> outputRangeBounds,
                            unsigned inputIndex, const ConvParams &params) {
  assert(outputRangeBounds.first <= outputRangeBounds.second);
  auto refinedOutputRangeBounds = getOutputRangeQuick(
      dim, {inputIndex, inputIndex + 1}, {0, params.kernelShape[dim]},
      outputRangeBounds, params);
  if (refinedOutputRangeBounds.isExact())
    return refinedOutputRangeBounds;
  unsigned outputBegin = 0, outputEnd = 0;
  for (unsigned i = refinedOutputRangeBounds.begin();
       i != refinedOutputRangeBounds.end(); ++i) {
    if (getKernelIndex(dim, i, inputIndex, params) == ~0U) {
      continue;
    }
    outputBegin = i;
    break;
  }
  for (unsigned i = refinedOutputRangeBounds.end();
       i != refinedOutputRangeBounds.begin(); --i) {
    if (getKernelIndex(dim, i - 1, inputIndex, params) == ~0U) {
      continue;
    }
    outputEnd = i;
    break;
  }
  return {outputBegin, outputEnd};
}

std::pair<unsigned, unsigned> getOutputRangeForKernelRange(
    unsigned dim, std::pair<unsigned, unsigned> outputRangeBounds,
    std::pair<unsigned, unsigned> kernelIndexRange, const ConvParams &params) {
  assert(kernelIndexRange.second >= kernelIndexRange.first);
  auto refinedOutputRangeBounds =
      getOutputRangeQuick(dim, {0, params.inputFieldShape[dim]},
                          kernelIndexRange, outputRangeBounds, params);
  if (refinedOutputRangeBounds.isExact())
    return refinedOutputRangeBounds;
  unsigned outputBegin = 0, outputEnd = 0;
  bool first = true;
  for (unsigned kernelIndex = kernelIndexRange.first;
       kernelIndex != kernelIndexRange.second; ++kernelIndex) {
    const auto trimmedOutputRange = getOutputRangeForKernelIndex(
        dim, refinedOutputRangeBounds, kernelIndex, params);
    if (trimmedOutputRange.first != trimmedOutputRange.second) {
      if (first) {
        outputBegin = trimmedOutputRange.first;
        outputEnd = trimmedOutputRange.second;
        first = false;
      } else {
        outputBegin = std::min(outputBegin, trimmedOutputRange.first);
        outputEnd = std::max(outputEnd, trimmedOutputRange.second);
      }
    }
  }
  return {outputBegin, outputEnd};
}

std::pair<unsigned, unsigned> getOutputRangeForInputRange(
    unsigned dim, std::pair<unsigned, unsigned> outputRangeBounds,
    std::pair<unsigned, unsigned> inputRange, const ConvParams &params) {
  assert(inputRange.second >= inputRange.first);
  auto refinedOutputRangeBounds = getOutputRangeQuick(
      dim, inputRange, {0, params.kernelShape[dim]}, outputRangeBounds, params);
  if (refinedOutputRangeBounds.isExact())
    return refinedOutputRangeBounds;
  unsigned outputBegin = 0, outputEnd = 0;
  bool first = true;
  for (unsigned inputIndex = inputRange.first; inputIndex != inputRange.second;
       ++inputIndex) {
    const auto trimmedOutputRange = getOutputRangeForInputIndex(
        dim, refinedOutputRangeBounds, inputIndex, params);
    if (trimmedOutputRange.first != trimmedOutputRange.second) {
      if (first) {
        outputBegin = trimmedOutputRange.first;
        outputEnd = trimmedOutputRange.second;
        first = false;
      } else {
        outputBegin = std::min(outputBegin, trimmedOutputRange.first);
        outputEnd = std::max(outputEnd, trimmedOutputRange.second);
      }
    }
  }
  return {outputBegin, outputEnd};
}

std::pair<unsigned, unsigned>
getInputRange(unsigned dim, std::pair<unsigned, unsigned> outputRange,
              unsigned kernelIndex, const ConvParams &params) {
  unsigned inputBegin = 0, inputEnd = 0;
  auto trimmedOutputRange =
      getOutputRangeForKernelIndex(dim, outputRange, kernelIndex, params);
  if (trimmedOutputRange.first != trimmedOutputRange.second) {
    inputBegin =
        getInputIndex(dim, trimmedOutputRange.first, kernelIndex, params);
    assert(inputBegin != ~0U);
    auto inputLast =
        getInputIndex(dim, trimmedOutputRange.second - 1, kernelIndex, params);
    assert(inputLast != ~0U);
    if (params.inputTransform.flip[dim]) {
      std::swap(inputBegin, inputLast);
    }
    inputEnd = inputLast + 1;
  }
  assert(inputBegin <= inputEnd);
  return {inputBegin, inputEnd};
}

std::pair<unsigned, unsigned>
getKernelRange(unsigned dim, std::pair<unsigned, unsigned> outputRange,
               unsigned inputIndex, const ConvParams &params) {
  unsigned kernelBegin = 0, kernelEnd = 0;
  auto trimmedOutputRange =
      getOutputRangeForInputIndex(dim, outputRange, inputIndex, params);
  if (trimmedOutputRange.first != trimmedOutputRange.second) {
    kernelBegin =
        getKernelIndex(dim, trimmedOutputRange.second - 1, inputIndex, params);
    assert(kernelBegin != ~0U);
    auto kernelLast =
        getKernelIndex(dim, trimmedOutputRange.first, inputIndex, params);
    assert(kernelLast != ~0U);
    if (params.kernelTransform.flip[dim]) {
      std::swap(kernelBegin, kernelLast);
    }
    kernelEnd = kernelLast + 1;
  }
  assert(kernelBegin <= kernelEnd);
  return {kernelBegin, kernelEnd};
}

std::pair<unsigned, unsigned>
getInputRange(unsigned dim, std::pair<unsigned, unsigned> outputRange,
              std::pair<unsigned, unsigned> kernelRange,
              const ConvParams &params) {
  assert(kernelRange.second >= kernelRange.first);
  auto inputRangeBounds = getInputRangeQuick(
      dim, {0, params.inputFieldShape[dim]}, kernelRange, outputRange, params);
  if (inputRangeBounds.isExact()) {
    return inputRangeBounds;
  }
  // Try to narrow the kernel range.
  kernelRange = getKernelRangeQuick(dim, inputRangeBounds, kernelRange,
                                    outputRange, params);
  if (kernelRange.first == kernelRange.second)
    return {0, 0};
  unsigned inputEnd = 0;
  unsigned minBegin = inputRangeBounds.begin();
  unsigned maxEnd = inputRangeBounds.end();
  const auto kernelRangeFwd =
      boost::irange(static_cast<int>(kernelRange.first),
                    static_cast<int>(kernelRange.second), 1);
  const auto kernelRangeBwd =
      boost::irange(static_cast<int>(kernelRange.second) - 1,
                    static_cast<int>(kernelRange.first) - 1, -1);
  bool flip = params.inputTransform.flip[dim];
  for (unsigned k : flip ? kernelRangeFwd : kernelRangeBwd) {
    auto inputRange = getInputRange(dim, outputRange, k, params);
    if (inputRange.first == inputRange.second)
      continue;
    inputEnd = std::max(inputEnd, inputRange.second);
    if (inputEnd == maxEnd)
      break;
  }
  unsigned inputBegin = inputEnd;
  for (unsigned k : flip ? kernelRangeBwd : kernelRangeFwd) {
    auto inputRange = getInputRange(dim, outputRange, k, params);
    if (inputRange.first == inputRange.second)
      continue;
    inputBegin = std::min(inputBegin, inputRange.first);
    if (inputBegin == minBegin)
      break;
  }
  return {inputBegin, inputEnd};
}

std::pair<unsigned, unsigned>
getKernelRange(unsigned dim, std::pair<unsigned, unsigned> outputRange,
               std::pair<unsigned, unsigned> inputRange,
               const ConvParams &params) {
  assert(inputRange.second >= inputRange.first);
  auto kernelRangeBounds = getKernelRangeQuick(
      dim, inputRange, {0, params.kernelShape[dim]}, outputRange, params);
  if (kernelRangeBounds.isExact()) {
    return kernelRangeBounds;
  }
  // Try to narrow the input range.
  inputRange = getInputRangeQuick(dim, inputRange, kernelRangeBounds,
                                  outputRange, params);
  if (inputRange.first == inputRange.second)
    return {0, 0};
  unsigned kernelEnd = 0;
  unsigned minBegin = kernelRangeBounds.begin();
  unsigned maxEnd = kernelRangeBounds.end();
  const auto inputRangeFwd =
      boost::irange(static_cast<int>(inputRange.first),
                    static_cast<int>(inputRange.second), 1);
  const auto inputRangeBwd =
      boost::irange(static_cast<int>(inputRange.second) - 1,
                    static_cast<int>(inputRange.first) - 1, -1);
  bool flip = params.inputTransform.flip[dim];
  for (unsigned i : flip ? inputRangeBwd : inputRangeFwd) {
    auto kernelRange = getKernelRange(dim, outputRange, i, params);
    if (kernelRange.first == kernelRange.second)
      continue;
    kernelEnd = std::max(kernelEnd, kernelRange.second);
    if (kernelEnd == maxEnd)
      break;
  }
  unsigned kernelBegin = kernelEnd;
  for (unsigned i : flip ? inputRangeFwd : inputRangeBwd) {
    auto kernelRange = getKernelRange(dim, outputRange, i, params);
    if (kernelRange.first == kernelRange.second)
      continue;
    kernelBegin = std::min(kernelBegin, kernelRange.first);
    if (kernelBegin == minBegin)
      break;
  }
  return {kernelBegin, kernelEnd};
}

ConvParams getGradientParams(const ConvParams &params_) {
  const CanonicalConvParams canonicalParams(params_);
  // Note: we assume the caller explicitly flips the weights in each spatial
  // axis before the convolution. TODO: T12889 It may be more efficient to fold
  // the flipping of the weights into the convolution by setting the flipKernel
  // parameter appropriately.
  auto bwdInputPaddingLower = canonicalParams->outputTransform.truncationLower;
  auto bwdInputPaddingUpper = canonicalParams->outputTransform.truncationUpper;
  const auto numFieldDims = canonicalParams->getNumFieldDims();
  // The "valid" convolution in the forward pass becomes a "full" convolution
  // in the backward pass. We can express this as a "valid" convolution with
  // (kernelSize - 1) padding.
  for (unsigned dim = 0; dim != numFieldDims; ++dim) {
    const auto kernelSize = canonicalParams->getTransformedKernelSize(dim);
    bwdInputPaddingLower[dim] += kernelSize - 1;
    bwdInputPaddingUpper[dim] += kernelSize - 1;
  }
  // Going backwards the weights are flipped in each axis so we must flip
  // the upper and lower truncation / padding.
  auto bwdFlipInput = std::vector<bool>(numFieldDims);
  auto bwdFlipKernel = canonicalParams->kernelTransform.flip;
  for (unsigned dim = 0; dim != numFieldDims; ++dim) {
    if (canonicalParams->inputTransform.flip[dim]) {
      // If the input is flipped in the forward pass we must flip the output
      // in the backward pass. This is equivalent to flipping both the
      // input and the kernel in the backward pass.
      bwdFlipKernel[dim] = !bwdFlipKernel[dim];
      bwdFlipInput[dim] = !bwdFlipInput[dim];
    }
  }

  const poplin::ConvParams::InputTransform inputTransform{
      canonicalParams->outputTransform.paddingLower, // Truncation lower
      canonicalParams->outputTransform.paddingUpper, // Truncation upper
      canonicalParams->outputTransform.stride,       // Dilation
      bwdInputPaddingLower,                          // Padding lower
      bwdInputPaddingUpper,                          // Padding upper
      bwdFlipInput                                   // Flip
  };
  const poplin::ConvParams::InputTransform kernelTransform{
      canonicalParams->kernelTransform.truncationUpper, // Truncation lower
      canonicalParams->kernelTransform.truncationLower, // Truncation upper
      canonicalParams->kernelTransform.dilation,        // Dilation
      canonicalParams->kernelTransform.paddingUpper,    // Padding lower
      canonicalParams->kernelTransform.paddingLower,    // Padding upper
      bwdFlipKernel                                     // Flip
  };
  const poplin::ConvParams::OutputTransform outputTransform{
      canonicalParams->inputTransform.paddingLower,    // Truncation lower
      canonicalParams->inputTransform.paddingUpper,    // Truncation upper
      canonicalParams->inputTransform.dilation,        // Stride
      canonicalParams->inputTransform.truncationLower, // Padding lower
      canonicalParams->inputTransform.truncationUpper  // Padding upper
  };
  const poplin::ConvParams bwdParams{
      canonicalParams->inputType,
      canonicalParams->outputType,
      canonicalParams->batchSize,
      canonicalParams->getOutputFieldShape(),
      canonicalParams->kernelShape,
      canonicalParams->getNumOutputChansPerConvGroup(),
      canonicalParams->getNumInputChansPerConvGroup(),
      canonicalParams->getNumConvGroups(),
      inputTransform,
      kernelTransform,
      outputTransform};
  return bwdParams.canonicalize();
}

} // namespace poplin