/*
 * This file is part of a C++ interface to the Radiative Transfer for Energetics (RTE)
 * and Rapid Radiative Transfer Model for GCM applications Parallel (RRTMGP).
 *
 * The original code is found at https://github.com/RobertPincus/rte-rrtmgp.
 *
 * Contacts: Robert Pincus and Eli Mlawer
 * email: rrtmgp@aer.com
 *
 * Copyright 2015-2020,  Atmospheric and Environmental Research and
 * Regents of the University of Colorado.  All right reserved.
 *
 * This C++ interface can be downloaded from https://github.com/Chiil/rrtmgp_cpp
 *
 * Contact: Chiel van Heerwaarden
 * email: chiel.vanheerwaarden@wur.nl
 *
 * Copyright 2020, Wageningen University & Research.
 *
 * Use and duplication is permitted under the terms of the
 * BSD 3-clause license, see http://opensource.org/licenses/BSD-3-Clause
 *
 */

#include <boost/algorithm/string.hpp>
#include <cmath>

#include "Netcdf_interface.h"
#include "Array.h"
#include "Gas_concs.h"
#include "Gas_optics.h"
#include "Optical_props.h"
#include "Source_functions.h"
#include "Fluxes.h"
#include "Rte_lw.h"
#include "Rte_sw.h"

#ifdef FLOAT_SINGLE_RRTMGP
#define FLOAT_TYPE float
#else
#define FLOAT_TYPE double
#endif

namespace
{
    std::vector<std::string> get_variable_string(
            const std::string& var_name,
            std::vector<int> i_count,
            Netcdf_handle& input_nc,
            const int string_len,
            bool trim=true)
    {
        // Multiply all elements in i_count.
        int total_count = std::accumulate(i_count.begin(), i_count.end(), 1, std::multiplies<>());

        // Add the string length as the rightmost dimension.
        i_count.push_back(string_len);

        // Multiply all elements in i_count.
        // int total_count_char = std::accumulate(i_count.begin(), i_count.end(), 1, std::multiplies<>());

        // Read the entire char array;
        std::vector<char> var_char;
        var_char = input_nc.get_variable<char>(var_name, i_count);

        std::vector<std::string> var;

        for (int n=0; n<total_count; ++n)
        {
            std::string s(var_char.begin()+n*string_len, var_char.begin()+(n+1)*string_len);
            if (trim)
                boost::trim(s);
            var.push_back(s);
        }

        return var;
    }

    template<typename TF>
    Gas_optics<TF> load_and_init_gas_optics(
            Master& master,
            const Gas_concs<TF>& gas_concs,
            const std::string& coef_file)
    {
        // READ THE COEFFICIENTS FOR THE OPTICAL SOLVER.
        Netcdf_file coef_nc(master, coef_file, Netcdf_mode::Read);

        // Read k-distribution information.
        int n_temps = coef_nc.get_dimension_size("temperature");
        int n_press = coef_nc.get_dimension_size("pressure");
        int n_absorbers = coef_nc.get_dimension_size("absorber");
        int n_char = coef_nc.get_dimension_size("string_len");
        int n_minorabsorbers = coef_nc.get_dimension_size("minor_absorber");
        int n_extabsorbers = coef_nc.get_dimension_size("absorber_ext");
        int n_mixingfracs = coef_nc.get_dimension_size("mixing_fraction");
        int n_layers = coef_nc.get_dimension_size("atmos_layer");
        int n_bnds = coef_nc.get_dimension_size("bnd");
        int n_gpts = coef_nc.get_dimension_size("gpt");
        int n_pairs = coef_nc.get_dimension_size("pair");
        int n_minor_absorber_intervals_lower = coef_nc.get_dimension_size("minor_absorber_intervals_lower");
        int n_minor_absorber_intervals_upper = coef_nc.get_dimension_size("minor_absorber_intervals_upper");
        int n_contributors_lower = coef_nc.get_dimension_size("contributors_lower");
        int n_contributors_upper = coef_nc.get_dimension_size("contributors_upper");

        // Read gas names.
        Array<std::string,1> gas_names(
                get_variable_string("gas_names", {n_absorbers}, coef_nc, n_char, true), {n_absorbers});

        Array<int,3> key_species(
                coef_nc.get_variable<int>("key_species", {n_bnds, n_layers, 2}),
                {2, n_layers, n_bnds});
        Array<TF,2> band_lims(coef_nc.get_variable<TF>("bnd_limits_wavenumber", {n_bnds, 2}), {2, n_bnds});
        Array<int,2> band2gpt(coef_nc.get_variable<int>("bnd_limits_gpt", {n_bnds, 2}), {2, n_bnds});
        Array<TF,1> press_ref(coef_nc.get_variable<TF>("press_ref", {n_press}), {n_press});
        Array<TF,1> temp_ref(coef_nc.get_variable<TF>("temp_ref", {n_temps}), {n_temps});

        TF temp_ref_p = coef_nc.get_variable<TF>("absorption_coefficient_ref_P");
        TF temp_ref_t = coef_nc.get_variable<TF>("absorption_coefficient_ref_T");
        TF press_ref_trop = coef_nc.get_variable<TF>("press_ref_trop");

        Array<TF,3> kminor_lower(
                coef_nc.get_variable<TF>("kminor_lower", {n_temps, n_mixingfracs, n_contributors_lower}),
                {n_contributors_lower, n_mixingfracs, n_temps});
        Array<TF,3> kminor_upper(
                coef_nc.get_variable<TF>("kminor_upper", {n_temps, n_mixingfracs, n_contributors_upper}),
                {n_contributors_upper, n_mixingfracs, n_temps});

        Array<std::string,1> gas_minor(get_variable_string("gas_minor", {n_minorabsorbers}, coef_nc, n_char),
                {n_minorabsorbers});

        Array<std::string,1> identifier_minor(
                get_variable_string("identifier_minor", {n_minorabsorbers}, coef_nc, n_char), {n_minorabsorbers});

        Array<std::string,1> minor_gases_lower(
                get_variable_string("minor_gases_lower", {n_minor_absorber_intervals_lower}, coef_nc, n_char),
                {n_minor_absorber_intervals_lower});
        Array<std::string,1> minor_gases_upper(
                get_variable_string("minor_gases_upper", {n_minor_absorber_intervals_upper}, coef_nc, n_char),
                {n_minor_absorber_intervals_upper});

        Array<int,2> minor_limits_gpt_lower(
                coef_nc.get_variable<int>("minor_limits_gpt_lower", {n_minor_absorber_intervals_lower, n_pairs}),
                {n_pairs, n_minor_absorber_intervals_lower});
        Array<int,2> minor_limits_gpt_upper(
                coef_nc.get_variable<int>("minor_limits_gpt_upper", {n_minor_absorber_intervals_upper, n_pairs}),
                {n_pairs, n_minor_absorber_intervals_upper});

        Array<int,1> minor_scales_with_density_lower(
                coef_nc.get_variable<int>("minor_scales_with_density_lower", {n_minor_absorber_intervals_lower}),
                {n_minor_absorber_intervals_lower});
        Array<int,1> minor_scales_with_density_upper(
                coef_nc.get_variable<int>("minor_scales_with_density_upper", {n_minor_absorber_intervals_upper}),
                {n_minor_absorber_intervals_upper});

        Array<int,1> scale_by_complement_lower(
                coef_nc.get_variable<int>("scale_by_complement_lower", {n_minor_absorber_intervals_lower}),
                {n_minor_absorber_intervals_lower});
        Array<int,1> scale_by_complement_upper(
                coef_nc.get_variable<int>("scale_by_complement_upper", {n_minor_absorber_intervals_upper}),
                {n_minor_absorber_intervals_upper});

        Array<std::string,1> scaling_gas_lower(
                get_variable_string("scaling_gas_lower", {n_minor_absorber_intervals_lower}, coef_nc, n_char),
                {n_minor_absorber_intervals_lower});
        Array<std::string,1> scaling_gas_upper(
                get_variable_string("scaling_gas_upper", {n_minor_absorber_intervals_upper}, coef_nc, n_char),
                {n_minor_absorber_intervals_upper});

        Array<int,1> kminor_start_lower(
                coef_nc.get_variable<int>("kminor_start_lower", {n_minor_absorber_intervals_lower}),
                {n_minor_absorber_intervals_lower});
        Array<int,1> kminor_start_upper(
                coef_nc.get_variable<int>("kminor_start_upper", {n_minor_absorber_intervals_upper}),
                {n_minor_absorber_intervals_upper});

        Array<TF,3> vmr_ref(
                coef_nc.get_variable<TF>("vmr_ref", {n_temps, n_extabsorbers, n_layers}),
                {n_layers, n_extabsorbers, n_temps});

        Array<TF,4> kmajor(
                coef_nc.get_variable<TF>("kmajor", {n_temps, n_press+1, n_mixingfracs, n_gpts}),
                {n_gpts, n_mixingfracs, n_press+1, n_temps});

        // Keep the size at zero, if it does not exist.
        Array<TF,3> rayl_lower;
        Array<TF,3> rayl_upper;

        if (coef_nc.variable_exists("rayl_lower"))
        {
            rayl_lower.set_dims({n_gpts, n_mixingfracs, n_temps});
            rayl_upper.set_dims({n_gpts, n_mixingfracs, n_temps});
            rayl_lower = coef_nc.get_variable<TF>("rayl_lower", {n_temps, n_mixingfracs, n_gpts});
            rayl_upper = coef_nc.get_variable<TF>("rayl_upper", {n_temps, n_mixingfracs, n_gpts});
        }

        // Is it really LW if so read these variables as well.
        if (coef_nc.variable_exists("totplnk"))
        {
            int n_internal_sourcetemps = coef_nc.get_dimension_size("temperature_Planck");

            Array<TF,2> totplnk(
                    coef_nc.get_variable<TF>( "totplnk", {n_bnds, n_internal_sourcetemps}),
                    {n_internal_sourcetemps, n_bnds});
            Array<TF,4> planck_frac(
                    coef_nc.get_variable<TF>("plank_fraction", {n_temps, n_press+1, n_mixingfracs, n_gpts}),
                    {n_gpts, n_mixingfracs, n_press+1, n_temps});

            // Construct the k-distribution.
            return Gas_optics<TF>(
                    gas_concs,
                    gas_names,
                    key_species,
                    band2gpt,
                    band_lims,
                    press_ref,
                    press_ref_trop,
                    temp_ref,
                    temp_ref_p,
                    temp_ref_t,
                    vmr_ref,
                    kmajor,
                    kminor_lower,
                    kminor_upper,
                    gas_minor,
                    identifier_minor,
                    minor_gases_lower,
                    minor_gases_upper,
                    minor_limits_gpt_lower,
                    minor_limits_gpt_upper,
                    minor_scales_with_density_lower,
                    minor_scales_with_density_upper,
                    scaling_gas_lower,
                    scaling_gas_upper,
                    scale_by_complement_lower,
                    scale_by_complement_upper,
                    kminor_start_lower,
                    kminor_start_upper,
                    totplnk,
                    planck_frac,
                    rayl_lower,
                    rayl_upper);
        }
        else
        {
            Array<TF,1> solar_src(
                    coef_nc.get_variable<TF>("solar_source", {n_gpts}), {n_gpts});

            return Gas_optics<TF>(
                    gas_concs,
                    gas_names,
                    key_species,
                    band2gpt,
                    band_lims,
                    press_ref,
                    press_ref_trop,
                    temp_ref,
                    temp_ref_p,
                    temp_ref_t,
                    vmr_ref,
                    kmajor,
                    kminor_lower,
                    kminor_upper,
                    gas_minor,
                    identifier_minor,
                    minor_gases_lower,
                    minor_gases_upper,
                    minor_limits_gpt_lower,
                    minor_limits_gpt_upper,
                    minor_scales_with_density_lower,
                    minor_scales_with_density_upper,
                    scaling_gas_lower,
                    scaling_gas_upper,
                    scale_by_complement_lower,
                    scale_by_complement_upper,
                    kminor_start_lower,
                    kminor_start_upper,
                    solar_src,
                    rayl_lower,
                    rayl_upper);
        }
        // End reading of k-distribution.
    }
}

