diff --git a/Cards/Config/ktProcess_cfi.py b/Cards/Config/ktProcess_cfi.py
index 4fb9d0e..fd40eb0 100644
--- a/Cards/Config/ktProcess_cfi.py
+++ b/Cards/Config/ktProcess_cfi.py
@@ -1,19 +1,22 @@
 import Config.Core as cepgen
 from math import pi
 
 class ProtonFlux:
     PhotonElastic         = 0
     PhotonInelastic       = 1
     PhotonInelasticBudnev = 11
+    GluonKMR              = 20
+class HeavyIonFlux:
+    PhotonElastic         = 100
 
 process = cepgen.Module('ktProcess',
     outKinematics = cepgen.Parameters(
         qt = (0., 50.),
         phiqt = (0., 2.*pi),
         #--- cuts on individual particles defining the central system
         rapidity = (-6., 6.),
         #--- cuts on the pt(outgoing system) (hyper-)plane
         ptdiff = (0., 500.),
         phiptdiff = (0., 2.*pi),
     ),
 )
diff --git a/Cards/patoccbar_cfg.py b/Cards/patoccbar_cfg.py
new file mode 100644
index 0000000..c3d6834
--- /dev/null
+++ b/Cards/patoccbar_cfg.py
@@ -0,0 +1,40 @@
+import Config.Core as cepgen
+import Config.ktProcess_cfi as ktfactor
+from Config.integrators_cff import vegas as integrator
+from Config.pdg_cff import PDG
+
+from Config.logger_cfi import logger
+logger.enabledModules += ('GenericKTProcess.registerVariable',)
+
+process = ktfactor.process.clone('patoll',
+    processParameters = cepgen.Parameters(
+        pair = PDG.charm,
+    ),
+    inKinematics = cepgen.Parameters(
+        pz = (6500., 2562.2),
+        structureFunctions = cepgen.StructureFunctions.SuriYennie,
+        #structureFunctions = cepgen.StructureFunctions.FioreBrasse,
+        ktFluxes = (ktfactor.ProtonFlux.GluonKMR, ktfactor.HeavyIonFlux.PhotonElastic),
+        #ktFluxes = (ktfactor.ProtonFlux.PhotonElastic, ktfactor.HeavyIonFlux.PhotonElastic),
+        heavyIonB = (208, 82),
+        kmrGridPath = 'gluon_mmht2014nlo_Watt.dat',
+    ),
+    outKinematics = ktfactor.process.outKinematics.clone(
+        pt = (0.,),
+        energy = (0.,),
+        rapidity = (-7., 9.),
+        #qt = (0.,1000.),
+        #eta = (-2.5, 2.5),
+        mx = (1.07, 1000.),
+        #--- extra cuts on the p1t(l) and p2t(l) plane
+        #ptdiff = (0., 2.5),
+        #--- distance in rapidity between l^+ and l^-
+        #dely = (4., 5.),
+    ),
+)
+
+#--- events generation
+from Config.generator_cff import generator
+generator.numEvents = 10000
+generator.numThreads = 1
+
diff --git a/Cards/patoll_cfg.py b/Cards/patoll_cfg.py
new file mode 100644
index 0000000..25b9830
--- /dev/null
+++ b/Cards/patoll_cfg.py
@@ -0,0 +1,35 @@
+import Config.Core as cepgen
+import Config.ktProcess_cfi as ktfactor
+from Config.integrators_cff import vegas as integrator
+from Config.pdg_cff import PDG
+
+process = ktfactor.process.clone('patoll',
+    processParameters = cepgen.Parameters(
+        pair = PDG.muon,
+    ),
+    inKinematics = cepgen.Parameters(
+        pz = (6500., 2562.2),
+        #structureFunctions = cepgen.StructureFunctions.SuriYennie,
+        #structureFunctions = cepgen.StructureFunctions.FioreBrasse,
+        #structureFunctions = cepgen.StructureFunctions.ALLM91,
+        structureFunctions = cepgen.StructureFunctions.LUXlike,
+        ktFluxes = (ktfactor.ProtonFlux.PhotonInelasticBudnev, ktfactor.HeavyIonFlux.PhotonElastic),
+        heavyIonB = (208, 82),
+    ),
+    outKinematics = ktfactor.process.outKinematics.clone(
+        pt = (4.,),
+        energy = (0.,),
+        rapidity = (-6., 7.),
+        #eta = (-2.5, 2.5),
+        mx = (1.07, 1000.),
+        #--- extra cuts on the p1t(l) and p2t(l) plane
+        #ptdiff = (0., 2.5),
+        #--- distance in rapidity between l^+ and l^-
+        #dely = (4., 5.),
+    ),
+)
+
+#--- events generation
+from Config.generator_cff import generator
+generator.numEvents = 100000
+
diff --git a/CepGen/CMakeLists.txt b/CepGen/CMakeLists.txt
index 55b517c..42483b1 100644
--- a/CepGen/CMakeLists.txt
+++ b/CepGen/CMakeLists.txt
@@ -1,28 +1,34 @@
 file(GLOB core_sources Core/*.cpp *.cpp)
 file(GLOB phys_sources Physics/*.cpp)
 file(GLOB sf_sources StructureFunctions/*.cpp)
 
 include_directories(${PROJECT_SOURCE_DIR})
 
 #----- check the external dependencies for SFs
 
 set(GRV_PATH ${PROJECT_SOURCE_DIR}/External)
 file(GLOB grv_sources ${GRV_PATH}/grv_*.f)
 if(grv_sources)
   message(STATUS "GRV PDFset found in ${grv_sources}!")
   add_definitions(-DGRVPDF)
   list(APPEND sf_sources ${grv_sources})
 else()
   message(STATUS "GRV PDFset not found. Will proceed without it")
 endif()
 
+#----- check the external dependencies for physics utilities
+
+if(alphas_sources)
+  list(APPEND phys_sources ${alphas_sources})
+endif()
+
 #----- build the object
 
 add_library(CepGenCore SHARED ${core_sources} ${phys_sources} ${sf_sources})
 target_link_libraries(CepGenCore ${CEPGEN_EXTERNAL_CORE_REQS})
 target_link_libraries(CepGenCore ${CEPGEN_EXTERNAL_STRF_REQS})
 
 #----- installation rules
 
 install(TARGETS CepGenCore DESTINATION lib)
 
diff --git a/CepGen/Cards/LpairHandler.cpp b/CepGen/Cards/LpairHandler.cpp
index 5c11a03..cfb0b7c 100644
--- a/CepGen/Cards/LpairHandler.cpp
+++ b/CepGen/Cards/LpairHandler.cpp
@@ -1,192 +1,205 @@
 #include "CepGen/Cards/LpairHandler.h"
 #include "CepGen/Core/ParametersList.h"
 #include "CepGen/Core/Exception.h"
 
 #include "CepGen/Physics/PDG.h"
 #include "CepGen/Processes/GamGamLL.h"
 #include "CepGen/Processes/PPtoFF.h"
 #include "CepGen/Processes/PPtoWW.h"
 
 #include "CepGen/Hadronisers/Pythia8Hadroniser.h"
 
 #include <fstream>
 
 namespace CepGen
 {
   namespace Cards
   {
     const int LpairHandler::kInvalid = 99999;
 
     //----- specialization for LPAIR input cards
 
     LpairHandler::LpairHandler( const char* file ) :
-      proc_params_( new ParametersList )
+      proc_params_( new ParametersList ),
+      hi_1_( { 0, 0 } ), hi_2_( { 0, 0 } )
     {
       std::ifstream f( file, std::fstream::in );
       if ( !f.is_open() )
         throw CG_FATAL( "LpairHandler" ) << "Failed to parse file \"" << file << "%s\".";
 
       init( &params_ );
 
       std::unordered_map<std::string, std::string> m_params;
       std::string key, value;
       std::ostringstream os;
       while ( f >> key >> value ) {
         if ( key[0] == '#' ) continue; // FIXME need to ensure there is no extra space before!
         setParameter( key, value );
         m_params.insert( { key, value } );
         if ( getDescription( key ) != "null" )
           os << "\n>> " << key << " = " << std::setw( 15 ) << getParameter( key )
              << " (" << getDescription( key ) << ")";
       }
       f.close();
 
       if ( proc_name_ == "lpair" )
         params_.setProcess( new Process::GamGamLL( *proc_params_ ) );
       else if ( proc_name_ == "pptoll" || proc_name_ == "pptoff" )
         params_.setProcess( new Process::PPtoFF( *proc_params_ ) );
       else if ( proc_name_ == "pptoww" )
         params_.setProcess( new Process::PPtoWW( *proc_params_ ) );
       else
         throw CG_FATAL( "LpairHandler" ) << "Unrecognised process name: " << proc_name_ << "!";
 
       if ( integr_type_ == "plain" )
         params_.integrator.type = Integrator::Type::plain;
       else if ( integr_type_ == "Vegas" )
         params_.integrator.type = Integrator::Type::Vegas;
       else if ( integr_type_ == "MISER" )
         params_.integrator.type = Integrator::Type::MISER;
       else if ( integr_type_ != "" )
         throw CG_FATAL( "LpairHandler" ) << "Unrecognized integrator type: " << integr_type_ << "!";
 
       if ( hadr_name_ == "pythia8" )
         params_.setHadroniser( new Hadroniser::Pythia8Hadroniser( params_ ) );
 
       if ( m_params.count( "IEND" ) )
         setValue<bool>( "IEND", ( std::stoi( m_params["IEND"] ) > 1 ) );
 
+      HeavyIon hi1{ hi_1_.first, (Element)hi_1_.second }, hi2{ hi_2_.first, (Element)hi_2_.second };
+      if ( hi1 )
+        params_.kinematics.incoming_beams.first.pdg = hi1;
+      if ( hi2 )
+        params_.kinematics.incoming_beams.second.pdg = hi2;
+
       CG_INFO( "LpairHandler" ) << "File '" << file << "' succesfully opened!\n\t"
         << "The following parameters are set:" << os.str();
     }
 
     void
     LpairHandler::init( Parameters* params )
     {
       //-------------------------------------------------------------------------------------------
       // Process/integration/hadronisation parameters
       //-------------------------------------------------------------------------------------------
 
       registerParameter<std::string>( "PROC", "Process name to simulate", &proc_name_ );
       registerParameter<std::string>( "ITYP", "Integration algorithm", &integr_type_ );
       registerParameter<std::string>( "HADR", "Hadronisation algorithm", &hadr_name_ );
+      registerParameter<std::string>( "KMRG", "KMR grid interpolation path", &params_.kinematics.kmr_grid_path );
 
       //-------------------------------------------------------------------------------------------
       // General parameters
       //-------------------------------------------------------------------------------------------
 
       registerParameter<bool>( "IEND", "Generation type", &params->generation.enabled );
       registerParameter<bool>( "NTRT", "Smoothen the integrand", &params->generation.treat );
       registerParameter<int>( "DEBG", "Debugging verbosity", (int*)&Logger::get().level );
       registerParameter<int>( "NCVG", "Number of function calls", (int*)&params->integrator.ncvg );
       registerParameter<int>( "ITVG", "Number of integration iterations", (int*)&params->integrator.vegas.iterations );
       registerParameter<int>( "SEED", "Random generator seed", (int*)&params->integrator.rng_seed );
       registerParameter<int>( "NTHR", "Number of threads to use for events generation", (int*)&params->generation.num_threads );
       registerParameter<int>( "MODE", "Subprocess' mode", (int*)&params->kinematics.mode );
       registerParameter<int>( "NCSG", "Number of points to probe", (int*)&params->generation.num_points );
       registerParameter<int>( "NGEN", "Number of events to generate", (int*)&params->generation.maxgen );
       registerParameter<int>( "NPRN", "Number of events before printout", (int*)&params->generation.gen_print_every );
 
       //-------------------------------------------------------------------------------------------
       // Process-specific parameters
       //-------------------------------------------------------------------------------------------
 
       registerParameter<int>( "METH", "Computation method (kT-factorisation)", &proc_params_->operator[]<int>( "method" ) );
       registerParameter<int>( "IPOL", "Polarisation states to consider", &proc_params_->operator[]<int>( "polarisationStates" ) );
 
       //-------------------------------------------------------------------------------------------
       // Process kinematics parameters
       //-------------------------------------------------------------------------------------------
 
       registerParameter<int>( "PMOD", "Outgoing primary particles' mode", (int*)&params->kinematics.structure_functions );
       registerParameter<int>( "EMOD", "Outgoing primary particles' mode", (int*)&params->kinematics.structure_functions );
       registerParameter<int>( "PAIR", "Outgoing particles' PDG id", (int*)&proc_params_->operator[]<int>( "pair" ) );
+
+      registerParameter<int>( "INA1", "Heavy ion atomic weight (1st incoming beam)", (int*)&hi_1_.first );
+      registerParameter<int>( "INZ1", "Heavy ion atomic number (1st incoming beam)", (int*)&hi_1_.second );
+      registerParameter<int>( "INA2", "Heavy ion atomic weight (1st incoming beam)", (int*)&hi_2_.first );
+      registerParameter<int>( "INZ2", "Heavy ion atomic number (1st incoming beam)", (int*)&hi_2_.second );
       registerParameter<double>( "INP1", "Momentum (1st primary particle)", &params->kinematics.incoming_beams.first.pz );
       registerParameter<double>( "INP2", "Momentum (2nd primary particle)", &params->kinematics.incoming_beams.second.pz );
       registerParameter<double>( "INPP", "Momentum (1st primary particle)", &params->kinematics.incoming_beams.first.pz );
       registerParameter<double>( "INPE", "Momentum (2nd primary particle)", &params->kinematics.incoming_beams.second.pz );
       registerParameter<double>( "PTCT", "Minimal transverse momentum (single central outgoing particle)", &params->kinematics.cuts.central.pt_single.min() );
       registerParameter<double>( "MSCT", "Minimal central system mass", &params->kinematics.cuts.central.mass_sum.min() );
       registerParameter<double>( "ECUT", "Minimal energy (single central outgoing particle)", &params->kinematics.cuts.central.energy_single.min() );
       registerParameter<double>( "ETMN", "Minimal pseudo-rapidity (central outgoing particles)", &params->kinematics.cuts.central.eta_single.min() );
       registerParameter<double>( "ETMX", "Maximal pseudo-rapidity (central outgoing particles)", &params->kinematics.cuts.central.eta_single.max() );
       registerParameter<double>( "YMIN", "Minimal rapidity (central outgoing particles)", &params->kinematics.cuts.central.rapidity_single.min() );
       registerParameter<double>( "YMAX", "Maximal rapidity (central outgoing particles)", &params->kinematics.cuts.central.rapidity_single.max() );
       registerParameter<double>( "Q2MN", "Minimal Q² = -q² (exchanged parton)", &params->kinematics.cuts.initial.q2.min() );
       registerParameter<double>( "Q2MX", "Maximal Q² = -q² (exchanged parton)", &params->kinematics.cuts.initial.q2.max() );
       registerParameter<double>( "MXMN", "Minimal invariant mass of proton remnants", &params->kinematics.cuts.remnants.mass_single.min() );
       registerParameter<double>( "MXMX", "Maximal invariant mass of proton remnants", &params->kinematics.cuts.remnants.mass_single.max() );
     }
 
     void
     LpairHandler::store( const char* file )
     {
       std::ofstream f( file, std::fstream::out | std::fstream::trunc );
       if ( !f.is_open() ) {
         CG_ERROR( "LpairHandler" ) << "Failed to open file \"" << file << "%s\" for writing.";
       }
       for ( const auto& it : p_strings_ )
         if ( it.second.value )
           f << it.first << " = " << *it.second.value << "\n";
       for ( const auto& it : p_ints_ )
         if ( it.second.value )
           f << it.first << " = " << *it.second.value << "\n";
       for ( const auto& it : p_doubles_ )
         if ( it.second.value )
           f << it.first << " = " << *it.second.value << "\n";
       for ( const auto& it : p_bools_ )
         if ( it.second.value )
           f << it.first << " = " << *it.second.value << "\n";
       f.close();
     }
 
     void
     LpairHandler::setParameter( const std::string& key, const std::string& value )
     {
       try { setValue<double>( key.c_str(), std::stod( value ) ); } catch ( std::invalid_argument& ) {}
       try { setValue<int>( key.c_str(), std::stoi( value ) ); } catch ( std::invalid_argument& ) {}
       //setValue<bool>( key.c_str(), std::stoi( value ) );
       setValue<std::string>( key.c_str(), value );
     }
 
     std::string
     LpairHandler::getParameter( std::string key ) const
     {
       double dd = getValue<double>( key.c_str() );
       if ( dd != -999. )
         return std::to_string( dd );
 
       int ui = getValue<int>( key.c_str() );
       if ( ui != 999 )
         return std::to_string( ui );
 
       //if ( out = getValue<bool>( key.c_str() )  );
 
       return getValue<std::string>( key.c_str() );
     }
 
     std::string
     LpairHandler::getDescription( std::string key ) const
     {
       if ( p_strings_.count( key ) )
         return p_strings_.find( key )->second.description;
       if ( p_ints_.count( key ) )
         return p_ints_.find( key )->second.description;
       if ( p_doubles_.count( key ) )
         return p_doubles_.find( key )->second.description;
       if ( p_bools_.count( key ) )
         return p_bools_.find( key )->second.description;
       return "null";
     }
   }
 }
 
diff --git a/CepGen/Cards/LpairHandler.h b/CepGen/Cards/LpairHandler.h
index 79ba803..78cf708 100644
--- a/CepGen/Cards/LpairHandler.h
+++ b/CepGen/Cards/LpairHandler.h
@@ -1,109 +1,110 @@
 #ifndef CepGen_Cards_LpairReader_h
 #define CepGen_Cards_LpairReader_h
 
 #include "CepGen/Cards/Handler.h"
 #include <unordered_map>
 
 using std::string;
 
 namespace CepGen
 {
   class ParametersList;
   namespace Cards
   {
     /// LPAIR-like steering cards parser and writer
     class LpairHandler : public Handler
     {
       public:
         /// Read a LPAIR steering card
         explicit LpairHandler( const char* file );
 
         /// Store a configuration into a LPAIR steering card
         void store( const char* file );
 
       private:
         template<class T> struct Parameter {
           Parameter( const char* key, const char* descr, T* value ) : key( key ), description( descr ), value( value ) {}
           std::string key, description;
           T* value;
         };
         /// Register a parameter to be steered to a configuration variable
         template<class T> void registerParameter( const char* key, const char* description, T* def ) {}
         /// Set a parameter value
         template<class T> void setValue( const char* key, const T& value ) {}
         /// Retrieve a parameter value
         template<class T> T getValue( const char* key ) const {}
 
         void setParameter( const std::string& key, const std::string& value );
         std::string getParameter( std::string key ) const;
         std::string getDescription( std::string key ) const;
 
         static const int kInvalid;
 
         std::unordered_map<std::string, Parameter<std::string> > p_strings_;
         std::unordered_map<std::string, Parameter<double> > p_doubles_;
         std::unordered_map<std::string, Parameter<int> > p_ints_;
         std::unordered_map<std::string, Parameter<bool> > p_bools_;
 
         void init( Parameters* );
         std::shared_ptr<ParametersList> proc_params_;
         std::string proc_name_, hadr_name_, integr_type_;
+        std::pair<unsigned short,unsigned short> hi_1_, hi_2_;
     };
 
     //----- specialised registerers
 
     template<> inline void LpairHandler::registerParameter<std::string>( const char* key, const char* description, std::string* def ) { p_strings_.insert( std::make_pair( key, Parameter<std::string>( key, description, def ) ) ); }
     template<> inline void LpairHandler::registerParameter<double>( const char* key, const char* description, double* def ) { p_doubles_.insert( std::make_pair( key, Parameter<double>( key, description, def ) ) ); }
     template<> inline void LpairHandler::registerParameter<int>( const char* key, const char* description, int* def ) { p_ints_.insert( std::make_pair( key, Parameter<int>( key, description, def ) ) ); }
     template<> inline void LpairHandler::registerParameter<bool>( const char* key, const char* description, bool* def ) { p_bools_.insert( std::make_pair( key, Parameter<bool>( key, description, def ) ) ); }
 
     //----- specialised setters
 
     template<> inline void LpairHandler::setValue<std::string>( const char* key, const std::string& value ) {
       auto it = p_strings_.find( key );
       if ( it != p_strings_.end() ) *it->second.value = value;
     }
     template<> inline void LpairHandler::setValue<double>( const char* key, const double& value ) {
       auto it = p_doubles_.find( key );
       if ( it != p_doubles_.end() ) *it->second.value = value;
     }
     template<> inline void LpairHandler::setValue<int>( const char* key, const int& value ) {
       auto it = p_ints_.find( key );
       if ( it != p_ints_.end() ) *it->second.value = value;
     }
     template<> inline void LpairHandler::setValue<bool>( const char* key, const bool& value ) {
       auto it = p_bools_.find( key );
       if ( it != p_bools_.end() ) *it->second.value = value;
     }
 
     //----- specialised getters
 
     /// Retrieve a string parameter value
     template<> inline std::string LpairHandler::getValue( const char* key ) const {
       const auto& it = p_strings_.find( key );
       if ( it != p_strings_.end() ) return *it->second.value;
       return "null";
     }
     /// Retrieve a floating point parameter value
     template<> inline double LpairHandler::getValue( const char* key ) const {
       const auto& it = p_doubles_.find( key );
       if ( it != p_doubles_.end() ) return *it->second.value;
       return -999.;
     }
     /// Retrieve an integer parameter value
     template<> inline int LpairHandler::getValue( const char* key ) const {
       const auto& it = p_ints_.find( key );
       if ( it != p_ints_.end() ) return *it->second.value;
       return 999;
     }
     /// Retrieve a boolean parameter value
     template<> inline bool LpairHandler::getValue( const char* key ) const {
       const auto& it = p_bools_.find( key );
       if ( it != p_bools_.end() ) return *it->second.value;
       return true;
     }
   }
 }
 
 #endif
 
diff --git a/CepGen/Cards/PythonHandler.cpp b/CepGen/Cards/PythonHandler.cpp
index 9f64ef7..f6949c2 100644
--- a/CepGen/Cards/PythonHandler.cpp
+++ b/CepGen/Cards/PythonHandler.cpp
@@ -1,399 +1,415 @@
 #include "CepGen/Cards/PythonHandler.h"
 #include "CepGen/Core/Exception.h"
 
 #ifdef PYTHON
 
 #include "CepGen/Core/TamingFunction.h"
 #include "CepGen/Core/Exception.h"
 #include "CepGen/Core/ParametersList.h"
 
 #include "CepGen/Processes/GamGamLL.h"
 #include "CepGen/Processes/PPtoFF.h"
 #include "CepGen/Processes/PPtoWW.h"
+#include "CepGen/Processes/FortranKTProcess.h"
 
 #include "CepGen/StructureFunctions/StructureFunctionsBuilder.h"
 #include "CepGen/StructureFunctions/LHAPDF.h"
 #include "CepGen/StructureFunctions/MSTWGrid.h"
 #include "CepGen/StructureFunctions/Schaefer.h"
 
 #include "CepGen/Hadronisers/Pythia8Hadroniser.h"
 
 #include <algorithm>
 
+extern "C"
+{
+  extern void nucl_to_ff_( double& );
+}
+
 #if PY_MAJOR_VERSION < 3
 #  define PYTHON2
 #endif
 
 namespace CepGen
 {
   namespace Cards
   {
     //----- specialization for CepGen input cards
     PythonHandler::PythonHandler( const char* file )
     {
       setenv( "PYTHONPATH", ".:..:Cards", 1 );
       std::string filename = getPythonPath( file );
       const size_t fn_len = filename.length()+1;
 
       //Py_DebugFlag = 1;
       //Py_VerboseFlag = 1;
 
 #ifdef PYTHON2
       char* sfilename = new char[fn_len];
       snprintf( sfilename, fn_len, "%s", filename.c_str() );
 #else
       wchar_t* sfilename = new wchar_t[fn_len];
       swprintf( sfilename, fn_len, L"%s", filename.c_str() );
 #endif
       if ( sfilename )
         Py_SetProgramName( sfilename );
 
       Py_InitializeEx( 1 );
 
       if ( sfilename )
         delete [] sfilename;
       if ( !Py_IsInitialized() )
         throw CG_FATAL( "PythonHandler" ) << "Failed to initialise the Python cards parser!";
 
       CG_INFO( "PythonHandler" )
         << "Initialised the Python cards parser\n\t"
         << "Python version: " << Py_GetVersion() << "\n\t"
         << "Platform: " << Py_GetPlatform() << ".";
 
       PyObject* cfg = PyImport_ImportModule( filename.c_str() ); // new
       if ( !cfg )
         throwPythonError( Form( "Failed to parse the configuration card %s", file ) );
 
       PyObject* process = PyObject_GetAttrString( cfg, PROCESS_NAME ); // new
       if ( !process )
         throwPythonError( Form( "Failed to extract a \"%s\" keyword from the configuration card %s", PROCESS_NAME, file ) );
 
       //--- list of process-specific parameters
       ParametersList proc_params;
       fillParameter( process, "processParameters", proc_params );
 
       //--- type of process to consider
       PyObject* pproc_name = getElement( process, MODULE_NAME ); // borrowed
       if ( !pproc_name )
         throwPythonError( Form( "Failed to extract the process name from the configuration card %s", file ) );
       const std::string proc_name = get<std::string>( pproc_name );
 
       //--- process mode
       params_.kinematics.mode = (KinematicsMode)proc_params.get<int>( "mode", (int)KinematicsMode::invalid );
 
       if ( proc_name == "lpair" )
         params_.setProcess( new Process::GamGamLL( proc_params ) );
       else if ( proc_name == "pptoll" || proc_name == "pptoff" )
         params_.setProcess( new Process::PPtoFF( proc_params ) );
       else if ( proc_name == "pptoww" )
         params_.setProcess( new Process::PPtoWW( proc_params ) );
+      else if ( proc_name == "patoll" )
+        params_.setProcess( new Process::FortranKTProcess( proc_params, "nucltoff", "(p/A)(p/A) ↝ (g/ɣ)ɣ → f⁺f¯", nucl_to_ff_ ) );
       else throw CG_FATAL( "PythonHandler" ) << "Unrecognised process: " << proc_name << ".";
 
       //--- process kinematics
       PyObject* pin_kinematics = getElement( process, "inKinematics" ); // borrowed
       if ( pin_kinematics )
         parseIncomingKinematics( pin_kinematics );
 
       PyObject* pout_kinematics = getElement( process, "outKinematics" ); // borrowed
       if ( pout_kinematics )
         parseOutgoingKinematics( pout_kinematics );
 
       //--- taming functions
       PyObject* ptam = getElement( process, "tamingFunctions" ); // borrowed
       if ( ptam )
         parseTamingFunctions( ptam );
 
       Py_CLEAR( process );
 
       PyObject* plog = PyObject_GetAttrString( cfg, "logger" ); // new
       if ( plog ) {
         parseLogging( plog );
         Py_CLEAR( plog );
       }
 
       //--- hadroniser parameters
       PyObject* phad = PyObject_GetAttrString( cfg, "hadroniser" ); // new
       if ( phad ) {
         parseHadroniser( phad );
         Py_CLEAR( phad );
       }
 
       //--- generation parameters
       PyObject* pint = PyObject_GetAttrString( cfg, "integrator" ); // new
       if ( pint ) {
         parseIntegrator( pint );
         Py_CLEAR( pint );
       }
 
       PyObject* pgen = PyObject_GetAttrString( cfg, "generator" ); // new
       if ( pgen ) {
         parseGenerator( pgen );
         Py_CLEAR( pgen );
       }
 
       //--- finalisation
       Py_CLEAR( cfg );
     }
 
     PythonHandler::~PythonHandler()
     {
       if ( Py_IsInitialized() )
         Py_Finalize();
     }
 
     void
     PythonHandler::parseIncomingKinematics( PyObject* kin )
     {
       //--- retrieve the beams PDG ids
       std::vector<double> beams_pz;
       fillParameter( kin, "pz", beams_pz );
       if ( beams_pz.size() == 2 ) {
         params_.kinematics.incoming_beams.first.pz = beams_pz.at( 0 );
         params_.kinematics.incoming_beams.second.pz = beams_pz.at( 1 );
       }
       //--- retrieve the beams longitudinal momentum
       std::vector<int> beams_pdg;
       fillParameter( kin, "pdgIds", beams_pdg );
       if ( beams_pdg.size() == 2 ) {
         params_.kinematics.incoming_beams.first.pdg = (PDG)beams_pdg.at( 0 );
         params_.kinematics.incoming_beams.second.pdg = (PDG)beams_pdg.at( 1 );
       }
       double sqrt_s = -1.;
       fillParameter( kin, "cmEnergy", sqrt_s );
+      fillParameter( kin, "kmrGridPath", params_.kinematics.kmr_grid_path );
       if ( sqrt_s != -1. )
         params_.kinematics.setSqrtS( sqrt_s );
       PyObject* psf = getElement( kin, "structureFunctions" ); // borrowed
       if ( psf )
         parseStructureFunctions( psf, params_.kinematics.structure_functions );
       std::vector<int> kt_fluxes;
       fillParameter( kin, "ktFluxes", kt_fluxes );
       if ( kt_fluxes.size() > 0 )
         params_.kinematics.incoming_beams.first.kt_flux = (KTFlux)kt_fluxes.at( 0 );
       if ( kt_fluxes.size() > 1 )
         params_.kinematics.incoming_beams.second.kt_flux = (KTFlux)kt_fluxes.at( 1 );
+      std::vector<int> hi_beam1, hi_beam2;
+      fillParameter( kin, "heavyIonA", hi_beam1 );
+      if ( hi_beam1.size() == 2 )
+        params_.kinematics.incoming_beams.first.pdg = HeavyIon{ (unsigned short)hi_beam1[0], (Element)hi_beam1[1] };
+      fillParameter( kin, "heavyIonB", hi_beam2 );
+      if ( hi_beam2.size() == 2 )
+        params_.kinematics.incoming_beams.second.pdg = HeavyIon{ (unsigned short)hi_beam2[0], (Element)hi_beam2[1] };
     }
 
     void
     PythonHandler::parseStructureFunctions( PyObject* psf, std::shared_ptr<StructureFunctions>& sf_handler )
     {
       int str_fun = 0;
       fillParameter( psf, "id", str_fun );
       sf_handler = StructureFunctionsBuilder::get( (SF::Type)str_fun );
       switch( (SF::Type)str_fun ) {
         case SF::Type::LHAPDF: {
           auto sf = std::dynamic_pointer_cast<SF::LHAPDF>( params_.kinematics.structure_functions );
           fillParameter( psf, "pdfSet", sf->params.pdf_set );
           fillParameter( psf, "numFlavours", (unsigned int&)sf->params.num_flavours );
           fillParameter( psf, "pdfMember", (unsigned int&)sf->params.pdf_member );
           fillParameter( psf, "mode", (unsigned int&)sf->params.mode );
         } break;
         case SF::Type::MSTWgrid: {
           auto sf = std::dynamic_pointer_cast<mstw::Grid>( params_.kinematics.structure_functions );
           fillParameter( psf, "gridPath", sf->params.grid_path );
         } break;
         case SF::Type::Schaefer: {
           auto sf = std::dynamic_pointer_cast<SF::Schaefer>( params_.kinematics.structure_functions );
           fillParameter( psf, "Q2cut", sf->params.q2_cut );
           std::vector<double> w2_lims;
           fillParameter( psf, "W2limits", w2_lims );
           if ( w2_lims.size() != 0 ) {
             if ( w2_lims.size() != 2 )
               throwPythonError( Form( "Invalid size for W2limits attribute: %d != 2!", w2_lims.size() ) );
             else {
               sf->params.w2_lo = *std::min_element( w2_lims.begin(), w2_lims.end() );
               sf->params.w2_hi = *std::max_element( w2_lims.begin(), w2_lims.end() );
             }
           }
           PyObject* pcsf = getElement( psf, "continuumSF" ); // borrowed
           if ( pcsf )
             parseStructureFunctions( pcsf, sf->params.continuum_model );
           PyObject* ppsf = getElement( psf, "perturbativeSF" ); // borrowed
           if ( ppsf )
             parseStructureFunctions( ppsf, sf->params.perturbative_model );
           PyObject* prsf = getElement( psf, "resonancesSF" ); // borrowed
           if ( prsf )
             parseStructureFunctions( prsf, sf->params.resonances_model );
           fillParameter( psf, "higherTwist", (bool&)sf->params.higher_twist );
         } break;
         default: break;
       }
     }
 
     void
     PythonHandler::parseOutgoingKinematics( PyObject* kin )
     {
       PyObject* pparts = getElement( kin, "minFinalState" ); // borrowed
       if ( pparts && PyTuple_Check( pparts ) )
         for ( unsigned short i = 0; i < PyTuple_Size( pparts ); ++i )
           params_.kinematics.minimum_final_state.emplace_back( (PDG)get<int>( PyTuple_GetItem( pparts, i ) ) );
 
       PyObject* pcuts = getElement( kin, "cuts" ); // borrowed
       if ( pcuts )
         parseParticlesCuts( pcuts );
 
       // for LPAIR/collinear matrix elements
       fillLimits( kin, "q2", params_.kinematics.cuts.initial.q2 );
 
       // for the kT factorised matrix elements
       fillLimits( kin, "qt", params_.kinematics.cuts.initial.qt );
       fillLimits( kin, "phiqt", params_.kinematics.cuts.initial.phi_qt );
       fillLimits( kin, "ptdiff", params_.kinematics.cuts.central.pt_diff );
       fillLimits( kin, "phiptdiff", params_.kinematics.cuts.central.phi_pt_diff );
       fillLimits( kin, "rapiditydiff", params_.kinematics.cuts.central.rapidity_diff );
 
       // generic phase space limits
       fillLimits( kin, "rapidity", params_.kinematics.cuts.central.rapidity_single );
       fillLimits( kin, "eta", params_.kinematics.cuts.central.eta_single );
       fillLimits( kin, "pt", params_.kinematics.cuts.central.pt_single );
 
       fillLimits( kin, "ptsum", params_.kinematics.cuts.central.pt_sum );
       fillLimits( kin, "invmass", params_.kinematics.cuts.central.mass_sum );
 
       fillLimits( kin, "mx", params_.kinematics.cuts.remnants.mass_single );
     }
 
     void
     PythonHandler::parseParticlesCuts( PyObject* cuts )
     {
       if ( !PyDict_Check( cuts ) )
         throwPythonError( "Particle cuts object should be a dictionary!" );
       PyObject* pkey = nullptr, *pvalue = nullptr;
       Py_ssize_t pos = 0;
       while ( PyDict_Next( cuts, &pos, &pkey, &pvalue ) ) {
         const PDG pdg = (PDG)get<int>( pkey );
         fillLimits( pvalue, "pt", params_.kinematics.cuts.central_particles[pdg].pt_single );
         fillLimits( pvalue, "energy", params_.kinematics.cuts.central_particles[pdg].energy_single );
         fillLimits( pvalue, "eta", params_.kinematics.cuts.central_particles[pdg].eta_single );
         fillLimits( pvalue, "rapidity", params_.kinematics.cuts.central_particles[pdg].rapidity_single );
       }
     }
 
     void
     PythonHandler::parseLogging( PyObject* log )
     {
       fillParameter( log, "level", (int&)Logger::get().level );
       std::vector<std::string> enabled_modules;
       fillParameter( log, "enabledModules", enabled_modules );
       for ( const auto& mod : enabled_modules )
         Logger::get().addExceptionRule( mod );
     }
 
     void
     PythonHandler::parseIntegrator( PyObject* integr )
     {
       if ( !PyDict_Check( integr ) )
         throwPythonError( "Integrator object should be a dictionary!" );
       PyObject* palgo = getElement( integr, MODULE_NAME ); // borrowed
       if ( !palgo )
         throwPythonError( "Failed to retrieve the integration algorithm name!" );
       std::string algo = get<std::string>( palgo );
       if ( algo == "plain" )
         params_.integrator.type = Integrator::Type::plain;
       else if ( algo == "Vegas" ) {
         params_.integrator.type = Integrator::Type::Vegas;
         fillParameter( integr, "alpha", (double&)params_.integrator.vegas.alpha );
         fillParameter( integr, "iterations", params_.integrator.vegas.iterations );
         fillParameter( integr, "mode", (int&)params_.integrator.vegas.mode );
         fillParameter( integr, "verbosity", (int&)params_.integrator.vegas.verbose );
         std::string vegas_logging_output = "cerr";
         fillParameter( integr, "loggingOutput", vegas_logging_output );
         if ( vegas_logging_output == "cerr" )
           // redirect all debugging information to the error stream
           params_.integrator.vegas.ostream = stderr;
         else if ( vegas_logging_output == "cout" )
           // redirect all debugging information to the standard stream
           params_.integrator.vegas.ostream = stdout;
         else
           params_.integrator.vegas.ostream = fopen( vegas_logging_output.c_str(), "w" );
       }
       else if ( algo == "MISER" ) {
         params_.integrator.type = Integrator::Type::MISER;
         fillParameter( integr, "estimateFraction", (double&)params_.integrator.miser.estimate_frac );
         fillParameter( integr, "minCalls", params_.integrator.miser.min_calls );
         fillParameter( integr, "minCallsPerBisection", params_.integrator.miser.min_calls_per_bisection );
         fillParameter( integr, "alpha", (double&)params_.integrator.miser.alpha );
         fillParameter( integr, "dither", (double&)params_.integrator.miser.dither );
       }
       else
         throwPythonError( Form( "Invalid integration algorithm: %s", algo.c_str() ) );
 
       fillParameter( integr, "numFunctionCalls", params_.integrator.ncvg );
       fillParameter( integr, "seed", (unsigned long&)params_.integrator.rng_seed );
       unsigned int rng_engine;
       fillParameter( integr, "rngEngine", rng_engine );
       switch ( rng_engine ) {
         case 0: default: params_.integrator.rng_engine = (gsl_rng_type*)gsl_rng_mt19937; break;
         case 1: params_.integrator.rng_engine = (gsl_rng_type*)gsl_rng_taus2; break;
         case 2: params_.integrator.rng_engine = (gsl_rng_type*)gsl_rng_gfsr4; break;
         case 3: params_.integrator.rng_engine = (gsl_rng_type*)gsl_rng_ranlxs0; break;
       }
       fillParameter( integr, "chiSqCut", params_.integrator.vegas_chisq_cut );
     }
 
     void
     PythonHandler::parseGenerator( PyObject* gen )
     {
       if ( !PyDict_Check( gen ) )
         throwPythonError( "Generation information object should be a dictionary!" );
       params_.generation.enabled = true;
       fillParameter( gen, "treat", params_.generation.treat );
       fillParameter( gen, "numEvents", params_.generation.maxgen );
       fillParameter( gen, "printEvery", params_.generation.gen_print_every );
       fillParameter( gen, "numThreads", params_.generation.num_threads );
       fillParameter( gen, "numPoints", params_.generation.num_points );
     }
 
     void
     PythonHandler::parseTamingFunctions( PyObject* tf )
     {
       if ( !PyList_Check( tf ) )
         throwPythonError( "Taming functions list should be a list!" );
 
       for ( Py_ssize_t i = 0; i < PyList_Size( tf ); ++i ) {
         PyObject* pit = PyList_GetItem( tf, i ); // borrowed
         if ( !pit )
           continue;
         if ( !PyDict_Check( pit ) )
           throwPythonError( Form( "Item %d has invalid type %s", i, pit->ob_type->tp_name ) );
         PyObject* pvar = getElement( pit, "variable" ), *pexpr = getElement( pit, "expression" ); // borrowed
         params_.taming_functions->add( get<std::string>( pvar ).c_str(), get<std::string>( pexpr ).c_str() );
       }
     }
 
     void
     PythonHandler::parseHadroniser( PyObject* hadr )
     {
       if ( !PyDict_Check( hadr ) )
         throwPythonError( "Hadroniser object should be a dictionary!" );
 
       PyObject* pname = getElement( hadr, MODULE_NAME ); // borrowed
       if ( !pname )
         throwPythonError( "Hadroniser name is required!" );
       std::string hadr_name = get<std::string>( pname );
 
       fillParameter( hadr, "maxTrials", params_.hadroniser_max_trials );
       PyObject* pseed = getElement( hadr, "seed" ); // borrowed
       long long seed = -1ll;
       if ( pseed && is<int>( pseed ) ) {
         seed = PyLong_AsLongLong( pseed );
         CG_DEBUG( "PythonHandler:hadroniser" ) << "Hadroniser seed set to " << seed;
       }
       if ( hadr_name == "pythia8" ) {
         params_.setHadroniser( new Hadroniser::Pythia8Hadroniser( params_ ) );
         std::vector<std::string> config;
         auto pythia8 = dynamic_cast<Hadroniser::Pythia8Hadroniser*>( params_.hadroniser() );
         pythia8->setSeed( seed );
         fillParameter( hadr, "pythiaPreConfiguration", config );
         pythia8->readStrings( config );
         pythia8->init();
         fillParameter( hadr, "pythiaConfiguration", config );
         pythia8->readStrings( config );
         fillParameter( hadr, "pythiaProcessConfiguration", config );
         pythia8->readStrings( config );
       }
     }
   }
 }
 
 #endif
 
diff --git a/CepGen/Event/Event.cpp b/CepGen/Event/Event.cpp
index f27e6f6..eda5a35 100644
--- a/CepGen/Event/Event.cpp
+++ b/CepGen/Event/Event.cpp
@@ -1,337 +1,341 @@
 #include "CepGen/Event/Event.h"
 #include "CepGen/Physics/PDG.h"
+#include "CepGen/Physics/HeavyIon.h"
 #include "CepGen/Core/Exception.h"
 #include "CepGen/Core/utils.h"
 
 #include <algorithm>
 #include <math.h>
 
 namespace CepGen
 {
   Event::Event() :
     num_hadronisation_trials( 0 ),
     time_generation( -1. ), time_total( -1. )
   {}
 
   Event::Event( const Event& rhs ) :
     num_hadronisation_trials( rhs.num_hadronisation_trials ),
     time_generation( rhs.time_generation ), time_total( rhs.time_total ),
     particles_( rhs.particles_ ),
     evtcontent_( rhs.evtcontent_ )
   {}
 
   void
   Event::clear()
   {
     particles_.clear();
     time_generation = -1.;
     time_total = -1.;
   }
 
   void
   Event::freeze()
   {
     //--- store a snapshot of the primordial event block
     if ( particles_.count( Particle::CentralSystem ) > 0 )
       evtcontent_.cs = particles_[Particle::CentralSystem].size();
     if ( particles_.count( Particle::OutgoingBeam1 ) > 0 )
       evtcontent_.op1 = particles_[Particle::OutgoingBeam1].size();
     if ( particles_.count( Particle::OutgoingBeam2 ) > 0 )
       evtcontent_.op2 = particles_[Particle::OutgoingBeam2].size();
   }
 
   void
   Event::restore()
   {
     //--- remove all particles after the primordial event block
     if ( particles_.count( Particle::CentralSystem ) > 0 )
       particles_[Particle::CentralSystem].resize( evtcontent_.cs );
     if ( particles_.count( Particle::OutgoingBeam1 ) > 0 )
       particles_[Particle::OutgoingBeam1].resize( evtcontent_.op1 );
     if ( particles_.count( Particle::OutgoingBeam2 ) > 0 )
       particles_[Particle::OutgoingBeam2].resize( evtcontent_.op2 );
   }
 
   double
   Event::cmEnergy() const
   {
     return CMEnergy( getOneByRole( Particle::IncomingBeam1 ), getOneByRole( Particle::IncomingBeam2 ) );
   }
 
   Particles&
   Event::getByRole( Particle::Role role )
   {
     //--- retrieve all particles with a given role
     return particles_[role];
   }
 
   const Particles&
   Event::getByRole( Particle::Role role ) const
   {
     if ( particles_.count( role ) == 0 )
       throw CG_FATAL( "Event" ) << "Failed to retrieve a particle with " << role << " role.";
     //--- retrieve all particles with a given role
     return particles_.at( role );
   }
 
   ParticlesIds
   Event::getIdsByRole( Particle::Role role ) const
   {
     ParticlesIds out;
     //--- retrieve all particles ids with a given role
     if ( particles_.count( role ) == 0 )
       return out;
 
     for ( const auto& part : particles_.at( role ) )
       out.insert( part.id() );
 
     return out;
   }
 
   Particle&
   Event::getOneByRole( Particle::Role role )
   {
     //--- retrieve the first particle a the given role
     Particles& parts_by_role = getByRole( role );
     if ( parts_by_role.size() == 0 )
       throw CG_FATAL( "Event" ) << "No particle retrieved with " << role << " role.";
     if ( parts_by_role.size() > 1 )
       throw CG_FATAL( "Event" ) << "More than one particle with " << role << " role: "
         << parts_by_role.size() << " particles.";
     return *parts_by_role.begin();
   }
 
   const Particle&
   Event::getOneByRole( Particle::Role role ) const
   {
     if ( particles_.count( role ) == 0 )
       throw CG_FATAL( "Event" ) << "Failed to retrieve a particle with " << role << " role.";
     //--- retrieve the first particle a the given role
     const Particles& parts_by_role = particles_.at( role );
     if ( parts_by_role.size() == 0 )
       throw CG_FATAL( "Event" ) << "No particle retrieved with " << role << " role.";
     if ( parts_by_role.size() > 1 )
       throw CG_FATAL( "Event" ) << "More than one particle with " << role << " role: "
         << parts_by_role.size() << " particles";
     return *parts_by_role.begin();
   }
 
   Particle&
   Event::operator[]( int id )
   {
     for ( auto& role_part : particles_ )
       for ( auto& part : role_part.second )
         if ( part.id() == id )
           return part;
 
     throw CG_FATAL( "Event" ) << "Failed to retrieve the particle with id=" << id << ".";
   }
 
   const Particle&
   Event::getConstById( int id ) const
   {
     for ( const auto& role_part : particles_ )
       for ( const auto& part : role_part.second )
         if ( part.id() == id )
           return part;
 
     throw CG_FATAL( "Event" ) << "Failed to retrieve the particle with id=" << id << ".";
   }
 
   Particles
   Event::getByIds( const ParticlesIds& ids ) const
   {
     Particles out;
     for ( const auto& id : ids )
       out.emplace_back( getConstById( id ) );
 
     return out;
   }
 
   Particles
   Event::mothers( const Particle& part )
   {
     return getByIds( part.mothers() );
   }
 
   Particles
   Event::daughters( const Particle& part )
   {
     return getByIds( part.daughters() );
   }
 
   ParticleRoles
   Event::roles() const
   {
     ParticleRoles out;
     ParticlesMap::const_iterator it, end = particles_.end();
     for ( it = particles_.begin(); it != end; it = particles_.upper_bound( it->first ) ) {
       out.emplace_back( it->first );
     }
     return out;
   }
 
   Particle&
   Event::addParticle( Particle& part, bool replace )
   {
     CG_DEBUG_LOOP( "Event" ) << "Particle with PDGid = " << part.integerPdgId() << " has role " << part.role();
     if ( part.role() <= 0 )
       throw CG_FATAL( "Event" ) << "Trying to add a particle with role=" << (int)part.role() << ".";
 
     //--- retrieve the list of particles with the same role
     Particles& part_with_same_role = getByRole( part.role() );
 
     //--- specify the id
     if ( part_with_same_role.empty() && part.id() < 0 ) part.setId( numParticles() ); // set the id if previously invalid/inexistent
     if ( !part_with_same_role.empty() ) {
       if ( replace ) part.setId( part_with_same_role[0].id() ); // set the previous id if replacing a particle
       else part.setId( numParticles() );
     }
 
     //--- add the particle to the collection
     if ( replace ) part_with_same_role = Particles( 1, part ); // generate a vector containing only this particle
     else part_with_same_role.emplace_back( part );
 
     return part_with_same_role.back();
   }
 
   Particle&
   Event::addParticle( Particle::Role role, bool replace )
   {
     Particle np( role, PDG::invalid );
     return addParticle( np, replace );
   }
 
   size_t
   Event::numParticles() const
   {
     size_t out = 0;
     for ( const auto& role_part : particles_ )
       out += role_part.second.size();
     return out;
   }
 
   const Particles
   Event::particles() const
   {
     Particles out;
     for ( const auto& role_part : particles_ )
       out.insert( out.end(), role_part.second.begin(), role_part.second.end() );
 
     std::sort( out.begin(), out.end() );
     return out;
   }
 
   const Particles
   Event::stableParticles() const
   {
     Particles out;
     for ( const auto& role_part : particles_ )
       for ( const auto& part : role_part.second )
         if ( (short)part.status() > 0 )
           out.emplace_back( part );
 
     std::sort( out.begin(), out.end() );
     return out;
   }
 
   void
   Event::checkKinematics() const
   {
     // check the kinematics through parentage
     for ( const auto& part : particles() ) {
       ParticlesIds daughters = part.daughters();
       if ( daughters.empty() )
         continue;
       Particle::Momentum ptot;
       for ( const auto& daugh : daughters ) {
         const Particle& d = getConstById( daugh );
         const ParticlesIds mothers = d.mothers();
         ptot += d.momentum();
         if ( mothers.size() < 2 )
           continue;
         for ( const auto& moth : mothers ) {
           if ( moth == part.id() )
             continue;
           ptot -= getConstById( moth ).momentum();
         }
       }
       const double mass_diff = ( ptot-part.momentum() ).mass();
       if ( fabs( mass_diff ) > minimal_precision_ ) {
         dump();
         throw CG_FATAL( "Event" ) << "Error in momentum balance for particle " << part.id() << ": mdiff = " << mass_diff << ".";
       }
     }
   }
 
   void
   Event::dump( std::ostream& out, bool stable ) const
   {
     const Particles parts = ( stable ) ? stableParticles() : particles();
 
     std::ostringstream os;
 
     Particle::Momentum p_total;
     for ( const auto& part : parts ) {
       const ParticlesIds mothers = part.mothers();
       {
         std::ostringstream oss_pdg;
         if ( part.pdgId() == PDG::invalid && mothers.size() > 0 ) {
           for ( unsigned short i = 0; i < mothers.size(); ++i )
             oss_pdg << ( i > 0 ? "/" : "" ) << getConstById( *std::next( mothers.begin(), i ) ).pdgId();
           os << Form( "\n %2d\t\t%-10s", part.id(), oss_pdg.str().c_str() );
         }
         else {
-          oss_pdg << part.pdgId();
+          if ( (HeavyIon)part.pdgId() )
+            oss_pdg << (HeavyIon)part.pdgId();
+          else
+            oss_pdg << part.pdgId();
           os << Form( "\n %2d\t%-+7d %-10s", part.id(), part.integerPdgId(), oss_pdg.str().c_str() );
         }
       }
       os << "\t";
       if ( part.charge() != 999. )
         os << Form( "%-.2f\t", part.charge() );
       else
         os << "\t";
       { std::ostringstream oss; oss << part.role(); os << Form( "%-8s %6d\t", oss.str().c_str(), part.status() ); }
       if ( !mothers.empty() ) {
         std::ostringstream oss;
         unsigned short i = 0;
         for ( const auto& moth : mothers ) {
           oss << ( i > 0 ? "+" : "" ) << moth;
           ++i;
         }
         os << Form( "%6s ", oss.str().c_str() );
       }
       else os << "       ";
       const Particle::Momentum mom = part.momentum();
       os << Form( "% 9.6e % 9.6e % 9.6e % 9.6e % 12.5f", mom.px(), mom.py(), mom.pz(), part.energy(), part.mass() );
 
       // discard non-primary, decayed particles
       if ( part.status() >= Particle::Status::Undefined ) {
         const int sign = ( part.status() == Particle::Status::Undefined )
           ? -1
           : +1;
         p_total += sign*mom;
       }
     }
     //--- set a threshold to the computation precision
     p_total.truncate();
     //
     CG_INFO( "Event" )
      << Form( "Dump of event content:\n"
               " Id\tPDG id\tName\t\tCharge\tRole\t Status\tMother\tpx            py            pz            E      \t M         \n"
               " --\t------\t----\t\t------\t----\t ------\t------\t----GeV/c---  ----GeV/c---  ----GeV/c---  ----GeV/c---\t --GeV/c²--"
               "%s\n"
               " ----------------------------------------------------------------------------------------------------------------------------------\n"
               "\t\t\t\t\t\t\tBalance% 9.6e % 9.6e % 9.6e % 9.6e", os.str().c_str(), p_total.px(), p_total.py(), p_total.pz(), p_total.energy() );
   }
 
   //------------------------------------------------------------------------------------------------
 
   Event::NumParticles::NumParticles() :
     cs( 0 ), op1( 0 ), op2( 0 )
   {}
 
   Event::NumParticles::NumParticles( const NumParticles& np ) :
     cs( np.cs ), op1( np.op1 ), op2( np.op2 )
   {}
 }
diff --git a/CepGen/IO/FortranInterface.cpp b/CepGen/IO/FortranInterface.cpp
index e7c6d0f..19dc902 100644
--- a/CepGen/IO/FortranInterface.cpp
+++ b/CepGen/IO/FortranInterface.cpp
@@ -1,58 +1,67 @@
 #include "CepGen/StructureFunctions/StructureFunctionsBuilder.h"
 #include "CepGen/StructureFunctions/StructureFunctions.h"
 
 #include "CepGen/Physics/KTFlux.h"
+#include "CepGen/Physics/HeavyIon.h"
 #include "CepGen/Physics/ParticleProperties.h"
 
 #include "CepGen/Core/Exception.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
   void
   cepgen_structure_functions_( int& sfmode, double& xbj, double& q2, double& f2, double& fl )
   {
     using namespace CepGen;
     SF::Type sf_mode = (SF::Type)sfmode;
 
     CG_DEBUG( "cepgen_structure_functions" ) << sf_mode;
 
     static StructureFunctions& val = StructureFunctionsBuilder::get( sf_mode )->operator()( xbj, q2 );
     f2 = val.F2;
     fl = val.FL;
   }
 
   double
   cepgen_kt_flux_( int& fmode, double& x, double& kt2, int& sfmode, double& mx )
   {
     using namespace CepGen;
     static auto sf = StructureFunctionsBuilder::get( (SF::Type)sfmode );
     return ktFlux(
       (KTFlux)fmode, x, kt2, *sf, mx );
   }
 
   double
+  cepgen_kt_flux_hi_( int& fmode, double& x, double& kt2, int& a, int& z )
+  {
+    using namespace CepGen;
+    return ktFlux(
+      (KTFlux)fmode, x, kt2, HeavyIon{ (unsigned short)a, (Element)z } );
+  }
+
+  double
   cepgen_particle_mass_( int& pdg_id )
   {
     try {
       return CepGen::ParticleProperties::mass( (CepGen::PDG)pdg_id );
     } catch ( const CepGen::Exception& e ) {
       e.dump();
       exit( 0 );
     }
   }
 
   double
   cepgen_particle_charge_( int& pdg_id )
   {
     try {
       return CepGen::ParticleProperties::charge( pdg_id );
     } catch ( const CepGen::Exception& e ) {
       e.dump();
       exit( 0 );
     }
   }
 #ifdef __cplusplus
 }
 #endif
 
diff --git a/CepGen/Physics/GluonGrid.cpp b/CepGen/Physics/GluonGrid.cpp
new file mode 100644
index 0000000..bcc25e7
--- /dev/null
+++ b/CepGen/Physics/GluonGrid.cpp
@@ -0,0 +1,55 @@
+#include "CepGen/Physics/GluonGrid.h"
+#include "CepGen/Core/Exception.h"
+
+#include <fstream>
+#include <set>
+
+namespace kmr
+{
+  GluonGrid&
+  GluonGrid::get( const char* filename )
+  {
+    Parameterisation p;
+    p.grid_path = filename;
+    static GluonGrid instance( p );
+    return instance;
+  }
+
+  GluonGrid::GluonGrid( const Parameterisation& param ) :
+    CepGen::GridHandler<3,1>( CepGen::GridType::linear ),
+    params( param )
+  {
+    std::set<double> kt2_vals, x_vals, mu2_vals;
+
+    { // file readout part
+      std::ifstream file( params.grid_path, std::ios::in );
+      if ( !file.is_open() )
+        throw CG_FATAL( "GluonGrid" ) << "Impossible to load grid file \"" << params.grid_path << "\"!";
+
+      std::string x_tmp, kt2_tmp, mu2_tmp, fg_tmp;
+      while ( file >> x_tmp >> kt2_tmp >> mu2_tmp >> fg_tmp ) {
+        const double x = stod( x_tmp ), kt2 = stod( kt2_tmp ), mu2 = stod( mu2_tmp ), fg = stod( fg_tmp );
+        x_vals.insert( x );
+        kt2_vals.insert( kt2 );
+        mu2_vals.insert( mu2 );
+        insert( { x, kt2, mu2 }, { fg } );
+      }
+      file.close();
+    }
+
+    init();
+
+    CG_INFO( "GluonGrid" )
+      << "KMR grid evaluator built!\n\t"
+      << " kt² in range [" << *kt2_vals.begin() << ":" << *kt2_vals.rbegin() << "]\n\t"
+      << " x in range [" << *x_vals.begin() << ":" << *x_vals.rbegin() << "]\n\t"
+      << " µ² in range ["  << *mu2_vals.begin() << ":" << *mu2_vals.rbegin() << "].";
+  }
+
+  double
+  GluonGrid::operator()( double x, double kt2, double mu2 ) const
+  {
+    return CepGen::GridHandler<3,1>::eval( { x, kt2, mu2 } ).at( 0 );
+  }
+}
+
diff --git a/CepGen/Physics/GluonGrid.h b/CepGen/Physics/GluonGrid.h
new file mode 100644
index 0000000..5580ae5
--- /dev/null
+++ b/CepGen/Physics/GluonGrid.h
@@ -0,0 +1,40 @@
+#ifndef CepGen_Physics_GluonGrid_h
+#define CepGen_Physics_GluonGrid_h
+
+#include "CepGen/IO/GridHandler.h"
+
+#define DEFAULT_KMR_GRID_PATH "gluon_mmht2014nlo_Watt.dat"
+
+/// Kimber-Martin-Ryskin unintegrated gluon densities
+namespace kmr
+{
+  /// A KMR unintegrated gluon densities grid interpolator
+  class GluonGrid : private CepGen::GridHandler<3,1>
+  {
+    public:
+      struct Parameterisation {
+        Parameterisation() : grid_path( DEFAULT_KMR_GRID_PATH ) {}
+        std::string grid_path;
+      };
+
+    public:
+      /// Retrieve the grid interpolator (singleton)
+      static GluonGrid& get( const char* path = DEFAULT_KMR_GRID_PATH );
+
+      /// Compute the gluon flux
+      double operator()( double x, double kt2, double mu2 ) const;
+      Parameterisation params;
+
+    public:
+      GluonGrid( const GluonGrid& ) = delete;
+      void operator=( const GridHandler& ) = delete;
+
+    private:
+      explicit GluonGrid( const Parameterisation& = Parameterisation() );
+  };
+}
+
+#undef DEFAULT_KMR_GRID_PATH
+
+#endif
+
diff --git a/CepGen/Physics/HeavyIon.cpp b/CepGen/Physics/HeavyIon.cpp
new file mode 100644
index 0000000..2477e74
--- /dev/null
+++ b/CepGen/Physics/HeavyIon.cpp
@@ -0,0 +1,46 @@
+#include "CepGen/Physics/HeavyIon.h"
+
+#include <sstream>
+
+namespace CepGen
+{
+  HeavyIon::HeavyIon( const PDG& pdg ) :
+    A( (unsigned int)pdg % 1000 ),
+    Z( (Element)( (unsigned int)pdg/1000000 == 0 ? 0
+                : ( (unsigned int)pdg/1000 ) % 1000 ) )
+  {}
+
+  HeavyIon::operator PDG() const
+  {
+    // Pythia8 convention/10-1e10+1e6
+    return (PDG)( 1000000+1000*(unsigned short)Z+A );
+  }
+
+  std::ostream&
+  operator<<( std::ostream& os, const HeavyIon& hi )
+  {
+    std::ostringstream oss; oss << hi.Z;
+    if ( oss.str().empty() )
+      return os << "HI{Z=" << (unsigned short)hi.Z << ", A=" << hi.A << "}";
+    return os << hi.A << oss.str();
+  }
+
+  std::ostream&
+  operator<<( std::ostream& os, const Element& elem )
+  {
+    switch ( elem ) {
+      case Element::invalid:
+        return os;
+      case Element::H:  return os << "H";
+      case Element::C:  return os << "C";
+      case Element::O:  return os << "O";
+      case Element::Al: return os << "Al";
+      case Element::Cu: return os << "Cu";
+      case Element::Xe: return os << "Xe";
+      case Element::Au: return os << "Au";
+      case Element::Pb: return os << "Pb";
+      case Element::U:  return os << "U";
+    }
+    return os;
+  }
+}
diff --git a/CepGen/Physics/HeavyIon.h b/CepGen/Physics/HeavyIon.h
new file mode 100644
index 0000000..e2b996c
--- /dev/null
+++ b/CepGen/Physics/HeavyIon.h
@@ -0,0 +1,44 @@
+#ifndef CepGen_Physics_HeavyIon_h
+#define CepGen_Physics_HeavyIon_h
+
+#include <ostream>
+
+namespace CepGen
+{
+  enum class PDG;
+  /// Enumeration of chemical elements
+  enum class Element
+  {
+    invalid = 0,
+    H = 1, C = 6, O = 8,
+    Al = 13, Cu = 29,
+    Xe = 54, Au = 79, Pb = 82,
+    U = 92
+  };
+  std::ostream& operator<<( std::ostream& os, const Element& elem );
+
+  /// Heavy ion container (Z+A)
+  struct HeavyIon
+  {
+    /// General constructor from mass and atomic number
+    HeavyIon( unsigned short a, const Element& z ) : A( a ), Z( z ) {}
+    /// Build from a custom PDG id
+    HeavyIon( const PDG& pdg );
+    /// Simple proton
+    static inline HeavyIon proton() { return HeavyIon( 1, Element::H ); }
+    /// Convert the HI into a custom PDG id
+    operator PDG() const;
+    /// Check the validity of the heavy ion
+    inline operator bool() const {
+      return Z > Element::invalid && A > 1; // skip the proton
+    }
+    /// Human-readable expression of the ion
+    friend std::ostream& operator<<( std::ostream& os, const HeavyIon& hi );
+    /// Mass number
+    unsigned short A;
+    /// Atomic number
+    Element Z;
+  };
+}
+
+#endif
diff --git a/CepGen/Physics/KTFlux.cpp b/CepGen/Physics/KTFlux.cpp
index 5702fc7..65064b8 100644
--- a/CepGen/Physics/KTFlux.cpp
+++ b/CepGen/Physics/KTFlux.cpp
@@ -1,68 +1,103 @@
 #include "CepGen/Physics/KTFlux.h"
 #include "CepGen/Physics/FormFactors.h"
 #include "CepGen/Physics/ParticleProperties.h"
+#include "CepGen/Physics/HeavyIon.h"
+#include "CepGen/Physics/GluonGrid.h"
+
 #include "CepGen/StructureFunctions/StructureFunctions.h"
 
 #include "CepGen/Core/Exception.h"
 
 namespace CepGen
 {
   const double KTFluxParameters::kMinKTFlux = 1.e-20;
   const double KTFluxParameters::kMP = ParticleProperties::mass( PDG::proton );
   const double KTFluxParameters::kMP2 = KTFluxParameters::kMP*KTFluxParameters::kMP;
 
   double
   ktFlux( const KTFlux& type, double x, double kt2, StructureFunctions& sf, double mx )
   {
     double flux = 0.;
     const double mp2 = KTFluxParameters::kMP2;
     switch ( type ) {
       case KTFlux::P_Photon_Elastic: {
         const double x2 = x*x;
         const double q2min = x2*mp2/( 1.-x ), q2 = q2min + kt2/( 1.-x );
 
         // electromagnetic form factors
         const auto& ff = FormFactors::protonElastic( q2 );
 
         const double ela1 = ( 1.-x )*( 1.-q2min/q2 );
         //const double ela3 = 1.-( q2-kt2 )/q2;
 
         flux = Constants::alphaEM*M_1_PI*( 1.-x )/q2*( ela1*ff.FE + 0.5*x2*ff.FM );
       } break;
       case KTFlux::P_Photon_Inelastic_Budnev: {
         const double mx2 = mx*mx, x2 = x*x;
         const double q2min = ( x*( mx2-mp2 ) + x2*mp2 )/( 1.-x ), q2 = q2min + kt2/( 1.-x );
         const double xbj = q2 / ( q2+mx2-mp2 );
 
         // structure functions
         auto& str_fun = sf( xbj, q2 );
         str_fun.computeFL( xbj, q2 );
 
         const double f_D = str_fun.F2/( q2+mx2-mp2 )* ( 1.-x )*( 1.-q2min/q2 );
         const double f_C = str_fun.F1( xbj, q2 ) * 2./q2;
 
         flux = Constants::alphaEM*M_1_PI*( 1.-x )/q2*( f_D+0.5*x2*f_C );
       } break;
+      case KTFlux::P_Gluon_KMR: {
+        flux = kmr::GluonGrid::get()( log10( x ), log10( kt2 ), 2.*log10( mx ) );
+      } break;
       default:
         throw CG_FATAL( "GenericKTProcess:flux" ) << "Invalid flux type: " << type;
     }
     if ( flux < KTFluxParameters::kMinKTFlux )
       return 0.;
     return flux;
   }
 
+  double
+  ktFlux( const KTFlux& type, double x, double kt2, const HeavyIon& hi )
+  {
+    double flux = 0.;
+    switch ( type ) {
+      case KTFlux::HI_Photon_Elastic: {
+        const double r_a = 1.1*std::pow( hi.A, 1./3 ), a0 = 0.7, m_a = hi.A*KTFluxParameters::kMP;
+        const double q2_ela = ( kt2+x*x*m_a*m_a )/( 1.-x ), cons = sqrt( q2_ela )/0.1973;
+        const double tau = cons*r_a, tau1 = cons*a0;
+        // "Realistic nuclear form-factor" as used in STARLIGHT
+        const double ff1 = 3.*( sin( tau )-tau*cos( tau ) )/pow( tau+1.e-10, 3 );
+        const double ff2 = 1./( 1.+tau1*tau1 );
+        const double ela1 = pow( kt2/( kt2+x*x*m_a*m_a ), 2 );
+        const double ela2 = pow( ff1*ff2, 2 )/*, ela3 = 1.-( q2_ela-kt2 )/q2_ela*/;
+        const unsigned int z = (unsigned short)hi.Z;
+        flux = Constants::alphaEM*M_1_PI*z*z*ela1*ela2/q2_ela;
+      } break;
+      default:
+        throw CG_FATAL("GenericKTProcess:flux") << "Invalid flux type: " << type;
+    }
+    if ( flux < KTFluxParameters::kMinKTFlux )
+      return 0.;
+    return flux;
+  }
+
   std::ostream&
   operator<<( std::ostream& os, const KTFlux& type )
   {
     switch ( type ) {
       case KTFlux::P_Photon_Elastic:
         return os << "elastic photon from proton";
       case KTFlux::P_Photon_Inelastic:
         return os << "inelastic photon from proton";
       case KTFlux::P_Photon_Inelastic_Budnev:
         return os << "inelastic photon from proton (Budnev)";
+      case KTFlux::P_Gluon_KMR:
+        return os << "elastic gluon from proton (KMR)";
+      case KTFlux::HI_Photon_Elastic:
+        return os << "elastic photon from HI";
       case KTFlux::invalid: default:
         return os << "unrecognized flux (" << (int)type << ")";
     }
   }
 }