template<typename TF>
void solve_radiation(Master& master)
{
    Netcdf_file input_nc(master, "rrtmgp-inputs-outputs.nc", Netcdf_mode::Read);

    // READ THE ATMOSPHERIC DATA.
    int n_lay = input_nc.get_dimension_size("lay");
    int n_lev = input_nc.get_dimension_size("lev");
    int n_col = input_nc.get_dimension_size("col");

    Array<TF,2> p_lay(input_nc.get_variable<TF>("p_lay", {n_lay, n_col}), {n_col, n_lay});
    Array<TF,2> t_lay(input_nc.get_variable<TF>("t_lay", {n_lay, n_col}), {n_col, n_lay});
    Array<TF,2> p_lev(input_nc.get_variable<TF>("p_lev", {n_lev, n_col}), {n_col, n_lev});
    Array<TF,2> t_lev(input_nc.get_variable<TF>("t_lev", {n_lev, n_col}), {n_col, n_lev});

    const int top_at_1 = p_lay({1, 1}) < p_lay({1, n_lay});

    Gas_concs<TF> gas_concs;
    Gas_concs<TF> gas_concs_subset;

    gas_concs.set_vmr("h2o",
            Array<TF,2>(input_nc.get_variable<TF>("vmr_h2o", {n_lay, n_col}), {n_col, n_lay}));
    gas_concs.set_vmr("co2",
            Array<TF,2>(input_nc.get_variable<TF>("vmr_co2", {n_lay, n_col}), {n_col, n_lay}));
    gas_concs.set_vmr("o3",
            Array<TF,2>(input_nc.get_variable<TF>("vmr_o3", {n_lay, n_col}), {n_col, n_lay}));
    gas_concs.set_vmr("n2o",
            Array<TF,2>(input_nc.get_variable<TF>("vmr_n2o", {n_lay, n_col}), {n_col, n_lay}));
    gas_concs.set_vmr("co",
            Array<TF,2>(input_nc.get_variable<TF>("vmr_co", {n_lay, n_col}), {n_col, n_lay}));
    gas_concs.set_vmr("ch4",
            Array<TF,2>(input_nc.get_variable<TF>("vmr_ch4", {n_lay, n_col}), {n_col, n_lay}));
    gas_concs.set_vmr("o2",
            Array<TF,2>(input_nc.get_variable<TF>("vmr_o2", {n_lay, n_col}), {n_col, n_lay}));
    gas_concs.set_vmr("n2",
            Array<TF,2>(input_nc.get_variable<TF>("vmr_n2", {n_lay, n_col}), {n_col, n_lay}));

    // Construct the gas optics class.
    Gas_optics<TF> kdist = load_and_init_gas_optics(master, gas_concs, "coefficients.nc");

    // Fetch the col_dry in case present.
    Array<TF,2> col_dry({n_col, n_lay});
    if (input_nc.variable_exists("col_dry"))
        col_dry = input_nc.get_variable<TF>("col_dry", {n_lay, n_col});
    else
        kdist.get_col_dry(col_dry, gas_concs.get_vmr("h2o"), p_lev);

    const int n_gpt = kdist.get_ngpt();
    const int n_bnd = kdist.get_nband();

    // Boundary conditions for longwave.
    Array<TF,2> emis_sfc;
    Array<TF,1> t_sfc;

    // Boundary conditions for shortwave.
    Array<TF,1> sza;
    Array<TF,1> tsi;
    Array<TF,2> sfc_alb_dir;
    Array<TF,2> sfc_alb_dif;

    const int n_col_block = 4;

    std::unique_ptr<Optical_props_arry<TF>> optical_props;
    std::unique_ptr<Optical_props_arry<TF>> optical_props_subset;
    std::unique_ptr<Optical_props_arry<TF>> optical_props_left;

    if (kdist.source_is_internal())
    {
        ////// SOLVING THE OPTICAL PROPERTIES FOR LONGWAVE RADIATION //////
        master.print_message("STEP 1: Computing optical depths for longwave radiation.\n");

        // Download surface boundary conditions for long wave.
        Array<TF,2> emis_sfc_tmp(
                input_nc.get_variable<TF>(
                        "emis_sfc", {n_col, n_bnd}), {n_bnd, n_col});
        Array<TF,1> t_sfc_tmp(
                input_nc.get_variable<TF>(
                        "t_sfc", {n_col}), {n_col});

        emis_sfc = emis_sfc_tmp;
        t_sfc = t_sfc_tmp;

        // Read the sources and create containers for the substeps.
        int n_blocks = n_col / n_col_block;
        int n_col_block_left = n_col % n_col_block;

        optical_props        = std::make_unique<Optical_props_1scl<TF>>(n_col      , n_lay, kdist);
        optical_props_subset = std::make_unique<Optical_props_1scl<TF>>(n_col_block, n_lay, kdist);

        Source_func_lw<TF> sources       (n_col      , n_lay, kdist);
        Source_func_lw<TF> sources_subset(n_col_block, n_lay, kdist);

        auto calc_optical_props_subset = [&](
                const int col_s_in, const int col_e_in,
                std::unique_ptr<Optical_props_arry<TF>>& optical_props_subset_in,
                Source_func_lw<TF>& sources_subset_in)
        {
            const int n_col_in = col_e_in - col_s_in + 1;
            Gas_concs<TF> gas_concs_subset(gas_concs, col_s_in, n_col_in);

            kdist.gas_optics(
                    p_lay.subset({{ {col_s_in, col_e_in}, {1, n_lay} }}),
                    p_lev.subset({{ {col_s_in, col_e_in}, {1, n_lev} }}),
                    t_lay.subset({{ {col_s_in, col_e_in}, {1, n_lay} }}),
                    t_sfc.subset({{ {col_s_in, col_e_in} }}),
                    gas_concs_subset,
                    optical_props_subset_in,
                    sources_subset_in,
                    col_dry.subset({{ {col_s_in, col_e_in}, {1, n_lay} }}),
                    t_lev.subset  ({{ {col_s_in, col_e_in}, {1, n_lev} }}) );

            optical_props->set_subset(optical_props_subset_in, col_s_in, col_e_in);
            sources.set_subset(sources_subset_in, col_s_in, col_e_in);
        };

        for (int b=1; b<=n_blocks; ++b)
        {
            const int col_s = (b-1) * n_col_block + 1;
            const int col_e =  b    * n_col_block;

            calc_optical_props_subset(
                    col_s, col_e,
                    optical_props_subset,
                    sources_subset);
        }

        if (n_col_block_left > 0)
        {
            optical_props_left = std::make_unique<Optical_props_1scl<TF>>(n_col_block_left, n_lay, kdist);
            Source_func_lw<TF> sources_left(n_col_block_left, n_lay, kdist);

            const int col_s = n_col - n_col_block_left + 1;
            const int col_e = n_col;

            calc_optical_props_subset(
                    col_s, col_e,
                    optical_props_left,
                    sources_left);
        }

        ////// SOLVING THE FLUXES FOR LONGWAVE RADIATION //////
        master.print_message("STEP 2: Computing the longwave radiation fluxes.\n");

        const int n_ang = input_nc.get_variable<TF>("angle");

        Array<TF,2> flux_up ({n_col, n_lev});
        Array<TF,2> flux_dn ({n_col, n_lev});
        Array<TF,2> flux_net({n_col, n_lev});

        Array<TF,3> bnd_flux_up ({n_col, n_lev, n_bnd});
        Array<TF,3> bnd_flux_dn ({n_col, n_lev, n_bnd});
        Array<TF,3> bnd_flux_net({n_col, n_lev, n_bnd});

        auto calc_fluxes_subset = [&](
                const int col_s_in, const int col_e_in,
                const std::unique_ptr<Optical_props_arry<TF>>& optical_props_subset_in,
                const Source_func_lw<TF>& sources_subset_in,
                const Array<TF,2> emis_sfc_subset_in,
                std::unique_ptr<Fluxes_broadband<TF>>& fluxes_subset_in)
        {
            const int n_col_block_subset = col_e_in - col_s_in + 1;

            Array<TF,3> gpt_flux_up({n_col_block_subset, n_lev, n_gpt});
            Array<TF,3> gpt_flux_dn({n_col_block_subset, n_lev, n_gpt});

            // CvH: I removed the pointer assignments of the fluxes, as this is unportable Fortran code.
            Rte_lw<TF>::rte_lw(
                    optical_props_subset_in,
                    top_at_1,
                    sources_subset_in,
                    emis_sfc_subset_in,
                    Array<TF,2>(), // Add an empty array, no inc_flux.
                    gpt_flux_up,
                    gpt_flux_dn,
                    n_ang);

            fluxes_subset_in->reduce(
                    gpt_flux_up, gpt_flux_dn, optical_props, top_at_1);

            // Copy the data to the output.
            for (int ilev=1; ilev<=n_lev; ++ilev)
                for (int icol=1; icol<=n_col_block_subset; ++icol)
                {
                    flux_up ({icol+col_s_in-1, ilev}) = fluxes_subset_in->get_flux_up ()({icol, ilev});
                    flux_dn ({icol+col_s_in-1, ilev}) = fluxes_subset_in->get_flux_dn ()({icol, ilev});
                    flux_net({icol+col_s_in-1, ilev}) = fluxes_subset_in->get_flux_net()({icol, ilev});
                }

            // Copy the data to the output.
            for (int ibnd=1; ibnd<=n_bnd; ++ibnd)
                for (int ilev=1; ilev<=n_lev; ++ilev)
                    for (int icol=1; icol<=n_col_block_subset; ++icol)
                    {
                        bnd_flux_up ({icol+col_s_in-1, ilev, ibnd}) = fluxes_subset_in->get_bnd_flux_up ()({icol, ilev, ibnd});
                        bnd_flux_dn ({icol+col_s_in-1, ilev, ibnd}) = fluxes_subset_in->get_bnd_flux_dn ()({icol, ilev, ibnd});
                        bnd_flux_net({icol+col_s_in-1, ilev, ibnd}) = fluxes_subset_in->get_bnd_flux_net()({icol, ilev, ibnd});
                    }
        };

        for (int b=1; b<=n_blocks; ++b)
        {
            const int col_s = (b-1) * n_col_block + 1;
            const int col_e =  b    * n_col_block;

            optical_props_subset->get_subset(optical_props, col_s, col_e);
            sources_subset.get_subset(sources, col_s, col_e);

            Array<TF,2> emis_sfc_subset = emis_sfc.subset({{ {1, n_bnd}, {col_s, col_e} }});

            std::unique_ptr<Fluxes_broadband<TF>> fluxes_subset =
                    std::make_unique<Fluxes_byband<TF>>(n_col_block, n_lev, n_bnd);

            calc_fluxes_subset(
                    col_s, col_e,
                    optical_props_subset,
                    sources_subset,
                    emis_sfc_subset,
                    fluxes_subset);
        }

        if (n_col_block_left > 0)
        {
            const int col_s = n_col - n_col_block_left + 1;
            const int col_e = n_col;

            // CvH, check for reuse of this field.
            Source_func_lw<TF> sources_left(n_col_block_left, n_lay, kdist);

            optical_props_left->get_subset(optical_props, col_s, col_e);
            sources_left.get_subset(sources, col_s, col_e);

            Array<TF,2> emis_sfc_left = emis_sfc.subset({{ {1, n_bnd}, {col_s, col_e} }});

            std::unique_ptr<Fluxes_broadband<TF>> fluxes_left =
                    std::make_unique<Fluxes_byband<TF>>(n_col_block_left, n_lev, n_bnd);

            calc_fluxes_subset(
                    col_s, col_e,
                    optical_props_left,
                    sources_left,
                    emis_sfc_left,
                    fluxes_left);
        }

        ////// SAVING THE MODEL OUTPUT //////
        master.print_message("STEP 3: Saving the output to NetCDF.\n");

        // Save the output of the optical solver to disk.
        Netcdf_file output_nc(master, "test_rrtmgp_output.nc", Netcdf_mode::Create);
        output_nc.add_dimension("col", n_col);
        output_nc.add_dimension("lay", n_lay);
        output_nc.add_dimension("lev", n_lev);
        output_nc.add_dimension("gpt", n_gpt);
        output_nc.add_dimension("band", n_bnd);
        output_nc.add_dimension("pair", 2);

        // WARNING: The storage in the NetCDF interface uses C-ordering and indexing.
        // First, store the optical properties.
        auto nc_band_lims_wvn = output_nc.add_variable<TF>("band_lims_wvn", {"band", "pair"});
        auto nc_band_lims_gpt = output_nc.add_variable<int>   ("band_lims_gpt", {"band", "pair"});
        auto nc_tau = output_nc.add_variable<TF>("tau", {"gpt", "lay", "col"});

        nc_band_lims_wvn.insert(optical_props->get_band_lims_wavenumber().v(), {0, 0});
        nc_band_lims_gpt.insert(optical_props->get_band_lims_gpoint().v()    , {0, 0});
        nc_tau.insert(optical_props->get_tau().v(), {0, 0, 0});

        // Second, store the sources.
        auto nc_lay_src     = output_nc.add_variable<TF>("lay_src"    , {"gpt", "lay", "col"});
        auto nc_lev_src_inc = output_nc.add_variable<TF>("lev_src_inc", {"gpt", "lay", "col"});
        auto nc_lev_src_dec = output_nc.add_variable<TF>("lev_src_dec", {"gpt", "lay", "col"});

        auto nc_sfc_src = output_nc.add_variable<TF>("sfc_src", {"gpt", "col"});

        nc_lay_src.insert    (sources.get_lay_source().v()    , {0, 0, 0});
        nc_lev_src_inc.insert(sources.get_lev_source_inc().v(), {0, 0, 0});
        nc_lev_src_dec.insert(sources.get_lev_source_dec().v(), {0, 0, 0});

        nc_sfc_src.insert(sources.get_sfc_source().v(), {0, 0});

        // Save the output of the flux calculation to disk.
        auto nc_flux_up  = output_nc.add_variable<TF>("flux_up" , {"lev", "col"});
        auto nc_flux_dn  = output_nc.add_variable<TF>("flux_dn" , {"lev", "col"});
        auto nc_flux_net = output_nc.add_variable<TF>("flux_net", {"lev", "col"});

        auto nc_bnd_flux_up  = output_nc.add_variable<TF>("bnd_flux_up" , {"band", "lev", "col"});
        auto nc_bnd_flux_dn  = output_nc.add_variable<TF>("bnd_flux_dn" , {"band", "lev", "col"});
        auto nc_bnd_flux_net = output_nc.add_variable<TF>("bnd_flux_net", {"band", "lev", "col"});

        nc_flux_up .insert(flux_up .v(), {0, 0});
        nc_flux_dn .insert(flux_dn .v(), {0, 0});
        nc_flux_net.insert(flux_net.v(), {0, 0});

        nc_bnd_flux_up .insert(bnd_flux_up .v(), {0, 0, 0});
        nc_bnd_flux_dn .insert(bnd_flux_dn .v(), {0, 0, 0});
        nc_bnd_flux_net.insert(bnd_flux_net.v(), {0, 0, 0});
    }
    else
    {
        ////// SOLVING THE OPTICAL PROPERTIES FOR SHORTWAVE RADIATION //////
        master.print_message("STEP 1: Computing optical depths for shortwave radiation.\n");

        // Read the boundary conditions from disk.
        sza.set_dims({n_col});
        tsi.set_dims({n_col});
        sfc_alb_dir.set_dims({n_bnd, n_col});
        sfc_alb_dif.set_dims({n_bnd, n_col});

        sza = input_nc.get_variable<TF>("solar_zenith_angle", {n_col});
        tsi = input_nc.get_variable<TF>("total_solar_irradiance", {n_col});
        sfc_alb_dir = input_nc.get_variable<TF>("sfc_alb_direct" , {n_col, n_bnd});
        sfc_alb_dif = input_nc.get_variable<TF>("sfc_alb_diffuse", {n_col, n_bnd});

        TF tsi_scaling = -999.;
        if (input_nc.variable_exists("tsi_scaling"))
            tsi_scaling = input_nc.get_variable<TF>("tsi_scaling");

        Array<TF,1> mu0({n_col});
        for (int icol=1; icol<=n_col; ++icol)
        {
            const TF pi = std::atan(1.)*4.;
            mu0({icol}) = std::cos(sza({icol}) * pi/180.);
        }

        Array<TF,2> toa_src({n_col, n_gpt});

        // Read the sources and create containers for the substeps.
        int n_blocks = n_col / n_col_block;
        int n_col_block_left = n_col % n_col_block;

        optical_props        = std::make_unique<Optical_props_2str<TF>>(n_col      , n_lay, kdist);
        optical_props_subset = std::make_unique<Optical_props_2str<TF>>(n_col_block, n_lay, kdist);

        auto calc_optical_props_subset = [&](
                const int col_s_in, const int col_e_in,
                std::unique_ptr<Optical_props_arry<TF>>& optical_props_subset_in)
        {
            const int n_col_in = col_e_in - col_s_in + 1;
            Gas_concs<TF> gas_concs_subset(gas_concs, col_s_in, n_col_in);
            Array<TF,2> toa_src_subset({n_col_in, n_gpt});

            kdist.gas_optics(
                    p_lay.subset({{ {col_s_in, col_e_in}, {1, n_lay} }}),
                    p_lev.subset({{ {col_s_in, col_e_in}, {1, n_lev} }}),
                    t_lay.subset({{ {col_s_in, col_e_in}, {1, n_lay} }}),
                    gas_concs_subset,
                    optical_props_subset_in,
                    toa_src_subset,
                    col_dry.subset({{ {col_s_in, col_e_in}, {1, n_lay} }}) );

            optical_props->set_subset(optical_props_subset_in, col_s_in, col_e_in);

            // Copy the data to the output.
            for (int igpt=1; igpt<=n_gpt; ++igpt)
                for (int icol=1; icol<=n_col_in; ++icol)
                    toa_src({icol+col_s_in-1, igpt}) = toa_src_subset({icol, igpt});
        };

        for (int b=1; b<=n_blocks; ++b)
        {
            const int col_s = (b-1) * n_col_block + 1;
            const int col_e =  b    * n_col_block;

            calc_optical_props_subset(
                    col_s, col_e,
                    optical_props_subset);
        }

        if (n_col_block_left > 0)
        {
            optical_props_left = std::make_unique<Optical_props_2str<TF>>(n_col_block_left, n_lay, kdist);

            const int col_s = n_col - n_col_block_left + 1;
            const int col_e = n_col;

            calc_optical_props_subset(
                    col_s, col_e,
                    optical_props_left);
        }

        ////// SOLVING THE FLUXES FOR SHORTWAVE RADIATION //////
        master.print_message("STEP 2: Computing the shortwave radiation fluxes.\n");

        Array<TF,2> flux_up    ({n_col, n_lev});
        Array<TF,2> flux_dn    ({n_col, n_lev});
        Array<TF,2> flux_dn_dir({n_col, n_lev});
        Array<TF,2> flux_net   ({n_col, n_lev});

        Array<TF,3> bnd_flux_up    ({n_col, n_lev, n_bnd});
        Array<TF,3> bnd_flux_dn    ({n_col, n_lev, n_bnd});
        Array<TF,3> bnd_flux_dn_dir({n_col, n_lev, n_bnd});
        Array<TF,3> bnd_flux_net   ({n_col, n_lev, n_bnd});

        auto calc_fluxes_subset = [&](
                const int col_s_in, const int col_e_in,
                const std::unique_ptr<Optical_props_arry<TF>>& optical_props_subset_in,
                const Array<TF,1>& mu0_subset_in,
                const Array<TF,2>& toa_src_subset_in,
                const Array<TF,2>& sfc_alb_dir_subset_in,
                const Array<TF,2>& sfc_alb_dif_subset_in,
                std::unique_ptr<Fluxes_broadband<TF>>& fluxes_subset_in)
        {
            const int n_col_block_subset = col_e_in - col_s_in + 1;

            Array<TF,3> gpt_flux_up    ({n_col_block_subset, n_lev, n_gpt});
            Array<TF,3> gpt_flux_dn    ({n_col_block_subset, n_lev, n_gpt});
            Array<TF,3> gpt_flux_dn_dir({n_col_block_subset, n_lev, n_gpt});

            Rte_sw<TF>::rte_sw(
                    optical_props_subset_in,
                    top_at_1,
                    mu0_subset_in,
                    toa_src_subset_in,
                    sfc_alb_dir_subset_in,
                    sfc_alb_dif_subset_in,
                    Array<TF,2>(), // Add an empty array, no inc_flux.
                    gpt_flux_up,
                    gpt_flux_dn,
                    gpt_flux_dn_dir);

            fluxes_subset_in->reduce(
                    gpt_flux_up, gpt_flux_dn, gpt_flux_dn,
                    optical_props_subset_in, top_at_1);

            // Copy the data to the output.
            for (int ilev=1; ilev<=n_lev; ++ilev)
                for (int icol=1; icol<=n_col_block_subset; ++icol)
                {
                    flux_up    ({icol+col_s_in-1, ilev}) = fluxes_subset_in->get_flux_up    ()({icol, ilev});
                    flux_dn    ({icol+col_s_in-1, ilev}) = fluxes_subset_in->get_flux_dn    ()({icol, ilev});
                    flux_dn_dir({icol+col_s_in-1, ilev}) = fluxes_subset_in->get_flux_dn_dir()({icol, ilev});
                    flux_net   ({icol+col_s_in-1, ilev}) = fluxes_subset_in->get_flux_net   ()({icol, ilev});
                }

            // Copy the data to the output.
            for (int ibnd=1; ibnd<=n_bnd; ++ibnd)
                for (int ilev=1; ilev<=n_lev; ++ilev)
                    for (int icol=1; icol<=n_col_block_subset; ++icol)
                    {
                        bnd_flux_up    ({icol+col_s_in-1, ilev, ibnd}) = fluxes_subset_in->get_bnd_flux_up    ()({icol, ilev, ibnd});
                        bnd_flux_dn    ({icol+col_s_in-1, ilev, ibnd}) = fluxes_subset_in->get_bnd_flux_dn    ()({icol, ilev, ibnd});
                        bnd_flux_dn_dir({icol+col_s_in-1, ilev, ibnd}) = fluxes_subset_in->get_bnd_flux_dn_dir()({icol, ilev, ibnd});
                        bnd_flux_net   ({icol+col_s_in-1, ilev, ibnd}) = fluxes_subset_in->get_bnd_flux_net   ()({icol, ilev, ibnd});
                    }
        };

        for (int b=1; b<=n_blocks; ++b)
        {
            const int col_s = (b-1) * n_col_block + 1;
            const int col_e =  b    * n_col_block;

            optical_props_subset->get_subset(optical_props, col_s, col_e);

            Array<TF,1> mu0_subset = mu0.subset({{ {col_s, col_e} }});
            Array<TF,2> toa_src_subset = toa_src.subset({{ {col_s, col_e}, {1, n_gpt} }});
            Array<TF,2> sfc_alb_dir_subset = sfc_alb_dir.subset({{ {1, n_bnd}, {col_s, col_e} }});
            Array<TF,2> sfc_alb_dif_subset = sfc_alb_dif.subset({{ {1, n_bnd}, {col_s, col_e} }});

            std::unique_ptr<Fluxes_broadband<TF>> fluxes_subset =
                    std::make_unique<Fluxes_byband<TF>>(n_col_block, n_lev, n_bnd);

            calc_fluxes_subset(
                    col_s, col_e,
                    optical_props_subset,
                    mu0_subset,
                    toa_src_subset,
                    sfc_alb_dir_subset,
                    sfc_alb_dif_subset,
                    fluxes_subset);
        }

        if (n_col_block_left > 0)
        {
            const int col_s = n_col - n_col_block_left + 1;
            const int col_e = n_col;

            optical_props_left->get_subset(optical_props, col_s, col_e);

            Array<TF,1> mu0_left = mu0.subset({{ {col_s, col_e} }});
            Array<TF,2> toa_src_left = toa_src.subset({{ {col_s, col_e}, {1, n_gpt} }});
            Array<TF,2> sfc_alb_dir_left = sfc_alb_dir.subset({{ {1, n_bnd}, {col_s, col_e} }});
            Array<TF,2> sfc_alb_dif_left = sfc_alb_dif.subset({{ {1, n_bnd}, {col_s, col_e} }});

            std::unique_ptr<Fluxes_broadband<TF>> fluxes_left =
                    std::make_unique<Fluxes_byband<TF>>(n_col_block_left, n_lev, n_bnd);

            calc_fluxes_subset(
                    col_s, col_e,
                    optical_props_left,
                    mu0_left,
                    toa_src_left,
                    sfc_alb_dir_left,
                    sfc_alb_dif_left,
                    fluxes_left);
        }

        ////// SAVING THE MODEL OUTPUT //////
        master.print_message("STEP 3: Saving the output to NetCDF.\n");

        // Save the output of the optical solver to disk.
        Netcdf_file output_nc(master, "test_rrtmgp_output.nc", Netcdf_mode::Create);
        output_nc.add_dimension("col", n_col);
        output_nc.add_dimension("lay", n_lay);
        output_nc.add_dimension("lev", n_lev);
        output_nc.add_dimension("gpt", n_gpt);
        output_nc.add_dimension("band", n_bnd);
        output_nc.add_dimension("pair", 2);

        // WARNING: The storage in the NetCDF interface uses C-ordering and indexing.
        // First, store the optical properties.
        auto nc_band_lims_wvn = output_nc.add_variable<TF> ("band_lims_wvn", {"band", "pair"});
        auto nc_band_lims_gpt = output_nc.add_variable<int>("band_lims_gpt", {"band", "pair"});
        auto nc_tau = output_nc.add_variable<TF>("tau", {"gpt", "lay", "col"});
        auto nc_ssa = output_nc.add_variable<TF>("ssa", {"gpt", "lay", "col"});
        auto nc_g   = output_nc.add_variable<TF>("g"  , {"gpt", "lay", "col"});

        nc_band_lims_wvn.insert(optical_props->get_band_lims_wavenumber().v(), {0, 0});
        nc_band_lims_gpt.insert(optical_props->get_band_lims_gpoint().v()    , {0, 0});

        nc_tau.insert(optical_props->get_tau().v(), {0, 0, 0});
        nc_ssa.insert(optical_props->get_ssa().v(), {0, 0, 0});
        nc_g  .insert(optical_props->get_g  ().v(), {0, 0, 0});

        auto nc_toa_src = output_nc.add_variable<TF>("toa_src", {"gpt", "col"});
        nc_toa_src.insert(toa_src.v(), {0, 0});

        // Save the output of the flux calculation to disk.
        auto nc_flux_up     = output_nc.add_variable<TF>("flux_up"    , {"lev", "col"});
        auto nc_flux_dn     = output_nc.add_variable<TF>("flux_dn"    , {"lev", "col"});
        auto nc_flux_dn_dir = output_nc.add_variable<TF>("flux_dn_dir", {"lev", "col"});
        auto nc_flux_net    = output_nc.add_variable<TF>("flux_net"   , {"lev", "col"});

        auto nc_bnd_flux_up     = output_nc.add_variable<TF>("bnd_flux_up"    , {"band", "lev", "col"});
        auto nc_bnd_flux_dn     = output_nc.add_variable<TF>("bnd_flux_dn"    , {"band", "lev", "col"});
        auto nc_bnd_flux_dn_dir = output_nc.add_variable<TF>("bnd_flux_dn_dir", {"band", "lev", "col"});
        auto nc_bnd_flux_net    = output_nc.add_variable<TF>("bnd_flux_net"   , {"band", "lev", "col"});

        nc_flux_up    .insert(flux_up    .v(), {0, 0});
        nc_flux_dn    .insert(flux_dn    .v(), {0, 0});
        nc_flux_dn_dir.insert(flux_dn_dir.v(), {0, 0});
        nc_flux_net   .insert(flux_net   .v(), {0, 0});

        nc_bnd_flux_up    .insert(bnd_flux_up    .v(), {0, 0, 0});
        nc_bnd_flux_dn    .insert(bnd_flux_dn    .v(), {0, 0, 0});
        nc_bnd_flux_dn_dir.insert(bnd_flux_dn_dir.v(), {0, 0, 0});
        nc_bnd_flux_net   .insert(bnd_flux_net   .v(), {0, 0, 0});
    }
}

int main()
{
    Master master;
    try
    {
        master.start();
        master.init();

        solve_radiation<FLOAT_TYPE>(master);
    }

    // Catch any exceptions and return 1.
    catch (const std::exception& e)
    {
        master.print_message("EXCEPTION: %s\n", e.what());
        return 1;
    }
    catch (...)
    {
        master.print_message("UNHANDLED EXCEPTION!\n");
        return 1;
    }

    // Return 0 in case of normal exit.
    return 0;
}


//   GAMBIT: Global and Modular BSM Inference Tool
//   *********************************************
///  \file
///
///  Frontend for CalcHEP Backend
///
///  *********************************************
///
///  Authors (add name and date if you modify):
///
///  \author Sanjay Bloor
///          (sanjay.bloor12@imperial.ac.uk)
///  \date 2017 May, Oct
///        2018 Sep
///
///  *****************************************

#include <fstream>
#include <boost/algorithm/string/replace.hpp>

#include "gambit/Backends/frontend_macros.hpp"
#include "gambit/Models/partmap.hpp"
#include "gambit/Backends/frontends/CalcHEP_3_6_27.hpp"
#include "gambit/Models/SpectrumContents/RegisteredSpectra.hpp"
#include "gambit/Elements/decay_table.hpp"

#include "gambit/Utils/mpiwrapper.hpp"