diff --git a/CepGen/Physics/KTFlux.h b/CepGen/Physics/KTFlux.h
index dce02c5..db8df10 100644
--- a/CepGen/Physics/KTFlux.h
+++ b/CepGen/Physics/KTFlux.h
@@ -1,31 +1,35 @@
 #ifndef CepGen_Physics_KTFlux_h
 #define CepGen_Physics_KTFlux_h
 
 #include "CepGen/Physics/PDG.h"
 #include <ostream>
 
 namespace CepGen
 {
   class StructureFunctions;
+  class HeavyIon;
   struct KTFluxParameters
   {
     static const double kMinKTFlux, kMP, kMP2;
   };
   /// Type of incoming partons fluxes
   enum class KTFlux
   {
     invalid = -1,
     P_Photon_Elastic = 0,
     P_Photon_Inelastic = 1,
     P_Photon_Inelastic_Budnev = 11,
+    P_Gluon_KMR = 20,
+    HI_Photon_Elastic = 100
   };
   std::ostream& operator<<( std::ostream&, const KTFlux& );
   /// Get the flux at a given parton x/kT
   /// \param[in] x Parton momentum fraction
   /// \param[in] kt2 Transverse 2-momentum \f$\mathbf{q}_{\mathrm{T}}^2\f$ of the incoming parton
   /// \param[in] sf Structure functions evaluator
   /// \param[in] mx Outgoing diffractive proton mass
   double ktFlux( const KTFlux& type, double x, double kt2, StructureFunctions& sf, double mx = KTFluxParameters::kMP );
+  double ktFlux( const KTFlux& type, double x, double kt2, const HeavyIon& hi );
 }
 
 #endif
diff --git a/CepGen/Physics/Kinematics.cpp b/CepGen/Physics/Kinematics.cpp
index 90050a5..1d225a2 100644
--- a/CepGen/Physics/Kinematics.cpp
+++ b/CepGen/Physics/Kinematics.cpp
@@ -1,85 +1,89 @@
 #include "CepGen/Physics/Kinematics.h"
 #include "CepGen/Physics/PDG.h"
 #include "CepGen/Physics/KTFlux.h"
 
 #include "CepGen/Core/Exception.h"
 #include "CepGen/Core/utils.h"
 
 #include "CepGen/StructureFunctions/SuriYennie.h"
 #include "CepGen/Event/Particle.h"
 
 namespace CepGen
 {
   Kinematics::Kinematics() :
     incoming_beams( { { 6500., PDG::proton, KTFlux::invalid }, { 6500., PDG::proton, KTFlux::invalid } } ),
     mode( KinematicsMode::invalid ), structure_functions( new SF::SuriYennie )
   {}
 
   Kinematics::~Kinematics()
   {}
 
   void
   Kinematics::setSqrtS( double sqrts )
   {
     incoming_beams.first.pz = incoming_beams.second.pz = 0.5 * sqrts;
   }
 
   double
   Kinematics::sqrtS() const
   {
     return incoming_beams.first.pz + incoming_beams.second.pz;
   }
 
   //--------------------------------------------------------------------
   // User-friendly display of the kinematics mode
   //--------------------------------------------------------------------
 
   std::ostream&
   operator<<( std::ostream& os, const KinematicsMode& pm )
   {
     switch ( pm ) {
       case KinematicsMode::invalid:
         return os << "invalid";
       case KinematicsMode::ElectronElectron:
         return os << "electron/electron";
       case KinematicsMode::ElectronProton:
         return os << "electron/proton";
       case KinematicsMode::ProtonElectron:
         return os << "proton/electron";
       case KinematicsMode::ElasticElastic:
         return os << "elastic/elastic";
       case KinematicsMode::InelasticElastic:
         return os << "inelastic/elastic";
       case KinematicsMode::ElasticInelastic:
         return os << "elastic/inelastic";
       case KinematicsMode::InelasticInelastic:
         return os << "inelastic/inelastic";
     }
     return os;
   }
 
   //--------------------------------------------------------------------
   // User-friendly display of incoming particles
   //--------------------------------------------------------------------
 
   std::ostream&
   operator<<( std::ostream& os, const Kinematics::Beam& beam )
   {
-    os << beam.pdg << " (" << beam.pz << " GeV/c)";
+    if ( (HeavyIon)beam.pdg )
+      os << (HeavyIon)beam.pdg;
+    else
+      os << beam.pdg;
+    os << " (" << beam.pz << " GeV/c)";
     if ( beam.kt_flux != KTFlux::invalid )
       os << " [unint.flux: " << beam.kt_flux << "]";
     return os;
   }
 
   //--------------------------------------------------------------------
   // List of kinematics limits
   //--------------------------------------------------------------------
 
   Kinematics::CutsList::CutsList()
   {
     initial.q2 = { 0., 1.e5 };
     central.pt_single.min() = 0.;
     remnants.mass_single = { 1.07, 320. };
   }
 }
 