#include <unistd.h>

BE_INI_FUNCTION
{

  // Scan-level.
  static bool scan_level = true;

  if (scan_level)
  {
    // Declare backend path variables
    str BEpath;
    const char *path;
    char *modeltoset;
    std::string model;

    // All decays and xsecs added to a new model
    std::map< str, std::vector< std::vector<str> > > decays;
    std::map< std::vector<str>, std::vector< std::vector<str> > > xsecs;

    if (ModelInUse("ScalarSingletDM_Z2"))
    {
      // Set model within CalcHEP
      BEpath = backendDir + "/../models/ScalarSingletDM_Z2";
      path = BEpath.c_str();
      modeltoset = (char*)malloc(strlen(path)+11);
      sprintf(modeltoset, "%s", path);

      decays["h"] = std::vector< std::vector<str> >{ {"~S","~S"} };
      xsecs[std::vector<str>{"~S","~S"}] = std::vector< std::vector<str> >{ {"d'", "D'"}, {"u", "U"}, {"B", "b"}, {"h", "h"}, {"e", "E"}, {"Z", "Z"}, {"c", "C"}, {"s'", "S'"}, {"m", "M"}, {"t", "T"}, {"W+", "W-"}, {"ta+", "ta-"} };
      model = "ScalarSingletDM_Z2";
    }

    int error = setModel(modeltoset, 1);
    if (error != 0) backend_error().raise(LOCAL_INFO, "Unable to set model" + std::string(modeltoset) +
          " in CalcHEP. CalcHEP error code: " + std::to_string(error) + ". Please check your model files.\n");

    // Get the MPI rank, only let the first rank make the processes...
    int rank = 0;
    #ifdef WITH_MPI
      rank = GMPI::Comm().Get_rank();
    #endif

    // rank 0 can create all the libraries
    if (rank == 0)
    {
      // Decays first
      for (auto d : decays)
        for (auto fs : d.second)
          generate_decay_code(model, d.first, fs);

      // And two to twos
      for (auto x : xsecs)
        for (auto fs : x.second)
          generate_xsec_code(model, x.first, fs);
    }
    #ifdef WITH_MPI
      // Wait here until the first rank has generated all matrix elements.
      MPI_Barrier(MPI_COMM_WORLD);
    #endif

    free(modeltoset);
  }

  // Point-level.
  scan_level = false;

  if (ModelInUse("ScalarSingletDM_Z2"))
  {
    // Obtain model contents
    static const SpectrumContents::ScalarSingletDM_Z2 ScalarSingletDM_Z2_contents;

    // Obtain list of all parameters within model
    static const std::vector<SpectrumParameter> ScalarSingletDM_Z2_params = ScalarSingletDM_Z2_contents.all_parameters();

    // Obtain spectrum information to pass to CalcHEP
    const Spectrum& spec = *Dep::ScalarSingletDM_Z2_spectrum;

    Assign_All_Values(spec, ScalarSingletDM_Z2_params);
  }

}
END_BE_INI_FUNCTION

BE_NAMESPACE
{
  /// Create matrix element code for a decay
  numout* generate_decay_code(str model, str in, std::vector<str> out)
  {
    // Generate process from in and out states
    char *process = new char[(in + " -> " + out[0] + "," + out[1]).length() + 1];
    strcpy(process, (in + " -> " + out[0] + "," + out[1]).c_str());

    std::string incpy = in;
    std::string out0cpy = out[0];
    std::string out1cpy = out[1];

    // Replace any instance of a tilde with "bar"
    boost::replace_all(incpy, "~", "bar");
    boost::replace_all(out0cpy, "~", "bar");
    boost::replace_all(out1cpy, "~", "bar");

    // Remove all non-alpha numeric characters from the library names
    incpy.resize(std::remove_if(incpy.begin(), incpy.end(), [](char x) {return !isalnum(x) && !isspace(x);})-incpy.begin());
    out0cpy.resize(std::remove_if(out0cpy.begin(), out0cpy.end(), [](char x) {return !isalnum(x) && !isspace(x);})-out0cpy.begin());
    out1cpy.resize(std::remove_if(out1cpy.begin(), out1cpy.end(), [](char x) {return !isalnum(x) && !isspace(x);})-out1cpy.begin());

    // Generate libname from model and process name
    char *libname = new char[(model + "_" + incpy + "_to_" + out0cpy + out1cpy).length() + 1];
    strcpy(libname, (model + "_" + incpy + "_to_" + out0cpy + out1cpy).c_str());

    char *excludeVirtual = NULL; // Exclude any internal particles
    char *excludeOut = NULL;     // Exclude any products
    int twidth = 0;              // T-channel propagator width
    int UG = 0;                  // Unitary gauge

    // Generates shared object file based on libName - unless it already exists.
    numout* cc = getMEcode(twidth, UG, process, excludeVirtual, excludeOut, libname);

    // Release all memory allocated by "new" before returning
    delete process;
    delete libname;

    return cc;
  }

  /// For cross-sections, just wrap the decay code
  numout* generate_xsec_code(str model, std::vector<str> in, std::vector<str> out)
  {
    str newin = in[0] + "," + in[1];
    return generate_decay_code(model, newin, out);
  }

  /// Assigns gambit value to parameter, with error-checking.
  void Assign_Value(char *parameter, double value)
  {
    int error;
    error = assignVal(parameter, value);
    if (error != 0) backend_error().raise(LOCAL_INFO, "Unable to set " + std::string(parameter) +
          " in CalcHEP. CalcHEP error code: " + std::to_string(error) + ". Please check your model files.\n");
  }

  /// Assigns gambit value to parameter, with error-checking, for parameters that may
  /// have two different names in CalcHEP, such as alphainv : aEWM1 (FeynRules) / aEWinv (SARAH)
  void Assign_Value(char *parameter1, char *parameter2, double value)
  {
    int error;
    error = assignVal(parameter1, value);
    // If name 1 is successful, awesome.
    if (error == 0) return;
    // If not, then try the second one
    error = assignVal(parameter2, value);
    // If that doesn't work, we can throw an error, eh.
    if (error != 0) backend_error().raise(LOCAL_INFO, "Unable to set " + std::string(parameter1) +
          " or " + std::string(parameter2) + " in CalcHEP. " +
          " CalcHEP error code: " + std::to_string(error) + ". Please check your model files.\n");
  }

  /// Takes all parameters in a model, and assigns them by
  /// value to the appropriate CalcHEP parameter names.
  void Assign_All_Values(const Spectrum& spec, std::vector<SpectrumParameter> params)
  {
    // Iterate through the expected spectrum parameters of the model. Pass the value of pole masses
    // to CalcHEP from the spectrum, by PDG code.
    for (auto it = params.begin(); it != params.end(); ++it)
    {
      // Don't add the SM vev, Gauge couplings, Yukawas,
      // as CalcHEP computes these internally.
      std::list<std::string> doNotAssign = {"g1", "g2", "g3", "v", "Yu", "Ye", "Yd", "sinW2", "vev"};

      // Fetch the iterator of element with the parameter name
      std::list<std::string>::iterator it2 = std::find(doNotAssign.begin(), doNotAssign.end(), it->name());

      // If we find the parameter in the "do not assign" list, then don't try and assign it,
      // or this will throw an error.
      if (it2 != doNotAssign.end())
      {
        continue;
      }

      // Pole masses
      if (it->tag() == Par::Pole_Mass)
      {
        // Scalar case
        if (it->shape()[0] == 1)
        {
          std::pair<int,int> PDG_code = Models::ParticleDB().partmap::pdg_pair(it->name());
          Assign_Value(pdg2mass(PDG_code.first), spec.get(Par::Pole_Mass, PDG_code.first, PDG_code.second));
        }
        // Vector case
        else
        {
          for (int i=0; i<it->shape()[0]; ++i)
          {
            str long_name = it->name() + "_" + std::to_string(i+1);
            std::pair<int,int> PDG_code = Models::ParticleDB().partmap::pdg_pair(long_name);
            Assign_Value(pdg2mass(PDG_code.first), spec.get(Par::Pole_Mass, PDG_code.first, PDG_code.second));
          }
        }
        // Ignore any matrix cases, as Par::Pole_Mass should only be scalars or vectors
      }
      // Otherwise, should all have the right names
      else
      {
        const SubSpectrum& HE = spec.get_HE();

        // Scalar case
        if (it->shape().size()==1 and it->shape()[0] == 1)
        {
          char *chepname = const_cast<char*> ( it->name().c_str() );
          Assign_Value(chepname, HE.get(it->tag(), it->name()));
        }
        // Vector case
        else if (it->shape().size()==1 and it->shape()[0] > 1)
        {
          for (int i=0; i<it->shape()[0]; ++i)
          {
            str long_name = it->name() + std::to_string(i+1);
            char *chepname = const_cast<char*> ( long_name.c_str() );
            Assign_Value(chepname, HE.get(it->tag(), it->name(), i+1));
          }
        }
        // Matrix
        else if(it->shape().size()==2)
        {
          for (int i=0; i<it->shape()[0]; ++i)
          {
            for (int j=0; j<it->shape()[0]; ++j)
            {
              str long_name = it->name() + std::to_string(i+1) + "x" + std::to_string(j+1);
              char *chepname = const_cast<char*> ( long_name.c_str() );
              Assign_Value(chepname, HE.get(it->tag(), it->name(), i+1, j+1));
            }
          }
        }
      }
    }

    // Now go through the input parameters of the model. **NOTE**: this structure will only work for the case
    // where we want to scan over fundamental Lagrangian parameters. This will change post-SpecBit redesign.

    // Create SMInputs struct from spectrum
    const SMInputs& sminputs = spec.get_SMInputs();

    // Assign SMInputs
    Assign_Value((char*)"Gf", (char*)"GF", sminputs.GF);                    // Fermi
    Assign_Value((char*)"aS", sminputs.alphaS);                             // Strong coupling (unspecified scale if SARAH)
    Assign_Value((char*)"alfSMZ", (char*)"aS", sminputs.alphaS);            // Strong coupling (mZ) for both.
    Assign_Value((char*)"aEWM1", (char*)"aEWinv", sminputs.alphainv);       // Inverse EM coupling

    // Then, SM particle masses (by PDG code)
    Assign_Value(pdg2mass(1), sminputs.mD);                        // Down
    Assign_Value(pdg2mass(2), sminputs.mU);                        // Up
    Assign_Value(pdg2mass(3), sminputs.mS);                        // Strange
    Assign_Value(pdg2mass(4), sminputs.mCmC);                      // Charm (mC) MSbar
    Assign_Value(pdg2mass(11), spec.get(Par::Pole_Mass, "e-"));    // Electron
    Assign_Value(pdg2mass(13), spec.get(Par::Pole_Mass, "mu-"));   // Muon
    Assign_Value(pdg2mass(15), spec.get(Par::Pole_Mass, "tau-"));  // Tau
    Assign_Value(pdg2mass(23), spec.get(Par::Pole_Mass, "Z0"));    // Z
    Assign_Value(pdg2mass(5), spec.get(Par::Pole_Mass, "d_3"));    // mB(mB) MSbar
    Assign_Value(pdg2mass(6), spec.get(Par::Pole_Mass, "u_3"));    // mT(mT) MSbar
  }

  /// Passes the width of each BSM particle in the model, from DecayTable to CalcHEP.
  /// Don't set the widths of anything SM, except the top, which can get BSM contributions.
  void Assign_Widths(const DecayTable& tbl)
  {
    // Obtain all generic pdg codes. We can't set these widths..
    const std::vector<std::pair<int, int>> generic_particles = Models::ParticleDB().partmap::get_generic_particles();
    const std::vector<std::pair<int, int>> SM_particles = Models::ParticleDB().partmap::get_SM_particles();

    // Iterate through DecayTable. If it is not in the generic particles, or the SM, then go for it.
    for (std::map<std::pair<int, int>, Gambit::DecayTable::Entry>::const_iterator it = tbl.particles.begin();
          it != tbl.particles.end(); ++it)
    {
      if (std::find(generic_particles.begin(), generic_particles.end(), it->first) != generic_particles.end())
      {
        continue;
      }
      if (std::find(SM_particles.begin(), SM_particles.end(), it->first) != SM_particles.end())
      {
        continue;
      }
      Assign_Value(pdg2width(it->first.first), tbl.at(it->first).width_in_GeV);
    }
    // Assign the top separately as they can have BSM contributions e.g. decaying to charged higgses
    Assign_Value(pdg2width(6), tbl.at("t").width_in_GeV);
  }

  /// Provides spin-averaged decay width for 2 body decay process in CM frame at tree-level.
  // TODO: remove dependence on g3 (for alphaS(mZ)).
  double CH_Decay_Width(str& model, str& in, std::vector<str>& out)
  {
    // Check size of in and out states;
    if (out.size() != 2) backend_error().raise(LOCAL_INFO, "Output vector"
                        " must have only 2 entries for a 1 to 2 process.");

    // Calculates and updates all PUBLIC (model-dependent) parameters. These come from $CALCHEP/aux/VandP.c, generated by setModel() in the INI_FUNCTION.
    int err = calcMainFunc();

    if(err != 0) backend_error().raise(LOCAL_INFO, "Unable to calculate parameter " + std::string(varNames[err]) +
          " in CalcHEP. Please check your model files.\n");

    // Check if channel is kinematically open before doing anything. No need to compile processes that are not relevant.
    char *inbound = new char[(in).length() + 1];
    strcpy(inbound, (in).c_str());

    char *outbound_1 = new char[(out[0]).length() + 1];
    strcpy(outbound_1, (out[0]).c_str());

    char *outbound_2 = new char[(out[1]).length() + 1];
    strcpy(outbound_2, (out[1]).c_str());

    // Obtain mass of decaying particle
    double M =  pMass(inbound);
    double m1 = pMass(outbound_1);
    double m2 = pMass(outbound_2);

    // If channel is kinematically closed, return 0.
    if (m1 + m2 > M) { return 0; }

    // Generate process from in and out states
    char *process = new char[(in + " -> " + out[0] + "," + out[1]).length() + 1];
    strcpy(process, (in + " -> " + out[0] + "," + out[1]).c_str());

    std::string incpy = in;
    std::string out0cpy = out[0];
    std::string out1cpy = out[1];

    // Replace any instance of a tilde with "bar"
    boost::replace_all(incpy, "~", "bar");
    boost::replace_all(out0cpy, "~", "bar");
    boost::replace_all(out1cpy, "~", "bar");

    // Remove all non-alpha numeric characters from the library names
    incpy.resize(std::remove_if(incpy.begin(), incpy.end(), [](char x) {return !isalnum(x) && !isspace(x);})-incpy.begin());
    out0cpy.resize(std::remove_if(out0cpy.begin(), out0cpy.end(), [](char x) {return !isalnum(x) && !isspace(x);})-out0cpy.begin());
    out1cpy.resize(std::remove_if(out1cpy.begin(), out1cpy.end(), [](char x) {return !isalnum(x) && !isspace(x);})-out1cpy.begin());

    // Generate libname from model and process name
    char *libname = new char[(model + "_" + incpy + "_to_" + out0cpy + out1cpy).length() + 1];
    strcpy(libname, (model + "_" + incpy + "_to_" + out0cpy + out1cpy).c_str());

    char *excludeVirtual = NULL; // Exclude any internal particles
    char *excludeOut = NULL;     // Exclude any products
    int twidth = 0;              // T-channel propagator width
    int UG = 0;                  // Unitary gauge

    // Generates shared object file based on libName - unless it already exists.
    numout* cc = getMEcode(twidth, UG, process, excludeVirtual, excludeOut, libname);

    // Export numerical values of parameters to link to dynamical code
    err=passParameters(cc);

    if(err != 0) backend_error().raise(LOCAL_INFO, "Unable to calculate parameter " + std::string(varNames[err]) +
          " in CalcHEP. Please check your model files.\n");

    // Kinematic factors.
    double m_plus = m1+m2;
    double m_minus = m1-m2;
    double Msquared = M*M;
    double p = sqrt((Msquared - m_plus*m_plus)*(Msquared - m_minus*m_minus))/(2*M); // Magnitude of momentum in CM frame
    double E_1 = (Msquared + m1*m1 - m2*m2)/(2*M);                                  // Energy of first particle

    // Momentum vector for decay. This is 3 4-vectors, for the decaying particle and the products, respectively. 1 <--- M ---> 2
    double pvect[12] = {M, 0, 0, 0, E_1, 0, 0, p, (M-E_1), 0, 0, -p};

    // Compute squared matrix element
    double matElement = cc -> interface -> sqme(1, 0, pvect, NULL, &err);

    if(err != 0) backend_error().raise(LOCAL_INFO, "Unable to calculate matrix element associated with " + std::string(process) +
          " in CalcHEP. Please check your model files.\n");

    // Compute kinematic prefactor for X -> Y, Z decay
    double prefactor = p/(8*pi*Msquared);

    // Release all memory allocated by "new" before returning
    delete libname;
    delete inbound;
    delete outbound_1;
    delete outbound_2;

    // Return partial width
    return prefactor*matElement;
  }

  /// Computes annihilation cross-section for 2->2 process, DM+DMbar -> X + Y at tree level.
  /// Coannihilations not currently supported; we require the mass of both in states are equal.
  double CH_Sigma_V(str& model, std::vector<str>& in, std::vector<str>& out, double& v_rel, const DecayTable& decays)
  {
    // Check size of in and out states;
    if (in.size() != 2 or out.size() != 2) backend_error().raise(LOCAL_INFO, "Input and output vectors"
                        " must have only 2 entries for a 2 to 2 process.");

    // Calculates and updates all PUBLIC (model-dependent) parameters. These come from $CALCHEP/aux/VandP.c, generated by setModel() in the INI_FUNCTION.
    int err = calcMainFunc();

    if(err != 0) backend_error().raise(LOCAL_INFO, "Unable to calculate parameter " + std::string(varNames[err]) +
          " in CalcHEP. Please check your model files.\n");

    // Check if channel is kinematically open before doing anything. No need to compile processes that are not relevant.
    char *inbound_1 = new char[(in[0]).length() + 1];
    strcpy(inbound_1, (in[0]).c_str());

    char *inbound_2 = new char[(in[1]).length() + 1];
    strcpy(inbound_2, (in[1]).c_str());

    char *outbound_1 = new char[(out[0]).length() + 1];
    strcpy(outbound_1, (out[0]).c_str());

    char *outbound_2 = new char[(out[1]).length() + 1];
    strcpy(outbound_2, (out[1]).c_str());

    // Obtain mass of in & out states
    double m_DM = pMass(inbound_1);
    if (pMass(inbound_2) != m_DM) backend_error().raise(LOCAL_INFO, "Mass for both in states must be identical for CH_Sigma_V. "
                                  "Coannihilations not currently supported.");
    double m3 = pMass(outbound_1);
    double m4 = pMass(outbound_2);

    // Kinematics (in states)
    double s = 16*m_DM*m_DM/(4-v_rel*v_rel);
    double E_cm = sqrt(s);
    double E_1 = E_cm/2;
    double E_2 = E_1;
    double p_in = m_DM*v_rel/(sqrt(4-v_rel*v_rel));

    // Not enough energy for the process. Closed.
    if (m3+m4 > E_cm) { return 0; }

    // Pass particle widths - for propagators.
    Assign_Widths(decays);

    // Generate process from in and out states
    char *process = new char[(in[0]+ "," + in[1] + " -> " + out[0] + "," + out[1]).length() + 1];
    strcpy(process, (in[0] + "," + in[1] + " -> " + out[0] + "," + out[1]).c_str());

    str DM = in[0]; // Create copy of the string, as we are about to manipulate.
    str DMbar = in[1];

    // Remove all non-alpha numeric characters from the library names
    DM.resize(std::remove_if(DM.begin(), DM.end(), [](char x) {return !isalnum(x) && !isspace(x);})-DM.begin());
    DMbar.resize(std::remove_if(DMbar.begin(), DMbar.end(), [](char x) {return !isalnum(x) && !isspace(x);})-DMbar.begin());
    out[0].resize(std::remove_if(out[0].begin(), out[0].end(), [](char x) {return !isalnum(x) && !isspace(x);})-out[0].begin());
    out[1].resize(std::remove_if(out[1].begin(), out[1].end(), [](char x) {return !isalnum(x) && !isspace(x);})-out[1].begin());

    // Generate libname from model and process name
    char *libname = new char[(model + "_" + DM + DMbar + "_to_" + out[0] + out[1]).length() + 1];
    strcpy(libname, (model + "_" + DM + DMbar + "_to_" + out[0] + out[1]).c_str());

    /// TODO: are these options for the function..?
    char *excludeVirtual = NULL; // Exclude any internal particles
    char *excludeOut = NULL;     // Exclude any products
    int twidth = 0;              // T-channel propagator width
    int UG = 0;                  // Unitary gauge

    // Generates shared object file based on libName - unless it already exists.
    numout* cc = getMEcode(twidth, UG, process, excludeVirtual, excludeOut, libname);

    // Release all memory allocated by "new" before returning
    delete libname;
    delete inbound_1;
    delete inbound_2;
    delete outbound_1;
    delete outbound_2;

    // Export numerical values of parameters to link to dynamical code
    err=passParameters(cc);

    if(err != 0) backend_error().raise(LOCAL_INFO, "Unable to calculate parameter " + std::string(varNames[err]) +
          " in CalcHEP. Please check your model files.\n");

    // Kinematic factors (out states)
    double E_3 = (s - (m4*m4) + (m3*m3))/(2*E_cm);
    double E_4 = E_cm - E_3;
    double m_out_plus = m3+m4;
    double m_out_minus = m3-m4;
    double p_out = sqrt((s - m_out_plus*m_out_plus)*(s - m_out_minus*m_out_minus))/(2*E_cm); // Magnitude of outbound momentum in CM frame

    // Momentum vector for 2->2. This is 4 4-vectors, for particles 1->4 respectively.
    double pvect[16] = {E_1, 0, 0, p_in, E_2, 0, 0, -p_in, E_3, 0, 0, p_out, E_4, 0, 0, -p_out};

    // Kinematic prefactor
    double prefactor = p_out/(32*pi*m_DM*s/sqrt(4-v_rel*v_rel));

    // Squared matrix element - f(p(theta)) - to be integrated over.
    double M_squared = 0.0;

    int numsteps = 200;
    // Integrate between -1 < cos(theta) < 1
    for (int i=0; i < numsteps; i++)
    {
      double dcos = 2. / numsteps;
      double cosT = -1 + dcos*i;
      double sinT = sqrt(1-cosT*cosT);
      pvect[9] = p_out*sinT;
      pvect[11] = p_out*cosT;
      pvect[13] = -pvect[9];
      pvect[15] = -pvect[11];
      M_squared += dcos*(cc -> interface -> sqme(1, 0, pvect, NULL, &err)); // dcos * dM_squared/dcos
    }

    // If we get a negative ME (or a NaN), and the relative velocity is zero, then try
    // putting in an arbitrarily small value for the velocity.
    if ((M_squared < 0 or std::isnan(M_squared)) and v_rel == 0.)
    {
      // Choose velocity to be non-zero (but effectively zero) to avoid
      // potential unphysical values for p-wave suppressed xsecs.
      double newvel = 1e-6;

      logger() << "Square matrix element returned a negative value, but velocity is zero." << std::endl;
      logger() << "Trying with v = 1e-6 for final states " << out[0] << " " << out[1] << std::endl;

      // Compute new values for the kinematics
      s = 16*m_DM*m_DM/(4-newvel*newvel);
      E_cm = sqrt(s);
      E_1 = E_cm/2;
      E_2 = E_1;
      p_in = m_DM*newvel/(sqrt(4-newvel*newvel));
      E_3 = (s - (m4*m4) + (m3*m3))/(2*E_cm);
      E_4 = E_cm - E_3;
      p_out = sqrt((s - m_out_plus*m_out_plus)*(s - m_out_minus*m_out_minus))/(2*E_cm);
      pvect[0] = E_1;
      pvect[3] = p_in;
      pvect[4] = E_2;
      pvect[7] = -p_in;
      pvect[8] = E_3;
      pvect[9] = p_out;
      pvect[12] = E_4;
      pvect[15] = -p_out;
      prefactor = p_out/(32*pi*m_DM*s/sqrt(4-newvel*newvel));

      M_squared = 0.0;

      // Integrate again.
      for (int i=0; i < numsteps; i++)
      {
        double dcos = 2. / numsteps;
        double cosT = -1 + dcos*i;
        double sinT = sqrt(1-cosT*cosT);
        pvect[9] = p_out*sinT;
        pvect[11] = p_out*cosT;
        pvect[13] = -pvect[9];
        pvect[15] = -pvect[11];
        M_squared += dcos*(cc -> interface -> sqme(1, 0, pvect, NULL, &err)); // dcos * dM_squared/dcos

      }
    }
    // If it's a NaN, throw an error
    else if (std::isnan(M_squared))
    {
      std::ostringstream err;
      err << "ERROR: CalcHEP returned a NaN matrix element for the process "
          << in[0]  << " " << in[1]  << " to "
          << out[0] << " " << out[1] << " for relative velocity " << v_rel << ".";
      backend_error().raise(LOCAL_INFO, err.str());
    }

    // If it's negative, just return 0. We'll conservatively assume it's just numerical noise.
    else if (M_squared < 0)
    {
      logger() << "Squared matrix element has returned a negative value from CalcHEP." << std::endl;
      logger() << "Final states are " << out[0] << " " << out[1] << " for relative velocity " << v_rel << std::endl;
      logger() << "Returning 0 instead, assuming it's numerical noise from the crude integration." << EOM;
      return 0.;
    }

    // Total sigma_v
    return prefactor*M_squared;
  }
}
END_BE_NAMESPACE