diff --git a/CepGen/Physics/Kinematics.h b/CepGen/Physics/Kinematics.h
index fadb57d..bafb8b8 100644
--- a/CepGen/Physics/Kinematics.h
+++ b/CepGen/Physics/Kinematics.h
@@ -1,77 +1,79 @@
 #ifndef CepGen_Physics_Kinematics_h
 #define CepGen_Physics_Kinematics_h
 
 #include "CepGen/Core/Hasher.h"
 
 #include "CepGen/Physics/Cuts.h"
+#include "CepGen/Physics/HeavyIon.h"
 
 #include <ostream>
 #include <vector>
 #include <unordered_map>
 #include <memory>
 
 namespace CepGen
 {
   enum class PDG;
   enum class KTFlux;
   class StructureFunctions;
   /// Type of kinematics to consider for the process
   enum class KinematicsMode
   {
     invalid = -1,
     ElectronProton = 0,     ///< electron-proton elastic case
     ElasticElastic = 1,     ///< proton-proton elastic case
     ElasticInelastic = 2,   ///< proton-proton single-dissociative (or inelastic-elastic) case
     InelasticElastic = 3,   ///< proton-proton single-dissociative (or elastic-inelastic) case
     InelasticInelastic = 4, ///< proton-proton double-dissociative case
     ProtonElectron,
     ElectronElectron
   };
   /// Human-readable format of a process mode (elastic/dissociative parts)
   std::ostream& operator<<( std::ostream&, const KinematicsMode& );
   /// List of kinematic constraints to apply on the process phase space.
   class Kinematics
   {
     public:
       Kinematics();
       ~Kinematics();
 
       struct Beam
       {
         /// Incoming particle's momentum (in \f$\text{GeV}/c\f$)
         double pz;
         PDG pdg;
         KTFlux kt_flux;
       };
       friend std::ostream& operator<<( std::ostream&, const Beam& );
       /// Beam/primary particle's kinematics
       std::pair<Beam,Beam> incoming_beams;
       /// Set the incoming particles' momenta (if the collision is symmetric)
       void setSqrtS( double sqrts );
       /// Process centre of mass energy
       double sqrtS() const;
       /// Minimum list of central particles required
       std::vector<PDG> minimum_final_state;
 
       /// Type of kinematics to consider for the phase space
       KinematicsMode mode;
       /// Type of structure functions to consider
       std::shared_ptr<StructureFunctions> structure_functions;
 
       struct CutsList
       {
         CutsList();
         /// Cuts on the initial particles kinematics
         Cuts initial;
         /// Cuts on the central system produced
         Cuts central;
         std::unordered_map<PDG,Cuts,EnumHash<PDG> > central_particles;
         /// Cuts on the beam remnants system
         Cuts remnants;
       };
       CutsList cuts;
+      std::string kmr_grid_path;
   };
 }
 
 #endif
 
diff --git a/CepGen/Processes/CMakeLists.txt b/CepGen/Processes/CMakeLists.txt
index 017c1b7..8050ba9 100644
--- a/CepGen/Processes/CMakeLists.txt
+++ b/CepGen/Processes/CMakeLists.txt
@@ -1,7 +1,7 @@
-file(GLOB sources *.cpp)
+file(GLOB sources *.cpp Fortran/*.f)
 
 include_directories(${PROJECT_SOURCE_DIR})
 
 add_library(CepGenProcesses SHARED ${sources})
 
 install(TARGETS CepGenProcesses DESTINATION lib)
diff --git a/CepGen/Processes/Fortran/cepgen_blocks.inc b/CepGen/Processes/Fortran/cepgen_blocks.inc
new file mode 100644
index 0000000..ff15b10
--- /dev/null
+++ b/CepGen/Processes/Fortran/cepgen_blocks.inc
@@ -0,0 +1,61 @@
+c     ================================================================
+c     F77 process-to-CepGen helper
+c     > this header file defines a set of common blocks useful for
+c     > the interfacing of any kt-factorised matrix element computation
+c     > to a mother CepGen instance
+c     ================================================================
+
+c     =================================================================
+c     collection of fundamental physics constants
+c     =================================================================
+      common/constants/am_p,units,pi,alpha_em
+      double precision am_p,units,pi,alpha_em
+
+c     =================================================================
+c     information on the full process
+c       inp1 = proton energy in lab frame
+c       inp2 = nucleus energy **per nucleon** in LAB frame
+c       Collision is along z-axis
+c     =================================================================
+      common/params/icontri,iflux1,iflux2,imethod,sfmod,pdg_l,
+     &     a_nuc1,z_nuc1,a_nuc2,z_nuc2,
+     &     inp1,inp2
+      integer icontri,iflux1,iflux2,imethod,sfmod,pdg_l,
+     &     a_nuc1,z_nuc1,a_nuc2,z_nuc2
+      double precision inp1,inp2
+
+c     =================================================================
+c     kt-factorisation kinematics
+c     =================================================================
+      common/ktkin/q1t,q2t,phiq1t,phiq2t,y1,y2,ptdiff,phiptdiff,
+     &     am_x,am_y
+      double precision q1t,q2t,phiq1t,phiq2t,y1,y2,ptdiff,phiptdiff,
+     &     am_x,am_y
+
+c     =================================================================
+c     phase space cuts
+c     =================================================================
+      common/kincuts/ipt,iene,ieta,iinvm,iptsum,idely,
+     &     pt_min,pt_max,ene_min,ene_max,eta_min,eta_max,
+     &     invm_min,invm_max,ptsum_min,ptsum_max,
+     &     dely_min,dely_max
+      integer ipt,iene,ieta,iinvm,iptsum,idely
+      double precision pt_min,pt_max,ene_min,ene_max,eta_min,eta_max,
+     &     invm_min,invm_max,ptsum_min,ptsum_max,
+     &     dely_min,dely_max
+
+c     =================================================================
+c     generated event kinematics
+c     =================================================================
+      common/evtkin/px,py,nout,idum,ipdg,pc
+      integer nout,idum,ipdg(4)
+      double precision px(4),py(4),pc(4,4)
+
+c     =================================================================
+c     helpers for the evaluation of unintegrated parton fluxes
+c     =================================================================
+      external CepGen_kT_flux,CepGen_kT_flux_HI
+      double precision CepGen_kT_flux,CepGen_kT_flux_HI
+      external CepGen_particle_charge,CepGen_particle_mass
+      double precision CepGen_particle_charge,CepGen_particle_mass
+
diff --git a/CepGen/Processes/Fortran/cepgen_print.f b/CepGen/Processes/Fortran/cepgen_print.f
new file mode 100644
index 0000000..38954f6
--- /dev/null
+++ b/CepGen/Processes/Fortran/cepgen_print.f
@@ -0,0 +1,40 @@
+      subroutine cepgen_print
+      implicit none
+      include 'cepgen_blocks.inc'
+      logical params_shown
+      data params_shown/.false./
+      save params_shown
+
+      if(params_shown) return
+
+      print *,'========================================================'
+      print *,'Parameter                                  value(s)'
+      print *,'--------------------------------------------------------'
+      print 101,'Process mode:',icontri
+      print 101,'Computation method:',imethod
+      print 101,'Structure functions:',sfmod
+      print 101,'Central system PDG:',pdg_l
+      print 103,'Beams momenta:',inp1,inp2
+      print 102,'Fluxes modes:',iflux1,iflux2
+      print 104,'Beams (A,Z):',a_nuc1,z_nuc1,a_nuc2,z_nuc2
+      print *,'========================================================'
+      print *,'Cut                        enabled   minimum     maximum'
+      print *,'--------------------------------------------------------'
+      print 100,'pt(single)',ipt,pt_min,pt_max
+      print 100,'energy(single)',iene,ene_min,ene_max
+      print 100,'eta(single)',ieta,eta_min,eta_max
+      print 100,'m(sum)',iinvm,invm_min,invm_max
+      print 100,'pt(sum)',iptsum,ptsum_min,ptsum_max
+      print 100,'delta(y)',idely,dely_min,dely_max
+      print *,'========================================================'
+
+      params_shown=.true.
+
+100   format(A26,'     ',L2,f12.4,f12.4)
+101   format(A33,I12)
+102   format(A33,I12,I12)
+103   format(A33,f12.2,f12.2)
+104   format(A33,'   (',I3,',',I3,'),  (',I3,',',I3,')')
+
+      end
+
diff --git a/CepGen/Processes/Fortran/nucl_to_ff.f b/CepGen/Processes/Fortran/nucl_to_ff.f
new file mode 100644
index 0000000..a699f74
--- /dev/null
+++ b/CepGen/Processes/Fortran/nucl_to_ff.f
@@ -0,0 +1,490 @@
+      subroutine nucl_to_ff(aintegrand)
+      implicit none
+      double precision aintegrand
+c     =================================================================
+c     CepGen common blocks for kinematics definition
+c     =================================================================
+      include 'cepgen_blocks.inc'
+      data iflux1,iflux2,sfmod,pdg_l/10,100,11,13/
+      data a_nuc1,z_nuc1,a_nuc2,z_nuc2/1,1,208,82/
+
+c     =================================================================
+c     local variables
+c     =================================================================
+      double precision s,s12
+      double precision alpha1,alpha2,amt1,amt2
+      double precision pt1,pt2,eta1,eta2,dely
+      double precision pt1x,pt1y,pt2x,pt2y
+      double precision ak10,ak1x,ak1y,ak1z,ak20,ak2x,ak2y,ak2z
+      double precision beta1,beta2,x1,x2
+      double precision z1p,z1m,z2p,z2m
+      double precision q1tx,q1ty,q1z,q10,q1t2
+      double precision q2tx,q2ty,q2z,q20,q2t2
+      double precision ptsum
+      double precision that1,that2,that,uhat1,uhat2,uhat
+      double precision term1,term2,term3,term4,term5,term6,term7
+      double precision term8,term9,term10,amat2
+      double precision ak1_x,ak1_y,ak2_x,ak2_y
+      double precision eps12,eps22
+      double precision aux2_1,aux2_2,f1,f2
+      double precision Phi10,Phi102,Phi11_x,Phi11_y,Phi112
+      double precision Phi20,Phi202,Phi21_x,Phi21_y,Phi212
+      double precision Phi11_dot_e,Phi11_cross_e
+      double precision Phi21_dot_e,Phi21_cross_e
+      double precision aintegral
+      integer imat1,imat2
+
+      double precision px_plus,px_minus,py_plus,py_minus
+      double precision r1,r2
+      double precision ptdiffx,ptsumx,ptdiffy,ptsumy
+      double precision invm,invm2,s1_eff,s2_eff
+      double precision t1abs,t2abs
+      double precision am_l,q_l
+      double precision inp_A,inp_B,am_A,am_B
+      double precision p1_plus, p2_minus
+      double precision amat2_1,amat2_2
+      integer iterm11,iterm22,iterm12,itermtt
+
+      double precision coupling
+
+c     =================================================================
+c     quarks production
+c     =================================================================
+#ifdef ALPHA_S
+      double precision t_max,amu2,alphas
+#endif
+      logical first_init
+      data first_init/.true./
+      save first_init,am_l,q_l
+
+      call CepGen_print
+
+      if(first_init) then
+        am_l = CepGen_particle_mass(pdg_l) ! central particles mass
+        q_l = CepGen_particle_charge(pdg_l) ! central particles charge
+        if(iflux1.ge.20.and.iflux1.lt.40) then
+          if(icontri.eq.3.or.icontri.eq.4) then
+            print *,'Invalid process mode for collinear gluon emission!'
+            stop
+          endif
+#ifdef ALPHA_S
+          print *,'Initialisation of the alpha(S) evolution algorithm..'
+          call initAlphaS(0,1.d0,1.d0,0.5d0,
+     &       CepGen_particle_mass(4), ! charm
+     &       CepGen_particle_mass(5), ! bottom
+     &       CepGen_particle_mass(6)) ! top
+#endif
+        endif
+        first_init = .false.
+      endif
+
+c     =================================================================
+c     FIXME
+c     =================================================================
+
+      aintegrand = 0.d0
+      q10 = 0.d0
+      q1z = 0.d0
+      q20 = 0.d0
+      q2z = 0.d0
+
+c     =================================================================
+c     go to energy of Nucleus = A*inp in order that generator puts out
+c     proper momenta in LAB frame
+c     =================================================================
+
+      inp_A = inp1*a_nuc1
+      am_A = am_p*a_nuc1
+      inp_B = inp2*a_nuc2
+      am_B = am_p*a_nuc2
+
+c     =================================================================
+c     four-momenta for incoming beams in LAB !!!!
+c     =================================================================
+
+      r1 = dsqrt(1.d0+am_A**2/inp_A**2)
+      r2 = dsqrt(1.d0+am_B**2/inp_B**2)
+
+      ak10 = inp_A*r1
+      ak1x = 0.d0
+      ak1y = 0.d0
+      ak1z = inp_A
+
+      ak20 = inp_B*r2
+      ak2x = 0.d0
+      ak2y = 0.d0
+      ak2z = -inp_B
+
+      s = 4.*inp_A*inp_B*(1.d0 + r1*r2)/2.d0+(am_A**2+am_B**2)
+      s12 = dsqrt(s)
+
+      p1_plus = (ak10+ak1z)/dsqrt(2.d0)
+      p2_minus = (ak20-ak2z)/dsqrt(2.d0)
+
+c     terms in the matrix element
+c
+      iterm11 = 1         ! LL
+      iterm22 = 1         ! TT
+      iterm12 = 1         ! LT
+      itermtt = 1         ! TT'
+
+c     =================================================================
+c     two terms in the Wolfgang's formula for
+c     off-shell gamma gamma --> l^+ l^-
+c     =================================================================
+      imat1 = 1
+      imat2 = 1
+c     =================================================================
+c     Outgoing proton final state's mass
+c     =================================================================
+      if((icontri.eq.1).or.(icontri.eq.2)) am_x = am_A
+      if((icontri.eq.1).or.(icontri.eq.3)) am_y = am_B
+
+      q1tx = q1t*cos(phiq1t)
+      q1ty = q1t*sin(phiq1t)
+
+      q2tx = q2t*cos(phiq2t)
+      q2ty = q2t*sin(phiq2t)
+
+      ptsumx = q1tx+q2tx
+      ptsumy = q1ty+q2ty
+
+      ptsum = sqrt(ptsumx**2+ptsumy**2)
+
+c     =================================================================
+c     a window in final state transverse momentum
+c     =================================================================
+
+      if(iptsum.eq.1) then
+        if(ptsum.lt.ptsum_min.or.ptsum.gt.ptsum_max) return
+      endif
+
+c     =================================================================
+c     compute the individual central particles momentum
+c     =================================================================
+
+      ptdiffx = ptdiff*cos(phiptdiff)
+      ptdiffy = ptdiff*sin(phiptdiff)
+
+      pt1x = 0.5*(ptsumx+ptdiffx)
+      pt1y = 0.5*(ptsumy+ptdiffy)
+
+      pt2x = 0.5*(ptsumx-ptdiffx)
+      pt2y = 0.5*(ptsumy-ptdiffy)
+
+      pt1 = sqrt(pt1x**2+pt1y**2)
+      pt2 = sqrt(pt2x**2+pt2y**2)
+
+      if(ipt.eq.1) then
+        if(pt1.lt.pt_min.or.pt2.lt.pt_min) return
+      endif
+
+      amt1 = dsqrt(pt1**2+am_l**2)
+      amt2 = dsqrt(pt2**2+am_l**2)
+
+      invm2 = amt1**2 + amt2**2 + 2.d0*amt1*amt2*dcosh(y1-y2)
+     &       -ptsum**2
+
+      invm = dsqrt(invm2)
+
+c     =================================================================
+c     a window in final state invariant mass
+c     =================================================================
+
+      if(iinvm.eq.1) then
+        if(invm.lt.invm_min.or.invm.gt.invm_max) return
+      endif
+
+c     =================================================================
+c     a window in rapidity distance
+c     =================================================================
+
+      dely = dabs(y1-y2)
+      if(idely.eq.1) then
+        if(dely.lt.dely_min.or.dely.gt.dely_max) return
+      endif
+
+c     =================================================================
+c     auxiliary quantities
+c     =================================================================
+
+      alpha1 = amt1/(dsqrt(2.d0)*p1_plus)*dexp( y1)
+      alpha2 = amt2/(dsqrt(2.d0)*p1_plus)*dexp( y2)
+      beta1  = amt1/(dsqrt(2.d0)*p2_minus)*dexp(-y1)
+      beta2  = amt2/(dsqrt(2.d0)*p2_minus)*dexp(-y2)
+
+      q1t2 = q1tx**2 + q1ty**2
+      q2t2 = q2tx**2 + q2ty**2
+
+      x1 = alpha1 + alpha2
+      x2 = beta1  + beta2
+
+      z1p = alpha1/x1
+      z1m = alpha2/x1
+      z2p = beta1/x2
+      z2m = beta2/x2
+
+c     -----------------------------------------------------------------
+      if(x1.gt.1.0.or.x2.gt.1.0) return
+c     -----------------------------------------------------------------
+
+      s1_eff = x1*s - q1t**2
+      s2_eff = x2*s - q2t**2
+
+c     -----------------------------------------------------------------
+c     Additional conditions for energy-momentum conservation
+c     -----------------------------------------------------------------
+      if(((icontri.eq.2).or.(icontri.eq.4))
+     1       .and.(dsqrt(s1_eff).le.(am_y+invm))) return
+      if(((icontri.eq.3).or.(icontri.eq.4))
+     1       .and.(dsqrt(s2_eff).le.(am_x+invm))) return
+
+c     =================================================================
+c     >>> TO THE OUTPUT COMMON BLOCK
+c     =================================================================
+
+c     =================================================================
+c     four-momenta of the outgoing protons (or remnants)
+c     =================================================================
+
+      px_plus = (1.d0-x1) * p1_plus
+      px_minus = ((a_nuc1*am_x)**2 + q1tx**2 + q1ty**2)/2.d0/px_plus
+
+      px(1) = - q1tx
+      px(2) = - q1ty
+      px(3) = (px_plus - px_minus)/dsqrt(2.d0)
+      px(4) = (px_plus + px_minus)/dsqrt(2.d0)
+
+      py_minus = (1.d0-x2) * p2_minus
+      py_plus =  ((a_nuc2*am_y)**2 + q2tx**2 + q2ty**2)/2.d0/py_minus
+
+      py(1) = - q2tx
+      py(2) = - q2ty
+      py(3) = (py_plus - py_minus)/dsqrt(2.d0)
+      py(4) = (py_plus + py_minus)/dsqrt(2.d0)
+
+      q1t = dsqrt(q1t2)
+      q2t = dsqrt(q2t2)
+
+c     =================================================================
+c     four-momenta of the outgoing central particles
+c     =================================================================
+
+      nout = 2
+
+      ipdg(1) = pdg_l
+      pc(1,1) = pt1x
+      pc(2,1) = pt1y
+      pc(3,1) = alpha1*ak1z + beta1*ak2z
+      pc(4,1) = alpha1*ak10 + beta1*ak20
+
+      ipdg(2) = -pdg_l
+      pc(1,2) = pt2x
+      pc(2,2) = pt2y
+      pc(3,2) = alpha2*ak1z + beta2*ak2z
+      pc(4,2) = alpha2*ak10 + beta2*ak20
+
+      eta1 = 0.5d0*dlog((dsqrt(amt1**2*(dcosh(y1))**2 - am_l**2) +
+     2       amt1*dsinh(y1))/(dsqrt(amt1**2*(dcosh(y1))**2 - am_l**2)
+     3     - amt1*dsinh(y1)))
+
+      eta2 = 0.5d0*dlog((dsqrt(amt2**2*(dcosh(y2))**2 - am_l**2) +
+     2       amt2*dsinh(y2))/(dsqrt(amt2**2*(dcosh(y2))**2 - am_l**2)
+     3     - amt2*dsinh(y2)))
+
+      if(ieta.eq.1) then
+        if(eta1.lt.eta_min.or.eta1.gt.eta_max) return
+        if(eta2.lt.eta_min.or.eta2.gt.eta_max) return
+      endif
+
+c     =================================================================
+c     matrix element squared
+c     averaged over initial spin polarizations
+c     and summed over final spin polarizations
+c     (--> see Wolfgang's notes
+c     =================================================================
+
+c     =================================================================
+c     Mendelstam variables
+c     =================================================================
+
+      that1 = (q10-pc(1,4))**2
+     &       -(q1tx-pc(1,1))**2-(q1ty-pc(2,1))**2-(q1z-pc(3,1))**2
+      uhat1 = (q10-pc(2,4))**2
+     &       -(q1tx-pc(1,2))**2-(q1ty-pc(2,2))**2-(q1z-pc(3,2))**2
+      that2 = (q20-pc(2,4))**2
+     &       -(q2tx-pc(1,2))**2-(q2ty-pc(2,2))**2-(q2z-pc(3,2))**2
+      uhat2 = (q20-pc(1,4))**2
+     &       -(q2tx-pc(1,1))**2-(q2ty-pc(2,1))**2-(q2z-pc(3,1))**2
+
+      that = (that1+that2)/2.d0
+      uhat = (uhat1+uhat2)/2.d0
+
+c     =================================================================
+c     matrix elements
+c     =================================================================
+c     How matrix element is calculated
+c         imethod = 0: on-shell formula
+c         imethod = 1: off-shell formula
+c     =================================================================
+      if(imethod.eq.0) then
+c     =================================================================
+c     on-shell formula for M^2
+c     =================================================================
+      term1 = 6.d0*am_l**8
+      term2 = -3.d0*am_l**4*that**2
+      term3 = -14.d0*am_l**4*that*uhat
+      term4 = -3.d0*am_l**4*uhat**2
+      term5 = am_l**2*that**3
+      term6 = 7.d0*am_l**2*that**2*uhat
+      term7 = 7.d0*am_l**2*that*uhat**2
+      term8 = am_l**2*uhat**3
+      term9  = -that**3*uhat
+      term10 = -that*uhat**3
+
+      amat2 = -2.d0*(  term1+term2+term3+term4+term5
+     2                    +term6+term7+term8+term9+term10 )
+     3             / ( (am_l**2-that)**2 * (am_l**2-uhat)**2 )
+
+      elseif(imethod.eq.1)then
+c     =================================================================
+c     Wolfgang's formulae
+c     =================================================================
+
+      ak1_x = z1m*pt1x-z1p*pt2x
+      ak1_y = z1m*pt1y-z1p*pt2y
+
+      ak2_x = z2m*pt1x-z2p*pt2x
+      ak2_y = z2m*pt1y-z2p*pt2y
+
+      !FIXME FIXME FIXME am_p or am_A/B???
+      t1abs = (q1t2+x1*(am_x**2-am_p**2)+x1**2*am_p**2)/(1.d0-x1)
+      t2abs = (q2t2+x2*(am_y**2-am_p**2)+x2**2*am_p**2)/(1.d0-x2)
+
+      eps12 = am_l**2 + z1p*z1m*t1abs
+      eps22 = am_l**2 + z2p*z2m*t2abs
+
+      Phi10 = 1.d0/((ak1_x+z1p*q2tx)**2+(ak1_y+z1p*q2ty)**2+eps12)
+     2      - 1.d0/((ak1_x-z1m*q2tx)**2+(ak1_y-z1m*q2ty)**2+eps12)
+      Phi11_x = (ak1_x+z1p*q2tx)/
+     2          ((ak1_x+z1p*q2tx)**2+(ak1_y+z1p*q2ty)**2+eps12)
+     3        - (ak1_x-z1m*q2tx)/
+     4          ((ak1_x-z1m*q2tx)**2+(ak1_y-z1m*q2ty)**2+eps12)
+      Phi11_y = (ak1_y+z1p*q2ty)/
+     2          ((ak1_x+z1p*q2tx)**2+(ak1_y+z1p*q2ty)**2+eps12)
+     3        - (ak1_y-z1m*q2ty)/
+     4          ((ak1_x-z1m*q2tx)**2+(ak1_y-z1m*q2ty)**2+eps12)
+
+      Phi102 = Phi10*Phi10
+      Phi112 = Phi11_x**2+Phi11_y**2
+
+      Phi20 = 1.d0/((ak2_x+z2p*q1tx)**2+(ak2_y+z2p*q1ty)**2+eps22)
+     2      - 1.d0/((ak2_x-z2m*q1tx)**2+(ak2_y-z2m*q1ty)**2+eps22)
+
+      Phi21_x = (ak2_x+z2p*q1tx)/
+     2          ((ak2_x+z2p*q1tx)**2+(ak2_y+z2p*q1ty)**2+eps22)
+     3        - (ak2_x-z2m*q1tx)/
+     4          ((ak2_x-z2m*q1tx)**2+(ak2_y-z2m*q1ty)**2+eps22)
+      Phi21_y = (ak2_y+z2p*q1ty)/
+     2          ((ak2_x+z2p*q1tx)**2+(ak2_y+z2p*q1ty)**2+eps22)
+     3        - (ak2_y-z2m*q1ty)/
+     4          ((ak2_x-z2m*q1tx)**2+(ak2_y-z2m*q1ty)**2+eps22)
+
+      Phi202 = Phi20*Phi20
+      Phi212 = Phi21_x**2+Phi21_y**2
+
+      Phi11_dot_e = (Phi11_x*q1tx + Phi11_y*q1ty)/dsqrt(q1t2)
+      Phi11_cross_e = (Phi11_x*q1ty - Phi11_y*q1tx)/dsqrt(q1t2)
+
+      Phi21_dot_e = (Phi21_x*q2tx +Phi21_y*q2ty)/dsqrt(q2t2)
+      Phi21_cross_e = (Phi21_x*q2ty -Phi21_y*q2tx)/dsqrt(q2t2)
+
+      aux2_1 = iterm11*(am_l**2+4.d0*z1p**2*z1m**2*t1abs)*Phi102
+     1      +iterm22*( (z1p**2 + z1m**2)*(Phi11_dot_e**2 +
+     2      Phi11_cross_e**2) )
+     3      + itermtt*( Phi11_cross_e**2 - Phi11_dot_e**2)
+     4      - iterm12*4.d0*z1p*z1m*(z1p-z1m)*Phi10
+     5      *(q1tx*Phi11_x+q1ty*Phi11_y)
+
+      aux2_2 = iterm11*(am_l**2+4.d0*z2p**2*z2m**2*t2abs)*Phi202
+     1     +iterm22*( (z2p**2 + z2m**2)*(Phi21_dot_e**2 +
+     2     Phi21_cross_e**2) )
+     3     + itermtt*( Phi21_cross_e**2 - Phi21_dot_e**2)
+     4     - iterm12*4.d0*z2p*z2m*(z2p-z2m)*Phi20
+     5     *(q2tx*Phi21_x+q2ty*Phi21_y)
+
+c     =================================================================
+c     convention of matrix element as in our kt-factorization
+c     for heavy flavours
+c     =================================================================
+
+      amat2_1 = (x1*x2*s)**2 * aux2_1 * 2.*z1p*z1m*q1t2 / (q1t2*q2t2)
+      amat2_2 = (x1*x2*s)**2 * aux2_2 * 2.*z2p*z2m*q2t2 / (q1t2*q2t2)
+
+c     =================================================================
+c     symmetrization
+c     =================================================================
+
+      amat2 = (imat1*amat2_1 + imat2*amat2_2)/2.d0
+
+      endif
+
+      coupling = 1.d0
+c     =================================================================
+c     first parton coupling
+c     =================================================================
+      if(iflux1.ge.20.and.iflux1.lt.40) then ! at least one gluon exchanged
+#ifdef ALPHA_S
+        t_max = max(amt1**2,amt2**2)
+        amu2 = max(eps12,t_max)
+        am_x = dsqrt(amu2)
+        coupling = coupling * 4.d0*pi*alphaS(am_x)/2.d0 ! colour flow
+#else
+        print *,'alphaS not linked to this instance!'
+        stop
+#endif
+      else ! photon exchange
+        coupling = coupling * 4.d0*pi*alpha_em*q_l**2
+      endif
+c     =================================================================
+c     second parton coupling
+c     =================================================================
+      coupling = coupling * 4.d0*pi*alpha_em*q_l**2 ! photon exchange
+      coupling = coupling * 3.d0
+
+c     ============================================
+c     unintegrated parton distributions
+c     ============================================
+
+      if(a_nuc1.le.1) then
+        f1 = CepGen_kT_flux(iflux1,x1,q1t2,sfmod,am_x)
+      else
+        f1 = CepGen_kT_flux_HI(iflux1,x1,q1t2,a_nuc1,z_nuc1)
+      endif
+      if(a_nuc2.le.1) then
+        f2 = CepGen_kT_flux(iflux2,x2,q2t2,sfmod,am_y)
+      else
+        f2 = CepGen_kT_flux_HI(iflux2,x2,q2t2,a_nuc2,z_nuc2)
+      endif
+
+c     =================================================================
+c     factor 2.*pi below from integration over phi_sum
+c     factor 1/4 below from jacobian of transformations
+c     factors 1/pi and 1/pi due to integration
+c     over d^2 kappa_1 d^2 kappa_2 instead d kappa_1^2 d kappa_2^2
+c     =================================================================
+
+      aintegral = (2.d0*pi)*1.d0/(16.d0*pi**2*(x1*x2*s)**2) * amat2
+     &          * coupling
+     &          * f1/pi * f2/pi * (1.d0/4.d0) * units
+     &          * 0.5d0 * 4.0d0 / (4.d0*pi)
+
+c     *****************************************************************
+c     =================================================================
+      aintegrand = aintegral*q1t*q2t*ptdiff
+c     =================================================================
+c     *****************************************************************
+c      print *,aintegrand,aintegral,coupling
+
+      return
+      end
diff --git a/CepGen/Processes/FortranKTProcess.cpp b/CepGen/Processes/FortranKTProcess.cpp
new file mode 100644
index 0000000..c5c6f01
--- /dev/null
+++ b/CepGen/Processes/FortranKTProcess.cpp
@@ -0,0 +1,187 @@
+#include "CepGen/Processes/FortranKTProcess.h"
+#include "CepGen/Core/ParametersList.h"
+
+#include "CepGen/StructureFunctions/StructureFunctions.h"
+#include "CepGen/Event/Event.h"
+
+#include "CepGen/Physics/KTFlux.h"
+#include "CepGen/Physics/Constants.h"
+#include "CepGen/Physics/PDG.h"
+
+extern "C"
+{
+  struct Constants {
+    double m_p, units, pi, alpha_em;
+  };
+  struct Parameters {
+    int icontri, iflux1, iflux2, imethod, sfmod, pdg_l, a_nuc1, z_nuc1, a_nuc2, z_nuc2;
+    double inp1, inp2;
+  };
+  struct KtKinematics {
+   double q1t, q2t, phiq1t, phiq2t, y1, y2, ptdiff, phiptdiff, m_x, m_y;
+  };
+  struct KinematicsCuts {
+    int ipt, iene, ieta, iinvm, iptsum, idely;
+    double pt_min, pt_max, ene_min, ene_max, eta_min, eta_max;
+    double invm_min, invm_max, ptsum_min, ptsum_max;
+    double dely_min, dely_max;
+  };
+  struct EventKinematics {
+    double px[4], py[4];
+    int nout, idum, pdg[4];
+    double pc[4][4];
+  };
+
+  extern Constants constants_;
+  extern Parameters params_;
+  extern KtKinematics ktkin_;
+  extern KinematicsCuts kincuts_;
+  extern EventKinematics evtkin_;
+}
+
+namespace CepGen
+{
+  namespace Process
+  {
+    FortranKTProcess::FortranKTProcess( const ParametersList& params, const char* name, const char* descr, std::function<void( double& )> func ) :
+      GenericKTProcess( params, name, descr, { { PDG::photon, PDG::photon } }, { PDG::muon, PDG::muon } ),
+      pair_( params.get<int>( "pair", 13 ) ),
+      method_( params.get<int>( "method", 1 ) ),
+      func_( func )
+    {
+      constants_.m_p = ParticleProperties::mass( PDG::proton );
+      constants_.units = Constants::GeV2toBarn;
+      constants_.pi = M_PI;
+      constants_.alpha_em = Constants::alphaEM;
+    }
+
+    void
+    FortranKTProcess::preparePhaseSpace()
+    {
+      mom_ip1_ = event_->getOneByRole( Particle::IncomingBeam1 ).momentum();
+      mom_ip2_ = event_->getOneByRole( Particle::IncomingBeam2 ).momentum();
+
+      registerVariable( y1_, Mapping::linear, cuts_.cuts.central.rapidity_single, { -6., 6. }, "First central particle rapidity" );
+      registerVariable( y2_, Mapping::linear, cuts_.cuts.central.rapidity_single, { -6., 6. }, "Second central particle rapidity" );
+      registerVariable( pt_diff_, Mapping::linear, cuts_.cuts.central.pt_diff, { 0., 50. }, "Transverse momentum difference between central particles" );
+      registerVariable( phi_pt_diff_, Mapping::linear, cuts_.cuts.central.phi_pt_diff, { 0., 2.*M_PI }, "Central particles azimuthal angle difference" );
+
+      //===========================================================================================
+      // feed phase space cuts to the common block
+      //===========================================================================================
+
+      cuts_.cuts.central.pt_single.save( (bool&)kincuts_.ipt, kincuts_.pt_min, kincuts_.pt_max );
+      cuts_.cuts.central.energy_single.save( (bool&)kincuts_.iene, kincuts_.ene_min, kincuts_.ene_max );
+      cuts_.cuts.central.eta_single.save( (bool&)kincuts_.ieta, kincuts_.eta_min, kincuts_.eta_max );
+      cuts_.cuts.central.mass_sum.save( (bool&)kincuts_.iinvm, kincuts_.invm_min, kincuts_.invm_max );
+      cuts_.cuts.central.pt_sum.save( (bool&)kincuts_.iptsum, kincuts_.ptsum_min, kincuts_.ptsum_max );
+      cuts_.cuts.central.rapidity_diff.save( (bool&)kincuts_.idely, kincuts_.dely_min, kincuts_.dely_max );
+
+      //===========================================================================================
+      // feed run parameters to the common block
+      //===========================================================================================
+
+      params_.icontri = (int)cuts_.mode;
+      params_.imethod = method_;
+      params_.sfmod = (int)cuts_.structure_functions->type;
+      params_.pdg_l = pair_;
+
+      //-------------------------------------------------------------------------------------------
+      // incoming beams information
+      //-------------------------------------------------------------------------------------------
+
+      params_.inp1 = cuts_.incoming_beams.first.pz;
+      params_.inp2 = cuts_.incoming_beams.second.pz;
+      const HeavyIon in1 = (HeavyIon)cuts_.incoming_beams.first.pdg;
+      if ( in1 ) {
+        params_.a_nuc1 = in1.A;
+        params_.z_nuc1 = (unsigned short)in1.Z;
+        if ( params_.z_nuc1 > 1 ) {
+          event_->getOneByRole( Particle::IncomingBeam1 ).setPdgId( (PDG)in1 );
+          event_->getOneByRole( Particle::OutgoingBeam1 ).setPdgId( (PDG)in1 );
+        }
+      }
+      else
+        params_.a_nuc1 = params_.z_nuc1 = 1;
+
+      const HeavyIon in2 = (HeavyIon)cuts_.incoming_beams.second.pdg;
+      if ( in2 ) {
+        params_.a_nuc2 = in2.A;
+        params_.z_nuc2 = (unsigned short)in2.Z;
+        if ( params_.z_nuc2 > 1 ) {
+          event_->getOneByRole( Particle::IncomingBeam2 ).setPdgId( (PDG)in2 );
+          event_->getOneByRole( Particle::OutgoingBeam2 ).setPdgId( (PDG)in2 );
+        }
+      }
+      else
+        params_.a_nuc2 = params_.z_nuc2 = 1;
+
+      //-------------------------------------------------------------------------------------------
+      // intermediate partons information
+      //-------------------------------------------------------------------------------------------
+
+      params_.iflux1 = (int)cuts_.incoming_beams.first.kt_flux;
+      params_.iflux2 = (int)cuts_.incoming_beams.second.kt_flux;
+      if ( (KTFlux)params_.iflux1 == KTFlux::P_Gluon_KMR )
+        event_->getOneByRole( Particle::Parton1 ).setPdgId( PDG::gluon );
+      if ( (KTFlux)params_.iflux2 == KTFlux::P_Gluon_KMR )
+        event_->getOneByRole( Particle::Parton2 ).setPdgId( PDG::gluon );
+    }
+
+    double
+    FortranKTProcess::computeKTFactorisedMatrixElement()
+    {
+      ktkin_.q1t = qt1_;
+      ktkin_.q2t = qt2_;
+      ktkin_.phiq1t = phi_qt1_;
+      ktkin_.phiq2t = phi_qt2_;
+      ktkin_.y1 = y1_;
+      ktkin_.y2 = y2_;
+      ktkin_.ptdiff = pt_diff_;
+      ktkin_.phiptdiff = phi_pt_diff_;
+      ktkin_.m_x = MX_;
+      ktkin_.m_y = MY_;
+      double weight = 0.;
+      func_( weight );
+      return weight;
+    }
+
+    void
+    FortranKTProcess::fillCentralParticlesKinematics()
+    {
+      //===========================================================================================
+      // outgoing beam remnants
+      //===========================================================================================
+
+      PX_ = Particle::Momentum( evtkin_.px );
+      PY_ = Particle::Momentum( evtkin_.py );
+      // express these momenta per nucleon
+      PX_ *= 1./params_.a_nuc1;
+      PY_ *= 1./params_.a_nuc2;
+
+//std::cout << PX_ << PY_ << std::endl;
+
+      //===========================================================================================
+      // intermediate partons
+      //===========================================================================================
+
+      const Particle::Momentum mom_par1 = mom_ip1_-PX_, mom_par2 = mom_ip2_-PY_;
+      event_->getOneByRole( Particle::Parton1 ).setMomentum( mom_par1 );
+      event_->getOneByRole( Particle::Parton2 ).setMomentum( mom_par2 );
+      event_->getOneByRole( Particle::Intermediate ).setMomentum( mom_par1+mom_par2 );
+
+      //===========================================================================================
+      // central system
+      //===========================================================================================
+
+      Particles& oc = event_->getByRole( Particle::CentralSystem );
+      for ( int i = 0; i < evtkin_.nout; ++i ) {
+        auto& p = oc[i];
+        p.setPdgId( evtkin_.pdg[i] );
+        p.setStatus( Particle::Status::FinalState );
+        p.setMomentum( Particle::Momentum( evtkin_.pc[i] ) );
+      }
+    }
+  }
+}
+
diff --git a/CepGen/Processes/FortranKTProcess.h b/CepGen/Processes/FortranKTProcess.h
new file mode 100644
index 0000000..af15df0
--- /dev/null
+++ b/CepGen/Processes/FortranKTProcess.h
@@ -0,0 +1,35 @@
+#ifndef CepGen_Processes_FortranKTProcess_h
+#define CepGen_Processes_FortranKTProcess_h
+
+#include "GenericKTProcess.h"
+#include <functional>
+
+namespace CepGen
+{
+  namespace Process
+  {
+    /// Compute the matrix element for a generic \f$k_T\f$-factorised process defined in a Fortran subroutine
+    class FortranKTProcess : public GenericKTProcess
+    {
+      public:
+        FortranKTProcess( const ParametersList& params, const char* name, const char* descr, std::function<void(double&)> func );
+        ProcessPtr clone() const override { return ProcessPtr( new FortranKTProcess( *this ) ); }
+
+      private:
+        void preparePhaseSpace() override;
+        double computeKTFactorisedMatrixElement() override;
+        void fillCentralParticlesKinematics() override;
+
+        int pair_;
+        int method_;
+        /// Subroutine to be called for weight computation
+        std::function<void(double&)> func_;
+        double y1_, y2_, pt_diff_, phi_pt_diff_;
+
+        Particle::Momentum mom_ip1_, mom_ip2_;
+    };
+  }
+}
+
+#endif
+
diff --git a/CepGen/Processes/GenericKTProcess.cpp b/CepGen/Processes/GenericKTProcess.cpp
index b8105a4..df22774 100644
--- a/CepGen/Processes/GenericKTProcess.cpp
+++ b/CepGen/Processes/GenericKTProcess.cpp
@@ -1,356 +1,370 @@
 #include "CepGen/Processes/GenericKTProcess.h"
 
 #include "CepGen/Core/Exception.h"
 #include "CepGen/Core/ParametersList.h"
 
 #include "CepGen/Event/Event.h"
 
 #include "CepGen/StructureFunctions/StructureFunctions.h"
 #include "CepGen/StructureFunctions/SigmaRatio.h"
 
 #include "CepGen/Physics/Constants.h"
 #include "CepGen/Physics/KTFlux.h"
 #include "CepGen/Physics/FormFactors.h"
 #include "CepGen/Physics/PDG.h"
 
 #include <iomanip>
 
 namespace CepGen
 {
   namespace Process
   {
     GenericKTProcess::GenericKTProcess( const ParametersList& params,
                                         const std::string& name,
                                         const std::string& description,
                                         const std::array<PDG,2>& partons,
                                         const std::vector<PDG>& central ) :
       GenericProcess( name, description+" (kT-factorisation approach)" ),
       num_dimensions_( 0 ), kt_jacobian_( 0. ),
       qt1_( 0. ), phi_qt1_( 0. ), qt2_( 0. ), phi_qt2_( 0. ),
       kIntermediateParts( partons ), kProducedParts( central )
     {}
 
     void
     GenericKTProcess::addEventContent()
     {
       GenericProcess::setEventContent(
         { // incoming state
           { Particle::IncomingBeam1, PDG::proton },
           { Particle::IncomingBeam2, PDG::proton },
           { Particle::Parton1, kIntermediateParts[0] },
           { Particle::Parton2, kIntermediateParts[1] }
         },
         { // outgoing state
           { Particle::OutgoingBeam1, { PDG::proton } },
           { Particle::OutgoingBeam2, { PDG::proton } },
           { Particle::CentralSystem, kProducedParts }
         }
       );
       setExtraContent();
     }
 
     unsigned int
     GenericKTProcess::numDimensions() const
     {
       return num_dimensions_;
     }
 
     void
     GenericKTProcess::setKinematics( const Kinematics& kin )
     {
       cuts_ = kin;
 
       const KTFlux flux1 = (KTFlux)cuts_.incoming_beams.first.kt_flux,
                    flux2 = (KTFlux)cuts_.incoming_beams.second.kt_flux;
 
       if ( cuts_.mode == KinematicsMode::invalid ) {
-        bool el1 = ( flux1 == KTFlux::P_Photon_Elastic );
-        bool el2 = ( flux2 == KTFlux::P_Photon_Elastic );
+        bool el1 = ( flux1 == KTFlux::P_Photon_Elastic
+                  || flux1 == KTFlux::HI_Photon_Elastic
+                  || flux1 == KTFlux::P_Gluon_KMR );
+        bool el2 = ( flux2 == KTFlux::P_Photon_Elastic
+                  || flux2 == KTFlux::HI_Photon_Elastic
+                  || flux2 == KTFlux::P_Gluon_KMR );
         if ( el1 && el2 )
           cuts_.mode = KinematicsMode::ElasticElastic;
         else if ( el1 )
           cuts_.mode = KinematicsMode::ElasticInelastic;
         else if ( el2 )
           cuts_.mode = KinematicsMode::InelasticElastic;
         else
           cuts_.mode = KinematicsMode::InelasticInelastic;
       }
       else {
         //==========================================================================================
         // ensure the first incoming flux is compatible with the kinematics mode
         //==========================================================================================
         if ( ( cuts_.mode == KinematicsMode::ElasticElastic
             || cuts_.mode == KinematicsMode::ElasticInelastic )
           && ( flux1 != KTFlux::P_Photon_Elastic ) ) {
-          cuts_.incoming_beams.first.kt_flux = KTFlux::P_Photon_Elastic;
+          cuts_.incoming_beams.first.kt_flux = ( (HeavyIon)cuts_.incoming_beams.first.pdg )
+            ? KTFlux::HI_Photon_Elastic
+            : KTFlux::P_Photon_Elastic;
           CG_DEBUG( "GenericKTProcess:kinematics" )
             << "Set the kt flux for first incoming photon to \""
             << cuts_.incoming_beams.first.kt_flux << "\".";
         }
         else if ( flux1 != KTFlux::P_Photon_Inelastic
                && flux1 != KTFlux::P_Photon_Inelastic_Budnev ) {
+          if ( (HeavyIon)cuts_.incoming_beams.first.pdg )
+            throw CG_FATAL( "GenericKTProcess:kinematics" )
+              << "Inelastic photon emission from HI not yet supported!";
           cuts_.incoming_beams.first.kt_flux = KTFlux::P_Photon_Inelastic_Budnev;
           CG_DEBUG( "GenericKTProcess:kinematics" )
             << "Set the kt flux for first incoming photon to \""
             << cuts_.incoming_beams.first.kt_flux << "\".";
         }
         //==========================================================================================
         // ensure the second incoming flux is compatible with the kinematics mode
         //==========================================================================================
         if ( ( cuts_.mode == KinematicsMode::ElasticElastic
             || cuts_.mode == KinematicsMode::InelasticElastic )
           && ( flux2 != KTFlux::P_Photon_Elastic ) ) {
-          cuts_.incoming_beams.second.kt_flux = KTFlux::P_Photon_Elastic;
+          cuts_.incoming_beams.second.kt_flux = ( (HeavyIon)cuts_.incoming_beams.second.pdg )
+            ? KTFlux::HI_Photon_Elastic
+            : KTFlux::P_Photon_Elastic;
           CG_DEBUG( "GenericKTProcess:kinematics" )
             << "Set the kt flux for second incoming photon to \""
             << cuts_.incoming_beams.second.kt_flux << "\".";
         }
         else if ( flux2 != KTFlux::P_Photon_Inelastic
                && flux2 != KTFlux::P_Photon_Inelastic_Budnev ) {
+          if ( (HeavyIon)cuts_.incoming_beams.second.pdg )
+            throw CG_FATAL( "GenericKTProcess:kinematics" )
+              << "Inelastic photon emission from HI not yet supported!";
           cuts_.incoming_beams.second.kt_flux = KTFlux::P_Photon_Inelastic_Budnev;
           CG_DEBUG( "GenericKTProcess:kinematics" )
             << "Set the kt flux for second incoming photon to \""
             << cuts_.incoming_beams.second.kt_flux << "\".";
         }
       }
 
       //============================================================================================
       // initialise the "constant" (wrt x) part of the Jacobian
       //============================================================================================
 
       kt_jacobian_ = 1.;
       num_dimensions_ = 0;
       mapped_variables_.clear();
 
       //============================================================================================
       // register the incoming partons' variables
       //============================================================================================
 
       registerVariable( qt1_, Mapping::logarithmic, cuts_.cuts.initial.qt, { 1.e-10, 500. }, "First incoming parton virtuality" );
       registerVariable( qt2_, Mapping::logarithmic, cuts_.cuts.initial.qt, { 1.e-10, 500. }, "Second incoming parton virtuality" );
       registerVariable( phi_qt1_, Mapping::linear, cuts_.cuts.initial.phi_qt, { 0., 2.*M_PI }, "First incoming parton azimuthal angle" );
       registerVariable( phi_qt2_, Mapping::linear, cuts_.cuts.initial.phi_qt, { 0., 2.*M_PI }, "Second incoming parton azimuthal angle" );
 
       //============================================================================================
       // register all process-dependent variables
       //============================================================================================
 
       preparePhaseSpace();
 
       //============================================================================================
       // register the outgoing remnants' variables
       //============================================================================================
 
       MX_ = MY_ = event_->getOneByRole( Particle::IncomingBeam1 ).mass();
       if ( cuts_.mode == KinematicsMode::InelasticElastic || cuts_.mode == KinematicsMode::InelasticInelastic )
         registerVariable( MX_, Mapping::square, cuts_.cuts.remnants.mass_single, { 1.07, 1000. }, "Positive z proton remnant mass" );
       if ( cuts_.mode == KinematicsMode::ElasticInelastic || cuts_.mode == KinematicsMode::InelasticInelastic )
         registerVariable( MY_, Mapping::square, cuts_.cuts.remnants.mass_single, { 1.07, 1000. }, "Negative z proton remnant mass" );
 
       prepareKinematics();
     }
 
     double
     GenericKTProcess::computeWeight()
     {
       if ( mapped_variables_.size() == 0 )
         throw CG_FATAL( "GenericKTProcess:weight" )
           << "No variables are mapped with this process!";
       if ( kt_jacobian_ == 0. )
         throw CG_FATAL( "GenericKTProcess:weight" )
           << "Point-independant component of the Jacobian for this "
           << "kt-factorised process is null.\n\t"
           << "Please check the validity of the phase space!";
 
       //============================================================================================
       // generate and initialise all variables, and auxiliary (x-dependent) part of the Jacobian
       // for this phase space point.
       //============================================================================================
 
       const double aux_jacobian = generateVariables();
       if ( aux_jacobian <= 0. )
         return 0.;
 
       //============================================================================================
       // compute the integrand and combine together into a single weight for the phase space point.
       //============================================================================================
 
       const double integrand = computeKTFactorisedMatrixElement();
       if ( integrand <= 0. )
         return 0.;
 
       const double weight = ( kt_jacobian_*aux_jacobian ) * integrand;
 
       CG_DEBUG_LOOP( "GenericKTProcess:weight" )
         << "Jacobian: " << kt_jacobian_ << " * " << aux_jacobian
         << " = " << ( kt_jacobian_*aux_jacobian ) << ".\n\t"
         << "Integrand = " << integrand << "\n\t"
         << "dW = " << weight << ".";
 
       return weight;
     }
 
     std::pair<double,double>
     GenericKTProcess::incomingFluxes( double x1, double q1t2, double x2, double q2t2 ) const
     {
       //--- compute fluxes according to modelling specified in parameters card
       std::pair<double,double> fluxes = {
         ktFlux( (KTFlux)cuts_.incoming_beams.first.kt_flux, x1, q1t2, *cuts_.structure_functions, MX_ ),
         ktFlux( (KTFlux)cuts_.incoming_beams.second.kt_flux, x2, q2t2, *cuts_.structure_functions, MY_ )
       };
 
       CG_DEBUG_LOOP( "GenericKTProcess:fluxes" )
         << "KT fluxes: " << fluxes.first << " / " << fluxes.second << ".";
       return fluxes;
     }
 
     void
     GenericKTProcess::registerVariable( double& out, const Mapping& type,
                                         const Limits& in, Limits default_limits,
                                         const char* description )
     {
       Limits lim = in;
       out = 0.; // reset the variable
       if ( !in.valid() ) {
         CG_DEBUG( "GenericKTProcess:registerVariable" )
           << description << " could not be retrieved from the user configuration!\n\t"
           << "Setting it to the default value: " << default_limits << ".";
         lim = default_limits;
       }
       if ( type == Mapping::logarithmic )
         lim = {
           std::max( log( lim.min() ), -10. ),
           std::min( log( lim.max() ), +10. )
         };
       mapped_variables_.emplace_back( MappingVariable{ description, lim, out, type, num_dimensions_++ } );
       switch ( type ) {
         case Mapping::square:
           kt_jacobian_ *= 2.*lim.range();
           break;
         default:
           kt_jacobian_ *= lim.range();
           break;
       }
       CG_DEBUG( "GenericKTProcess:registerVariable" )
         << description << " has been mapped to variable " << num_dimensions_ << ".\n\t"
         << "Allowed range for integration: " << lim << ".\n\t"
         << "Variable integration mode: " << type << ".";
     }
 
     void
     GenericKTProcess::dumpVariables() const
     {
       std::ostringstream os;
       for ( const auto& var : mapped_variables_ )
         os << "\n\t(" << var.index << ") " << var.type << " mapping (" << var.description << ") in range " << var.limits;
       CG_INFO( "GenericKTProcess:dumpVariables" )
         << "List of variables handled by this kt-factorised process:"
         << os.str();
     }
 
     double
     GenericKTProcess::generateVariables() const
     {
       double jacobian = 1.;
       for ( const auto& cut : mapped_variables_ ) {
         if ( !cut.limits.valid() )
           continue;
         const double xv = x( cut.index ); // between 0 and 1
         switch ( cut.type ) {
           case Mapping::linear: {
             cut.variable = cut.limits.x( xv );
           } break;
           case Mapping::logarithmic: {
             cut.variable = exp( cut.limits.x( xv ) );
             jacobian *= cut.variable;
           } break;
           case Mapping::square: {
             cut.variable = cut.limits.x( xv );
             jacobian *= cut.variable;
           } break;
         }
       }
       if ( CG_EXCEPT_MATCH( "KtProcess:vars", debugInsideLoop ) ) {
         std::ostringstream oss;
         for ( const auto& cut : mapped_variables_ ) {
           oss << "variable " << cut.index
               << " in range " << std::left << std::setw( 20 ) << cut.limits << std::right
               << " has value " << cut.variable << "\n\t";
         }
         CG_DEBUG_LOOP( "KtProcess:vars" ) << oss.str();
       }
       return jacobian;
     }
 
     void
     GenericKTProcess::fillKinematics( bool )
     {
       fillCentralParticlesKinematics(); // process-dependent!
       fillPrimaryParticlesKinematics();
     }
 
     void
     GenericKTProcess::fillPrimaryParticlesKinematics()
     {
       //============================================================================================
       //     outgoing protons
       //============================================================================================
 
       Particle& op1 = event_->getOneByRole( Particle::OutgoingBeam1 ),
                &op2 = event_->getOneByRole( Particle::OutgoingBeam2 );
 
       op1.setMomentum( PX_ );
       op2.setMomentum( PY_ );
 
       switch ( cuts_.mode ) {
         case KinematicsMode::ElasticElastic:
           op1.setStatus( Particle::Status::FinalState );
           op2.setStatus( Particle::Status::FinalState );
           break;
         case KinematicsMode::ElasticInelastic:
           op1.setStatus( Particle::Status::FinalState );
           op2.setStatus( Particle::Status::Unfragmented ); op2.setMass( MY_ );
           break;
         case KinematicsMode::InelasticElastic:
           op1.setStatus( Particle::Status::Unfragmented ); op1.setMass( MX_ );
           op2.setStatus( Particle::Status::FinalState );
           break;
         case KinematicsMode::InelasticInelastic:
           op1.setStatus( Particle::Status::Unfragmented ); op1.setMass( MX_ );
           op2.setStatus( Particle::Status::Unfragmented ); op2.setMass( MY_ );
           break;
         default: {
           throw CG_FATAL( "GenericKTProcess" )
             << "This kT factorisation process is intended for p-on-p collisions! Aborting.";
         } break;
       }
 
       //============================================================================================
       //     incoming partons (photons, pomerons, ...)
       //============================================================================================
 
       Particle& g1 = event_->getOneByRole( Particle::Parton1 );
       g1.setMomentum( event_->getOneByRole( Particle::IncomingBeam1 ).momentum()-PX_, true );
 
       Particle& g2 = event_->getOneByRole( Particle::Parton2 );
       g2.setMomentum( event_->getOneByRole( Particle::IncomingBeam2 ).momentum()-PY_, true );
 
       //============================================================================================
       //     two-parton system
       //============================================================================================
 
       event_->getOneByRole( Particle::Intermediate ).setMomentum( g1.momentum()+g2.momentum() );
     }
 
     std::ostream&
     operator<<( std::ostream& os, const GenericKTProcess::Mapping& type )
     {
       switch ( type ) {
         case GenericKTProcess::Mapping::linear: return os << "linear";
         case GenericKTProcess::Mapping::logarithmic: return os << "logarithmic";
         case GenericKTProcess::Mapping::square: return os << "squared";
       }
       return os;
     }
   }
 }
diff --git a/External/alphaS.f b/External/alphaS.f
new file mode 100755
index 0000000..2128285
--- /dev/null
+++ b/External/alphaS.f
@@ -0,0 +1,703 @@
+C----------------------------------------------------------------------
+C--   Stand-alone code for alpha_s cannibalised (with permission)
+C--   from Andreas Vogt's QCD-PEGASUS package (hep-ph/0408244).
+C--   The running coupling alpha_s is obtained at N^mLO (m = 0,1,2,3)
+C--   by solving the renormalisation group equation in the MSbar scheme
+C--   by a fourth-order Runge-Kutta integration.  Transitions from
+C--   n_f to n_f+1 flavours are made when the factorisation scale
+C--   mu_f equals the pole masses m_h (h = c,b,t).  At exactly
+C--   the thresholds m_{c,b,t}, the number of flavours n_f = {3,4,5}.
+C--   The top quark mass should be set to be very large to evolve with
+C--   a maximum of five flavours.  The factorisation scale mu_f may be
+C--   a constant multiple of the renormalisation scale mu_r.  The input
+C--   factorisation scale mu_(f,0) should be less than or equal to
+C--   the charm quark mass.  However, if it is greater than the
+C--   charm quark mass, the value of alpha_s at mu_(f,0) = 1 GeV will
+C--   be found using a root-finding algorithm.
+C--
+C--   Example of usage.
+C--   First call the initialisation routine (only needed once):
+C--
+C--    IORD = 2                  ! perturbative order (N^mLO,m=0,1,2,3)
+C--    FR2 = 1.D0                ! ratio of mu_f^2 to mu_r^2
+C--    MUR = 1.D0                ! input mu_r in GeV
+C--    ASMUR = 0.5D0             ! input value of alpha_s at mu_r
+C--    MC = 1.4D0                ! charm quark mass
+C--    MB = 4.75D0               ! bottom quark mass
+C--    MT = 1.D10                ! top quark mass
+C--    CALL INITALPHAS(IORD, FR2, MUR, ASMUR, MC, MB, MT)
+C--
+C--   Then get alpha_s at a renormalisation scale mu_r with:
+C--
+C--    MUR = 100.D0              ! renormalisation scale in GeV
+C--    ALFAS = ALPHAS(MUR)
+C--
+C----------------------------------------------------------------------
+C--   Comments to Graeme Watt <watt(at)hep.ucl.ac.uk>
+C----------------------------------------------------------------------
+
+      SUBROUTINE INITALPHAS(IORD, FR2, MUR, ASMUR, MC, MB, MT)
+C--   IORD = 0 (LO), 1 (NLO), 2 (NNLO), 3 (NNNLO).
+C--   FR2 = ratio of mu_f^2 to mu_r^2 (must be a fixed value).
+C--   MUR = input renormalisation scale (in GeV) for alpha_s.
+C--   ASMUR = input value of alpha_s at the renormalisation scale MUR.
+C--   MC,MB,MT = heavy quark masses in GeV.
+      IMPLICIT NONE
+      INTEGER IORD,IORDc,MAXF,MODE
+      DOUBLE PRECISION FR2,MUR,ASMUR,MC,MB,MT,EPS,A,B,DZEROX,
+     &     R0c,FR2c,MURc,ASMURc,MCc,MBc,MTc,FINDALPHASR0,R0,ASI
+      COMMON / DZEROXcommon / FR2c,MURc,ASMURc,MCc,MBc,MTc,R0c,IORDc
+      PARAMETER(EPS=1.D-10,MAXF=10000,MODE=1)
+      EXTERNAL FINDALPHASR0
+
+      IF (MUR*sqrt(FR2).LE.MC) THEN ! Check that MUF <= MC.
+         R0 = MUR
+         ASI = ASMUR
+      ELSE                      ! Solve for alpha_s at R0 = 1 GeV.
+C--   Copy variables to common block.
+         R0c = 1.D0/sqrt(FR2)
+         IORDc = IORD
+         FR2c = FR2
+         MURc = MUR
+         ASMURc = ASMUR
+         MCc = MC
+         MBc = MB
+         MTc = MT
+C--   Now get alpha_s(R0) corresponding to alpha_s(MUR).
+         A = 0.02D0              ! lower bound for alpha_s(R0)
+         B = 2.00D0              ! upper bound for alpha_s(R0)
+         R0 = R0c
+         ASI = DZEROX(A,B,EPS,MAXF,FINDALPHASR0,MODE)
+      END IF
+
+      CALL INITALPHASR0(IORD, FR2, R0, ASI, MC, MB, MT)
+
+      RETURN
+      END
+
+C----------------------------------------------------------------------
+
+C--   Find the zero of this function using DZEROX.
+      DOUBLE PRECISION FUNCTION FINDALPHASR0(ASI)
+      IMPLICIT NONE
+      INTEGER IORD
+      DOUBLE PRECISION FR2, R0, ASI, MC, MB, MT, MUR, ASMUR, ALPHAS
+      COMMON / DZEROXcommon / FR2, MUR, ASMUR, MC, MB, MT, R0, IORD
+
+      CALL INITALPHASR0(IORD, FR2, R0, ASI, MC, MB, MT)
+      FINDALPHASR0 = ALPHAS(MUR) - ASMUR ! solve equal to zero
+
+      RETURN
+      END
+
+C----------------------------------------------------------------------
+
+      SUBROUTINE INITALPHASR0(IORD, FR2, R0, ASI, MC, MB, MT)
+C--   IORD = 0 (LO), 1 (NLO), 2 (NNLO), 3 (NNNLO).
+C--   FR2 = ratio of mu_f^2 to mu_r^2 (must be a fixed value).
+C--   R0 = input renormalisation scale (in GeV) for alphas_s.
+C--   ASI = input value of alpha_s at the renormalisation scale R0.
+C--   MC,MB,MT = heavy quark masses in GeV.
+C--   Must have R0*sqrt(FR2) <= MC to call this subroutine.
+      IMPLICIT NONE
+      INTEGER IORD,NAORD,NASTPS,IVFNS,NFF
+      DOUBLE PRECISION FR2,R0,ASI,MC,MB,MT,LOGFR,R20,
+     &     PI,ZETA,CF,CA,TR,AS0,M20,MC2,MB2,MT2
+      PARAMETER(PI = 3.1415 92653 58979 D0)
+
+      COMMON / RZETA  / ZETA(6)
+      COMMON / COLOUR / CF, CA, TR
+      COMMON / ASINP  / AS0, M20
+      COMMON / ASPAR  / NAORD, NASTPS
+      COMMON / VARFLV / IVFNS
+      COMMON / NFFIX  / NFF
+      COMMON / FRRAT  / LOGFR
+
+c
+c ..QCD colour factors
+c
+      CA = 3.D0
+      CF = 4./3.D0
+      TR = 0.5 D0
+c
+c ..The lowest integer values of the Zeta function
+c
+      ZETA(1) = 0.5772 15664 90153 D0
+      ZETA(2) = 1.64493 40668 48226 D0
+      ZETA(3) = 1.20205 69031 59594 D0
+      ZETA(4) = 1.08232 32337 11138 D0
+      ZETA(5) = 1.03692 77551 43370 D0
+      ZETA(6) = 1.01734 30619 84449 D0
+
+      IVFNS = 1                 ! variable flavour-number scheme (VFNS)
+c      IVFNS = 0                 ! fixed flavour-number scheme (FFNS)
+      NFF = 4                   ! number of flavours for FFNS
+      NAORD = IORD              ! perturbative order of alpha_s
+      NASTPS = 20               ! num. steps in Runge-Kutta integration
+      R20 = R0**2               ! input renormalisation scale
+      MC2 = MC**2               ! mu_f^2 for charm threshold
+      MB2 = MB**2               ! mu_f^2 for bottom threshold
+      MT2 = MT**2               ! mu_f^2 for top threshold
+      LOGFR = LOG(FR2)          ! log of ratio of mu_f^2 to mu_r^2
+      M20 = R20 * FR2           ! input factorisation scale
+
+c
+c ..Stop some nonsense
+c
+      IF ( (IVFNS .EQ. 0) .AND. (NFF .LT. 3) ) THEN
+         WRITE (6,*) 'Wrong flavour number for FFNS evolution. STOP'
+         STOP
+      END IF
+      IF ( (IVFNS .EQ. 0) .AND. (NFF .GT. 5) ) THEN
+         WRITE (6,*) 'Wrong flavour number for FFNS evolution. STOP'
+         STOP
+      END IF
+c
+      IF ( NAORD .GT. 3 ) THEN
+         WRITE (6,*) 'Specified order in a_s too high. STOP'
+         STOP
+      END IF
+c
+      IF ( (IVFNS .NE. 0) .AND. (FR2 .GT. 4.001D0) ) THEN
+         WRITE (6,*) 'Too low mu_r for VFNS evolution. STOP'
+         STOP
+      END IF
+c
+      IF ( (IVFNS .EQ. 1) .AND. (M20 .GT. MC2) ) THEN
+         WRITE (6,*) 'Too high mu_0 for VFNS evolution. STOP'
+         STOP
+      END IF
+c
+      IF ( (ASI .GT. 2.D0) .OR. (ASI .LT. 2.D-2) ) THEN
+         WRITE (6,*) 'alpha_s out of range. STOP'
+         STOP
+      END IF
+c
+      IF ( (IVFNS .EQ. 1) .AND. (MC2 .GT. MB2) ) THEN
+         WRITE (6,*) 'Wrong charm-bottom mass hierarchy. STOP'
+         STOP
+      END IF
+      IF ( (IVFNS .EQ. 1) .AND. (MB2 .GT. MT2) ) THEN
+         WRITE (6,*) 'Wrong bottom-top mass hierarchy. STOP'
+         STOP
+      END IF
+c
+
+C--   Store the beta function coefficients in a COMMON block.
+      CALL BETAFCT
+
+C--   Store a_s = alpha_s(mu_r^2)/(4 pi) at the input scale R0.
+      AS0 = ASI / (4.D0* PI)
+
+C--   Store alpha_s at the heavy flavour thresholds in a COMMON block.
+       IF (IVFNS .NE. 0) THEN
+          CALL EVNFTHR (MC2, MB2, MT2)
+       END IF
+
+      RETURN
+      END
+
+C----------------------------------------------------------------------
+
+      DOUBLE PRECISION FUNCTION ALPHAS(MUR)
+      IMPLICIT NONE
+      INTEGER NFF,IVFNS,NF
+      DOUBLE PRECISION PI,LOGFR,AS0,M20,ASC,M2C,ASB,M2B,AST,M2T,M2,MUR,
+     &     R2,ASI,ASF,R20,R2T,R2B,R2C,AS
+      PARAMETER ( PI = 3.1415 92653 58979 D0 )
+c
+c ..Input common blocks
+c
+       COMMON / NFFIX  / NFF
+       COMMON / VARFLV / IVFNS
+       COMMON / FRRAT  / LOGFR
+       COMMON / ASINP  / AS0, M20
+       COMMON / ASFTHR / ASC, M2C, ASB, M2B, AST, M2T
+
+       R2 = MUR**2
+       M2 = R2 * EXP(+LOGFR)
+       IF (IVFNS .EQ. 0) THEN
+c
+c   Fixed number of flavours
+c
+          NF  = NFF
+          R20 = M20 * R2/M2
+          ASI = AS0
+          ASF = AS (R2, R20, AS0, NF)
+c
+      ELSE
+c
+c ..Variable number of flavours
+c
+          IF (M2 .GT. M2T) THEN
+             NF = 6
+             R2T = M2T * R2/M2
+             ASI = AST
+             ASF = AS (R2, R2T, AST, NF)
+c
+          ELSE IF (M2 .GT. M2B) THEN
+             NF = 5
+             R2B = M2B * R2/M2
+             ASI = ASB
+             ASF = AS (R2, R2B, ASB, NF)
+c
+          ELSE IF (M2 .GT. M2C) THEN
+             NF = 4
+             R2C = M2C * R2/M2
+             ASI = ASC
+             ASF = AS (R2, R2C, ASC, NF)
+c
+          ELSE
+             NF = 3
+             R20 = M20 * R2/M2
+             ASI = AS0
+             ASF = AS (R2, R20, AS0, NF)
+c
+          END IF
+c
+       END IF
+c
+c ..Final value of alpha_s
+c
+       ALPHAS = 4.D0*PI*ASF
+
+       RETURN
+       END
+c
+c =================================================================av==
+
+
+c =====================================================================
+c
+c ..The threshold matching of the QCD coupling in the MS(bar) scheme,
+c    a_s = alpha_s(mu_r^2)/(4 pi),  for  NF -> NF + 1  active flavours
+c    up to order a_s^4 (NNNLO).
+c
+c ..The value  ASNF  of a_s for NF flavours at the matching scale, the
+c    logarithm  LOGRH = ln (mu_r^2/m_H^2) -- where m_H is the pole mass
+c    of the heavy quark -- and  NF  are passed as arguments to the
+c    function  ASNF1.  The order of the expansion  NAORD  (defined as
+c    the 'n' in N^nLO) is provided by the common-block  ASPAR.
+c
+c ..The matching coefficients are inverted from Chetyrkin, Kniehl and
+c    Steinhauser, Phys. Rev. Lett. 79 (1997) 2184. The QCD colour
+c    factors have been hard-wired in these results. The lowest integer
+c    values of the Zeta function are given by the common-block  RZETA.
+c
+c =====================================================================
+c
+
+      DOUBLE PRECISION FUNCTION ASNF1 (ASNF, LOGRH, NF)
+c
+      IMPLICIT NONE
+      INTEGER NF, NAORD, NASTPS, PRVCLL, K1, K2
+      DOUBLE PRECISION ASNF,LOGRH,ZETA,CMC,CMCI30,CMCF30,CMCF31,
+     &     CMCI31,ASP,LRHP
+
+      DIMENSION CMC(3,0:3)
+c
+c ---------------------------------------------------------------------
+c
+c ..Input common-blocks
+c
+      COMMON / ASPAR  / NAORD, NASTPS
+      COMMON / RZETA  / ZETA(6)
+c
+c ..Variables to be saved for the next call
+c
+      SAVE CMC, CMCI30, CMCF30, CMCF31, CMCI31, PRVCLL
+c
+c ---------------------------------------------------------------------
+c
+c ..The coupling-constant matching coefficients (CMC's) up to NNNLO
+c   (calculated and saved in the first call of this routine)
+c
+       IF (PRVCLL .NE. 1) THEN
+c
+         CMC(1,0) =  0.D0
+         CMC(1,1) =  2./3.D0
+c
+         CMC(2,0) = 14./3.D0
+         CMC(2,1) = 38./3.D0
+         CMC(2,2) =  4./9.D0
+c
+         CMCI30 = + 80507./432.D0 * ZETA(3) + 58933./1944.D0
+     1            + 128./3.D0 * ZETA(2) * (1.+ DLOG(2.D0)/3.D0)
+         CMCF30 = - 64./9.D0 * (ZETA(2) + 2479./3456.D0)
+         CMCI31 =   8941./27.D0
+         CMCF31 = - 409./27.D0
+         CMC(3,2) = 511./9.D0
+         CMC(3,3) = 8./27.D0
+c
+         PRVCLL = 1
+c
+       END IF
+c
+c ---------------------------------------------------------------------
+c
+c ..The N_f dependent CMC's, and the alpha_s matching at order NAORD
+c
+       CMC(3,0) = CMCI30 + NF * CMCF30
+       CMC(3,1) = CMCI31 + NF * CMCF31
+c
+       ASNF1 = ASNF
+       IF (NAORD .EQ. 0) GO TO 1
+       ASP   = ASNF
+c
+       DO 11 K1 = 1, NAORD
+         ASP = ASP * ASNF
+         LRHP = 1.D0
+c
+       DO 12 K2 = 0, K1
+         ASNF1 = ASNF1 + ASP * CMC(K1,K2) * LRHP
+         LRHP = LRHP * LOGRH
+c
+  12   CONTINUE
+  11   CONTINUE
+c
+c ---------------------------------------------------------------------
+c
+   1   RETURN
+       END
+
+c
+c =================================================================av==
+c
+c ..The subroutine  EVNFTHR  for the evolution of  a_s = alpha_s/(4 pi)
+c    from a three-flavour initial scale to the four- to six-flavour
+c    thresholds (identified with the squares of the corresponding quark
+c    masses).  The results are written to the common-block  ASFTHR.
+c
+c ..The input scale  M20 = mu_(f,0)^2  and the corresponding value
+c    AS0  of a_s  are provided by  ASINP.  The fixed scale logarithm
+c    LOGFR = ln (mu_f^2/mu_r^2) is specified in  FRRAT.  The alpha_s
+c    matching is done by the function ASNF1.
+c
+c =====================================================================
+c
+c
+       SUBROUTINE EVNFTHR (MC2, MB2, MT2)
+c
+       IMPLICIT NONE
+       DOUBLE PRECISION MC2, MB2, MT2, M20, M2C, M2B, M2T, R20, R2C,
+     1                  R2B, R2T, AS, ASNF1, AS0, ASC, ASB, AST,
+     2                  ASC3, ASB4, AST5, LOGFR, SC, SB, ST
+c
+c ---------------------------------------------------------------------
+c
+c ..Input common blocks
+c
+       COMMON / ASINP  / AS0, M20
+       COMMON / FRRAT  / LOGFR
+c
+c ..Output common blocks
+c
+       COMMON / ASFTHR / ASC, M2C, ASB, M2B, AST, M2T
+
+c ---------------------------------------------------------------------
+c
+c ..Coupling constants at and evolution distances to/between thresholds
+c
+       R20 = M20 * EXP(-LOGFR)
+c
+c ..Charm
+c
+       M2C  = MC2
+       R2C  = M2C * R20/M20
+       ASC3 = AS (R2C, R20, AS0, 3)
+       SC   = LOG (AS0 / ASC3)
+       ASC  = ASNF1 (ASC3, -LOGFR, 3)
+c
+c ..Bottom
+c
+       M2B  = MB2
+       R2B  = M2B * R20/M20
+       ASB4 = AS (R2B, R2C, ASC, 4)
+       SB   = LOG (ASC / ASB4)
+       ASB  = ASNF1 (ASB4, -LOGFR, 4)
+c
+c ..Top
+c
+       M2T  = MT2
+       R2T  = M2T * R20/M20
+       AST5 = AS (R2T, R2B, ASB, 5)
+       ST   = LOG (ASB / AST5)
+       AST  = ASNF1 (AST5, -LOGFR, 5)
+
+       RETURN
+       END
+
+c
+c =================================================================av==
+c
+c ..The running coupling of QCD,
+c
+c         AS  =  a_s  =  alpha_s(mu_r^2)/(4 pi),
+c
+c    obtained by integrating the evolution equation for a fixed number
+c    of massless flavours  NF.  Except at leading order (LO),  AS  is
+c    obtained using a fourth-order Runge-Kutta integration.
+c
+c ..The initial and final scales  R20  and  R2,  the value  AS0  at
+c    R20, and  NF  are passed as function arguments.  The coefficients
+c    of the beta function up to  a_s^5 (N^3LO)  are provided by the
+c    common-block  BETACOM.  The order of the expansion  NAORD (defined
+c    as the 'n' in N^nLO) and the number of steps  NASTPS  for the
+c    integration beyond LO are given by the common-block  ASPAR.
+c
+c =====================================================================
+c
+c
+      DOUBLE PRECISION FUNCTION AS (R2, R20, AS0, NF)
+c
+      IMPLICIT NONE
+      INTEGER NFMIN, NFMAX, NF, NAORD, NASTPS, K1
+      DOUBLE PRECISION R2, R20, AS0, SXTH, BETA0, BETA1, BETA2, BETA3,
+     &     FBETA1,FBETA2,FBETA3,A,LRRAT,DLR,XK0,XK1,XK2,XK3
+      PARAMETER (NFMIN = 3, NFMAX = 6)
+      PARAMETER ( SXTH = 0.16666 66666 66666 D0 )
+c
+c ---------------------------------------------------------------------
+c
+c ..Input common-blocks
+c
+       COMMON / ASPAR  / NAORD, NASTPS
+       COMMON / BETACOM   / BETA0 (NFMIN:NFMAX), BETA1 (NFMIN:NFMAX),
+     ,                   BETA2 (NFMIN:NFMAX), BETA3 (NFMIN:NFMAX)
+c
+c ..The beta functions FBETAn at N^nLO for n = 1, 2, and 3
+c
+       FBETA1(A) = - A**2 * ( BETA0(NF) + A *   BETA1(NF) )
+       FBETA2(A) = - A**2 * ( BETA0(NF) + A * ( BETA1(NF)
+     ,                        + A * BETA2(NF) ) )
+       FBETA3(A) = - A**2 * ( BETA0(NF) + A * ( BETA1(NF)
+     ,                        + A * (BETA2(NF) + A * BETA3(NF)) ) )
+c
+c ---------------------------------------------------------------------
+c
+c ..Initial value, evolution distance and step size
+c
+       AS = AS0
+       LRRAT = LOG (R2/R20)
+       DLR = LRRAT / NASTPS
+c
+c ..Solution of the evolution equation depending on  NAORD
+c   (fourth-order Runge-Kutta beyond the leading order)
+c
+       IF (NAORD .EQ. 0) THEN
+c
+         AS = AS0 / (1.+ BETA0(NF) * AS0 * LRRAT)
+c
+       ELSE IF (NAORD .EQ. 1) THEN
+c
+       DO 2 K1 = 1, NASTPS
+         XK0 = DLR * FBETA1 (AS)
+         XK1 = DLR * FBETA1 (AS + 0.5 * XK0)
+         XK2 = DLR * FBETA1 (AS + 0.5 * XK1)
+         XK3 = DLR * FBETA1 (AS + XK2)
+         AS = AS + SXTH * (XK0 + 2.* XK1 + 2.* XK2 + XK3)
+  2    CONTINUE
+c
+       ELSE IF (NAORD .EQ. 2) THEN
+c
+       DO 3 K1 = 1, NASTPS
+         XK0 = DLR * FBETA2 (AS)
+         XK1 = DLR * FBETA2 (AS + 0.5 * XK0)
+         XK2 = DLR * FBETA2 (AS + 0.5 * XK1)
+         XK3 = DLR * FBETA2 (AS + XK2)
+         AS = AS + SXTH * (XK0 + 2.* XK1 + 2.* XK2 + XK3)
+  3    CONTINUE
+c
+       ELSE IF (NAORD .EQ. 3) THEN
+c
+       DO 4 K1 = 1, NASTPS
+         XK0 = DLR * FBETA3 (AS)
+         XK1 = DLR * FBETA3 (AS + 0.5 * XK0)
+         XK2 = DLR * FBETA3 (AS + 0.5 * XK1)
+         XK3 = DLR * FBETA3 (AS + XK2)
+         AS = AS + SXTH * (XK0 + 2.* XK1 + 2.* XK2 + XK3)
+  4    CONTINUE
+       END IF
+c
+c ---------------------------------------------------------------------
+c
+       RETURN
+       END
+
+c
+c =================================================================av==
+c
+c ..The subroutine BETAFCT for the coefficients  BETA0...BETA3  of the
+c    beta function of QCD up to order alpha_s^5 (N^3LO), normalized by
+c
+c
+c        d a_s / d ln mu_r^2  =  - BETA0 a_s^2 - BETA1 a_s^3 - ...
+c
+c    with  a_s = alpha_s/(4*pi).
+c
+c ..The MSbar coefficients are written to the common-block BETACOM for
+c
+c   NF = 3...6  (parameters NFMIN, NFMAX) quark flavours.
+c
+c ..The factors CF, CA and TF  are taken from the common-block  COLOUR.
+c    Beyond NLO the QCD colour factors are hard-wired in this routine,
+c    and the numerical coefficients are truncated to six digits.
+c
+c =====================================================================
+c
+c
+       SUBROUTINE BETAFCT
+c
+       IMPLICIT DOUBLE PRECISION (A - Z)
+       INTEGER NFMIN, NFMAX, NF
+       PARAMETER (NFMIN = 3, NFMAX = 6)
+c
+c ---------------------------------------------------------------------
+c
+c ..Input common-block
+c
+       COMMON / COLOUR / CF, CA, TR
+c
+c ..Output common-block
+c
+       COMMON / BETACOM   / BETA0 (NFMIN:NFMAX), BETA1 (NFMIN:NFMAX),
+     1                   BETA2 (NFMIN:NFMAX), BETA3 (NFMIN:NFMAX)
+
+c
+c ---------------------------------------------------------------------
+c
+c ..The full LO and NLO coefficients
+c
+       B00 =  11./3.D0 * CA
+       B01 =  -4./3.D0 * TR
+       B10 =  34./3.D0 * CA**2
+       B11 = -20./3.D0 * CA*TR - 4.* CF*TR
+c
+c ..Flavour-number loop and output to the array
+c
+       DO 1 NF = NFMIN, NFMAX
+c
+       BETA0(NF) = B00 + B01 * NF
+       BETA1(NF) = B10 + B11 * NF
+c
+       BETA2(NF) = 1428.50 - 279.611 * NF + 6.01852 * NF**2
+       BETA3(NF) = 29243.0 - 6946.30 * NF + 405.089 * NF**2
+     1             + 1.49931 * NF**3
+c
+c ---------------------------------------------------------------------
+c
+  1    CONTINUE
+c
+       RETURN
+       END
+c
+c =================================================================av==
+
+
+C--   G.W. DZEROX taken from CERNLIB to find the zero of a function.
+      DOUBLE PRECISION FUNCTION DZEROX(A0,B0,EPS,MAXF,F,MODE)
+      IMPLICIT DOUBLE PRECISION (A-H,O-Z)
+C     Based on
+C
+C        J.C.P. Bus and T.J. Dekker, Two Efficient Algorithms with
+C        Guaranteed Convergence for Finding a Zero of a Function,
+C        ACM Trans. Math. Software 1 (1975) 330-345.
+C
+C        (MODE = 1: Algorithm M;    MODE = 2: Algorithm R)
+      CHARACTER*80 ERRTXT
+      LOGICAL LMT
+      DIMENSION IM1(2),IM2(2),LMT(2)
+      PARAMETER (Z1 = 1, HALF = Z1/2)
+      DATA IM1 /2,3/, IM2 /-1,3/
+      DZEROX = 0.D0             ! G.W. to prevent compiler warning
+      IF(MODE .NE. 1 .AND. MODE .NE. 2) THEN
+       C=0
+       WRITE(ERRTXT,101) MODE
+       WRITE(6,*) ERRTXT
+       GO TO 99
+      ENDIF
+      FA=F(B0)
+      FB=F(A0)
+      IF(FA*FB .GT. 0) THEN
+       C=0
+       WRITE(ERRTXT,102) A0,B0
+       WRITE(6,*) ERRTXT
+       GO TO 99
+      ENDIF
+      ATL=ABS(EPS)
+      B=A0
+      A=B0
+      LMT(2)=.TRUE.
+      MF=2
+    1 C=A
+      FC=FA
+    2 IE=0
+    3 IF(ABS(FC) .LT. ABS(FB)) THEN
+       IF(C .NE. A) THEN
+        D=A
+        FD=FA
+       END IF
+       A=B
+       B=C
+       C=A
+       FA=FB
+       FB=FC
+       FC=FA
+      END IF
+      TOL=ATL*(1+ABS(C))
+      H=HALF*(C+B)
+      HB=H-B
+      IF(ABS(HB) .GT. TOL) THEN
+       IF(IE .GT. IM1(MODE)) THEN
+        W=HB
+       ELSE
+        TOL=TOL*SIGN(Z1,HB)
+        P=(B-A)*FB
+        LMT(1)=IE .LE. 1
+        IF(LMT(MODE)) THEN
+         Q=FA-FB
+         LMT(2)=.FALSE.
+        ELSE
+         FDB=(FD-FB)/(D-B)
+         FDA=(FD-FA)/(D-A)
+         P=FDA*P
+         Q=FDB*FA-FDA*FB
+        END IF
+        IF(P .LT. 0) THEN
+         P=-P
+         Q=-Q
+        END IF
+        IF(IE .EQ. IM2(MODE)) P=P+P
+        IF(P .EQ. 0 .OR. P .LE. Q*TOL) THEN
+         W=TOL
+        ELSEIF(P .LT. HB*Q) THEN
+         W=P/Q
+        ELSE
+         W=HB
+        END IF
+       END IF
+       D=A
+       A=B
+       FD=FA
+       FA=FB
+       B=B+W
+       MF=MF+1
+       IF(MF .GT. MAXF) THEN
+        WRITE(6,*) "Error in DZEROX: TOO MANY FUNCTION CALLS"
+        GO TO 99
+       ENDIF
+       FB=F(B)
+       IF(FB .EQ. 0 .OR. SIGN(Z1,FC) .EQ. SIGN(Z1,FB)) GO TO 1
+       IF(W .EQ. HB) GO TO 2
+       IE=IE+1
+       GO TO 3
+      END IF
+      DZEROX=C
+   99 CONTINUE
+      RETURN
+  101 FORMAT('Error in DZEROX: MODE = ',I3,' ILLEGAL')
+  102 FORMAT('Error in DZEROX: F(A) AND F(B) HAVE THE SAME SIGN, A = ',
+     1     1P,D15.8,', B = ',D15.8)
+      END
+
+C ---------------------------------------------------------------------
diff --git a/cmake/UseEnvironment.cmake b/cmake/UseEnvironment.cmake
index fde651c..5fa5738 100644
--- a/cmake/UseEnvironment.cmake
+++ b/cmake/UseEnvironment.cmake
@@ -1,97 +1,104 @@
 set(LXPLUS_GCC_ENV "source /afs/cern.ch/sw/lcg/external/gcc/4.9.3/x86_64-slc6-gcc49-opt/setup.sh")
+set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} -Wall -cpp")
 if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -pedantic-errors -g")
 else()
   if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.7)
     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -pedantic-errors -g")
   else()
     message(STATUS "gcc version >= 4.7 is required")
     if($ENV{HOSTNAME} MATCHES "^lxplus[0-9]+.cern.ch")
       message(STATUS "Compiling on LXPLUS. Did you properly source the environment variables?")
       message(STATUS "e.g. `${LXPLUS_GCC_ENV}`")
     endif()
     message(FATAL_ERROR "Please clean up this build environment and try again...")
   endif()
 endif()
 if(NOT CMAKE_VERSION VERSION_LESS 3.1)
   set(CMAKE_CXX_STANDARD 11)
 else()
   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
 endif()
 
 if($ENV{HOSTNAME} MATCHES "^lxplus[0-9]+.cern.ch")
   set(BASE_DIR "/afs/cern.ch/sw/lcg/external")
   list(APPEND CMAKE_PREFIX_PATH "${BASE_DIR}/CMake/2.8.9/Linux-i386/share/cmake-2.8/Modules")
   set(GSL_DIR "${BASE_DIR}/GSL/2.2.1/x86_64-slc6-gcc48-opt")
   set(HEPMC_DIR "${BASE_DIR}/HepMC/2.06.08/x86_64-slc6-gcc48-opt")
   #set(LHAPDF_DIR "${BASE_DIR}/MCGenerators/lhapdf/5.8.9/x86_64-slc6-gcc46-opt")
   set(LHAPDF_DIR "${BASE_DIR}/MCGenerators_lcgcmt67c/lhapdf/6.2.0/x86_64-slc6-gcc48-opt")
   set(PYTHIA8_DIR "${BASE_DIR}/MCGenerators_lcgcmt67c/pythia8/230/x86_64-slc6-gcc48-opt")
   set(PYTHON_DIR "${BASE_DIR}/Python/2.7.4/x86_64-slc6-gcc48-opt")
   set(PYTHON_LIBRARY "${PYTHON_DIR}/lib/libpython2.7.so")
   set(PYTHON_EXECUTABLE "${PYTHON_DIR}/bin/python")
   set(PYTHON_INCLUDE_DIR "${PYTHON_DIR}/include/python2.7")
   set(ROOTSYS "${BASE_DIR}/../app/releases/ROOT/6.06.08/x86_64-slc6-gcc49-opt/root")
 
   message(STATUS "Compiling on LXPLUS. Do not forget to source the environment variables!")
   message(STATUS "e.g. `${LXPLUS_GCC_ENV}`")
   #--- searching for GSL
   find_library(GSL_LIB gsl HINTS ${GSL_DIR} PATH_SUFFIXES lib REQUIRED)
   find_library(GSL_CBLAS_LIB gslcblas HINTS ${GSL_DIR} PATH_SUFFIXES lib)
   #--- searching for LHAPDF
   find_library(LHAPDF LHAPDF HINTS ${LHAPDF_DIR} PATH_SUFFIXES lib)
   find_path(LHAPDF_INCLUDE LHAPDF HINTS ${LHAPDF_DIR} PATH_SUFFIXES include)
   #--- searching for HepMC
   find_library(HEPMC_LIB HepMC HINTS ${HEPMC_DIR} PATH_SUFFIXES lib)
   find_path(HEPMC_INCLUDE HepMC HINTS ${HEPMC_DIR} PATH_SUFFIXES include)
 else()
   find_library(GSL_LIB gsl REQUIRED)
   find_library(GSL_CBLAS_LIB gslcblas)
   find_library(LHAPDF LHAPDF)
   find_path(LHAPDF_INCLUDE LHAPDF)
   find_library(HEPMC_LIB HepMC)
   find_path(HEPMC_INCLUDE HepMC)
 endif()
 #--- searching for Pythia 8
 set(PYTHIA8_DIRS $ENV{PYTHIA8_DIR} ${PYTHIA8_DIR} /usr /usr/local /opt/pythia8)
 find_library(PYTHIA8 pythia8 HINTS ${PYTHIA8_DIRS} PATH_SUFFIXES lib)
 find_path(PYTHIA8_INCLUDE Pythia8 HINTS ${PYTHIA8_DIRS} PATH_SUFFIXES include include/Pythia8 include/pythia8)
 
 message(STATUS "GSL found in ${GSL_LIB}")
 list(APPEND CEPGEN_EXTERNAL_IO_REQS ${GSL_LIB} ${GSL_CBLAS_LIB})
 list(APPEND CEPGEN_EXTERNAL_CORE_REQS ${GSL_LIB} ${GSL_CBLAS_LIB})
 list(APPEND CEPGEN_EXTERNAL_STRF_REQS ${GSL_LIB} ${GSL_CBLAS_LIB})
 
 if(LHAPDF)
   message(STATUS "LHAPDF found in ${LHAPDF}")
   list(APPEND CEPGEN_EXTERNAL_STRF_REQS ${LHAPDF})
   add_definitions(-DLIBLHAPDF)
   include_directories(${LHAPDF_INCLUDE})
 endif()
 find_package(PythonLibs 2.7)
 if(PYTHONLIBS_FOUND)
   list(APPEND CEPGEN_EXTERNAL_CARDS_REQS ${PYTHON_LIBRARIES})
   add_definitions(-DPYTHON)
   message(STATUS "Python v${PYTHONLIBS_VERSION_STRING} found")
   include_directories(${PYTHON_INCLUDE_DIRS})
 endif()
 if(PYTHIA8)
   message(STATUS "Pythia8 found in ${PYTHIA8}")
   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-misleading-indentation")
   list(APPEND CEPGEN_EXTERNAL_HADR_REQS ${PYTHIA8} dl)
   add_definitions(-DPYTHIA8)
   include_directories(${PYTHIA8_INCLUDE})
 endif()
 if(HEPMC_LIB)
   message(STATUS "HepMC found in ${HEPMC_INCLUDE}")
   list(APPEND CEPGEN_EXTERNAL_IO_REQS ${HEPMC_LIB})
   add_definitions(-DLIBHEPMC)
   include_directories(${HEPMC_INCLUDE})
 endif()
 find_library(MUPARSER muparser)
 if(MUPARSER)
   message(STATUS "muParser found in ${MUPARSER}")
   list(APPEND CEPGEN_EXTERNAL_CORE_REQS ${MUPARSER})
   add_definitions(-DMUPARSER)
 endif()
+set(ALPHAS_PATH ${PROJECT_SOURCE_DIR}/External)
+file(GLOB alphas_sources ${ALPHAS_PATH}/alphaS.f)
+if(alphas_sources)
+  message(STATUS "alpha(s) evolution found in ${alphas_sources}")
+  add_definitions(-DALPHA_S)
+endif()