// Copyright (C) 2011-2012 by the BEM++ Authors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

#include "bempp/common/config_ahmed.hpp"
#include "bempp/common/config_trilinos.hpp"
#ifdef WITH_AHMED

#include "discrete_aca_boundary_operator.hpp"
#include "../common/shared_ptr.hpp"

#include "ahmed_aux.hpp"
#include "aca_approximate_lu_inverse.hpp"

#include "../common/chunk_statistics.hpp"
#include "../common/complex_aux.hpp"
#include "../fiber/explicit_instantiation.hpp"
#include "../fiber/serial_blas_region.hpp"

#include <fstream>
#include <iostream>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <boost/type_traits/is_complex.hpp>

#include <tbb/blocked_range.h>
#include <tbb/concurrent_queue.h>
#include <tbb/parallel_reduce.h>
#include <tbb/task_scheduler_init.h>

#ifdef WITH_TRILINOS
#include <Thyra_SpmdVectorSpaceDefaultBase.hpp>
#endif

namespace Bempp
{

void dumpblcluster(const blcluster* bl, const std::string& indent)
{
    std::cout << indent << bl << " " << bl->getb1() << " " << bl->getb2() << " "
              << bl->getn1() << " " << bl->getn2() << "\n";
    std::string sonIndent = indent + "  ";
    for (int r = 0; r < bl->getnrs(); ++r)
        for (int c = 0; c < bl->getncs(); ++c) {
            blcluster* son = bl->getson(r, c);
            if (son)
                dumpblcluster(son, sonIndent);
            else
                std::cout << sonIndent << "0\n";
        }
}

namespace
{

template <typename ValueType>
class MblockMultiplicationLoopBody
{
    typedef mblock<typename AhmedTypeTraits<ValueType>::Type> AhmedMblock;
public:
    typedef tbb::concurrent_queue<size_t> LeafClusterIndexQueue;

    MblockMultiplicationLoopBody(
            TranspositionMode trans,
            ValueType multiplier,
            arma::Col<ValueType>& x,
            arma::Col<ValueType>& y,
            AhmedLeafClusterArray& leafClusters,
            boost::shared_array<AhmedMblock*> blocks,
            LeafClusterIndexQueue& leafClusterIndexQueue,
            std::vector<ChunkStatistics>& stats) :
        m_trans(trans),
        m_multiplier(multiplier), m_x(x), m_local_y(y),
        m_leafClusters(leafClusters), m_blocks(blocks),
        m_leafClusterIndexQueue(leafClusterIndexQueue),
        m_stats(stats)
    {
        if (trans != NO_TRANSPOSE && trans != TRANSPOSE &&
            trans != CONJUGATE_TRANSPOSE)
            throw std::invalid_argument(
                "MblockMultiplicationLoopBody::MblockMultiplicationLoopBody(): "
                "unsupported transposition mode");
        // m_local_y.fill(static_cast<ValueType>(0.));
    }

    MblockMultiplicationLoopBody(MblockMultiplicationLoopBody& other, tbb::split) :
        m_trans(other.m_trans),
        m_multiplier(other.m_multiplier),
        m_x(other.m_x), m_local_y(other.m_local_y.n_rows),
        m_leafClusters(other.m_leafClusters), m_blocks(other.m_blocks),
        m_leafClusterIndexQueue(other.m_leafClusterIndexQueue),
        m_stats(other.m_stats)
    {
        m_local_y.fill(static_cast<ValueType>(0.));
    }

    template <typename Range>
    void operator() (const Range& r) {
        for (typename Range::const_iterator i = r.begin(); i != r.end(); ++i) {
            size_t leafClusterIndex = -1;
            if (!m_leafClusterIndexQueue.try_pop(leafClusterIndex)) {
                std::cerr << "MblockMultiplicationLoopBody::operator(): "
                             "Warning: try_pop failed; this shouldn't happen!"
                          << std::endl;
                continue;
            }
            m_stats[leafClusterIndex].valid = true;
            m_stats[leafClusterIndex].chunkStart = r.begin();
            m_stats[leafClusterIndex].chunkSize = r.size();
            m_stats[leafClusterIndex].startTime = tbb::tick_count::now();

            blcluster* cluster = m_leafClusters[leafClusterIndex];
            if (m_trans == NO_TRANSPOSE)
                m_blocks[cluster->getidx()]->mltaVec(
                    ahmedCast(m_multiplier),
                    ahmedCast(&m_x(cluster->getb2())),
                    ahmedCast(&m_local_y(cluster->getb1())));
            else if (m_trans == TRANSPOSE)
                m_blocks[cluster->getidx()]->mltatVec(
                    ahmedCast(m_multiplier),
                    ahmedCast(&m_x(cluster->getb1())),
                    ahmedCast(&m_local_y(cluster->getb2())));
            else // m_trans == CONJUGATE_TRANSPOSE)
                m_blocks[cluster->getidx()]->mltahVec(
                    ahmedCast(m_multiplier),
                    ahmedCast(&m_x(cluster->getb1())),
                    ahmedCast(&m_local_y(cluster->getb2())));
            m_stats[leafClusterIndex].endTime = tbb::tick_count::now();
        }
    }

    void join(const MblockMultiplicationLoopBody& other) {
        m_local_y += other.m_local_y;
    }

private:
    TranspositionMode m_trans;
    ValueType m_multiplier;
    arma::Col<ValueType>& m_x;
public:
    arma::Col<ValueType> m_local_y;
private:
    AhmedLeafClusterArray& m_leafClusters;
    boost::shared_array<AhmedMblock*> m_blocks;
    LeafClusterIndexQueue& m_leafClusterIndexQueue;
    std::vector<ChunkStatistics>& m_stats;
};

bool areEqual(const blcluster* op1, const blcluster* op2)
{
    if (!op1 || !op2)
        return (!op1 && !op2);
    if (op1->getnrs() != op2->getnrs() || op1->getncs() != op2->getncs())
        return false;
    if (op1->isleaf())
        return (op1->getb1() == op2->getb1() && op1->getb2() == op2->getb2() &&
                op1->getn1() == op2->getn1() && op1->getn2() == op2->getn2());
    for (unsigned int rs = 0; rs < op1->getnrs(); ++rs)
        for (unsigned int cs = 0; cs < op1->getncs(); ++cs)
            if (!areEqual(op1->getson(rs, cs), op2->getson(rs, cs)))
                return false;
    return true;
}

inline void mltaSyHVec(double d, blcluster* bl, mblock<double>** A, double* x,
                       double* y)
{
    mltaHeHVec(d, bl, A, x, y);
}

inline void mltaSyHVec(float d, blcluster* bl, mblock<float>** A, float* x,
                       float* y)
{
    mltaHeHVec(d, bl, A, x, y);
}

inline void mltaSyHVec(scomp d, blcluster* bl, mblock<scomp>** A, scomp* x,
                       scomp* y)
{
    throw std::runtime_error("mltaSyHVec(): the overload of this function for "
                             "complex single-precision numbers does not exist "
                             "in AHMED");
}

// For real types, SyHh = SyH
inline void mltaSyHhVec(double d, blcluster* bl, mblock<double>** A, double* x,
                        double* y)
{
    mltaSyHVec(d, bl, A, x, y);
}

inline void mltaSyHhVec(float d, blcluster* bl, mblock<float>** A, float* x,
                        float* y)
{
    mltaSyHVec(d, bl, A, x, y);
}

inline void mltaSyHhVec(scomp d, blcluster* bl, mblock<scomp>** A, scomp* x,
                        scomp* y)
{
    throw std::runtime_error("mltaSyHhVec(): the overload of this function for "
                             "complex single-precision numbers does not exist "
                             "in AHMED");
}

} // namespace

template <typename ValueType>
DiscreteAcaBoundaryOperator<ValueType>::
DiscreteAcaBoundaryOperator(
        unsigned int rowCount, unsigned int columnCount,
        double eps_,
        int maximumRank_,
        int symmetry_,
        const shared_ptr<const AhmedBemBlcluster>& blockCluster_,
        const AhmedMblockArray& blocks_,
        const IndexPermutation& domainPermutation_,
        const IndexPermutation& rangePermutation_,
        const ParallelizationOptions& parallelizationOptions_,
        const std::vector<AhmedConstMblockArray>& sharedBlocks_) :
#ifdef WITH_TRILINOS
    m_domainSpace(Thyra::defaultSpmdVectorSpace<ValueType>(columnCount)),
    m_rangeSpace(Thyra::defaultSpmdVectorSpace<ValueType>(rowCount)),
#else
    m_rowCount(rowCount), m_columnCount(columnCount),
#endif
    m_eps(eps_),
    m_maximumRank(maximumRank_),
    m_symmetry(symmetry_),
    m_blockCluster(blockCluster_), m_blocks(blocks_),
    m_domainPermutation(domainPermutation_),
    m_rangePermutation(rangePermutation_),
    m_parallelizationOptions(parallelizationOptions_),
    m_sharedBlocks(sharedBlocks_)
{
    if (eps_ <= 0 || eps_ > 1)
        std::cout << "DiscreteAcaBoundaryOperator::DiscreteAcaBoundaryOperator(): "
                     "warning: suspicious value of eps ("
                  << eps_ << ")" << std::endl;
    if (maximumRank_ < 0)
        std::cout << "DiscreteAcaBoundaryOperator::DiscreteAcaBoundaryOperator(): "
                     "warning: suspicious value of maximumRank ("
                  << maximumRank_ << ")" << std::endl;
}

template <typename ValueType>
DiscreteAcaBoundaryOperator<ValueType>::
DiscreteAcaBoundaryOperator(
        unsigned int rowCount, unsigned int columnCount,
        int maximumRank_,
        int symmetry_,
        std::auto_ptr<const AhmedBemBlcluster> blockCluster_,
        AhmedMblockArray blocks_,
        const IndexPermutation& domainPermutation_,
        const IndexPermutation& rangePermutation_,
        const ParallelizationOptions& parallelizationOptions_,
        const std::vector<AhmedConstMblockArray>& sharedBlocks_) :
#ifdef WITH_TRILINOS
    m_domainSpace(Thyra::defaultSpmdVectorSpace<ValueType>(columnCount)),
    m_rangeSpace(Thyra::defaultSpmdVectorSpace<ValueType>(rowCount)),
#else
    m_rowCount(rowCount), m_columnCount(columnCount),
#endif
    m_eps(1e-4), // just a guess...
    m_maximumRank(maximumRank_),
    m_symmetry(symmetry_),
    m_blockCluster(blockCluster_.release()), m_blocks(blocks_),
    m_domainPermutation(domainPermutation_),
    m_rangePermutation(rangePermutation_),
    m_parallelizationOptions(parallelizationOptions_),
    m_sharedBlocks(sharedBlocks_)
{
}

template <typename ValueType>
DiscreteAcaBoundaryOperator<ValueType>::
DiscreteAcaBoundaryOperator(
        unsigned int rowCount, unsigned int columnCount,
        int maximumRank_,
        int symmetry_,
        const shared_ptr<const AhmedBemBlcluster>& blockCluster_,
        const AhmedMblockArray& blocks_,
        const IndexPermutation& domainPermutation_,
        const IndexPermutation& rangePermutation_,
        const ParallelizationOptions& parallelizationOptions_,
        const std::vector<AhmedConstMblockArray>& sharedBlocks_) :
#ifdef WITH_TRILINOS
    m_domainSpace(Thyra::defaultSpmdVectorSpace<ValueType>(columnCount)),
    m_rangeSpace(Thyra::defaultSpmdVectorSpace<ValueType>(rowCount)),
#else
    m_rowCount(rowCount), m_columnCount(columnCount),
#endif
    m_eps(1e-4), // just a guess...
    m_maximumRank(maximumRank_),
    m_symmetry(symmetry_),
    m_blockCluster(blockCluster_), m_blocks(blocks_),
    m_domainPermutation(domainPermutation_),
    m_rangePermutation(rangePermutation_),
    m_parallelizationOptions(parallelizationOptions_),
    m_sharedBlocks(sharedBlocks_)
{
}

template <typename ValueType>
arma::Mat<ValueType>
DiscreteAcaBoundaryOperator<ValueType>::
asMatrix() const
{
    const unsigned int nRows = rowCount();
    const unsigned int nCols = columnCount();
    const blcluster* blockCluster = m_blockCluster.get();
    blcluster* nonconstBlockCluster = const_cast<blcluster*>(blockCluster);

    arma::Mat<ValueType> permutedOutput(nRows, nCols );
    permutedOutput.fill(0.);
    arma::Col<ValueType> unit(nCols );
    unit.fill(0.);

    for (unsigned int col = 0; col < nCols ; ++col)
    {
        if (col > 0)
            unit(col - 1) = 0.;
        unit(col) = 1.;
        if (m_symmetry & SYMMETRIC)
            mltaSyHVec(static_cast<ValueType>(1.), nonconstBlockCluster, m_blocks.get(),
                       ahmedCast(unit.memptr()),
                       ahmedCast(permutedOutput.colptr(col)));
        else if (m_symmetry & HERMITIAN)
            mltaHeHVec(1., nonconstBlockCluster, m_blocks.get(),
                       ahmedCast(unit.memptr()),
                       ahmedCast(permutedOutput.colptr(col)));
        else
            mltaGeHVec(1., nonconstBlockCluster, m_blocks.get(),
                       ahmedCast(unit.memptr()),
                       ahmedCast(permutedOutput.colptr(col)));
    }

    arma::Mat<ValueType> output(nRows, nCols );
    for (unsigned int col = 0; col < nCols ; ++col)
        for (unsigned int row = 0; row < nRows; ++row)
            output(row, col) =
                    permutedOutput(m_rangePermutation.permuted(row),
                                   m_domainPermutation.permuted(col));

    return output;
}

template <typename ValueType>
unsigned int
DiscreteAcaBoundaryOperator<ValueType>::
rowCount() const
{
#ifdef WITH_TRILINOS
    return m_rangeSpace->dim();
#else
    return m_rowCount;
#endif
}

template <typename ValueType>
unsigned int
DiscreteAcaBoundaryOperator<ValueType>::
columnCount() const
{
#ifdef WITH_TRILINOS
    return m_domainSpace->dim();
#else
    return m_columnCount;
#endif
}

template <typename ValueType>
void
DiscreteAcaBoundaryOperator<ValueType>::
addBlock(const std::vector<int>& rows,
         const std::vector<int>& cols,
         const ValueType alpha,
         arma::Mat<ValueType>& block) const
{
    throw std::runtime_error("DiscreteAcaBoundaryOperator::"
                             "addBlock(): not implemented yet");
}

template <typename ValueType>
shared_ptr<const DiscreteBoundaryOperator<ValueType> >
DiscreteAcaBoundaryOperator<ValueType>::asDiscreteAcaBoundaryOperator(
        double eps, int maximumRank, bool interleave) const
{
    return this->shared_from_this(); // this-> needed for template name resolution.
}

template <typename ValueType>
const DiscreteAcaBoundaryOperator<ValueType>&
DiscreteAcaBoundaryOperator<ValueType>::castToAca(
        const DiscreteBoundaryOperator<ValueType>& discreteOperator)
{
    return dynamic_cast<const DiscreteAcaBoundaryOperator<ValueType>&>(
                discreteOperator);
}

template <typename ValueType>
shared_ptr<const DiscreteAcaBoundaryOperator<ValueType> >
DiscreteAcaBoundaryOperator<ValueType>::castToAca(
        const shared_ptr<const DiscreteBoundaryOperator<ValueType> >&
        discreteOperator)
{
    shared_ptr<const DiscreteAcaBoundaryOperator<ValueType> > result =
        boost::dynamic_pointer_cast<const DiscreteAcaBoundaryOperator<ValueType> >(
                discreteOperator);
    if (result.get()==0 && (discreteOperator.get()!=0)) throw std::bad_cast();
    return result;
}

#ifdef WITH_TRILINOS
template <typename ValueType>
Teuchos::RCP<const Thyra::VectorSpaceBase<ValueType> >
DiscreteAcaBoundaryOperator<ValueType>::
domain() const
{
    return m_domainSpace;
}

template <typename ValueType>
Teuchos::RCP<const Thyra::VectorSpaceBase<ValueType> >
DiscreteAcaBoundaryOperator<ValueType>::
range() const
{
    return m_rangeSpace;
}

template <typename ValueType>
bool
DiscreteAcaBoundaryOperator<ValueType>::
opSupportedImpl(Thyra::EOpTransp M_trans) const
{
    // TODO: implement remaining variant (conjugate)
    return (M_trans == Thyra::NOTRANS || M_trans == Thyra::TRANS ||
            M_trans == Thyra::CONJTRANS);
}
#endif // WITH_TRILINOS

template <typename ValueType>
void
DiscreteAcaBoundaryOperator<ValueType>::
applyBuiltInImpl(const TranspositionMode trans,
                 const arma::Col<ValueType>& x_in,
                 arma::Col<ValueType>& y_inout,
                 const ValueType alpha,
                 const ValueType beta) const
{
    if (trans != NO_TRANSPOSE && trans != TRANSPOSE && trans != CONJUGATE_TRANSPOSE)
        throw std::runtime_error(
                "DiscreteAcaBoundaryOperator::applyBuiltInImpl(): "
                "transposition modes other than NO_TRANSPOSE, TRANSPOSE and "
                "CONJUGATE_TRANSPOSE are not supported");
    bool transposed = (trans & TRANSPOSE);

    const blcluster* blockCluster = m_blockCluster.get();
    blcluster* nonconstBlockCluster = const_cast<blcluster*>(blockCluster);

    if ((!transposed && (columnCount() != x_in.n_rows ||
                         rowCount() != y_inout.n_rows)) ||
            (transposed && (rowCount() != x_in.n_rows ||
                            columnCount() != y_inout.n_rows)))
        throw std::invalid_argument(
                "DiscreteAcaBoundaryOperator::applyBuiltInImpl(): "
                "incorrect vector length");

    if (beta == static_cast<ValueType>(0.))
        y_inout.fill(static_cast<ValueType>(0.));
    else
        y_inout *= beta;

    arma::Col<ValueType> permutedArgument;
    if (!transposed)
        m_domainPermutation.permuteVector(x_in, permutedArgument);
    else
        m_rangePermutation.permuteVector(x_in, permutedArgument);

    arma::Col<ValueType> permutedResult;
    if (!transposed)
        m_rangePermutation.permuteVector(y_inout, permutedResult);
    else
        m_domainPermutation.permuteVector(y_inout, permutedResult);

    if (m_symmetry & SYMMETRIC) {
        // TODO: parallelise
        if (trans == NO_TRANSPOSE || trans == TRANSPOSE)
            mltaSyHVec(ahmedCast(alpha), nonconstBlockCluster, m_blocks.get(),
                       ahmedCast(permutedArgument.memptr()),
                       ahmedCast(permutedResult.memptr()));
        else // trans == CONJUGATE_TRANSPOSE
            mltaSyHhVec(ahmedCast(alpha), nonconstBlockCluster, m_blocks.get(),
                       ahmedCast(permutedArgument.memptr()),
                       ahmedCast(permutedResult.memptr()));
    }
    else if (m_symmetry & HERMITIAN) {
        // NO_TRANSPOSE and CONJUGATE_TRANSPOSE are equivalent
        if (trans == NO_TRANSPOSE || trans == CONJUGATE_TRANSPOSE)
            mltaHeHVec(ahmedCast(alpha), nonconstBlockCluster, m_blocks.get(),
                       ahmedCast(permutedArgument.memptr()),
                       ahmedCast(permutedResult.memptr()));
        else { // trans == TRANSPOSE
            // alpha A^T x + beta y = (alpha^* A^H x^* + beta^* y^*)^*
            // = (alpha^* A x^* + beta^* y^*)^*
            permutedArgument = arma::conj(permutedArgument);
            permutedResult = arma::conj(permutedResult);
            ValueType alphaConj = conj(alpha);
            mltaHeHVec(ahmedCast(alphaConj), nonconstBlockCluster, m_blocks.get(),
                       ahmedCast(permutedArgument.memptr()),
                       ahmedCast(permutedResult.memptr()));
            permutedResult = arma::conj(permutedResult);
        }
    }
    else {
//        if (trans == NO_TRANSPOSE)
//            mltaGeHVec(ahmedCast(alpha), nonconstBlockCluster, m_blocks.get(),
//                       ahmedCast(permutedArgument.memptr()),
//                       ahmedCast(permutedResult.memptr()));
//        else // trans == CONJUGATE_TRANSPOSE
//            mltaGeHhVec(ahmedCast(alpha), nonconstBlockCluster, m_blocks.get(),
//                        ahmedCast(permutedArgument.memptr()),
//                        ahmedCast(permutedResult.memptr()));

        AhmedLeafClusterArray leafClusters(nonconstBlockCluster);
        leafClusters.sortAccordingToClusterSize();
        const size_t leafClusterCount = leafClusters.size();

        int maxThreadCount = 1;
        if (!m_parallelizationOptions.isOpenClEnabled()) {
            if (m_parallelizationOptions.maxThreadCount() ==
                    ParallelizationOptions::AUTO)
                maxThreadCount = tbb::task_scheduler_init::automatic;
            else
                maxThreadCount = m_parallelizationOptions.maxThreadCount();
        }
        tbb::task_scheduler_init scheduler(maxThreadCount);

        std::vector<ChunkStatistics> chunkStats(leafClusterCount);

        typedef MblockMultiplicationLoopBody<ValueType> Body;
        typename Body::LeafClusterIndexQueue leafClusterIndexQueue;
        for (size_t i = 0; i < leafClusterCount; ++i)
            leafClusterIndexQueue.push(i);

        // std::cout << "----------------------------\nperm Arg\n" << permutedArgument;
        // std::cout << "perm Res\n" << permutedResult;
        Body body(trans,
                  alpha, permutedArgument, permutedResult,
                  leafClusters, m_blocks,
                  leafClusterIndexQueue, chunkStats);
        {
            Fiber::SerialBlasRegion region;
            tbb::parallel_reduce(tbb::blocked_range<size_t>(0, leafClusterCount),
                                 body);
        }
        permutedResult = body.m_local_y;
    }
    if (!transposed)
        m_rangePermutation.unpermuteVector(permutedResult, y_inout);
    else
        m_domainPermutation.unpermuteVector(permutedResult, y_inout);
}

template <typename ValueType>
void
DiscreteAcaBoundaryOperator<ValueType>::
makeAllMblocksDense()
{
    for (unsigned int i = 0; i < m_blockCluster->nleaves(); ++i)
        if (m_blocks[i]->isLrM())
            m_blocks[i]->convLrM_toGeM();
}

template <typename ValueType>
double
DiscreteAcaBoundaryOperator<ValueType>::eps() const
{
    return m_eps;
}

template <typename ValueType>
int
DiscreteAcaBoundaryOperator<ValueType>::maximumRank() const
{
    return m_maximumRank;
}

template <typename ValueType>
int
DiscreteAcaBoundaryOperator<ValueType>::actualMaximumRank() const
{
    // const_cast because Ahmed is not const-correct
    return Hmax_rank(const_cast<AhmedBemBlcluster*>(m_blockCluster.get()),
                     m_blocks.get());
}

template <typename ValueType>
int
DiscreteAcaBoundaryOperator<ValueType>::symmetry() const
{
    return m_symmetry;
}

template <typename ValueType>
shared_ptr<const typename DiscreteAcaBoundaryOperator<ValueType>::AhmedBemBlcluster>
DiscreteAcaBoundaryOperator<ValueType>::blockCluster() const
{
    return m_blockCluster;
}

template <typename ValueType>
typename DiscreteAcaBoundaryOperator<ValueType>::AhmedMblockArray
DiscreteAcaBoundaryOperator<ValueType>::blocks() const
{
    return m_blocks;
}

template <typename ValueType>
size_t
DiscreteAcaBoundaryOperator<ValueType>::blockCount() const
{
    return m_blockCluster->nleaves();
}

template <typename ValueType>
const IndexPermutation&
DiscreteAcaBoundaryOperator<ValueType>::domainPermutation() const
{
    return m_domainPermutation;
}

template <typename ValueType>
const IndexPermutation&
DiscreteAcaBoundaryOperator<ValueType>::rangePermutation() const
{
    return m_rangePermutation;
}

template <typename ValueType>
const ParallelizationOptions&
DiscreteAcaBoundaryOperator<ValueType>::parallelizationOptions() const
{
    return m_parallelizationOptions;
}

template <typename ValueType>
std::vector<typename DiscreteAcaBoundaryOperator<ValueType>::AhmedConstMblockArray >
DiscreteAcaBoundaryOperator<ValueType>::sharedBlocks() const
{
    return m_sharedBlocks;
}

// Global routines

template <typename ValueType>
shared_ptr<const DiscreteBoundaryOperator<ValueType> > acaOperatorSum(
        const shared_ptr<const DiscreteBoundaryOperator<ValueType> >& op1,
        const shared_ptr<const DiscreteBoundaryOperator<ValueType> >& op2,
        double eps, int maximumRank)
{
    if (!op1 || !op2)
        throw std::invalid_argument("acaOperatorSum(): "
                                    "both operands must be non-null");
    if (eps <= 0)
        throw std::invalid_argument("acaOperatorSum(): eps must be positive");
    if (maximumRank < 1)
        maximumRank = 1;
    shared_ptr<const DiscreteAcaBoundaryOperator<ValueType> > acaOp1 =
            DiscreteAcaBoundaryOperator<ValueType>::castToAca(op1);
    shared_ptr<const DiscreteAcaBoundaryOperator<ValueType> > acaOp2 =
            DiscreteAcaBoundaryOperator<ValueType>::castToAca(op2);
    if (acaOp1->symmetry() != acaOp2->symmetry())
        throw std::runtime_error("acaOperatorSum(): addition of two H-matrices "
                                 "of different symmetry is not supported yet");
    if (!areEqual(acaOp1->blockCluster().get(), acaOp2->blockCluster().get()))
        throw std::invalid_argument("acaOperatorSum(): block cluster trees of "
                                    "both operands must be identical");
    if (acaOp1->m_domainPermutation != acaOp2->m_domainPermutation ||
            acaOp1->m_rangePermutation != acaOp2->m_rangePermutation)
        throw std::invalid_argument("acaOperatorSum(): domain and range "
                                    "index permutations of "
                                    "both operands must be identical");

    typedef typename DiscreteAcaBoundaryOperator<ValueType>::AhmedBemBlcluster
            AhmedBemBlcluster;
    typedef typename DiscreteAcaBoundaryOperator<ValueType>::AhmedMblock
            AhmedMblock;

    shared_ptr<const AhmedBemBlcluster> sumBlockCluster = acaOp1->blockCluster();
    blcluster* nonConstSumBlockCluster =
            const_cast<blcluster*>( // AHMED is not const-correct
                static_cast<const blcluster*>( // safe -- upcast
                                        sumBlockCluster.get()));
    boost::shared_array<AhmedMblock*> sumBlocks =
            allocateAhmedMblockArray<ValueType>(sumBlockCluster.get());
    copyH(nonConstSumBlockCluster, acaOp1->m_blocks.get(), sumBlocks.get());
    addGeHGeH(nonConstSumBlockCluster, sumBlocks.get(), acaOp2->m_blocks.get(),
              eps, maximumRank);
    shared_ptr<const DiscreteBoundaryOperator<ValueType> > result(
                new DiscreteAcaBoundaryOperator<ValueType> (
                    acaOp1->rowCount(), acaOp1->columnCount(), eps,
                    maximumRank,
                    acaOp1->m_symmetry & acaOp2->m_symmetry,
                    sumBlockCluster, sumBlocks,
                    acaOp1->m_domainPermutation, acaOp2->m_rangePermutation,
                    acaOp1->m_parallelizationOptions));
    return result;
}

template <typename ValueType>
shared_ptr<const DiscreteBoundaryOperator<ValueType> > scaledAcaOperator(
        const ValueType& multiplier,
        const shared_ptr<const DiscreteBoundaryOperator<ValueType> >& op)
{
    if (!op)
        throw std::invalid_argument("scaledAcaOperator(): "
                                    "operand must not be null");

    typedef typename DiscreteAcaBoundaryOperator<ValueType>::AhmedBemBlcluster
            AhmedBemBlcluster;
    typedef typename DiscreteAcaBoundaryOperator<ValueType>::AhmedMblock
            AhmedMblock;

    typedef typename AhmedTypeTraits<ValueType>::Type AhmedValueType;
    const AhmedValueType ahmedMultiplier = ahmedCast(multiplier);

    shared_ptr<const DiscreteAcaBoundaryOperator<ValueType> > acaOp =
            DiscreteAcaBoundaryOperator<ValueType>::castToAca(op);
    const int symmetry = acaOp->symmetry();
    if (imagPart(multiplier) != 0. &&
            symmetry & HERMITIAN && !(symmetry & SYMMETRIC))
        throw std::runtime_error(
                "scaledAcaOperator(): multiplication of non-symmetric Hermitian"
                "matrices by scalars with non-zero imaginary part is not "
                "supported yet");
    shared_ptr<const AhmedBemBlcluster> scaledBlockCluster = acaOp->blockCluster();
    blcluster* nonConstScaledBlockCluster =
            const_cast<blcluster*>( // AHMED is not const-correct
                static_cast<const blcluster*>( // safe -- upcast
                                        scaledBlockCluster.get()));
    boost::shared_array<AhmedMblock*> scaledBlocks =
            allocateAhmedMblockArray<ValueType>(scaledBlockCluster.get());
    copyH(nonConstScaledBlockCluster, acaOp->blocks().get(), scaledBlocks.get());

    const size_t blockCount = scaledBlockCluster->nleaves();
    for (size_t b = 0; b < blockCount; ++b) {
        AhmedMblock* block = scaledBlocks[b];
        typename AhmedTypeTraits<ValueType>::Type* data = block->getdata();
        if (block->isLrM()) // low-rank
            for (size_t i = 0; i < block->getn1() * block->rank(); ++i)
                data[i] *= ahmedMultiplier;
        else {
             // we don't support complex Hermitian blocks yet
            assert(!(block->isHeM() && boost::is_complex<ValueType>()));
            if (!block->isLtM()) // dense, but not lower-triangular
                for (size_t i = 0; i < block->nvals(); ++i)
                    data[i] *= ahmedMultiplier;
            else { // lower-triangular
                // diagonal entries have special meaning and should not be rescaled
                size_t size = block->getn1();
                assert(block->getn2() == size);
                for (size_t c = 0; c < size; ++c)
                    for (size_t r = 1; r < size; ++r)
                        data[c * size + r] *= ahmedMultiplier;
            }
        }
    }

    int scaledSymmetry = symmetry;
    if (imagPart(multiplier) != 0.)
        scaledSymmetry &= ~HERMITIAN;
    shared_ptr<const DiscreteBoundaryOperator<ValueType> > result(
                new DiscreteAcaBoundaryOperator<ValueType> (
                    acaOp->rowCount(), acaOp->columnCount(),
                    acaOp->eps(), acaOp->maximumRank(),
                    scaledSymmetry,
                    scaledBlockCluster, scaledBlocks,
                    acaOp->domainPermutation(), acaOp->rangePermutation(),
                    acaOp->parallelizationOptions()));
    return result;
}

template <typename ValueType>
shared_ptr<const DiscreteBoundaryOperator<ValueType> > scaledAcaOperator(
        const shared_ptr<const DiscreteBoundaryOperator<ValueType> >& op,
        const ValueType& multiplier)
{
    return scaledAcaOperator(multiplier, op);
}

template <typename ValueType>
shared_ptr<const DiscreteBoundaryOperator<ValueType> > acaOperatorApproximateLuInverse(
        const shared_ptr<const DiscreteBoundaryOperator<ValueType> >& op,
        double delta)
{
    shared_ptr<const DiscreteAcaBoundaryOperator<ValueType> > acaOp =
            DiscreteAcaBoundaryOperator<ValueType>::castToAca(op);
    shared_ptr<const DiscreteBoundaryOperator<ValueType> > result(
                new AcaApproximateLuInverse<ValueType>(*acaOp, delta));
    return result;
}

FIBER_INSTANTIATE_CLASS_TEMPLATED_ON_RESULT(DiscreteAcaBoundaryOperator);

#define INSTANTIATE_FREE_FUNCTIONS(RESULT) \
    template shared_ptr<const DiscreteBoundaryOperator<RESULT> > \
        acaOperatorSum( \
            const shared_ptr<const DiscreteBoundaryOperator<RESULT> >& op1, \
            const shared_ptr<const DiscreteBoundaryOperator<RESULT> >& op2, \
            double eps, int maximumRank); \
    template shared_ptr<const DiscreteBoundaryOperator<RESULT> > \
        scaledAcaOperator( \
            const RESULT& multiplier, \
            const shared_ptr<const DiscreteBoundaryOperator<RESULT> >& op); \
    template shared_ptr<const DiscreteBoundaryOperator<RESULT> > \
        scaledAcaOperator( \
            const shared_ptr<const DiscreteBoundaryOperator<RESULT> >& op, \
            const RESULT& multiplier); \
    template shared_ptr<const DiscreteBoundaryOperator<RESULT> > \
        acaOperatorApproximateLuInverse( \
            const shared_ptr<const DiscreteBoundaryOperator<RESULT> >& op, \
            double delta)

#if defined(ENABLE_SINGLE_PRECISION)
INSTANTIATE_FREE_FUNCTIONS(float);
#endif

#if defined(ENABLE_SINGLE_PRECISION) && (defined(ENABLE_COMPLEX_BASIS_FUNCTIONS) || defined(ENABLE_COMPLEX_KERNELS))
INSTANTIATE_FREE_FUNCTIONS(std::complex<float>);
#endif

#if defined(ENABLE_DOUBLE_PRECISION)
INSTANTIATE_FREE_FUNCTIONS(double);
#endif

#if defined(ENABLE_DOUBLE_PRECISION) && (defined(ENABLE_COMPLEX_BASIS_FUNCTIONS) || defined(ENABLE_COMPLEX_KERNELS))
INSTANTIATE_FREE_FUNCTIONS(std::complex<double>);
#endif

} // namespace Bempp

#endif // WITH_AHMED