From 80c2867e70733fb0ea6d30bcc592225c35e01d80 Mon Sep 17 00:00:00 2001
From: Oleg Kalashev <oleg.kalashev@gmail.com>
Date: Sat, 5 Dec 2020 02:06:09 +0300
Subject: [PATCH] first release version

---
 README.md                       |   15 +-
 src/app/crbeam/CMakeLists.txt   |   37 +
 src/app/crbeam/CRbeam.cpp       |  613 ++++++++++++++
 src/app/crbeam/CRbeam.h         |  106 +++
 src/app/crbeam/CmdLine.cpp      |  332 ++++++++
 src/app/crbeam/CmdLine.h        |  117 +++
 src/app/crbeam/README.md        |  172 ++++
 src/lib/Background.cpp          | 1165 +++++++++++++++++++++++++
 src/lib/Background.h            |  442 ++++++++++
 src/lib/CMakeLists.txt          |  134 +++
 src/lib/Cosmology.cpp           |  204 +++++
 src/lib/Cosmology.h             |  240 ++++++
 src/lib/Debug.cpp               |  265 ++++++
 src/lib/Debug.h                 |  140 +++
 src/lib/Deflection1D.cpp        |  127 +++
 src/lib/Deflection1D.h          |   97 +++
 src/lib/Deflection3D.cpp        |  325 +++++++
 src/lib/Deflection3D.h          |  119 +++
 src/lib/ElmagTest.cpp           |  350 ++++++++
 src/lib/ElmagTest.h             |  162 ++++
 src/lib/ExampleUserMain.cpp     |  113 +++
 src/lib/Filter.cpp              |  104 +++
 src/lib/Filter.h                |   98 +++
 src/lib/GZK.cpp                 |  189 +++++
 src/lib/GZK.h                   |   61 ++
 src/lib/GammaPP.cpp             |  496 +++++++++++
 src/lib/GammaPP.h               |   94 ++
 src/lib/ICS.cpp                 |  665 +++++++++++++++
 src/lib/ICS.h                   |  121 +++
 src/lib/Inoue12IROSpectrum.cpp  |   60 ++
 src/lib/Inoue12IROSpectrum.h    |   60 ++
 src/lib/Interaction.cpp         |   66 ++
 src/lib/Interaction.h           |  125 +++
 src/lib/Logger.cpp              |   72 ++
 src/lib/Logger.h                |   55 ++
 src/lib/MathUtils.cpp           | 1015 ++++++++++++++++++++++
 src/lib/MathUtils.h             |  263 ++++++
 src/lib/NeutronDecay.cpp        |  101 +++
 src/lib/NeutronDecay.h          |   56 ++
 src/lib/Nucleus.cpp             |  420 +++++++++
 src/lib/Nucleus.h               |  215 +++++
 src/lib/Output.cpp              |  279 ++++++
 src/lib/Output.h                |  108 +++
 src/lib/PPP.cpp                 |  485 +++++++++++
 src/lib/PPP.h                   |  149 ++++
 src/lib/Particle.cpp            |  195 +++++
 src/lib/Particle.h              |  223 +++++
 src/lib/ParticleStack.cpp       |  210 +++++
 src/lib/ParticleStack.h         |  122 +++
 src/lib/PhotoDisintegration.cpp |  384 +++++++++
 src/lib/PhotoDisintegration.h   |   86 ++
 src/lib/PrecisionTests.cpp      |   72 ++
 src/lib/PrecisionTests.h        |   49 ++
 src/lib/PropagationEngine.cpp   |  521 ++++++++++++
 src/lib/PropagationEngine.h     |   92 ++
 src/lib/ProtonPP.cpp            |  341 ++++++++
 src/lib/ProtonPP.h              |  102 +++
 src/lib/Randomizer.cpp          |   97 +++
 src/lib/Randomizer.h            |   58 ++
 src/lib/Sophia.cpp              |  180 ++++
 src/lib/Sophia.h                |   82 ++
 src/lib/Stecker16Background.cpp |   60 ++
 src/lib/Stecker16Background.h   |   82 ++
 src/lib/SteckerEBL.cpp          |  166 ++++
 src/lib/SteckerEBL.h            |   75 ++
 src/lib/TableBackgrounds.cpp    |  184 ++++
 src/lib/TableBackgrounds.h      |  115 +++
 src/lib/TableFunction.cpp       |  256 ++++++
 src/lib/TableFunction.h         |  261 ++++++
 src/lib/TableReader.cpp         |  270 ++++++
 src/lib/TableReader.h           |  105 +++
 src/lib/Test.cpp                | 1414 +++++++++++++++++++++++++++++++
 src/lib/Test.h                  |  232 +++++
 src/lib/Thinning.cpp            |   65 ++
 src/lib/Thinning.h              |   63 ++
 src/lib/Units.cpp               |   92 ++
 src/lib/Units.h                 |   83 ++
 src/lib/Utils.cpp               |   32 +
 src/lib/Utils.h                 |  234 +++++
 src/lib/main.cpp                |   63 ++
 src/lib/sophia2cpp.f            |   85 ++
 src/lib/z2t.cpp                 |   55 ++
 82 files changed, 17437 insertions(+), 1 deletion(-)
 create mode 100644 src/app/crbeam/CMakeLists.txt
 create mode 100644 src/app/crbeam/CRbeam.cpp
 create mode 100644 src/app/crbeam/CRbeam.h
 create mode 100644 src/app/crbeam/CmdLine.cpp
 create mode 100644 src/app/crbeam/CmdLine.h
 create mode 100644 src/app/crbeam/README.md
 create mode 100644 src/lib/Background.cpp
 create mode 100644 src/lib/Background.h
 create mode 100644 src/lib/CMakeLists.txt
 create mode 100644 src/lib/Cosmology.cpp
 create mode 100644 src/lib/Cosmology.h
 create mode 100644 src/lib/Debug.cpp
 create mode 100644 src/lib/Debug.h
 create mode 100644 src/lib/Deflection1D.cpp
 create mode 100644 src/lib/Deflection1D.h
 create mode 100644 src/lib/Deflection3D.cpp
 create mode 100644 src/lib/Deflection3D.h
 create mode 100644 src/lib/ElmagTest.cpp
 create mode 100644 src/lib/ElmagTest.h
 create mode 100644 src/lib/ExampleUserMain.cpp
 create mode 100644 src/lib/Filter.cpp
 create mode 100644 src/lib/Filter.h
 create mode 100644 src/lib/GZK.cpp
 create mode 100644 src/lib/GZK.h
 create mode 100644 src/lib/GammaPP.cpp
 create mode 100644 src/lib/GammaPP.h
 create mode 100644 src/lib/ICS.cpp
 create mode 100644 src/lib/ICS.h
 create mode 100644 src/lib/Inoue12IROSpectrum.cpp
 create mode 100644 src/lib/Inoue12IROSpectrum.h
 create mode 100644 src/lib/Interaction.cpp
 create mode 100644 src/lib/Interaction.h
 create mode 100644 src/lib/Logger.cpp
 create mode 100644 src/lib/Logger.h
 create mode 100644 src/lib/MathUtils.cpp
 create mode 100644 src/lib/MathUtils.h
 create mode 100644 src/lib/NeutronDecay.cpp
 create mode 100644 src/lib/NeutronDecay.h
 create mode 100644 src/lib/Nucleus.cpp
 create mode 100644 src/lib/Nucleus.h
 create mode 100644 src/lib/Output.cpp
 create mode 100644 src/lib/Output.h
 create mode 100644 src/lib/PPP.cpp
 create mode 100644 src/lib/PPP.h
 create mode 100644 src/lib/Particle.cpp
 create mode 100644 src/lib/Particle.h
 create mode 100644 src/lib/ParticleStack.cpp
 create mode 100644 src/lib/ParticleStack.h
 create mode 100644 src/lib/PhotoDisintegration.cpp
 create mode 100644 src/lib/PhotoDisintegration.h
 create mode 100644 src/lib/PrecisionTests.cpp
 create mode 100644 src/lib/PrecisionTests.h
 create mode 100644 src/lib/PropagationEngine.cpp
 create mode 100644 src/lib/PropagationEngine.h
 create mode 100644 src/lib/ProtonPP.cpp
 create mode 100644 src/lib/ProtonPP.h
 create mode 100644 src/lib/Randomizer.cpp
 create mode 100644 src/lib/Randomizer.h
 create mode 100644 src/lib/Sophia.cpp
 create mode 100644 src/lib/Sophia.h
 create mode 100644 src/lib/Stecker16Background.cpp
 create mode 100644 src/lib/Stecker16Background.h
 create mode 100644 src/lib/SteckerEBL.cpp
 create mode 100644 src/lib/SteckerEBL.h
 create mode 100644 src/lib/TableBackgrounds.cpp
 create mode 100644 src/lib/TableBackgrounds.h
 create mode 100644 src/lib/TableFunction.cpp
 create mode 100644 src/lib/TableFunction.h
 create mode 100644 src/lib/TableReader.cpp
 create mode 100644 src/lib/TableReader.h
 create mode 100644 src/lib/Test.cpp
 create mode 100644 src/lib/Test.h
 create mode 100644 src/lib/Thinning.cpp
 create mode 100644 src/lib/Thinning.h
 create mode 100644 src/lib/Units.cpp
 create mode 100644 src/lib/Units.h
 create mode 100644 src/lib/Utils.cpp
 create mode 100644 src/lib/Utils.h
 create mode 100644 src/lib/main.cpp
 create mode 100644 src/lib/sophia2cpp.f
 create mode 100644 src/lib/z2t.cpp

diff --git a/README.md b/README.md
index ffa6ba9..23a8644 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,14 @@
-# mcray
\ No newline at end of file
+# mcray
+Framework for Monte Carlo simulation of ultra-high energy cosmic rays and electromagnetic cascade propagation.
+
+### Authors:
+   Oleg Kalashev and Alexander Korochkin
+
+### Features
+ - propagation of protons, neutrons, nuclei, electon-photon cascade and neutrino can be simulated
+ - support for interactions with arbitray photon background including trajectory simulation for secondary particles, produced in the interactions
+ - trajectories of individual particles in presence of magnetic field can be calculated
+
+### Applications
+
+[CRbeam](src/app/crbeam) - cosmic ray beam simulation
\ No newline at end of file
diff --git a/src/app/crbeam/CMakeLists.txt b/src/app/crbeam/CMakeLists.txt
new file mode 100644
index 0000000..a16621e
--- /dev/null
+++ b/src/app/crbeam/CMakeLists.txt
@@ -0,0 +1,37 @@
+cmake_minimum_required (VERSION 2.6)
+project (CRbeam)
+
+add_definitions(-DUSE_GSL)
+
+include_directories ("../../lib" "../../OS/include")
+
+#include_directories(../../external/include ../../external/OS/include)
+set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -D_DEBUG -O0 -fopenmp")
+set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_DEBUG -O0 -fopenmp -pg")
+set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Wall -O2 -fopenmp")
+set(CMAKE_PREFIX_PATH "external")
+
+# -rdynamic flux is only supported on systems with ELF executable format
+IF(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+    set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -rdynamic")
+    set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -rdynamic")
+    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -rdynamic")
+    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -rdynamic")
+endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+
+FIND_LIBRARY(C_LIBRARY c)
+FIND_LIBRARY(GSL_LIBRARY gsl)
+FIND_LIBRARY(GSLCBLAS_LIBRARY gslcblas)
+add_subdirectory (../../lib mcray)
+
+set(SOURCE_FILES
+CmdLine.cpp
+CmdLine.h
+CRbeam.cpp
+CRbeam.h
+)
+
+add_executable(CRbeam ${SOURCE_FILES})
+target_link_libraries(CRbeam mcray ${C_LIBRARY} ${GSL_LIBRARY} ${GSLCBLAS_LIBRARY})
+
+
diff --git a/src/app/crbeam/CRbeam.cpp b/src/app/crbeam/CRbeam.cpp
new file mode 100644
index 0000000..8987c50
--- /dev/null
+++ b/src/app/crbeam/CRbeam.cpp
@@ -0,0 +1,613 @@
+/*
+ * CRbeam.cpp
+ *
+ * Authors:
+ *       Oleg Kalashev
+ *       Alexandr Korochkin
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include <cstdlib>
+#include "ParticleStack.h"
+#include "Utils.h"
+#include "Cosmology.h"
+#include "PropagationEngine.h"
+#include <iostream>
+#include <GZK.h>
+#include <Inoue12IROSpectrum.h>
+#include "PPP.h"
+#include "ProtonPP.h"
+#include "TableBackgrounds.h"
+#include "ICS.h"
+#include "CRbeam.h"
+#include "Test.h"
+#include "NeutronDecay.h"
+#include "Deflection3D.h"
+#include "SteckerEBL.h"
+#include "CmdLine.h"
+#include "Stecker16Background.h"
+#include <omp.h>
+#include <stdlib.h>
+#include "MathUtils.h"
+
+#ifndef _DEBUG
+#include <sys/time.h>
+#endif
+
+using namespace std;
+using namespace mcray;
+using namespace Utils;
+using namespace Backgrounds;
+using namespace Interactions;
+
+
+using namespace cors::cmdline;
+
+
+/*
+ * This is an example of user_main function performing user defined tasks
+ * the function should be provided by end user
+ */
+int user_main(int argc, char** argv) {
+	CRbeam prog(argc, argv);
+	return prog.run();
+}
+
+enum ClineParams
+{
+	PHelp = 1,PLog,PLogFilterThread,PLogLevel,PTrajectoryLog,
+    PNparticles,PBatchSize,
+    PRedshift,PFilamentZ,PEnergy,PMinEnergy,PPowerLaw,PMinSourceEnergy,PPrimary,PNoEMcascade,PSourceEvolM,
+	PBackground,PBackgroundMult,PExtraBackground,PExtraBackgroundPhysical,PEBLCut,PMonoCmb,PFixedCmbT,PExtDeltaZconst,PExtPowerLow,PExtDeltaZexp,
+	POutput,POverwriteOutput,PThinning,PRescalePPP,PEGMF,PEGMFL,PRandomEGMF,PTurbulentEGMF,PTauPrint,POutputSuffix
+};
+#define xstr(s) str(s)
+#define str(s) #s
+#define M_POINT_SOURCE 1000
+
+static CmdInfo commands[] = {
+		{ PHelp,    CmdInfo::FLAG_NULL,     "-h",   "--help",       NULL,   "Show help information" },
+		{ PLog,    CmdInfo::FLAG_NULL,     "-log",   "--log",       NULL,   "Enable logging" },
+		{ PLogFilterThread,    CmdInfo::FLAG_ARGUMENT, "-logT",   "--log-thread",   "-1",    "Log thread filter (set to -1 to disable)" },
+		{ PLogLevel,    CmdInfo::FLAG_ARGUMENT, "-logL",   "--log-level",   "1",    "Log level:\n"
+																					"\tError = 0,\n"
+																					"\tWarning = 1,\n"
+																					"\tMessage = 2,\n"
+																					"\tVerbose = 3"
+		},
+        { PTrajectoryLog,     CmdInfo::FLAG_NULL,     "-tlog",   "--tlog",       NULL,   "Log particle trajectories" },
+        { PNparticles,    CmdInfo::FLAG_ARGUMENT, "-N",   "--nparticles",   "10000",     "Number of particles" },
+        { PBatchSize,    CmdInfo::FLAG_ARGUMENT, "-bN",   "--batch",   "0",     "Number of particles in minibatch (0=auto)" },
+
+		{ PRedshift,    CmdInfo::FLAG_ARGUMENT, "-z",   "--z",       NULL,   "Source z (or maximal z if --m_z par is given)" },
+        { PFilamentZ,    CmdInfo::FLAG_ARGUMENT, "-zf",   "--z-filament",       "0.",   "Filament z (kill protons with z less than this value)" },
+		{ PEnergy,    CmdInfo::FLAG_ARGUMENT, "-E",   "--emax",   "1e18",    "Initial (or maximal for power law) particle energy in eV" },
+		{ PMinEnergy,    CmdInfo::FLAG_ARGUMENT, "-e",   "--emin",   "1e11",     "Minimal energy in eV" },
+		{ PPowerLaw,    CmdInfo::FLAG_ARGUMENT, "-pl",   "--power",   "0",     "if>0 use E^(-power) injection " },
+		{ PMinSourceEnergy,    CmdInfo::FLAG_ARGUMENT, "-es",   "--emin_source",   "0", "Minimal injection energy in eV (for power law injection only)" },
+
+		{ PPrimary,    CmdInfo::FLAG_ARGUMENT, "-p",   "--primary",   "10",    "Primary particle: \n"
+																					  "\tElectron = 0,\n"
+																					  "\tPositron = 1,\n"
+																					  "\tPhoton = 2,\n"
+																					  "\tNeutron 9,\n"
+																					  "\tProton 10" },
+		{ PNoEMcascade,    CmdInfo::FLAG_NULL,     "-nEM",   "--noEMcascade",   NULL,   "don't calculate spectra of electrons and photons" },
+		{ PSourceEvolM, CmdInfo::FLAG_ARGUMENT, "-m",   "--m_z",   xstr(M_POINT_SOURCE),    "if parameter is set (!=" xstr(M_POINT_SOURCE) "), use continuous source distribution ~(1+z)^m" },
+		{ PBackground,    CmdInfo::FLAG_ARGUMENT, "-b",   "--background",   "3",    "EBL used:\n"
+																							"0 - zero,\n"
+																							"1 - Kneiske best fit (ELMAG),\n"
+																							"2 - Kneiske minimal (ELMAG),\n"
+																							"3 - Inoue 2012 Baseline,\n"
+																							"4 - Inoue 2012 lower limit,\n"
+																							"5 - Inoue 2012 upper limit,\n"
+																							"6 - Franceschini 2008\n"
+																							"7 - Kneiske best fit (digitized)\n"
+																							"8 - Kneiske minimal (digitized)\n"
+																							"9 - Stecker 2005\n"
+																							"10 - Stecker 2016 lower limit,\n"
+																							"11 - Stecker 2016 upper limit,\n"
+                                                                                            "12 - Franceschini 2017,\n"
+																							  },
+        { PBackgroundMult,    CmdInfo::FLAG_ARGUMENT, "-bm",   "--backgr-mult",   "1",    "EBL multiplier" },
+        { PExtraBackground, CmdInfo::FLAG_ARGUMENT, "-badd", "--add-backgr", "", "Additional matrix background file path (relative to ./tables)"},
+        { PExtraBackgroundPhysical, CmdInfo::FLAG_NULL, "-badd_p",   "--add-backgr-phys",   NULL,   "Treat additional background data as physical (not comoving) density" },
+		{ PEBLCut,    CmdInfo::FLAG_ARGUMENT,     "-bcut",   "--backgr-cut",   "100",   "Cut EBL above this energy [eV]" },
+		{ PMonoCmb,    CmdInfo::FLAG_NULL,     "-mcmb",   "--monocmb",   NULL,   "Use monochromatic 6.3e-4 eV background instead of CMB" },
+		{ PFixedCmbT,    CmdInfo::FLAG_NULL,     "-fcmb",   "--fixedcmb",   NULL,   "use CMB with fixed temperature (1+Zmax)2.73K" },
+
+		{ PExtDeltaZconst,    CmdInfo::FLAG_ARGUMENT, "-bc",   "--eblDeltaZconst",   "-1",    "EBL extension DeltaZconst param (set to negative to disable EBL extension)"  },
+		{ PExtPowerLow,    CmdInfo::FLAG_ARGUMENT, "-bp",   "--eblPowerLow",   "0",    "EBL extension PowerLow param"  },
+		{ PExtDeltaZexp,    CmdInfo::FLAG_ARGUMENT, "-be",   "--eblDeltaZexp",   "1",    "EBL extension DeltaZexp param"  },
+
+		{ POutput,    CmdInfo::FLAG_ARGUMENT, "-o",   "--output",   NULL,    "Output directory path (must not exist, will be created)" },
+		{ POverwriteOutput,    CmdInfo::FLAG_NULL,     "-oo",   "--overwrite",   NULL,   "overwrite output dir if exists" },
+		{ PThinning,    CmdInfo::FLAG_ARGUMENT, "-t",   "--thinning",   "0.9",    "Alpha thinning" },
+		{ PRescalePPP,    CmdInfo::FLAG_ARGUMENT,   "-pt",   "--ppp-thinning",   "10",   "PPP Rescale Coefficient (double > 0, actual number of secondaries per interaction)" },
+
+		{ PEGMF,    CmdInfo::FLAG_ARGUMENT, "-mf",   "--EGMF",   "1e-15",    "Extragalactic magnetic field in Gauss" },
+		{ PEGMFL,    CmdInfo::FLAG_ARGUMENT, "-mfl",   "--lEGMF",   "1",    "Extragalactic magnetic field correlation length in Mpc at z=0" },
+		{ PRandomEGMF,    CmdInfo::FLAG_NULL,     "-mfr",   "--randomEGMF",   NULL,   "randomize EGMF (use different EGMF configurations for different initial particles)" },
+		{ PTurbulentEGMF,    CmdInfo::FLAG_NULL,     "-mft",   "--turbulentEGMF",   NULL,   "use turbulent EGMF with Kolmogorov spectrum" },
+        { PTauPrint,    CmdInfo::FLAG_NULL,     "-ptau",   "--print-tau",   NULL,   "print tau" },
+        { POutputSuffix, CmdInfo::FLAG_ARGUMENT, "-os", "--output-suffix", "", "Output dir name suffix (appended to autogenerated names)"},
+		{ 0 },
+};
+
+class CRbeamLogger : public Logger{
+    int fB_slot;
+    MagneticField* fB;
+public:
+    CRbeamLogger(std::ostream& aOut, int aB_slot=-1, MagneticField* aB=0):
+            Logger(aOut),
+            fB_slot(aB_slot),
+            fB(aB){
+    }
+    virtual void print_data(const Particle &p, std::ostream& aOut){
+        Logger::print_data(p, aOut);
+        MagneticField* pB = fB;
+        if(fB_slot>=0){
+            if(!pB)
+                pB = (MagneticField*)(p.interactionData[fB_slot]);
+        }
+        double Bperp = 0.;
+        if(pB){
+            std::vector<double> B0(3);
+            pB->GetValueGauss(p.X, p.Time, B0);
+            Bperp = sqrt(B0[0]*B0[0]+B0[1]*B0[1]);
+        }
+        aOut << "\t" << Bperp;
+    }
+
+    virtual void print_header(std::ostream& aOut){
+        Logger::print_header(aOut);
+        aOut << "\tB_perp/G";
+    }
+};
+
+cosmo_time CRbeam::FilamentFilter::GetMinTravelTimeLeft(const Particle& aParticle) const{
+    if (aParticle.ElectricCharge())
+        if (aParticle.Time.z() >= fFilamentLocation.z())
+            return fFilamentLocation.t() - aParticle.Time.t();
+        else if(aParticle.Time.z()<fFilamentLocation.z()*0.9999)
+            return 0.;// avoid rounding error when
+
+    return Result::GetMinTravelTimeLeft(aParticle);
+}
+
+CRbeam::CRbeam(int argc, char** argv):
+		fEmin_eV(0),
+		fEmax_eV(0),
+		fPowerLaw(0),
+		fEminSource_eV(0),
+		fZmax(0),
+        fZfilament(0),
+		fAlpha(0),
+		fPPPrescaleCoef(10),
+		fNoParticles(0),
+        fBatchSize(0),
+		fPrimary(Proton),
+		fNoEM(false),
+		fMz(M_POINT_SOURCE),
+		fBackgroundModel(0),
+        fEBLmult(1),
+        fCustomBackground(""),
+        fCustomBackgroundPhysical(false),
+		fMonoCMB(false),
+		fOutputDir(0),
+		fOverwriteOutput(false),
+		fFixedCmb(false),
+		fEGMF(0.),
+		fLcorEGMF(1.),
+		fRandomizeEGMF(false),
+		fTurbulentEGMF(false),
+		//fMaxDeflection(0.5/180.*M_PI),
+		fLogging(false),
+        fTrajectoryLogging(false),
+		fLogThread(-1),
+		fLogLevel(WarningLL),
+		fEBLMaxE(100.),
+        fTauPrint(false),
+		cmd(argc,argv,commands)
+{
+	if( cmd.has_param("-h") )
+	{
+		cmd.printHelp(std::cerr);
+		exit(255);//enable binary_check
+	}
+	fLogging = cmd(PLog);
+    fTrajectoryLogging = cmd(PTrajectoryLog);
+	fLogThread = cmd(PLogFilterThread);
+	fLogLevel = (LogLevel)((int)cmd(PLogLevel));
+	fEmin_eV = cmd(PMinEnergy);
+	fEmax_eV = cmd(PEnergy);
+	fPowerLaw = cmd(PPowerLaw);
+	fEminSource_eV = cmd(PMinSourceEnergy);
+	if(fEminSource_eV<fEmin_eV)
+		fEminSource_eV=fEmin_eV;
+	fZmax = cmd(PRedshift);
+	fAlpha = cmd(PThinning);
+	fPPPrescaleCoef = cmd(PRescalePPP);
+
+	fEGMF = cmd(PEGMF);
+	fLcorEGMF = cmd(PEGMFL);
+	fRandomizeEGMF = cmd(PRandomEGMF);
+	fTurbulentEGMF = cmd(PTurbulentEGMF);
+	//fMaxDeflection = cmd(PMaxDeflection);
+	//fMaxDeflection *= (M_PI/180.);
+
+	fNoParticles = cmd(PNparticles);
+    fBatchSize = cmd(PBatchSize);
+	int primary = cmd(PPrimary);
+	fNoEM = cmd(PNoEMcascade);
+	fMz = cmd(PSourceEvolM);
+	fBackgroundModel = cmd(PBackground);
+    fEBLmult = cmd(PBackgroundMult);
+    fCustomBackground = cmd(PExtraBackground);
+    fCustomBackgroundPhysical = cmd(PExtraBackgroundPhysical);
+	fMonoCMB = cmd(PMonoCmb);
+	fFixedCmb = cmd(PFixedCmbT);
+	fOutputDir = cmd(POutput);
+	fOverwriteOutput = cmd(POverwriteOutput);
+	fExtDeltaZconst = cmd(PExtDeltaZconst);
+	fExtPowerLow = cmd(PExtPowerLow);
+	fExtDeltaZexp = cmd(PExtDeltaZexp);
+	fEBLMaxE = cmd(PEBLCut);
+    fTauPrint = cmd(PTauPrint);
+    fZfilament = cmd(PFilamentZ);
+
+	if(fOutputDir[0]=='\0')
+		fOutputDir = 0;
+
+	if(primary<Electron || primary>Proton)
+		Exception::Throw("invalid primary particle type");
+	fPrimary = (ParticleType)primary;
+}
+
+int CRbeam::run()
+{
+	int kAc = 1;
+	fEBLMaxE *= units.eV;
+	double Emax = fEmax_eV*units.eV;//eV
+	if(!cosmology.IsInitialized())
+		cosmology.Init(fZmax + 10);
+	double zMin = 0.;
+	double logStepK = pow(10,0.05/kAc);
+	double Emin = fEmin_eV*units.eV;
+	double EminSource = fEminSource_eV*units.eV;
+	double alphaThinning = fAlpha;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	std::string outputDir;
+
+	if(fOutputDir)
+		outputDir = fOutputDir;
+	else
+	{
+		outputDir = Particle::Name(fPrimary);
+		outputDir += ("_E" + ToString(fEmax_eV) + "_z" + ToString(fZmax) + "_N" + ToString(fNoParticles)) + "_EBL" + ToString(fBackgroundModel);
+        if(strlen(fCustomBackground)){
+            outputDir += 'c';
+        }
+        if(fZfilament>0.)
+            outputDir += "_zf" + ToString(fZfilament);
+		if(fEGMF!=0)
+		{
+			outputDir += "_B";
+			if(fRandomizeEGMF)
+				outputDir += "rand";
+			if(fTurbulentEGMF)
+				outputDir += "Turb";
+			outputDir += ToString(fEGMF);
+            outputDir += "_Lc";
+            outputDir += ToString(fLcorEGMF);
+		}
+		else
+		{
+			outputDir += "_B0";
+		}
+		if(fNoEM)
+			outputDir += "_noEM";
+		if(fMonoCMB)
+			outputDir += "_mono";
+		if(fFixedCmb)
+			outputDir += "_fixedCmb";
+		if(fPowerLaw>0)
+			outputDir += ("_p" + ToString(fPowerLaw));
+		if(fMz!=M_POINT_SOURCE)
+			outputDir += ("_m" + ToString(fMz));
+		const char* suffix = cmd(POutputSuffix);
+		outputDir += suffix;
+	}
+    std::cout << "output dir: " << outputDir << std::endl;
+    SmartPtr<RawOutput3D> pOutput = new RawOutput3D(outputDir, fOverwriteOutput, fMz!=M_POINT_SOURCE, fPowerLaw>0, fTrajectoryLogging);//this creates folder outputDir, later we will set output to outputDir/z0
+	if(fLogging){
+		debug.SetOutputFile(outputDir + "/log.txt");
+		debug.EnableTimestamp();
+		debug.SetThreadFilter(fLogThread);
+		debug.SetLevel(fLogLevel);
+	}
+	CompoundBackground backgr;
+
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+	if(fFixedCmb)
+		cmbTemp *= (1.+fZmax);
+	IBackground* b1 = new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp, 0., fZmax + 1., fFixedCmb);
+	IBackground* b2 = 0;
+	switch(fBackgroundModel)
+	{
+		case 0:
+			b2 = 0;
+			break;
+		case 1:
+			b2 = new ElmagKneiskeBestFit();
+			break;
+		case 2:
+			b2 = new ElmagKneiskeMinimal();
+			break;
+		case 3:
+			b2 = new Inoue12BaselineIROSpectrum();
+			break;
+		case 4:
+			b2 = new Inoue12LowPop3IROSpectrum();
+			break;
+		case 5:
+			b2 = new Inoue12UpperPop3IROSpectrum();
+			break;
+		case 6:
+			b2 = new Franceschini08EBL();
+			break;
+		case 7:
+			b2 = new Kneiske0309EBL();
+			break;
+		case 8:
+			b2 = new Kneiske1001EBL();
+			break;
+		case 9:
+			b2 = new Stecker2005EBL();
+			break;
+		case 10:
+			b2 = new Stecker16LowerBackground();
+			break;
+		case 11:
+			b2 = new Stecker16UpperBackground();
+			break;
+        case 12:
+            b2 = new Franceschini17EBL();
+            break;
+		default:
+			Exception::Throw("Invalid background model specified");
+	}
+
+	if(b2 && fEBLMaxE < b2->MaxE())
+		b2 = new CuttedBackground(b2, 0, fEBLMaxE);
+
+	if(b2 && fZmax>b2->MaxZ() && fExtDeltaZconst>=0)
+	{
+		b2 = new HighRedshiftBackgrExtension(b2, fExtDeltaZconst, fExtPowerLow, fExtDeltaZexp);
+	}
+
+	backgr.AddComponent(b1);
+	if(b2)
+	{
+		backgr.AddComponent(b2, fEBLmult);
+		double dens = BackgroundUtils::CalcIntegralDensity(*b2)*units.cm3;
+		std::cerr << "EBL integral density [cm^{-3}]:" << dens << std::endl;
+		std::ofstream backgrOut;
+		backgrOut.open((outputDir + "/backgr").c_str(),std::ios::out);
+		BackgroundUtils::Print(b2, 0., backgrOut, 100);
+	}
+    if(strlen(fCustomBackground)){
+        MatrixBackground* bc = new MatrixBackground(fCustomBackground, fCustomBackground, !fCustomBackgroundPhysical, false);
+        backgr.AddComponent(bc);
+        double dens = BackgroundUtils::CalcIntegralDensity(*bc)*units.cm3;
+        std::cerr << "Custom background integral density [cm^{-3}]: " << dens << std::endl;
+        std::ofstream backgrOut((outputDir + "/cbackgr").c_str());
+        BackgroundUtils::Print(bc, 0., backgrOut, 100);
+    }
+
+	double stepZ = fZmax<0.05 ? fZmax/2 : 0.025;
+	double epsRel = 1e-3;
+	double centralE1 = 6.3e-10/units.Eunit;//6.3e-4eV
+	double n1 = 413*units.Vunit;//413cm^-3
+	ConstFunction k1(centralE1);
+	ConstFunction c1(n1);
+	PBackgroundIntegral backgrI;
+	if(fMonoCMB)
+	{
+		backgrI = new MonochromaticBackgroundIntegral(c1, k1, logStepK, epsRel);
+	}
+	else
+	{
+		backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, fZmax, epsRel);
+	}
+
+	unsigned long int seed = 0;
+#ifdef _DEBUG
+	seed=2015;
+#else
+	{
+	    struct timeval tv;
+	    gettimeofday(&tv, NULL);
+	    seed = tv.tv_sec * 1000 + tv.tv_usec / 1000;
+	}
+#endif
+	Randomizer rand(seed);
+	SmartPtr<IFilter> resultFilter = new EnergyBasedFilter(Emin, M_PI);
+	bool useResultFilterInRuntime = true;
+    FilamentFilter result(fZfilament, resultFilter, useResultFilterInRuntime);
+	result.SetAutoFlushInterval(10);//save every 10 particle cascades
+	result.EnableFlushOnCtrlC();//make sure results are not lost if program is interrupted by Ctrl-C
+
+	result.AddOutput(pOutput);
+	ParticleStack particles;
+	PropagationEngine pe(particles, result, rand.CreateIndependent());
+
+	EnergyBasedThinning thinning(alphaThinning, BlackList);
+	thinning.EnableFor(Electron);
+	thinning.EnableFor(Positron);
+	thinning.EnableFor(Photon);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(new RedShift());
+
+	if(fNoEM) {
+		ParticleType discardedParticles[] = {Electron, Positron, Photon, ParticleTypeEOF};
+		pe.SetPropagationFilter(new ParticleTypeFilter(discardedParticles));
+	}
+	else
+	{
+		pe.AddInteraction(new Interactions::ICS(backgrI, Emin));
+		pe.AddInteraction(new Interactions::IcsCEL(backgrI, Emin));
+		pe.AddInteraction(new Interactions::GammaPP(backgrI));
+		pe.AddInteraction(new PPP(backgrI, fPPPrescaleCoef));//secondaries only
+	}
+	pe.AddInteraction(new GZK(backgrI));
+	pe.AddInteraction(new NeutronDecay());
+	pe.AddInteraction(new ProtonPPcel(backgrI));
+
+	double Beta = 0.1;
+	double Alpha = M_PI / 4;
+	double Theta = 0;
+	double Phi = 0;
+	int Bslot = -1;
+    SmartPtr<MagneticField> mf;
+	if(fEGMF>0.) {
+		if (fRandomizeEGMF) {
+			Deflection3D* defl = new Deflection3D();
+			Bslot = defl->MFSlot();
+			pe.AddInteraction(defl);
+		}
+		else {
+			mf = fTurbulentEGMF ? ((MagneticField*) new TurbulentMF(rand, fLcorEGMF, fEGMF)) :
+								((MagneticField*) new MonochromaticMF(fLcorEGMF, fEGMF, Beta, Alpha, Theta, Phi));
+			pe.AddInteraction(new Deflection3D(mf));
+		}
+	}
+	//// generating initial state
+
+	CosmoTime tEnd;
+	tEnd.setZ(zMin);
+	pOutput->SetOutputDir(outputDir + "/z" + ToString(zMin));
+	fstream paramsOut((outputDir + "/params").c_str(), std::ios_base::out);
+	cmd.printParamValues(paramsOut);
+	result.SetEndTime(tEnd);
+	CosmoTime tStart;
+	tStart.setZ(fZmax);
+	double dt = tEnd.t()-tStart.t();
+	double t0 = tStart.t();
+
+	double normW = 1.;
+	if(fPowerLaw>0 && fPowerLaw!=1.){//make sure total weight is roughly equal to number of particles
+		normW = log(Emax/EminSource)*(1.-fPowerLaw)/(pow(Emax,1.-fPowerLaw)-pow(EminSource,1.-fPowerLaw));
+	}
+    SafePtr<CRbeamLogger> tr_logger;
+    std::ofstream trLogOut;
+    if(fTrajectoryLogging){
+        trLogOut.open((outputDir + "/tr.log").c_str());
+        if(!trLogOut.is_open()){
+            Exception::Throw("Failed to create " + outputDir + "/tr.log");
+        }
+
+        tr_logger = new CRbeamLogger(trLogOut, Bslot, mf);
+        pe.SetLogger(tr_logger);
+		fBatchSize = fNoParticles; // this is needed to make sure particle Id's are unique
+    }
+	if(fBatchSize==0){
+		fBatchSize = omp_get_max_threads()*10;
+		std::cerr << "using auto batch size " << fBatchSize << std::endl;
+	}
+
+    std::vector<SafePtr<MagneticField> > fields(fBatchSize);
+    if(fTauPrint){
+        class Kern : public Function{
+            PropagationEngine& fPe;
+            mutable Particle   fParticle;
+        public:
+            Kern(PropagationEngine& pe, const Particle aParticle):
+                    fPe(pe),
+                    fParticle(aParticle){};
+            virtual double f(double t) const{
+                fParticle.Time = t;
+                return fPe.GetInteractionRate(fParticle);
+            }
+        };
+        CosmoTime tSource(fZmax);
+        //CosmoTime tEnd(0);
+        if(fPowerLaw<=0.)
+            EminSource = Emax;
+        double step = pow(10.,0.01);
+        MathUtils mu;
+        ofstream out_tau((outputDir + "/tau").c_str());
+        for (double E=EminSource; E<=Emax; E*=step){
+            Particle particle(fPrimary, tSource.z());
+            particle.Energy = E;
+            Kern k(pe, particle);
+            double tau = mu.Integration_qag(k, tSource.t(),tEnd.t(), 0, 1e-3, 1000);
+            out_tau << E/units.eV << "\t" << tau << std::endl;
+        }
+        out_tau.close();
+    }
+	for(int i=0; i<fNoParticles;)
+	{
+		CosmoTime tSource(fZmax);
+		double weight = 1.;
+		if(fMz!=M_POINT_SOURCE)
+		{
+			double t = t0 + rand.Rand()*dt;
+			tSource.setT(t);
+			double z = tSource.z();
+			weight = pow(1.+z,fMz);
+		}
+		Particle particle(fPrimary, tSource.z());
+		if(fPowerLaw<=0.){
+			particle.Energy = Emax;
+			particle.Weight = weight;
+		}
+		else{
+			particle.Energy = EminSource*pow(Emax/EminSource, rand.Rand());//choose E randomly in log scale
+			particle.Weight = weight*normW*pow(particle.Energy, 1.-fPowerLaw);
+		}
+
+		if(fRandomizeEGMF)
+		{
+            MagneticField* mf = fTurbulentEGMF ? ((MagneticField*) new TurbulentMF(rand, fLcorEGMF, fEGMF)) :
+						((MagneticField*) new MonochromaticMF(rand, fLcorEGMF, fEGMF));
+            fields[i%fBatchSize] = mf; //store till the end of current batch
+			particle.interactionData[Bslot] = mf;
+		}
+		particles.AddPrimary(particle);
+        i++;
+        if(i%fBatchSize==0 || i==fNoParticles){
+            std::cerr << "batch " << (i-1)/fBatchSize + 1 << " of " << ceil(fNoParticles/(double)fBatchSize) << std::endl;
+            pe.RunMultithread();
+            result.Flush();
+            if(result.AbortRequested())
+                break; //handling Ctrl-C
+        }
+	}
+	std::cout << "output dir: " << outputDir << std::endl;
+	return 0;
+}
+
+CRbeam::~CRbeam() {
+
+}
+
+
diff --git a/src/app/crbeam/CRbeam.h b/src/app/crbeam/CRbeam.h
new file mode 100644
index 0000000..a905ac2
--- /dev/null
+++ b/src/app/crbeam/CRbeam.h
@@ -0,0 +1,106 @@
+/*
+ * CRbeam.h
+ *
+ * Authors:
+ *       Oleg Kalashev
+ *       Alexandr Korochkin
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef CRBEAM_H_
+#define CRBEAM_H_
+
+#include <cstdlib>
+#include <iostream>
+#include "ParticleStack.h"
+#include "Utils.h"
+#include "Cosmology.h"
+#include "PropagationEngine.h"
+#include "Output.h"
+#include "TableBackgrounds.h"
+#include "ICS.h"
+#include "GammaPP.h"
+#include "CmdLine.h"
+
+using namespace std;
+using namespace mcray;
+using namespace Utils;
+using namespace Backgrounds;
+using namespace Interactions;
+
+class CRbeam {
+	class FilamentFilter : public Result{
+	public:
+		FilamentFilter(cosmo_time aRedshift, IFilter*	aOutputFilter, bool aIsPropagationFilter):
+				Result(aOutputFilter, aIsPropagationFilter),
+				fFilamentLocation(aRedshift)
+				{}
+		virtual cosmo_time GetMinTravelTimeLeft(const Particle& aParticles) const;
+	private:
+		CosmoTime fFilamentLocation;
+	};
+
+public:
+	CRbeam(int argc, char** argv);
+	virtual ~CRbeam();
+	void Test();
+	int run();
+private:
+	double			fEmin_eV;
+	double			fEmax_eV;
+	double			fPowerLaw;
+	double	 		fEminSource_eV;
+	double			fZmax;
+    double          fZfilament;
+	double			fAlpha;
+	double 			fPPPrescaleCoef;
+	int				fNoParticles;
+	int				fBatchSize;
+	ParticleType 	fPrimary;
+	bool 			fNoEM;
+	double			fMz;
+	int				fBackgroundModel;
+    double          fEBLmult;
+	const char*		fCustomBackground;
+	bool 			fCustomBackgroundPhysical;
+	bool			fMonoCMB;
+	double			fExtDeltaZconst;
+	double			fExtPowerLow;
+	double			fExtDeltaZexp;
+	const char*		fOutputDir;
+	bool 			fOverwriteOutput;
+	bool			fFixedCmb;
+	double			fEGMF;
+	double			fLcorEGMF;
+	bool 			fRandomizeEGMF;
+	bool 			fTurbulentEGMF;
+	bool            fLogging;
+	bool 			fTrajectoryLogging;
+	int             fLogThread;
+	LogLevel        fLogLevel;
+	double			fEBLMaxE;
+    bool            fTauPrint;
+	//double			fMaxDeflection;
+	cors::cmdline::CmdLine 		cmd;
+};
+
+#endif /* CRBEAM_H_ */
diff --git a/src/app/crbeam/CmdLine.cpp b/src/app/crbeam/CmdLine.cpp
new file mode 100644
index 0000000..09fb79f
--- /dev/null
+++ b/src/app/crbeam/CmdLine.cpp
@@ -0,0 +1,332 @@
+/**
+ *
+ * CmdLine.cpp
+ *
+ * Author:
+ *       Dmitry Ponomarev (Aguacero) <demdxx@gmail.com>
+ *
+ * Copyright (c) 2011 Dmitry Ponomarev V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <iosfwd>
+
+#include "CmdLine.h"
+
+using namespace cors::cmdline;
+
+// Command argument ---
+
+CmdArgument::CmdArgument(  const char* value ) : value( value ) {}
+
+CmdArgument::operator CmdArgument::ASCII_TYPE( void )
+{
+	return value;
+}
+
+CmdArgument::operator int( void )
+{
+	return value!=NULL ? atoi(value) : 0;
+}
+
+CmdArgument::operator float( void )
+{
+	return value!=NULL ? atof(value) : 0.;
+}
+
+CmdArgument::operator bool( void )
+{
+	return value!=NULL;
+}
+
+// Command Param ---
+
+CmdParam::CmdParam( const char* value, const CmdInfo* info )
+	: CmdArgument( value ), info( info ) {}
+
+CmdParam::operator CmdArgument::ASCII_TYPE( void )
+{
+	if( info && info->flags&CmdInfo::FLAG_ARGUMENT ) {
+		if( value!=NULL && value[0]!='\0' )
+			return value;
+		return info->default_value!=NULL ? info->default_value : "";
+	}
+	return info ? (value?"Y":"N") : value;
+}
+
+CmdParam::operator int( void )
+{
+	if( info && info->flags&CmdInfo::FLAG_ARGUMENT ) {
+		if( value!=NULL && value[0]!='\0' )
+			return atoi(value);
+		return info->default_value!=NULL ? atoi(info->default_value) : 0;
+	}
+	return info ? 1 : 0;
+}
+
+CmdParam::operator double( void )
+{
+	if( info && info->flags&CmdInfo::FLAG_ARGUMENT ) {
+		if( value!=NULL && value[0]!='\0' )
+			return atof(value);
+		return info->default_value!=NULL ? atof(info->default_value) : 0.;
+	}
+	return info ? 1. : 0.;
+}
+
+CmdParam::operator bool( void )
+{
+	return value!=NULL;
+}
+
+CmdParam::operator CmdParam::CmdInfoPtr( void )
+{
+	return info;
+}
+
+const char* CmdParam::key( bool sh )
+{
+	const char* n = info ? ( sh ? info->short_name : info->name ) : "";
+	return n ? n : "";
+}
+
+int CmdParam::code( void )
+{
+	return info ? info->code : 0;
+}
+
+// Command Line ---
+
+CmdLine::CmdLine( int argc, char** argv, CmdInfo* commands )
+		: _argc(argc), _argv(argv), _commands(commands) {}
+
+CmdInfo* CmdLine::get_command( const char* name )
+{
+	if( _commands ) {
+		for( int i=0 ; ; i++ ) {
+			if( _commands[i].code == 0 )
+				return NULL;
+			if( strcmp(_commands[i].short_name,name)==0 || strcmp(_commands[i].name,name)==0 )
+				return _commands+i;
+		}
+	}
+	return NULL;
+}
+
+CmdInfo* CmdLine::get_command( int code )
+{
+	if( _commands ) {
+		for( int i=0 ; ; i++ ) {
+			if( _commands[i].code == 0 )
+				return NULL;
+			if( _commands[i].code == code )
+				return _commands+i;
+		}
+	}
+	return NULL;
+}
+
+bool CmdLine::has_param( const char* name )
+{
+	if( _argc>1 && _commands ) {
+		CmdInfo* info 	= NULL;
+		for( int subindex = 1 ; subindex<_argc ; subindex++ ) {
+			info = get_command(_argv[subindex]);
+			if( !info ) continue;
+
+			if( info->flags&CmdInfo::FLAG_ARGUMENT )
+				subindex++;
+
+			if( !info->short_name || strcmp(info->short_name,name)!=0 )
+				if( info->name && strcmp(info->name,name)!=0 ) continue;
+
+			return true;
+		}
+	}
+	return false;
+}
+
+CmdParam CmdLine::get_param( const char* name )
+{
+	if( _argc>1 && _commands ) {
+		CmdInfo* info 	= NULL;
+		for( int subindex = 1 ; subindex<_argc ; subindex++ ) {
+			info = get_command(_argv[subindex]);
+			if( !info ) continue;
+
+			if( info->flags&CmdInfo::FLAG_ARGUMENT )
+				subindex++;
+
+			if( !info->short_name || strcmp(info->short_name,name)!=0 )
+				if( info->name && strcmp(info->name,name)!=0 ) continue;
+
+			return CmdParam( _argv[subindex], info );
+		}
+	}
+	return CmdParam(NULL,get_command(name));
+}
+
+CmdParam CmdLine::get_param( int index )
+{
+	if( _argc>1 && _commands ) {
+		CmdInfo* info 	= NULL;
+		for( int subindex = 1 ; subindex<_argc ; subindex++ ) {
+			info = get_command(_argv[subindex]);
+			if( !info ) continue;
+
+			if( info->flags&CmdInfo::FLAG_ARGUMENT )
+				subindex++;
+
+			if( --index<0 )
+				return CmdParam( _argv[subindex], info );
+		}
+	}
+	return CmdParam(NULL,NULL);
+}
+
+CmdParam CmdLine::get_param_by_code( int code )
+{
+	if( _argc>1 && _commands ) {
+		CmdInfo* info 	= NULL;
+		for( int subindex = 1 ; subindex<_argc ; subindex++ )
+		{
+			info = get_command(_argv[subindex]);
+			if( !info ) continue;
+
+			if( info->flags&CmdInfo::FLAG_ARGUMENT )
+				subindex++;
+
+			if( info->code == code)
+				return CmdParam( _argv[subindex], info );
+		}
+	}
+	return CmdParam(NULL,get_command(code));
+}
+
+int CmdLine::get_param_count( void )
+{
+	int icount 	= 0;
+
+	if( _argc>1 && _commands ) {
+		CmdInfo* info 	= NULL;
+		bool	b		= false;
+		for( int subindex = 1 ; subindex<_argc ; subindex++ ) {
+			if( b ) {
+				icount++;
+				b = false;
+			}
+			else {
+				info = get_command(_argv[subindex]);
+				if( !info ) continue;
+
+				if( info->flags&CmdInfo::FLAG_ARGUMENT )
+					b = true;
+				else
+					icount++;
+			}
+		}
+	}
+	return icount;
+}
+
+CmdArgument CmdLine::get_argument( int index )
+{
+	if( _argc>1 && _commands ) {
+		CmdInfo* info 	= NULL;
+		for( int subindex = 1 ; subindex<_argc ; subindex++ ) {
+			info = get_command(_argv[subindex]);
+			if( !info ) {
+				if( --index<0 )
+					return CmdArgument( _argv[subindex] );
+				continue;
+			}
+
+			if( info->flags&CmdInfo::FLAG_ARGUMENT )
+				subindex++;
+		}
+	}
+	return CmdArgument(NULL);
+}
+
+int CmdLine::get_argument_count( void )
+{
+	int icount 	= 0;
+
+	if( _argc>1 && _commands ) {
+		CmdInfo* info 	= NULL;
+		for( int subindex = 1 ; subindex<_argc ; subindex++ ) {
+			info = get_command(_argv[subindex]);
+			if( !info ) { icount++; continue; }
+
+			if( info->flags&CmdInfo::FLAG_ARGUMENT )
+				subindex++;
+		}
+	}
+	return icount;
+}
+
+CmdParam CmdLine::operator[]( const char* name )
+{
+	return get_param(name);
+}
+
+CmdParam CmdLine::operator[]( int index )
+{
+	return get_param(index);
+}
+
+CmdParam CmdLine::operator()( int code )
+{
+	return get_param_by_code(code);
+}
+
+CmdLine::operator int( void )
+{
+	return get_param_count();
+}
+
+void CmdLine::printHelp(std::ostream& aOut) {
+	aOut << "Command line parameters:" << std::endl
+	<< "--------------------------" << std::endl;
+
+	for (int i = 0; _commands[i].code > 0; i++) {
+		aOut << _commands[i].short_name << " or " << _commands[i].name
+		<< "\n\t\t" << _commands[i].description << std::endl;
+		if (_commands[i].default_value)
+			aOut << "\t\tdefault value:\t" << _commands[i].default_value << std::endl;
+	}
+}
+
+void CmdLine::printParamValues(std::ostream& aOut)
+{
+	aOut << "# Command line:\n#";
+	for(int i=0; i<_argc; i++){
+		aOut << " " << _argv[i];
+	}
+	aOut << "\n#\n# Param\tValue\n";
+	//start from i = 1 to skip "--help"
+	for (int i = 1; _commands[i].code > 0; i++) {
+		const char* val = (const char*)get_param_by_code(i+1);
+		aOut << (_commands[i].name) << "\t" << val << "\n";
+	}
+	aOut.flush();
+}
\ No newline at end of file
diff --git a/src/app/crbeam/CmdLine.h b/src/app/crbeam/CmdLine.h
new file mode 100644
index 0000000..4192fc8
--- /dev/null
+++ b/src/app/crbeam/CmdLine.h
@@ -0,0 +1,117 @@
+/**
+ *
+ * CmdLine.h
+ *
+ * Author:
+ *       Dmitry Ponomarev (Aguacero) <demdxx@gmail.com>
+ *
+ * Copyright (c) 2011 Dmitry Ponomarev V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <ostream>
+
+namespace cors
+{
+	namespace cmdline
+	{
+		struct CmdInfo
+		{
+			enum {
+				FLAG_NULL		= 0,
+				FLAG_ARGUMENT	= 0x01,
+			};
+
+			int			code;
+			char		flags;
+			const char*	short_name;
+			const char*	name;
+			const char* default_value;
+			const char*	description;
+		};
+
+		class CmdArgument
+		{
+			public:
+				typedef const char*		ASCII_TYPE;
+
+				const char* 	value;
+
+			public:
+				CmdArgument( const char* value );
+
+				operator ASCII_TYPE( void );
+				operator int( void );
+				operator float( void );
+				operator bool( void );
+		};
+
+		class CmdParam : public CmdArgument
+		{
+			public:
+				typedef const CmdInfo*	CmdInfoPtr;
+
+				const CmdInfo*  info;
+
+			public:
+				CmdParam( const char* value, const CmdInfo* info );
+
+				operator ASCII_TYPE( void );
+				operator int( void );
+				operator double( void );
+				operator bool( void );
+				operator CmdInfoPtr( void );
+
+				const char* key( bool sh = false );
+				int code( void );
+		};
+
+		class CmdLine
+		{
+			protected:
+				int			_argc;
+				char**  	_argv;
+				CmdInfo*	_commands;
+			public:
+				CmdLine( int argc, char** argv, CmdInfo* commands );
+				CmdInfo* get_command( const char* name );
+				CmdInfo* get_command( int code );
+
+				bool has_param( const char* name );
+				CmdParam get_param( const char* name );
+				CmdParam get_param( int index );
+				CmdParam get_param_by_code( int code );
+				int get_param_count( void );
+
+				CmdArgument get_argument( int index );
+				int get_argument_count( void );
+
+				CmdParam operator[]( const char* name );
+				CmdParam operator[]( int index );
+				CmdParam operator()( int index );
+				operator int( void );
+				void printHelp(std::ostream& aOut);
+				void printParamValues(std::ostream& aOut);
+		};
+
+	}
+}
diff --git a/src/app/crbeam/README.md b/src/app/crbeam/README.md
new file mode 100644
index 0000000..2a16963
--- /dev/null
+++ b/src/app/crbeam/README.md
@@ -0,0 +1,172 @@
+# CRbeam
+
+Monte Carlo simulation of the the cosmic ray and EM cascade beam propagation.
+
+### Authors:
+   Oleg Kalashev and Alexander Korochkin
+   
+### Build:
+
+    git clone git@github.com:okolo/mcray.git
+    cd mcray
+    cmake src/app/crbeam
+    make
+
+### Usage example:
+
+    ./CRbeam -z 0.43 --emax 1e18 --emin 3e11 -N 100000 -p 10 -b 12 -mf 1e-17 -mft -mfr --backgr-mult 3.35 --lEGMF 0.01
+ 
+### Command line parameters:
+
+--------------------------
+-h or --help
+		Show help information
+		
+-log or --log
+		Enable logging
+		
+-logT or --log-thread
+		Log thread filter (set to -1 to disable)
+		default value:	-1
+		
+-logL or --log-level
+		Log level:
+	Error = 0,
+	Warning = 1,
+	Message = 2,
+	Verbose = 3
+		default value:	1
+		
+-tlog or --tlog
+		Log particle trajectories
+		
+-N or --nparticles
+		Number of particles
+		default value:	10000
+		
+-bN or --batch
+		Number of particles in minibatch (0=auto)
+		default value:	0
+		
+-z or --z
+		Source z (or maximal z if --m_z par is given)
+		
+-zf or --z-filament
+		Filament z (kill protons with z less than this value)
+		default value:	0.
+		
+-E or --emax
+		Initial (or maximal for power law) particle energy in eV
+		default value:	1e18
+		
+-e or --emin
+		Minimal energy in eV
+		default value:	1e11
+		
+-pl or --power
+		if>0 use E^(-power) injection 
+		default value:	0
+		
+-es or --emin_source
+		Minimal injection energy in eV (for power law injection only)
+		default value:	0
+		
+-p or --primary
+		Primary particle: 
+	Electron = 0,
+	Positron = 1,
+	Photon = 2,
+	Neutron 9,
+	Proton 10
+		default value:	10
+		
+-nEM or --noEMcascade
+		don't calculate spectra of electrons and photons
+		
+-m or --m_z
+		if parameter is set (!=1000), use continuous source distribution ~(1+z)^m
+		default value:	1000
+		
+-b or --background
+		EBL used:
+0 - zero,
+1 - Kneiske best fit (ELMAG),
+2 - Kneiske minimal (ELMAG),
+3 - Inoue 2012 Baseline,
+4 - Inoue 2012 lower limit,
+5 - Inoue 2012 upper limit,
+6 - Franceschini 2008
+7 - Kneiske best fit (digitized)
+8 - Kneiske minimal (digitized)
+9 - Stecker 2005
+10 - Stecker 2016 lower limit,
+11 - Stecker 2016 upper limit,
+12 - Franceschini 2017;
+default value:	3
+		
+-bm or --backgr-mult
+		EBL multiplier
+		default value:	1
+		
+-badd or --add-backgr
+		Additional matrix background file path (relative to ./tables)
+
+-badd_p or --add-backgr-phys
+		Treat additional background data as physical (not comoving) density
+
+-bcut or --backgr-cut
+		Cut EBL above this energy [eV]
+		default value:	100
+
+-mcmb or --monocmb
+		Use monochromatic 6.3e-4 eV background instead of CMB
+
+-fcmb or --fixedcmb
+		use CMB with fixed temperature (1+Zmax)2.73K
+
+-bc or --eblDeltaZconst
+		EBL extension DeltaZconst param (set to negative to disable EBL extension)
+		default value:	-1
+
+-bp or --eblPowerLow
+		EBL extension PowerLow param
+		default value:	0
+
+-be or --eblDeltaZexp
+		EBL extension DeltaZexp param
+		default value:	1
+
+-o or --output
+		Output directory path (must not exist, will be created)
+
+-oo or --overwrite
+		overwrite output dir if exists
+
+-t or --thinning
+		Alpha thinning
+		default value:	0.9
+
+-pt or --ppp-thinning
+		PPP Rescale Coefficient (double > 0, actual number of secondaries per interaction)
+		default value:	10
+
+-mf or --EGMF
+		Extragalactic magnetic field in Gauss
+		default value:	1e-15
+
+-mfl or --lEGMF
+		Extragalactic magnetic field correlation length in Mpc at z=0
+		default value:	1
+
+-mfr or --randomEGMF
+		randomize EGMF (use different EGMF configurations for different initial particles)
+
+-mft or --turbulentEGMF
+		use turbulent EGMF with Kolmogorov spectrum
+
+-ptau or --print-tau
+		print tau
+
+-os or --output-suffix
+		Output dir name suffix (appended to autogenerated names)
+		default value:	
diff --git a/src/lib/Background.cpp b/src/lib/Background.cpp
new file mode 100644
index 0000000..6a85bcd
--- /dev/null
+++ b/src/lib/Background.cpp
@@ -0,0 +1,1165 @@
+/*
+ * Background.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include <math.h>
+#include <fstream>
+#include <iostream>
+#include <sstream>
+#include <iterator>
+#include <gsl/gsl_sf_erf.h>
+
+#include "Background.h"
+#include "Utils.h"
+#include "TableFunction.h"
+#include "PrecisionTests.h"
+
+
+namespace mcray {
+
+using namespace std;
+
+double BackgroundUtils::CalcIntegralDensity(const IBackground& aBackground, double aRelError)
+{
+	ASSERT_VALID_NO(aBackground.MinE());
+	ASSERT_VALID_NO(aBackground.MaxE());
+	double x1 = log(aBackground.MinE());
+	double x2 = log(aBackground.MaxE());
+	MathUtils math;
+	gsl_function f;
+	f.function = DensityCalculator;
+	f.params = (void*)(&aBackground);
+	return math.Integration_qag(f, x1, x2, 0, aRelError, 1000);
+}
+
+void BackgroundUtils::Print(const IBackground* aBackground, double aZ, std::ostream& aOut, int nIntervals)
+{
+	class Spec : public Function
+	{
+		const IBackground* fBackground;
+		double fZ;
+		virtual double f(double aE_eV) const
+		{
+			return fBackground->n(aE_eV*units.eV, fZ)*units.cm3;
+		}
+		virtual double Xmin() const {return fBackground->MinE()/units.eV; }
+		virtual double Xmax() const {return fBackground->MaxE()/units.eV; }
+	public:
+		Spec(const IBackground* aBackground, double aZ):fBackground(aBackground), fZ(aZ){}
+	};
+	Spec spec(aBackground, aZ);
+	spec.Print(aOut, nIntervals, true);
+}
+
+double BackgroundUtils::DensityCalculator (double x, void * params)
+{
+	IBackground* pBackgr = (IBackground*)params;
+	return pBackgr->n(exp(x),0);
+}
+
+void BackgroundUtils::UnitTest(const IBackground& aBackground, int aPointsZ, int aPointsE)
+{
+	double zMax=aBackground.MaxZ();
+	if(zMax==0)
+		aPointsZ = 1;
+	for(int iZ=0; iZ<aPointsZ; iZ++){
+		double z = zMax/aPointsZ*iZ;
+		ofstream out((aBackground.Name() + "z" + ToString(z)).c_str());
+		Print(&aBackground, z, out, aPointsE);
+	}
+	std::cerr << "density " << CalcIntegralDensity(aBackground)*units.cm3 << std::endl;
+}
+
+TableBackground::TableBackground(std::string aName):
+		fZtoIndex(0),
+		fMinE(DBL_MAX),
+		fMaxE(0),
+		fName(aName)
+{
+
+}
+
+void TableBackground::Init(std::string aDir, const char** aFileList, bool aIsLogscaleY)
+{
+	//the files should be ordered increasingly?
+	//x scale should by monotonic with increasing order
+	aDir = TABLES_DIR + aDir;
+	double lastZ = -1;
+	for(int iZ=0; aFileList[iZ]; iZ++)
+	{
+		double z = -1.;
+		z = atof(aFileList[iZ]);
+		if(z<0)
+			throw "Invalid table background file name";
+		if(z<lastZ)
+			throw "Invalid table background file order";
+
+		std::ifstream file;
+		file.open((aDir + DIR_DELIMITER_STR + aFileList[iZ]).c_str(), std::ios::in);
+		if(!file.is_open())
+			Exception::Throw("Failed to open " + aDir + DIR_DELIMITER_STR + aFileList[iZ]);
+		TableReader* reader = new TableReader(file, 2);
+		Vector& x = reader->getColumn(0);
+		Vector& y = reader->getColumn(1);
+		int iMax = x.size();
+		for(int iX=0; iX<iMax; iX++)
+		{//TODO add support of arbitrary ordered data
+			double E = recordToE(x[iX],z);
+			if(E<fMinE)
+				fMinE=E;
+			else if(E>fMaxE)
+				fMaxE=E;
+		}
+		double leftValue = aIsLogscaleY?(y[0]-1000):0;
+		double rightValue = aIsLogscaleY?(y[y.size()-1]-1000):0;
+
+		GSLTableFunc* f = new GSLTableFunc(reader,gsl_interp_linear,leftValue,rightValue);
+		fData.push_back(f);
+		fZ.push_back(z);
+		fZindex.push_back((double)iZ);
+	}
+	fZtoIndex = new GSLTableFunc(fZ, fZindex, gsl_interp_linear);
+}
+
+TableBackground::~TableBackground() {
+	delete fZtoIndex;
+	for(int i = fData.size()-1; i>=0; i--)
+		delete fData[i];
+}
+
+MatrixBackground::MatrixBackground(std::string aName, std::string aTableFile, bool aIsComoving, bool aExtendToZero):
+		fZtoIndex(0),
+		fLogEtoIndex(0),
+		fName(aName),
+		fExtendToZero(aExtendToZero),
+		fIsComoving(aIsComoving),
+		fBuffer(0)
+{
+	const size_t bufSize = 1048576;//1M
+	fBuffer = new char[bufSize];
+	std::istream_iterator<double> eos;
+	aTableFile = TABLES_DIR + aTableFile;
+	std::ifstream file(aTableFile.c_str());
+
+	if(!file.good())
+		Exception::Throw("Invalid background table file " + aTableFile);
+	file.getline(fBuffer, bufSize);
+	std::istringstream ist(fBuffer);
+	std::istream_iterator<double> iit (ist);
+	for(; iit!=eos; iit++)
+	{
+		fZ.push_back(*iit);
+		fZindex.push_back(fZ.size()-1);
+	}
+	double Epriv = 0;
+	while(!file.eof())
+	{
+		file.getline(fBuffer, bufSize);
+		std::istringstream ist(fBuffer);
+		std::istream_iterator<double> iit (ist);
+		if(iit==eos)
+			break;
+		double E = *iit;
+		if(E<=0 || E<Epriv)
+			Exception::Throw("Invalid background table file " + aTableFile + ": invalid energy value in line " +
+				ToString(fLogEdN_dE.size()+1));
+		Epriv = E;
+		fLogE.push_back(log(E*1e-6/units.Eunit));
+		fLogEindex.push_back(fLogE.size()-1);
+		std::vector<double>* pN = new std::vector<double>();
+		fLogEdN_dE.push_back(pN);
+		for(iit++; iit!=eos; iit++)
+		{
+			double dN_dE = *iit;
+			if(dN_dE<0)
+				Exception::Throw("Invalid background table file " + aTableFile + ": line " +
+									ToString(fLogEdN_dE.size()+1) + " contains " + ToString(pN->size()+1) +
+									" negative value");
+			pN->push_back(dN_dE>0?log(E*dN_dE*units.Vunit):-700.);
+		}
+		if(pN->size()!=fZ.size())
+			Exception::Throw("Invalid background table file " + aTableFile + ": line " +
+					ToString(fLogEdN_dE.size()+1) + " contains " + ToString(pN->size()+1) +
+					" records while " + ToString(fZ.size()+1) + " expected");
+	}
+	fZtoIndex = new GSLTableFunc(fZ, fZindex, gsl_interp_linear, aExtendToZero ? 0. : -1., -1.);
+	fLogEtoIndex = new GSLTableFunc(fLogE, fLogEindex, gsl_interp_linear, -1., -1.);
+	fMinE = exp(fLogE[0]);
+	fMaxE = exp(*(fLogE.end()-1));
+	delete fBuffer;
+	fBuffer = 0;
+}
+
+MatrixBackground::~MatrixBackground()
+{
+	delete fBuffer;
+	delete fZtoIndex;
+	delete fLogEtoIndex;
+	for(std::vector<Vector*>::iterator  it = fLogEdN_dE.begin(); it != fLogEdN_dE.end(); it++)
+		delete (*it);
+}
+
+double MatrixBackground::n(double aE, double aZ) const
+{
+	double logE = log(aE);
+	double indexE = fLogEtoIndex->f(logE);
+	if(indexE<0.)
+		return 0.;
+	double indexZ = fZtoIndex->f(aZ);
+	if(indexZ<0.)
+		return 0.;
+	int iE = indexE;
+	double fracE = indexE-iE;
+	int iZ = indexZ;
+	double fracZ = indexZ-iZ;
+	double result = fLogEdN_dE[iE]->at(iZ)*(1.-fracZ)*(1.-fracE);
+	if(fracZ>0)
+		result += (fLogEdN_dE[iE]->at(iZ+1)*fracZ*(1.-fracE));
+	if(fracE>0)
+	{
+		result += (fLogEdN_dE[iE+1]->at(iZ)*(1.-fracZ)*fracE);
+		if(fracZ>0)
+			result += (fLogEdN_dE[iE+1]->at(iZ+1)*fracZ*fracE);
+	}
+	result = exp(result);
+	if(fIsComoving)
+	{
+		double z1=1.+aZ;
+		result *= (z1*z1*z1);
+	}
+	return result;
+}
+
+PlankBackground::PlankBackground(double aT0, double aEmin, double aEmax, double aMinZ, double aMaxZ, bool aFixedTemperature):
+	fT0(aT0),
+	fMinE(aEmin),
+	fMaxE(aEmax),
+	fMinZ(aMinZ),
+	fMaxZ(aMaxZ),
+	fFixedTemperature(aFixedTemperature)
+{
+	ASSERT(aEmin<aEmax);
+	ASSERT(aMinZ<aMaxZ);
+	fName = "PlankT=" + ToString(aT0*units.Eunit*Units::phTemperature_mult) + "K";
+}
+
+double PlankBackground::n(double aE, double aZ) const
+{
+	double gamma=aE/fT0;
+	if(!fFixedTemperature)
+		gamma /= (1.0+aZ);
+	if(gamma<700)
+	{
+		double ex_1 = (gamma > 1e-2) ? exp(gamma)-1.0 : gamma*(1.+ 0.5*gamma*(1. + 0.333333333333333*gamma*(1. + 0.25*gamma)));
+		return aE*aE*aE/9.869604401089/ex_1;//that is E^3/Pi^2/(...)
+	}
+	return 0;
+}
+
+	/*!
+	@param[in]  aE background photons energy central value (internal units)
+	@param[in]  aSigma Gaussian distribution sigma (internal units)
+	@param[in]  aWidth width at which the distribution is cut (in units of sigma)
+	@param[in]  aConcentration integral concentration of photons (internal units)
+	@param[in]  aMinZ minimal background red shift
+	@param[in]  aMaxZ maximal background red shift
+	*/
+GaussianBackground::GaussianBackground(double aE, double aSigma, double aWidth,
+		double aConcentration, double aMinZ, double aMaxZ):
+		fE0(aE), fSigma(aSigma), fMinZ(aMinZ), fMaxZ(aMaxZ), fWidth(aWidth)
+{
+	fMinE = fE0 - 0.5*fSigma*aWidth;
+	fMaxE = fE0 + 0.5*fSigma*aWidth;
+	fNorm = 1./fSigma/sqrt(2.*M_PI);
+	double mult = 1./sqrt(2.)/fSigma;
+	double conc = 0.5*(gsl_sf_erf(mult*(fMaxE-fE0))-gsl_sf_erf(mult*(fMinE-fE0)));
+	fNorm *= aConcentration/conc;
+	fName = "Gaussian ( E0 = " + ToString(fE0*units.Eunit*1e6) + " eV; sigma/E0 = " +
+			ToString(fSigma/fE0)  + "; width/sigma = " + ToString(aWidth)  +
+			"; n = " + ToString(aConcentration/units.Vunit) + "cm^-3 )";
+}
+
+double GaussianBackground::n(double aE, double aZ) const
+{
+	double ratio = fabs((aE-fE0)/fSigma);
+	if(ratio>fWidth)
+		return 0.;
+	return fNorm*exp(-0.5*ratio*ratio)*aE;
+}
+
+double TableBackground::n(double aE, double aZ) const
+{
+	ASSERT(fMaxE>fMinE);//make sure Init() method was called prior
+	int iZ = (int)fZtoIndex->f(aZ);
+	if(iZ<0)
+		return 0;
+	double x = EtoRecordX(aE,aZ);
+	double leftY = fData[iZ]->f(x);
+	double y=0.;
+	if(iZ==((int)fZ.size())-1)
+		y = leftY;
+	else
+	{
+		double rightY = fData[iZ+1]->f(x);
+		double zL = fZ[iZ];
+		double zR = fZ[iZ+1];
+		y = leftY + (rightY-leftY)/(zR-zL)*(aZ-zL);
+	}
+	return recordToN(x, y, aZ);
+}
+
+double TableBackground::MinE() const
+{
+	ASSERT(fMaxE>fMinE);//make sure Init() method was called prior
+	return fMinE;
+}
+
+double TableBackground::MaxE() const
+{
+	ASSERT(fMaxE>fMinE);//make sure Init() method was called prior
+	return fMaxE;
+}
+
+double TableBackground::MinZ() const
+{
+	ASSERT(fMaxE>fMinE);//make sure Init() method was called prior
+	return fZ[0];
+}
+
+double TableBackground::MaxZ() const
+{
+	ASSERT(fMaxE>fMinE);//make sure Init() method was called prior
+	return fZ[fZ.size()-1];
+}
+
+void CompoundBackground::AddComponent(IBackground* aBackground, double aWeight)
+{
+	fBackgrounds.push_back(aBackground);
+	fWeights.push_back(aWeight);
+}
+
+double CompoundBackground::n(double aE, double aZ) const
+{
+	double sum = 0.;
+	vector<double>::const_iterator wit = fWeights.begin();
+	for(vector<IBackground*>::const_iterator it = fBackgrounds.begin(); it != fBackgrounds.end(); it++, wit++)
+	{
+		const IBackground* b = *it;
+		double weight = *wit;
+		if(aE>=b->MinE() && aE<=b->MaxE() && aZ>=b->MinZ() && aZ<=b->MaxZ())
+			sum += weight*b->n(aE,aZ);
+	}
+	return sum;
+}
+
+std::string CompoundBackground::Name() const
+{
+	std::string result = "";
+	for(vector<IBackground*>::const_iterator it = fBackgrounds.begin(); it != fBackgrounds.end(); it++)
+	{
+		const IBackground* b = *it;
+		if(result.length()>0)
+			result = result + " + " + b->Name();
+		else
+			result = b->Name();
+	}
+	return result;
+}
+
+double CompoundBackground::MinE() const
+{
+	double result = DBL_MAX;
+	for(vector<IBackground*>::const_iterator it = fBackgrounds.begin(); it != fBackgrounds.end(); it++)
+	{
+		const IBackground* b = *it;
+		double min = b->MinE();
+		if(result>min)
+			result = min;
+	}
+	return result;
+}
+
+double CompoundBackground::MaxE() const
+{
+	double result = 0;
+	for(vector<IBackground*>::const_iterator it = fBackgrounds.begin(); it != fBackgrounds.end(); it++)
+	{
+		const IBackground* b = *it;
+		double max = b->MaxE();
+		if(result<max)
+			result = max;
+	}
+	return result;
+}
+
+double CompoundBackground::MinZ() const
+{
+	double result = DBL_MAX;
+	for(vector<IBackground*>::const_iterator it = fBackgrounds.begin(); it != fBackgrounds.end(); it++)
+	{
+		const IBackground* b = *it;
+		double min = b->MinZ();
+		if(result>min)
+			result = min;
+	}
+	return result;
+}
+
+double CompoundBackground::MaxZ() const
+{
+	double result = 0;
+	for(vector<IBackground*>::const_iterator it = fBackgrounds.begin(); it != fBackgrounds.end(); it++)
+	{
+		const IBackground* b = *it;
+		double max = b->MaxZ();
+		if(result<max)
+			result = max;
+	}
+	return result;
+}
+
+CompoundBackground::~CompoundBackground()
+{
+	for(vector<IBackground*>::iterator it = fBackgrounds.begin(); it != fBackgrounds.end(); it++)
+		delete *it;
+}
+
+BackgroundIntegral* ContinuousBackgroundIntegral::Clone() const
+{
+	return new ContinuousBackgroundIntegral(*this);
+}
+
+ContinuousBackgroundIntegral::ContinuousBackgroundIntegral(const ContinuousBackgroundIntegral& aBackgroundIntegral):
+		fStepZ(aBackgroundIntegral.fStepZ),
+		fLogStepK(aBackgroundIntegral.fLogStepK),
+		fZmaxLimit(aBackgroundIntegral.fZmaxLimit),
+		fMaxK(aBackgroundIntegral.fMaxK),
+		fMinK(aBackgroundIntegral.fMinK),
+		fEpsRel(aBackgroundIntegral.fEpsRel),
+		fBinningK(aBackgroundIntegral.fBinningK),
+		fEnableDebugOutput(aBackgroundIntegral.fEnableDebugOutput)
+{
+	int size = aBackgroundIntegral.fIntegrals.size();
+	for(int iZ=0; iZ<size; iZ++)
+	{
+		fIntegrals.push_back(aBackgroundIntegral.fIntegrals[iZ]->Clone());
+	}
+}
+
+ContinuousBackgroundIntegral::ContinuousBackgroundIntegral(IBackground& aBackground, double aStepZ, double aLogStepK, double aZmaxLimit, double aEpsRel):
+fStepZ(aStepZ),
+fLogStepK(aLogStepK),
+fZmaxLimit(aZmaxLimit),
+fEpsRel(aEpsRel),
+fEnableDebugOutput(false)
+{
+	ASSERT(fEpsRel<=0.1);
+	if(aLogStepK<=1)
+		Exception::Throw("invalid BackgroundIntegral::BackgroundIntegral argument aLogStepE");
+	fMaxK = aBackground.MaxE();
+	fMinK = aBackground.MinE();
+	if(aBackground.MaxZ()<fZmaxLimit)
+		fZmaxLimit=aBackground.MaxZ();
+	int nIntervalsZ = (int)(fZmaxLimit/fStepZ + 1.0);
+	ASSERT(nIntervalsZ>=0);
+	fStepZ = (nIntervalsZ>0)?(fZmaxLimit/((double)nIntervalsZ)):0;
+	int nIntervalsE = (int)(log10(fMaxK/fMinK)/log10(fLogStepK) + 0.5);
+	if(nIntervalsE<10)
+		nIntervalsE = 2;
+	fLogStepK = pow(fMaxK/fMinK, 1./nIntervalsE);
+	if(fLogStepK<=1)
+			Exception::Throw("invalid BackgroundIntegral::BackgroundIntegral argument aBackground (zero energy range)");
+
+	{double k=fMinK;
+	for(int iK=0; iK<=nIntervalsE; iK++, k*=fLogStepK)
+	{//build energy binning
+		fBinningK.push_back(k);
+	}}
+	Kern kern(aBackground);
+	{for(int iZ=0; iZ<=nIntervalsZ; iZ++)
+	{
+		fData.push_back(new Vector(nIntervalsE+1,0.));
+		double z=iZ*fStepZ;
+		kern.Z = z;
+		double k=fMaxK/fLogStepK;
+		double sum = 0.;
+		(*fData[iZ])[nIntervalsE]=0.;//I(k_max)=0
+		for(int iK=nIntervalsE-1; iK>=0; iK--,k/=fLogStepK)
+		{
+			double I = math.Integration_qag(kern, k, k*fLogStepK, 0, fEpsRel, (int)(1./fEpsRel));
+			ASSERT_VALID_NO(I);
+			sum += I;
+			(*fData[iZ])[iK]=sum;
+		}
+	}}
+	{for(int iZ=0; iZ<=nIntervalsZ; iZ++)
+	{
+		GSLTableFunc* func = new GSLTableFunc(fBinningK, *fData[iZ], new LogScale(), new LogScale(), gsl_interp_linear);
+		func->SetAutoLimits();//for k<k_min I(k)=I(k_min); for k>k_max I(k)=I(k_max)=0
+		fIntegrals.push_back(func);
+	}}
+}
+
+double ContinuousBackgroundIntegral::Kern::f(double aX) const
+{
+	// TODO think about switching to log scale integration
+	return fBackground.n(aX, Z)/(aX*aX*aX);
+}
+
+ContinuousBackgroundIntegral::RateSKern::RateSKern(const Function&	aBackrgoundIntegral, const Function& aSigma, const Particle& aParticle) :
+	fBackrgoundIntegral(aBackrgoundIntegral),fSigma(aSigma)
+{
+	fM2 = aParticle.Mass();
+	fM2*=fM2;
+	fMaxEmult = 2.0*aParticle.Energy*(1.-aParticle.beta());
+	if(fMaxEmult>0)
+		fMaxEmult = 1./fMaxEmult;
+	else
+		fMaxEmult = -1.;
+
+	fMult = 0.5/aParticle.Energy/(1.+aParticle.beta());
+}
+//(s-m^2) sigma(s) I((s-m^2)/2/E/(1+beta),z)
+double ContinuousBackgroundIntegral::RateSKern::f(double s) const
+{
+	double bi = fBackrgoundIntegral((s-fM2)*fMult);
+	if(fMaxEmult>0)
+	{
+		bi -= fBackrgoundIntegral((s-fM2)*fMaxEmult);
+	}
+	return fSigma(s)*(s-fM2)*bi;
+}
+
+ContinuousBackgroundIntegral::RateKKern::RateKKern(const Function&	aBackrgoundIntegral, const Function& aSigma, const Particle& aParticle) :
+		fBackrgoundIntegral(aBackrgoundIntegral),fSigma(aSigma)
+{
+	double m = aParticle.Mass();
+
+	fMaxEmult = aParticle.Energy*(1.-aParticle.beta());
+	if(fMaxEmult>0)
+		fMaxEmult = m/fMaxEmult;
+	else
+		fMaxEmult = -1.;
+
+	fMult = m/aParticle.Energy/(1.+aParticle.beta());
+}
+
+double ContinuousBackgroundIntegral::RateKKern::f(double k) const
+{
+	double bi = fBackrgoundIntegral(k*fMult);
+	if(fMaxEmult>0)
+	{
+		bi -= fBackrgoundIntegral(k*fMaxEmult);
+	}
+	return fSigma(k)*k*bi;
+}
+
+ContinuousBackgroundIntegral::RateSKernZ::RateSKernZ(const Function&	aBackrgoundIntegralZ1, const Function&	aBackrgoundIntegralZ2, double aZ1, double aZ2, const Function& aSigma, const Particle& aParticle):
+		RateSKern(aBackrgoundIntegralZ1, aSigma, aParticle),
+		fBackrgoundIntegralZ1(aBackrgoundIntegralZ1),
+		fBackrgoundIntegralZ2(aBackrgoundIntegralZ2)
+{
+	//initializing linear interpolation coefficients
+	fCoef2 = (aParticle.Time.z()-aZ1)/(aZ2-aZ1);
+	fCoef1 = 1.-fCoef2;
+}
+
+//(s-m^2) sigma(s) I((s-m^2)/2/E/(1+beta),z)
+double ContinuousBackgroundIntegral::RateSKernZ::f(double s) const
+{
+	double k = (s-fM2)*fMult;
+	double bi1 = fBackrgoundIntegralZ1(k);
+	double bi2 = fBackrgoundIntegralZ2(k);
+	if(fMaxEmult>0)
+	{
+		k = (s-fM2)*fMaxEmult;
+		bi1 -= fBackrgoundIntegralZ1(k);
+		bi2 -= fBackrgoundIntegralZ2(k);
+	}
+	double bi = fCoef1*bi1 + fCoef2*bi2;
+	double result = fSigma(s)*(s-fM2)*bi;
+	//std::cout.precision(15);
+	//std::cout << s << "\t" << result << std::endl;
+	return result;
+}
+
+ContinuousBackgroundIntegral::RateKKernZ::RateKKernZ(const Function&	aBackrgoundIntegralZ1, const Function&	aBackrgoundIntegralZ2, double aZ1, double aZ2, const Function& aSigma, const Particle& aParticle):
+		RateKKern(aBackrgoundIntegralZ1, aSigma, aParticle),
+		fBackrgoundIntegralZ1(aBackrgoundIntegralZ1),
+		fBackrgoundIntegralZ2(aBackrgoundIntegralZ2)
+{
+	//initializing linear interpolation coefficients
+	fCoef2 = (aParticle.Time.z()-aZ1)/(aZ2-aZ1);
+	fCoef1 = 1.-fCoef2;
+}
+
+double ContinuousBackgroundIntegral::RateKKernZ::f(double k) const
+{
+	double eps = k*fMult;
+	double bi1 = fBackrgoundIntegralZ1(eps);
+	double bi2 = fBackrgoundIntegralZ2(eps);
+	if(fMaxEmult>0)
+	{
+		eps = k*fMaxEmult;
+		bi1 -= fBackrgoundIntegralZ1(eps);
+		bi2 -= fBackrgoundIntegralZ2(eps);
+	}
+	double bi = fCoef1*bi1 + fCoef2*bi2;
+	double result = fSigma(k)*k*bi;
+	return result;
+}
+
+double ContinuousBackgroundIntegral::GetRateAndSampleS(const Function& aSigma, const Particle& aParticle, double aRand, double& aS, double aAbsError)
+{
+	ASSERT(aRand>0 || aRand<1);
+	double SminSigma = aSigma.Xmin();
+	ASSERT_VALID_NO(SminSigma);
+	double beta = aParticle.beta();
+	double m=aParticle.Mass();
+	double Smax = m*m+2.*aParticle.Energy*fMaxK*(1.+beta);
+	double SminKinematic = m*m+2.*aParticle.Energy*fMinK*(1.-beta);
+	double Smin = SminKinematic<SminSigma ? SminSigma : SminKinematic;
+	if(aSigma.Xmax()<Smax)
+		Smax=aSigma.Xmax();
+	if(Smax<=Smin)
+		return 0.;
+
+	double z = aParticle.Time.z();
+	int iZ= fStepZ>0 ? ((int)(z/fStepZ)):0;
+	//int maxIntervals = (int)(0.1/fEpsRel + 10.5);
+
+	//int nStepsS = (int)(log10(Smax/Smin)/log10(fLogStepK)+0.5);
+	//if(nStepsS<5)
+	//	nStepsS = 5;
+	//double stepS = pow(Smax/Smin,1./nStepsS);
+	double totalRate = 0.;//rate times 8*E*beta
+
+	bool zDependenceExists = (iZ<(int)fIntegrals.size()-1);
+	SafePtr<RateSKern> kern;
+	if(zDependenceExists)
+		kern = new RateSKernZ(*fIntegrals[iZ], *fIntegrals[iZ + 1], iZ * fStepZ, (iZ + 1) * fStepZ, aSigma, aParticle);
+	else
+		kern = new RateSKern(*fIntegrals[iZ], aSigma, aParticle);
+	if(MathUtils::SampleLogDistribution(*kern, aRand, aS, totalRate, Smin, Smax, fEpsRel))
+	{
+		return totalRate/8./aParticle.Energy/aParticle.Energy/aParticle.beta();
+	}
+	aS=0;
+	return 0;
+}
+
+double ContinuousBackgroundIntegral::GetRateAndSampleK(const Function& aSigmaK, const Particle& aParticle, double aRand, double&aK, double aAbsError)
+{
+	ASSERT(aRand>0 || aRand<1);
+	double KminSigma = aSigmaK.Xmin();
+	ASSERT_VALID_NO(KminSigma);
+	double beta = aParticle.beta();
+	double m=aParticle.Mass();
+	double Kmax = aParticle.Energy * fMaxK * (1. + beta)/m;
+	double KminKinematic = aParticle.Energy * fMinK * (1. - beta)/m;
+	double Kmin = KminKinematic < KminSigma ? KminSigma : KminKinematic;
+	if(aSigmaK.Xmax() < Kmax)
+		Kmax = aSigmaK.Xmax();
+	if(Kmax <= Kmin)
+		return 0.;
+
+	double z = aParticle.Time.z();
+	int iZ= fStepZ>0 ? ((int)(z/fStepZ)):0;
+
+	double totalRate = 0.;//rate times 8*E*beta
+
+	bool zDependenceExists = (iZ<(int)fIntegrals.size()-1);
+	SafePtr<RateKKern> kern;
+	if(zDependenceExists)
+		kern = new RateKKernZ(*fIntegrals[iZ], *fIntegrals[iZ + 1], iZ * fStepZ, (iZ + 1) * fStepZ,
+											 aSigmaK, aParticle);
+	else
+		kern = new RateKKern(*fIntegrals[iZ], aSigmaK, aParticle);
+
+	if(MathUtils::SampleLogDistribution(*kern, aRand, aK, totalRate, Kmin, Kmax, fEpsRel))
+	{
+		double gamma=aParticle.Energy/m;
+		return totalRate/(2.0*gamma*gamma*aParticle.beta());
+	}
+	aK =0;
+	return 0;
+}
+
+double ContinuousBackgroundIntegral::GetRateS(const Function &aSigma, const Particle &aParticle, double aAbsError)
+{
+	double SminSigma = aSigma.Xmin();
+	ASSERT_VALID_NO(SminSigma);
+	double beta = aParticle.beta();
+	double m=aParticle.Mass();
+	double Smax = m*m+2.*aParticle.Energy*fMaxK*(1.+beta);
+	double SminKinematic = m*m+2.*aParticle.Energy*fMinK*(1.-beta);
+	double Smin = SminKinematic<SminSigma ? SminSigma : SminKinematic;
+
+	if(Smax<=Smin)
+		return 0.;
+	if(aSigma.Xmax()<Smax)
+		Smax=aSigma.Xmax();
+
+	double z = aParticle.Time.z();
+	int iZ= fStepZ>0 ? ((int)(z/fStepZ)):0;
+	int maxIntervals = (int)(log10(Smax/Smin)/log10(fLogStepK)/fEpsRel*1e-2 + 100);
+
+	RateSKern leftKern(*fIntegrals[iZ], aSigma, aParticle);
+
+	double leftRate = math.Integration_qag(leftKern,Smin,Smax,0,fEpsRel,maxIntervals);
+	ASSERT_VALID_NO(leftRate);
+
+	double result = 0;
+	if(iZ==(int)fIntegrals.size()-1)
+		result = leftRate;
+	else
+	{
+		RateSKern rightKern(*fIntegrals[iZ + 1], aSigma, aParticle);
+		double rightRate = math.Integration_qag(rightKern,Smin,Smax,aAbsError,fEpsRel,maxIntervals);
+		ASSERT_VALID_NO(rightRate);
+		result = leftRate + (rightRate-leftRate)/fStepZ*(z-iZ*fStepZ);
+		ASSERT_VALID_NO(result);
+	}
+	return result/8./aParticle.Energy/aParticle.Energy/aParticle.beta();
+}
+
+double ContinuousBackgroundIntegral::GetRateK(const Function &aSigmaK, const Particle &aParticle, double aAbsError)
+{
+	double KminSigma = aSigmaK.Xmin();
+	ASSERT_VALID_NO(KminSigma);
+	double beta = aParticle.beta();
+	double m=aParticle.Mass();
+	double Kmax = aParticle.Energy * fMaxK * (1. + beta)/m;
+	double KminKinematic = aParticle.Energy * fMinK * (1. - beta)/m;
+	double Kmin = KminKinematic < KminSigma ? KminSigma : KminKinematic;
+
+	if(Kmax <= Kmin)
+		return 0.;
+	if(aSigmaK.Xmax() < Kmax)
+		Kmax = aSigmaK.Xmax();
+
+	double z = aParticle.Time.z();
+	int iZ= fStepZ>0 ? ((int)(z/fStepZ)):0;
+	int maxIntervals = (int)(log10(Kmax / Kmin) / log10(fLogStepK) / fEpsRel * 1e-2 + 100);
+
+	RateKKern leftKern(*fIntegrals[iZ], aSigmaK, aParticle);
+
+	double leftRate = math.Integration_qag(leftKern, Kmin, Kmax, 0, fEpsRel, maxIntervals);
+	ASSERT_VALID_NO(leftRate);
+
+	double result = 0;
+	if(iZ==(int)fIntegrals.size()-1)
+		result = leftRate;
+	else
+	{
+		RateKKern rightKern(*fIntegrals[iZ + 1], aSigmaK, aParticle);
+		double rightRate = math.Integration_qag(rightKern, Kmin, Kmax, aAbsError, fEpsRel, maxIntervals);
+		ASSERT_VALID_NO(rightRate);
+		result = leftRate + (rightRate-leftRate)/fStepZ*(z-iZ*fStepZ);
+		ASSERT_VALID_NO(result);
+	}
+	double gamma=aParticle.Energy/m;
+	return result/(2.0*gamma*gamma*aParticle.beta());
+}
+
+ContinuousBackgroundIntegral::~ContinuousBackgroundIntegral()
+{
+	//ASSERT(fData.size()==fIntegrals.size());//fData may be empty if this object was created with Clone() method
+	for(int iZ=fData.size()-1; iZ>=0; iZ--)
+	{
+		delete fData[iZ];
+	}
+	for(int iZ=fIntegrals.size()-1; iZ>=0; iZ--)
+	{
+		delete fIntegrals[iZ];
+	}
+	fData.clear();
+	fIntegrals.clear();
+}
+
+void ContinuousBackgroundIntegral::UnitTest()
+{
+	double zMax = 1;
+	double kStep = pow(10,0.05);
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	////// Background integral calculation test
+
+	double kMin = 1e-3*cmbTemp;
+	double kMax = 1e3*cmbTemp;
+	PlankBackground plank(cmbTemp, kMin, kMax, 0, zMax);
+	ContinuousBackgroundIntegral bi(plank, 0.1, kStep, zMax, 1e-3);
+	Function& I = *(bi.fIntegrals[0]);
+	double kMaxI = PlankBackgroundIntegral(cmbTemp, kMax);
+	std::cout << "#Z=0" << "\n";
+	for(double k=0.5*kMin; k<2*kMax; k*=kStep)
+	{
+		std::cout << k/cmbTemp << "\t" << I(k) << "\t" << PlankBackgroundIntegral(cmbTemp, k) - kMaxI << '\n';
+	}
+	std::cout << "\n\n#table z=0\n";
+	I.Print(std::cout,1./cmbTemp,1.);
+
+	std::cout << "\n\n#Z=" << zMax << "\n";
+	Function& I1 = *(bi.fIntegrals[bi.fIntegrals.size()-1]);
+	kMaxI = PlankBackgroundIntegral((1.+zMax)*cmbTemp, kMax);
+	for(double k=0.5*kMin; k<2*kMax; k*=kStep)
+	{
+		std::cout << k/cmbTemp << "\t" << I1(k) << "\t" << PlankBackgroundIntegral((1.+zMax)*cmbTemp, k) - kMaxI << '\n';
+	}
+
+	///// Total rate and sampling test z=0
+
+	class TestSigma : public Function
+	{
+	public:
+		virtual double f(double s) const { return 1./s;}
+		virtual double Xmin() const {return sMin;}
+		virtual double Xmax() const {return sMax;}
+		double sMin;
+		double sMax;
+	};
+
+	kMin = 1e-6*cmbTemp;
+	kMax = 1e-5*cmbTemp;
+	PlankBackground plankLow(cmbTemp, kMin, kMax, 0, zMax);
+	ContinuousBackgroundIntegral biLow(plankLow, 0.1, kStep, zMax, 1e-3);
+	TestSigma ts;
+	Particle p(Photon, 0.);
+	p.Energy = 10;
+	ts.sMin = 2*kMin*4*p.Energy;
+	ts.sMax = 0.5*kMax*4*p.Energy;
+	double rate = biLow.GetRateS(ts, p);
+	kMaxI = PlankBackgroundIntegral(cmbTemp, kMax);
+	double thRate = PlankBackgroundTestRate(cmbTemp,p.Energy,ts.sMax) -
+			PlankBackgroundTestRate(cmbTemp,p.Energy,ts.sMin) - kMaxI*(ts.sMax-ts.sMin)/8./p.Energy/p.Energy;
+	double S;
+	biLow.fEnableDebugOutput = true;
+	double rateSample = biLow.GetRateAndSampleS(ts, p, 0.5, S);
+	biLow.fEnableDebugOutput = false;
+
+	cout << "#Total rate test (z=0): rate/sample rate/thRate : " << rate << " / " << rateSample <<  " / " << thRate << "\n";
+	cout << "#<rand> <S> <Smin> <Smax>\n";
+	for(double rand=0.; rand<=1.; rand+=0.1)
+	{
+		biLow.GetRateAndSampleS(ts, p, rand, S);
+		cout << rand << "\t" << S << "\t" << ts.sMin << "\t" << ts.sMax << "\n";
+	}
+
+	///// Total rate and sampling test z=0.55*zMax
+
+	double z = 0.55*zMax;
+	p.Time.setZ(z);
+	cmbTemp *= (1.+z);
+	rate = biLow.GetRateS(ts, p);
+
+	kMaxI = PlankBackgroundIntegral(cmbTemp, kMax);
+	thRate = PlankBackgroundTestRate(cmbTemp,p.Energy,ts.sMax) -
+			PlankBackgroundTestRate(cmbTemp,p.Energy,ts.sMin) - kMaxI*(ts.sMax-ts.sMin)/8./p.Energy/p.Energy;
+
+	biLow.fEnableDebugOutput = true;
+	rateSample = biLow.GetRateAndSampleS(ts, p, 0.5, S);
+	biLow.fEnableDebugOutput = false;
+
+	cout << "#Total rate test(z=" << z << ") : rate/sample rate/thRate : " << rate << " / " << rateSample <<  " / " << thRate << "\n";
+	cout << "#<rand> <S> <Smin> <Smax>\n";
+	for(double rand=0.; rand<=1.; rand+=0.1)
+	{
+		biLow.GetRateAndSampleS(ts, p, rand, S);
+		cout << rand << "\t" << S << "\t" << ts.sMin << "\t" << ts.sMax << "\n";
+	}
+}
+
+double ContinuousBackgroundIntegral::PlankBackgroundTestRate(double aT, double aE, double aS)
+{
+	double x = 0.25*aS/aE/aT;
+	return 0.5*aT*aT/aE/M_PI/M_PI*(0.25*x*x + x - x*log(x));
+}
+
+double ContinuousBackgroundIntegral::PlankBackgroundIntegral(double aT, double aK)
+{
+	double gamma = aK/aT;
+	double result = 0;
+	if(gamma<1e-5)
+		result = 0.5*gamma-gamma*gamma/24.0+gamma*gamma*gamma*gamma/2880.-log(gamma);
+	else if(gamma<700)
+		result = gamma-log(exp(gamma)-1);
+	else
+		result = 0;
+	return aT/M_PI/M_PI*result;
+}
+
+BackgroundIntegral* MonochromaticBackgroundIntegral::Clone() const
+{
+	return new MonochromaticBackgroundIntegral(*fConcentration.Clone(), *fEnergy.Clone(), fLogStepE, fEpsRel);
+}
+
+MonochromaticBackgroundIntegral::RateSKern::RateSKern(const Function& aSigma, const Particle& aParticle) :
+	fSigma(aSigma)
+{
+	fM2 = aParticle.Mass();
+	fM2*=fM2;
+}
+//(s-m^2) sigma(s) I((s-m^2)/2/E/(1+beta),z)
+double MonochromaticBackgroundIntegral::RateSKern::f(double s) const
+{
+	return fSigma(s)*(s-fM2);
+}
+
+double MonochromaticBackgroundIntegral::RateKKern::f(double k) const
+{
+	return fSigma(k)*k;
+}
+
+double MonochromaticBackgroundIntegral::GetRateAndSampleS(const Function& aSigma, const Particle& aParticle, double aRand, double& aS, double aAbsError)
+{
+	ASSERT(aRand>0 || aRand<1);
+	aS=0;
+	double z = aParticle.Time.z();
+	double n=fConcentration.f(z);
+	if(n<=0.)
+		return 0.;
+	double k=fEnergy.f(z);
+
+	double SminSigma = aSigma.Xmin();
+	ASSERT_VALID_NO(SminSigma);
+	double beta = aParticle.beta();
+	double m=aParticle.Mass();
+
+	double Smax = m*m+2.*aParticle.Energy*k*(1.+beta);
+	double SminKinematic = m*m+2.*aParticle.Energy*k*(1.-beta);
+	double Smin = SminKinematic<SminSigma ? SminSigma : SminKinematic;
+	if(aSigma.Xmax()<Smax)
+		Smax=aSigma.Xmax();
+	if(Smax<=Smin)
+		return 0.;
+
+	double totalRate = 0.;//rate times 8*E*beta
+	RateSKern kern(aSigma, aParticle);
+
+	if(MathUtils::SampleLogDistribution(kern, aRand, aS, totalRate, Smin, Smax, fEpsRel))
+	{
+		return totalRate*n/k/k/8./aParticle.Energy/aParticle.Energy/aParticle.beta();
+	}
+	aS=0;
+	return 0;
+}
+
+double MonochromaticBackgroundIntegral::GetRateS(const Function& aSigma, const Particle& aParticle, double aAbsError)
+{
+	double z = aParticle.Time.z();
+	double n=fConcentration.f(z);
+	if(n<=0.)
+		return 0.;
+	double k=fEnergy.f(z);
+	double SminSigma = aSigma.Xmin();
+	ASSERT_VALID_NO(SminSigma);
+	double beta = aParticle.beta();
+	double m=aParticle.Mass();
+	double Smax = m*m+2.*aParticle.Energy*k*(1.+beta);
+	double SminKinematic = m*m+2.*aParticle.Energy*k*(1.-beta);
+	double Smin = SminKinematic<SminSigma ? SminSigma : SminKinematic;
+
+	if(Smax<=Smin)
+		return 0.;
+	if(aSigma.Xmax()<Smax)
+		Smax=aSigma.Xmax();
+
+	int maxIntervals = (int)(log10(Smax/Smin)/log10(fLogStepE)/fEpsRel*1e-2 + 100);
+
+	RateSKern kern(aSigma, aParticle);
+
+	double rate = math.Integration_qag(kern,Smin,Smax,aAbsError,fEpsRel,maxIntervals);
+	ASSERT_VALID_NO(rate);
+
+	return rate*n/k/k/8./aParticle.Energy/aParticle.Energy/aParticle.beta();
+}
+
+HighRedshiftBackgrExtension::HighRedshiftBackgrExtension(IBackground* aBackground, double aDeltaZconst, double aPowerLow, double aDeltaZexp, double aZmax):
+		fBackground(aBackground),
+		fPowerLow(aPowerLow),
+		fDeltaZexp(aDeltaZexp),
+		fZmax(aZmax),
+		fInnerZmax(fBackground->MaxZ())
+{
+	if(aDeltaZconst<0)
+		Exception::Throw("HighRedshiftBackgrExtension: invalid DeltaZconst");
+	fZconst=fInnerZmax+aDeltaZconst;
+}
+
+double HighRedshiftBackgrExtension::n(double aE, double aZ) const
+{
+	if(aZ<=fInnerZmax)
+		return fBackground->n(aE, aZ);
+	if(aZ>fZmax)
+		return 0.;
+	double expansionFactor = (1.+aZ)/(1+fInnerZmax);
+	expansionFactor *= (expansionFactor*expansionFactor);// comoving volumes ratio
+
+	if(aZ<fZconst)
+		return expansionFactor*fBackground->n(aE, 0.9999*fInnerZmax);
+
+	double powerLowFactor = pow((1.+aZ)/(1.+fZconst),fPowerLow);
+	double expFactor = (fDeltaZexp>0)?exp((fZconst-aZ)/fDeltaZexp):1;
+
+	return expansionFactor*powerLowFactor*expFactor*fBackground->n(aE, 0.9999*fInnerZmax);
+}
+
+double HighRedshiftBackgrExtension::MinE() const
+{
+	return fBackground->MinE();
+}
+
+double HighRedshiftBackgrExtension::MaxE() const
+{
+	return fBackground->MaxE();
+}
+
+double HighRedshiftBackgrExtension::MinZ() const
+{
+	return fBackground->MinZ();
+}
+
+double HighRedshiftBackgrExtension::MaxZ() const
+{
+	return fZmax;
+}
+
+std::string HighRedshiftBackgrExtension::Name() const
+{
+	std::string name = "extended ";
+	name += fBackground->Name();
+	return name;
+}
+
+HighRedshiftBackgrExtension::~HighRedshiftBackgrExtension()
+{
+	delete fBackground;
+}
+
+CuttedBackground::CuttedBackground(IBackground* aBackgr, double aMinE, double aMaxE, double aMinZ, double aMaxZ):
+	fBackgr(aBackgr),fMinE(aMinE),fMaxE(aMaxE),fMinZ(aMinZ),fMaxZ(aMaxZ)
+{
+	if(aBackgr->MinZ()>aMinZ)
+		fMinZ = aBackgr->MinZ();
+	if(aBackgr->MinE()>aMinE)
+		fMinE = aBackgr->MinE();
+	if(aBackgr->MaxZ()<aMaxZ)
+		fMaxZ = aBackgr->MaxZ();
+	if(aBackgr->MaxE()<aMaxE)
+		fMaxE = aBackgr->MaxE();
+}
+
+double CuttedBackground::n(double aE, double aZ) const {
+	return (aE<=fMaxE && aE>=fMinE && aZ<=fMaxZ && aZ>=fMinZ) ? fBackgr->n(aE,aZ) : 0.;
+}
+
+double CuttedBackground::MinE() const {
+	return fMinE;
+}
+
+double CuttedBackground::MaxE() const {
+	return fMaxE;
+}
+
+double CuttedBackground::MinZ() const {
+	return fMinZ;
+}
+
+double CuttedBackground::MaxZ() const {
+	return fMaxZ;
+}
+
+std::string CuttedBackground::Name() const {
+	return "Cutted " + fBackgr->Name();
+}
+double MonochromaticBackgroundIntegral::GetRateAndSampleK(const Function &aSigmaK, const Particle &aParticle,
+														  Randomizer &aRandomizer, double &aK, double aAbsError) {
+	double aRand = aRandomizer.Rand();
+	aK=0;
+	double z = aParticle.Time.z();
+	double n=fConcentration.f(z);
+	if(n<=0.)
+		return 0.;
+	double eps =fEnergy.f(z);
+
+	double KminSigma = aSigmaK.Xmin();
+	ASSERT_VALID_NO(KminSigma);
+	double beta = aParticle.beta();
+	double m=aParticle.Mass();
+
+	double Kmax = aParticle.Energy * eps * (1. + beta)/m;
+	double KminKinematic = aParticle.Energy * eps * (1. - beta)/m;
+	double Kmin = KminKinematic < KminSigma ? KminSigma : KminKinematic;
+	if(aSigmaK.Xmax() < Kmax)
+		Kmax =aSigmaK.Xmax();
+	if(Kmax <= Kmin)
+		return 0.;
+
+	double totalRate = 0.;
+	RateKKern kern(aSigmaK);
+
+	if(MathUtils::SampleLogDistribution(kern, aRand, aK, totalRate, Kmin, Kmax, fEpsRel))
+	{
+		double gamma = aParticle.Energy/m;
+		return 0.5*totalRate * n /(eps * eps * gamma * gamma * aParticle.beta());
+	}
+	aK=0;
+	return 0;
+}
+
+double MonochromaticBackgroundIntegral::GetRateK(const Function &aSigmaK, const Particle &aParticle,
+												 double aAbsError) {
+	double z = aParticle.Time.z();
+	double n=fConcentration.f(z);
+	if(n<=0.)
+		return 0.;
+	double eps = fEnergy.f(z);
+	double KminSigma = aSigmaK.Xmin();
+	ASSERT_VALID_NO(KminSigma);
+	double KmaxSigma = aSigmaK.Xmin();
+	double beta = aParticle.beta();
+	double m=aParticle.Mass();
+	double KmaxKinematic = aParticle.Energy * eps * (1. + beta)/m;
+	double KminKinematic = aParticle.Energy * eps * (1. - beta)/m;
+
+	double Kmin = KminKinematic<KminSigma ? KminSigma : KminKinematic;
+	double Kmax = KmaxSigma<KmaxKinematic ? KmaxSigma : KmaxKinematic;
+
+	if(Kmax<=Kmin)
+		return 0.;
+
+	int maxIntervals = (int)(log10(Kmax/Kmin)/log10(fLogStepE)/fEpsRel*1e-2 + 100);
+
+	RateKKern kern(aSigmaK);
+
+	double rate = math.Integration_qag(kern,Kmin,Kmax,aAbsError,fEpsRel,maxIntervals);
+	ASSERT_VALID_NO(rate);
+	double gamma = aParticle.Energy/m;
+
+	return 0.5*rate * n /(eps * eps * gamma * gamma * aParticle.beta());
+}
+} /* namespace mcray */
diff --git a/src/lib/Background.h b/src/lib/Background.h
new file mode 100644
index 0000000..fbb6ab7
--- /dev/null
+++ b/src/lib/Background.h
@@ -0,0 +1,442 @@
+/*
+ * Background.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef BACKGROUND_H_
+#define BACKGROUND_H_
+
+#include "TableFunction.h"
+#include <limits>
+#include "Particle.h"
+#include "Randomizer.h"
+
+namespace mcray {
+
+using namespace Utils;
+
+class IBackground {
+public:
+	/*!
+	 Calculate density E*dn(E)/dE at given redshift in internal units. Physical density should be returned, not a comoving one
+	 @param[in]  aE  background photon energy (in internal units)
+	 @param[in]  aZ  red shift
+	 @return background photon density E*dn(E)/dE in internal units
+	 */
+	virtual double n(double aE, double aZ) const = 0;
+	virtual double MinE() const = 0;
+	virtual double MaxE() const = 0;
+	virtual double MinZ() const = 0;
+	virtual double MaxZ() const = 0;
+	virtual std::string Name() const = 0;
+	virtual ~IBackground(){};
+};
+
+class BackgroundUtils
+{
+public:
+	static double CalcIntegralDensity(const IBackground& aBackground, double aRelError=1e-3);
+	static void Print(const IBackground* aBackground, double aZ, std::ostream& aOut, int nIntervals = 100);
+	static void UnitTest(const IBackground& aBackground, int aPointsZ=5, int aPointsE = 100);
+private:
+	static double DensityCalculator (double x, void * params);
+};
+
+class TableBackground : public IBackground{
+public:
+	TableBackground(std::string aName);
+
+	virtual ~TableBackground();
+
+	double n(double aE, double aZ) const;
+	double MinE() const;
+	double MaxE() const;
+	double MinZ() const;
+	double MaxZ() const;
+	std::string Name() const { return fName; }
+protected:
+	/*!
+	@param[in]  aDir directory containing data files
+	@param[in]  aFileList list of file names convertable to double (represent value of z). Last element in the array should be NULL, the files should be ordered increasingly.
+	@param[in]  aIsLogscaleY if true left and right values of functions in fData will be set to -Infinity, otherwise they will be set to 0
+	*/
+	void Init(std::string aDir, const char** aFileList, bool aIsLogscaleY);
+	//convert energy in internal units to table x scale
+	virtual double EtoRecordX(double aE, double aZ) const = 0;
+	//retrieve energy in internal units from the data record
+	virtual double recordToE(double aX, double aZ) const = 0;
+	////retrieve concentration E*dn/dE in internal units from the data record
+	virtual double recordToN(double aX, double aY, double aZ) const = 0;
+private:
+	std::vector<GSLTableFunc*>	fData;
+	Vector						fZ;
+	Vector						fZindex;
+	GSLTableFunc*				fZtoIndex;
+	double						fMinE;
+	double						fMaxE;
+	std::string					fName;
+};
+
+/// This class was originally designed to read slightly modified Elmag background tables
+/// (see script ElmagTest/Tables/makeTablesFormcray.sh)
+/// The table file is supposed to use the following format
+/// 	 z1				z2				z3				...			zMax
+/// E1	 dN/dE(z1,E1)	dN/dE(z2,E1)	dN/dE(z3,E1)	...			dN/dE(zMax,E1)
+/// E2	 dN/dE(z1,E2)	dN/dE(z2,E2)	dN/dE(z3,E2)	...			dN/dE(zMax,E2)
+/// ................
+/// Emax dN/dE(z1,Emax)	dN/dE(z2,Emax)	dN/dE(z3,Emax)	...			dN/dE(zMax,Emax)
+///
+/// where [E]=eV, [dN/dE] = 1/cm^3/eV
+/// Linear interpolation in z, log(E) and log (N) is performed as in Elmag
+/// By default for all z<z_min dN/dE(z) == dN/dE(zMin)
+class MatrixBackground : public IBackground{
+public:
+	MatrixBackground(std::string aName, std::string aTableFile, bool aIsComoving, bool aExtendToZero);
+
+	virtual ~MatrixBackground();
+
+	double n(double aE, double aZ) const;
+	double MinE() const { return fMinE; }
+	double MaxE() const { return fMaxE; }
+	double MinZ() const { return fExtendToZero ? 0 : fZ[0]; } //For all z<z_min dN/dE(z) == dN/dE(zMin) if aExtendToZero == true
+	double MaxZ() const { return *(fZ.end()-1); }
+	std::string Name() const { return fName; }
+private:
+	std::vector<Vector*>		fLogEdN_dE;
+	Vector						fZ;
+	Vector						fZindex;
+	GSLTableFunc*				fZtoIndex;
+	Vector						fLogE;
+	Vector						fLogEindex;
+	GSLTableFunc*				fLogEtoIndex;
+	double						fMinE;
+	double						fMaxE;
+	std::string					fName;
+	bool						fExtendToZero;
+	bool						fIsComoving;
+	char*						fBuffer;
+};
+
+class PlankBackground : public IBackground{
+public:
+	/*!
+	@param[in]  aT0 background temperature (internal units)
+	@param[in]  aEmin0 minimal background energy (internal units)
+	@param[in]  aEmax maximal background energy (internal units)
+	@param[in]  aMinZ minimal background red shift
+	@param[in]  aMaxZ maximal background red shift
+	*/
+	PlankBackground(double aT0, double aEmin, double aEmax, double aMinZ=0, double aMaxZ=10000, bool aFixedTemperature = false);
+	double n(double aE, double aZ) const;
+	double MinE() const { return fMinE; }
+	double MaxE() const { return fMaxE; }
+	double MinZ() const { return fMinZ; }
+	double MaxZ() const { return fMaxZ; }
+	std::string Name() const { return fName; }
+	virtual ~PlankBackground(){};
+private:
+	double fT0;
+	double fMinE;
+	double fMaxE;
+	double fMinZ;
+	double fMaxZ;
+	std::string fName;
+	bool	fFixedTemperature;
+};
+
+class GaussianBackground : public IBackground{
+public:
+	/*!
+	@param[in]  aE background photons energy central value (internal units)
+	@param[in]  aSigma Gaussian distribution sigma (internal units)
+	@param[in]  aWidth width at which the distribution is cut (in units of sigma)
+	@param[in]  aConcentration integral concentration of photons (internal units)
+	@param[in]  aMinZ minimal background red shift
+	@param[in]  aMaxZ maximal background red shift
+	*/
+	GaussianBackground(double aE, double aSigma, double aWidth, double aConcentration, double aMinZ=0, double aMaxZ=10000);
+	double n(double aE, double aZ) const;
+	double MinE() const { return fMinE; }
+	double MaxE() const { return fMaxE; }
+	double MinZ() const { return fMinZ; }
+	double MaxZ() const { return fMaxZ; }
+	std::string Name() const { return fName; }
+	virtual ~GaussianBackground(){};
+private:
+	double fE0;
+	double fSigma;
+	double fMinE;
+	double fMaxE;
+	double fMinZ;
+	double fMaxZ;
+	double fNorm;
+	double fWidth;
+	std::string fName;
+};
+
+class CompoundBackground : public IBackground{
+public:
+	//argument ownership is transfered to CompoundBackground object
+	void AddComponent(IBackground* aBackground, double aWeight=1.);
+	double n(double aE, double aZ) const;
+	double MinE() const;
+	double MaxE() const;
+	double MinZ() const;
+	double MaxZ() const;
+	std::string Name() const;
+	virtual ~CompoundBackground();
+private:
+	std::vector<IBackground*> fBackgrounds;
+	std::vector<double>		  fWeights;
+};
+
+class BackgroundIntegral : public SmartReferencedObj
+{
+public:
+	///calculate rates using sigma(s) where s - total energy squared in center of mass frame
+	virtual double GetRateAndSampleS(const Function& aSigmaS, const Particle& aParticle, Randomizer& aRandomizer, double& aS, double aAbsError=0) = 0;//todo: make use of aAbsError parameter in propagation engine
+	virtual double GetRateS(const Function &aSigmaS, const Particle &aParticle, double aAbsError = 0) = 0;//todo: make use of aAbsError parameter in propagation engine
+
+	///calculate rates using sigma(k) where k - background photon energy in the massive particle rest frame
+	virtual double GetRateAndSampleK(const Function& aSigmaK, const Particle& aParticle, Randomizer& aRandomizer, double& aK, double aAbsError = 0) = 0;
+	virtual double GetRateK(const Function &aSigmaK, const Particle &aParticle, double aAbsError = 0) = 0;
+	virtual BackgroundIntegral* Clone() const = 0;
+};
+
+class MonochromaticBackgroundIntegral : public BackgroundIntegral
+{
+	class RateSKern : public Function
+	{
+	public:
+		RateSKern(const Function& aSigmaS, const Particle& aParticle);
+
+		//(s-m^2) sigma(s)
+		double f(double _x) const;
+	private:
+		const Function&	fSigma;
+		double				fM2;
+	};
+	class RateKKern : public Function
+	{
+	public:
+		RateKKern(const Function& aSigmaK):fSigma(aSigmaK){};
+		//k sigma(k)
+		double f(double _x) const;
+	private:
+		const Function&	fSigma;
+	};
+public:
+	/*!
+	@param[in]  aConcentration Z-dependence of background particles concentration (internal units)
+	@param[in]  aEnergy Z-dependence of background particles energy (internal units)
+	@param[in]  aLogStepE energy binning used to calculate rates and sample S
+	@param[in]  aEpsRel maximal relative error criterion (used in rate calculation)
+	*/
+	MonochromaticBackgroundIntegral(Function& aConcentration, Function& aEnergy, double aLogStepE, double aEpsRel):
+		fConcentration(aConcentration),
+		fEnergy(aEnergy),
+		fLogStepE(aLogStepE),
+		fEpsRel(aEpsRel){};
+	MonochromaticBackgroundIntegral(const MonochromaticBackgroundIntegral& aBackgroundIntegral);
+
+
+    double GetRateAndSampleS(const Function& aSigmaS, const Particle& aParticle, Randomizer& aRandomizer, double& aS, double aAbsError=0){
+        return GetRateAndSampleS(aSigmaS, aParticle, aRandomizer.Rand(), aS, aAbsError);
+    }
+
+	double GetRateS(const Function& aSigma, const Particle& aParticle, double aAbsError=0);
+
+	double GetRateAndSampleK(const Function& aSigmaK, const Particle& aParticle, Randomizer& aRandomizer, double& aK, double aAbsError = 0);
+	double GetRateK(const Function &aSigmaK, const Particle &aParticle, double aAbsError = 0);
+
+	BackgroundIntegral* Clone() const;
+	virtual ~MonochromaticBackgroundIntegral(){};
+private:
+    double GetRateAndSampleS(const Function& aSigmaS, const Particle& aParticle, double aRand, double& aS, double aAbsError=0);
+	MathUtils		math;
+	bool			fEnableDebugOutput;
+	Function&		fConcentration;
+	Function&		fEnergy;
+	double			fLogStepE;
+	const double 	fEpsRel;
+};
+
+/// calculates background integral function I(k,z)=Integrate[x^-2 * dn(x,z)/dx, {x, k, Infinity}]
+/// calculates Interaction rate Omega=1/8E^2/beta * Integrate[(s-m^2) sigma(s) I((s-m^2)/2/E/(1+beta),z), {s,s_min,Infinity}]
+/// The class is not thread safe. Copy should be created for each thread using copy constructor;
+class ContinuousBackgroundIntegral : public BackgroundIntegral{
+	class Kern : public Function
+	{
+	public:
+		Kern(IBackground&	aBackground) : fBackground(aBackground){}
+		//x^-2 * dn(x,z)/dx === x^-3 x dn/dx
+		virtual double f(double _x) const;
+		double			Z;
+	private:
+		const IBackground&	fBackground;
+	};
+	class RateSKern : public Function
+	{
+	public:
+		RateSKern(const Function&	aBackrgoundIntegral, const Function& aSigma, const Particle& aParticle);
+
+		//(s-m^2) sigma(s) I((s-m^2)/2/E/(1+beta),z)
+		double f(double _x) const;
+	private:
+		const Function&		fBackrgoundIntegral;
+	protected:
+		const Function&		fSigma;
+		double				fM2;
+		// 1/2/E/(1+beta)
+		double				fMult;
+		// 1/2/E/(1-beta) or -1 if beta=1
+		double				fMaxEmult;
+	};
+	class RateSKernZ : public RateSKern
+	{
+	public:
+		RateSKernZ(const Function&	aBackrgoundIntegralZ1, const Function&	aBackrgoundIntegralZ2, double aZ1, double aZ2, const Function& aSigma, const Particle& aParticle);
+
+		//(s-m^2) sigma(s) I((s-m^2)/2/E/(1+beta),z)
+		double f(double _x) const;
+	private:
+		const Function&		fBackrgoundIntegralZ1;
+		const Function&		fBackrgoundIntegralZ2;
+
+		double				fCoef1;
+		double				fCoef2;
+	};
+	class RateKKern : public Function
+	{
+	public:
+		RateKKern(const Function&	aBackrgoundIntegral, const Function& aSigma, const Particle& aParticle);
+		double f(double _x) const;
+	private:
+		const Function&		fBackrgoundIntegral;
+	protected:
+		const Function&		fSigma;
+
+		// m/E/(1+beta)
+		double				fMult;
+		// m/E/(1-beta) or -1 if beta=1
+		double				fMaxEmult;
+	};
+	class RateKKernZ : public RateKKern
+	{
+	public:
+		RateKKernZ(const Function&	aBackrgoundIntegralZ1, const Function&	aBackrgoundIntegralZ2, double aZ1, double aZ2, const Function& aSigma, const Particle& aParticle);
+		double f(double _x) const;
+	private:
+		const Function&		fBackrgoundIntegralZ1;
+		const Function&		fBackrgoundIntegralZ2;
+
+		double				fCoef1;
+		double				fCoef2;
+	};
+public:
+	ContinuousBackgroundIntegral(IBackground& aBackground, double aStepZ, double aLogStepK, double aZmaxLimit, double aEpsRel = 0);
+	BackgroundIntegral* Clone() const;
+	double GetRateAndSampleS(const Function& aSigmaS, const Particle& aParticle, Randomizer& aRandomizer, double& aS, double aAbsError = 0){
+        return GetRateAndSampleS(aSigmaS, aParticle, aRandomizer.Rand(), aS, aAbsError);
+    }
+	double GetRateS(const Function &aSigmaS, const Particle &aParticle, double aAbsError = 0);
+
+	double GetRateAndSampleK(const Function& aSigmaK, const Particle& aParticle, Randomizer& aRandomizer, double& aK, double aAbsError = 0){
+		return GetRateAndSampleK(aSigmaK, aParticle, aRandomizer.Rand(), aK, aAbsError);
+	}
+	double GetRateK(const Function &aSigmaK, const Particle &aParticle, double aAbsError = 0);
+
+	virtual ~ContinuousBackgroundIntegral();
+	static void UnitTest();
+private:
+    double GetRateAndSampleS(const Function& aSigmaS, const Particle& aParticle, double aRand, double& aS, double aAbsError = 0);
+	double GetRateAndSampleK(const Function& aSigmaK, const Particle& aParticle, double aRand, double&aK, double aAbsError = 0);
+	ContinuousBackgroundIntegral(const ContinuousBackgroundIntegral& aBackgroundIntegral);
+	//used in unit test
+	static double PlankBackgroundIntegral(double aT, double aE);
+	//used in unit test
+	static double PlankBackgroundTestRate(double aT, double aE, double aS);
+
+	double I(double k, double z);
+
+	double			fStepZ;
+	double			fLogStepK;
+	double			fZmaxLimit;
+	double			fMaxK;
+	double			fMinK;
+	const double 	fEpsRel;
+	std::vector<Function*> fIntegrals;//integrals I for different values of z
+	std::vector<Vector*> fData;
+	Vector			fBinningK;
+	MathUtils		math;
+	bool			fEnableDebugOutput;
+};
+
+class HighRedshiftBackgrExtension : public IBackground
+{
+public:
+	HighRedshiftBackgrExtension(IBackground* aBackground, double aDeltaZconst, double aPowerLow, double aDeltaZexp, double aZmax=1000);
+	virtual double n(double aE, double aZ) const;
+	virtual double MinE() const;
+	virtual double MaxE() const;
+	virtual double MinZ() const;
+	virtual double MaxZ() const;
+	virtual std::string Name() const;
+	virtual ~HighRedshiftBackgrExtension();
+private:
+	IBackground* fBackground;
+	double fZconst;
+	double fPowerLow;
+	double fDeltaZexp;
+	double fZmax;
+	double fInnerZmax;
+};
+
+class CuttedBackground : public IBackground
+{
+public:
+	//takes ownership of aBackgr
+	CuttedBackground(IBackground* aBackgr, double aMinE=0, double aMaxE=1e300, double aMinZ=0, double aMaxZ=1e300);
+	virtual double n(double aE, double aZ) const;
+	virtual double MinE() const;
+	virtual double MaxE() const;
+	virtual double MinZ() const;
+	virtual double MaxZ() const;
+	virtual std::string Name() const;
+private:
+	SafePtr<IBackground> fBackgr;
+	double fMinE;
+	double fMaxE;
+	double fMinZ;
+	double fMaxZ;
+};
+
+
+typedef SmartPtr<BackgroundIntegral> PBackgroundIntegral;
+
+
+
+} /* namespace mcray */
+#endif /* BACKGROUND_H_ */
diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt
new file mode 100644
index 0000000..4262323
--- /dev/null
+++ b/src/lib/CMakeLists.txt
@@ -0,0 +1,134 @@
+cmake_minimum_required (VERSION 2.6)
+project (mcray)
+enable_language (Fortran)
+
+# FFLAGS depend on the compiler
+get_filename_component (Fortran_COMPILER_NAME ${CMAKE_Fortran_COMPILER} NAME)
+
+if (Fortran_COMPILER_NAME MATCHES "gfortran.*")
+  # gfortran
+  set (CMAKE_Fortran_FLAGS "-ffixed-line-length-200 -fno-f2c")
+  set (CMAKE_Fortran_FLAGS_RELEASE "-O2")
+  set (CMAKE_Fortran_FLAGS_DEBUG   "-O0 -g")
+elseif (Fortran_COMPILER_NAME MATCHES "ifort.*")
+  # ifort (untested)
+  set (CMAKE_Fortran_FLAGS_RELEASE "-f77rtl -O2")
+  set (CMAKE_Fortran_FLAGS_DEBUG   "-f77rtl -O0 -g")
+elseif (Fortran_COMPILER_NAME MATCHES "g77")
+  # g77
+  set (CMAKE_Fortran_FLAGS_RELEASE "-fno-f2c -O2")
+  set (CMAKE_Fortran_FLAGS_DEBUG   "-fno-f2c -O0 -g")
+else (Fortran_COMPILER_NAME MATCHES "gfortran.*")
+  message ("CMAKE_Fortran_COMPILER full path: " ${CMAKE_Fortran_COMPILER})
+  message ("Fortran compiler: " ${Fortran_COMPILER_NAME})
+  message ("No optimized Fortran compiler flags are known, we just try -O2...")
+  set (CMAKE_Fortran_FLAGS_RELEASE "-O2")
+  set (CMAKE_Fortran_FLAGS_DEBUG   "-O0 -g")
+endif (Fortran_COMPILER_NAME MATCHES "gfortran.*")
+
+
+add_definitions(-DUSE_GSL)
+include_directories(../OS/include ../external)
+
+set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -D_DEBUG -O0 -fopenmp")
+set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_DEBUG -O0 -fopenmp -pg")
+set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Wall -O2 -fopenmp")
+set(CMAKE_PREFIX_PATH "../external")
+
+# -rdynamic flux is only supported on systems with ELF executable format
+IF(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+  set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -rdynamic")
+  set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -rdynamic")
+  set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -rdynamic")
+  set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -rdynamic")
+endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+
+#FIND_LIBRARY(FORTRAN_LIBRARY gfortran PATHS OS/lib)
+#FIND_LIBRARY(OMP_LIBRARY gomp PATHS OS/lib)
+FIND_LIBRARY(C_LIBRARY c)
+FIND_LIBRARY(XERCES_LIBRARY xerces-c)
+FIND_LIBRARY(GSL_LIBRARY gsl)
+FIND_LIBRARY(GSLCBLAS_LIBRARY gslcblas)
+
+set(SOURCE_FILES
+../external/SOPHIA/eventgen.f
+        ../external/SOPHIA/inpoutput.f
+        ../external/SOPHIA/jetset74dp.f
+        ../external/SOPHIA/rndm.f
+        ../external/SOPHIA/sampling.f
+sophia2cpp.f
+Sophia.cpp
+Sophia.h
+Background.cpp
+Background.h
+Cosmology.cpp
+Cosmology.h
+Debug.cpp
+Debug.h
+Deflection1D.cpp
+Deflection1D.h
+Deflection3D.cpp
+Deflection3D.h
+Filter.cpp
+Filter.h
+GammaPP.cpp
+GammaPP.h
+GZK.cpp
+GZK.h
+ICS.cpp
+ICS.h
+Inoue12IROSpectrum.cpp
+Inoue12IROSpectrum.h
+Interaction.cpp
+Interaction.h
+MathUtils.cpp
+MathUtils.h
+NeutronDecay.cpp
+NeutronDecay.h
+Nucleus.cpp
+Nucleus.h
+Output.cpp
+Output.h
+PPP.cpp
+PPP.h
+Particle.cpp
+Particle.h
+ParticleStack.cpp
+ParticleStack.h
+PropagationEngine.cpp
+PropagationEngine.h
+ProtonPP.cpp
+ProtonPP.h
+Randomizer.cpp
+Randomizer.h
+TableBackgrounds.cpp
+TableBackgrounds.h
+TableFunction.cpp
+TableFunction.h
+TableReader.cpp
+TableReader.h
+Test.cpp
+Test.h
+Thinning.cpp
+Thinning.h
+Units.cpp
+Units.h
+Utils.cpp
+Utils.h
+main.cpp
+PhotoDisintegration.cpp
+PhotoDisintegration.h
+SteckerEBL.cpp
+SteckerEBL.h
+Stecker16Background.cpp
+Stecker16Background.h
+Logger.cpp
+Logger.h)
+
+add_library(mcray ${SOURCE_FILES})
+add_executable(mcray_test ExampleUserMain.cpp)
+add_executable(z2t z2t.cpp)
+target_link_libraries(mcray gfortran gomp ${OMP_LIBRARY} ${C_LIBRARY} ${GSL_LIBRARY} ${GSLCBLAS_LIBRARY})
+target_link_libraries(mcray_test mcray)
+target_link_libraries(z2t mcray)
+
diff --git a/src/lib/Cosmology.cpp b/src/lib/Cosmology.cpp
new file mode 100644
index 0000000..17fea76
--- /dev/null
+++ b/src/lib/Cosmology.cpp
@@ -0,0 +1,204 @@
+/*
+ * Cosmology.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Cosmology.h"
+#include "Units.h"
+#include "gsl/gsl_sf_hyperg.h"
+#include "gsl/gsl_sf_gamma.h"
+#include <iostream>
+#include "Particle.h"
+
+namespace mcray {
+
+Cosmology cosmology;
+
+void Cosmology::Init(cosmo_time aMaxZ, cosmo_time aOmegaVac, cosmo_time aHubbleKm_sec_Mpc, int aAccuracy)
+{
+	ASSERT(t0==0);//init should be called only once
+	ASSERT(aMaxZ>0);
+	ASSERT(aOmegaVac>=0 && aOmegaVac<=1.);
+	ASSERT(aAccuracy>100);
+	Accuracy = aAccuracy;
+
+	H = 1e5*aHubbleKm_sec_Mpc/Units::Mpc_in_cm*units.Tunit;
+	fMaxZ = aMaxZ;
+
+	Lv = aOmegaVac;
+	Lm = 1.-Lv;
+	sqrtLv = sqrt(Lv);
+	sqrtLm = sqrt(Lm);
+
+	if(Lv <= 0.)//Lyambda = 0
+	{
+		t0 = 2./3./H;
+	}
+	else
+	{
+		t0 = log((2. + 2.*sqrtLv - Lm)/Lm)/(3.*sqrtLv)/H;
+		//t0 = 2./3./H/sqrtLv*log((1+sqrtLv)/sqrtLm);
+	}
+	initZ2T();
+	initZ2D();
+}
+
+Cosmology::Cosmology()
+{
+	Lv = 0.;
+	Lm = 0.;
+	t0 = 0.;
+	sqrtLm = 0.;
+	sqrtLv = 0.;
+	sqrtLv = 0.;
+	Accuracy = 0;
+}
+
+void Cosmology::initZ2D()
+{
+	ASSERT(fMaxZ>0.);// must be initialized
+
+	z.push_back(0);
+	d.push_back(0);
+
+	cosmo_time aMin = 1./(fMaxZ+1);
+	cosmo_time da = (1.-aMin)/Accuracy;
+	cosmo_time da2 = 0.5*da;
+	cosmo_time a = 1.-da;
+	cosmo_time prev = diffD(1.);
+	cosmo_time sum = 0.;
+
+	for(int i=1; i<=Accuracy; i++, a -= da)
+	{
+		sum += prev;
+		sum += 4.*diffD(a+da2);
+		prev = diffD(a);
+		sum += prev;
+		z.push_back(1./a-1.);
+		d.push_back(da*sum/6.);
+	}
+	z2dFunc = new LinearFuncX<cosmo_time >(z,d,0,d[Accuracy]);
+	d2zFunc = new LinearFuncX<cosmo_time >(d,z,0,z[Accuracy]);
+}
+
+void Cosmology::print() const
+{
+	std::cout << "#z\td\n";
+	for(int i=0; i<Accuracy; i++)
+	{
+		std::cout << z[i] << "\t" << d[i]*units.Lunit/Units::Mpc_in_cm << "\n";
+	}
+	std::cout << "\n\n";
+	std::cout << "#z\tt\n";
+	for(int j=0; j<Accuracy; j++)
+	{
+		std::cout << zt[j] << "\t" << tt[j]*units.Lunit/Units::Mpc_in_cm << "\n";
+	}
+}
+
+void Cosmology::UnitTest()
+{
+	cosmology.Init(10);
+	cosmo_time z=1;
+	Particle p(Photon, z);
+	std::cout << "z=" << z << "\tt=" << p.Time.t()*units.Lunit/Units::Mpc_in_cm << " Mpc d="
+			<< cosmology.z2d(z)*units.Lunit/Units::Mpc_in_cm << " Mpc\n\n";
+	std::cout << "Age of universe " << cosmology.z2t(0)*units.Lunit/Units::Mpc_in_cm << " Mpc\n\n";
+	cosmology.print();
+}
+
+void Cosmology::initZ2T()
+{
+	ASSERT(fMaxZ);// must be initialized
+
+	zt.push_back(0);
+	tt.push_back(0);
+
+	cosmo_time dz = fMaxZ/Accuracy;
+	cosmo_time dz2 = 0.5*dz;
+	cosmo_time z = dz;
+	cosmo_time prev = diffT(0.);
+	cosmo_time sum = 0.;
+
+	for(int i=1; i<=Accuracy; i++, z += dz)
+	{
+		sum += prev;
+		sum += 4.*diffT(z-dz2);
+		prev = diffT(z);
+		sum += prev;
+		zt.push_back(z);
+		tt.push_back(dz*sum/6.);
+	}
+	z2tFunc = new LinearFuncX<cosmo_time>(zt,tt,0,(tt)[Accuracy]);
+	t2zFunc = new LinearFuncX<cosmo_time>(tt,zt,0,(zt)[Accuracy]);
+}
+
+///The function is used to test red shift evolution in special case of the source with no evolution in comoving frame and alpha = 2
+cosmo_time Cosmology::TestIntegral(cosmo_time r /*r===min(Emax/E, 1+Zmax)*/) const
+{
+	return TestIntegralF(r)-TestIntegralF(1.);
+}
+
+cosmo_time Cosmology::Hy2F1(cosmo_time a, cosmo_time b, cosmo_time c, cosmo_time z)
+{
+	if(fabs(z)<=1)
+		return gsl_sf_hyperg_2F1(a,b,c,z);
+	z = 1./z;
+	cosmo_time mult1 = gsl_sf_gamma(b-a)*gsl_sf_gamma(c)/gsl_sf_gamma(b)/gsl_sf_gamma(c-a);
+	mult1 *= pow(-z, a);
+	cosmo_time mult2 = gsl_sf_gamma(a-b)*gsl_sf_gamma(c)/gsl_sf_gamma(a)/gsl_sf_gamma(c-b);
+	mult2 *= pow(-z, b);
+	cosmo_time f1 = gsl_sf_hyperg_2F1(a,a-c+1,a-b+1,z);
+	cosmo_time f2 = gsl_sf_hyperg_2F1(b,b-c+1,b-a+1,z);
+	cosmo_time result = mult1*f1 + mult2*f2;
+	return result;
+}
+
+///The function is used to test red shift evolution in special case of the source with no evolution in comoving frame and alpha = 2
+cosmo_time Cosmology::TestIntegralF(cosmo_time a/*a===1+z*/) const //F[1+z, alpha] = (1+z)^(-alpha)*(Lm*(1+z)^3 + Lv)^(-1/2)
+{//TestIntegralF === Integrate[F[1+z, 1], z]
+	cosmo_time a3 = a*a*a;
+	cosmo_time arg = -(a3*Lm)/Lv;
+	cosmo_time Hypergeometric2F1 = Hy2F1(0.6666666666666666,0.5,1.6666666666666667,
+         arg);
+	cosmo_time result = (sqrt(1 + (-1 + a3)*Lm)*
+     (4*(-1 + Lm)*sqrt((-1 + Lm - a3*Lm)/
+          (-1 + Lm)) +
+       a3*Lm*Hypergeometric2F1))/(4.*a*Lv*Lv*
+     sqrt((-1 + Lm - a3*Lm)/(-1 + Lm)));
+	return result;
+}
+
+void Cosmology::SuppressionTest() const
+{
+	std::cerr << "red shift spectrum suppression test for m-alpha = -2 and Zmax = " << fMaxZ << ":\n";
+//here we simulate how spectrum should be suppressed at highest energies due to red shifting
+//The analytic estimate was made for spectrum Q(E,z) = E^-alpha * (1+z)^(3+m) * Theta(Emax-E), for m-alpha = -2
+	cosmo_time norm = 1./TestIntegral(1.+fMaxZ);
+	for(cosmo_time r=1; r<fMaxZ + 1; r+=0.05)
+		std::cerr << r << "\t" << TestIntegral(r)*norm << "\n";
+}
+
+} /* namespace mcray */
diff --git a/src/lib/Cosmology.h b/src/lib/Cosmology.h
new file mode 100644
index 0000000..0ef3160
--- /dev/null
+++ b/src/lib/Cosmology.h
@@ -0,0 +1,240 @@
+/*
+ * Cosmology.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef COSMOLOGY_H_
+#define COSMOLOGY_H_
+
+#include "Utils.h"
+#include "TableFunction.h"
+#include <math.h>
+
+namespace mcray {
+
+using namespace Utils;
+typedef long double cosmo_time;
+
+class Cosmology
+{
+public:
+	Cosmology();
+	void Init(cosmo_time aMaxZ=100., cosmo_time aOmegaVac=0.692, cosmo_time aHubbleKm_sec_Mpc=67.8, int aAccuracy=100000);
+// z -> t, dz -> dt and back transformation
+	inline cosmo_time z2t0(cosmo_time _z) const;//t0==0 at z=0
+	inline cosmo_time t02z(cosmo_time _t) const;//t0==0 at z=0
+	inline cosmo_time z2t(cosmo_time _z) const;//t==0 at z=infinity
+	inline cosmo_time t2z(cosmo_time _t) const;//t==0 at z=infinity
+	inline cosmo_time z2d(cosmo_time _z) const;
+	inline cosmo_time d2z(cosmo_time _d) const;
+	inline cosmo_time getLv() const{return Lv;};
+	inline cosmo_time getAgeOfUniverse() const{return t0;};
+	void SuppressionTest() const;
+	//returns energy loss rate -1/E dE/dt due to redshift at
+	inline cosmo_time eLossRate(cosmo_time aZ) const;
+
+	// -dt/dz
+	inline cosmo_time diffT(cosmo_time aZ) const;
+
+	//Hubble parameter at given redshift
+	inline cosmo_time Hubble(cosmo_time aZ) const;
+	//test output
+	void print() const;
+	static void UnitTest();
+	inline bool IsInitialized() { return t0!=0; }
+	inline cosmo_time RelError() const { return 1./Accuracy; }
+private:
+	//test
+	static cosmo_time Hy2F1(cosmo_time a, cosmo_time b, cosmo_time c, cosmo_time z);
+	cosmo_time TestIntegralF(cosmo_time r) const;
+	cosmo_time TestIntegral(cosmo_time r) const;
+
+	inline cosmo_time diffD(cosmo_time a) const;
+	void initZ2D();
+	void initZ2T();
+
+	cosmo_time H;//Hubble parameter at z=0 (internal units)
+	cosmo_time Lv;// omega_lambda at z=0
+	cosmo_time Lm;// omega_matter at z=0
+	cosmo_time sqrtLv;//sqrt(Lv)
+	cosmo_time sqrtLm;//sqrt(Lv)
+	cosmo_time t0;//universe age at z=0
+	cosmo_time fMaxZ;
+	int Accuracy;
+	std::vector<cosmo_time>	zt;
+	std::vector<cosmo_time>	tt;
+	std::vector<cosmo_time>	z;
+	std::vector<cosmo_time>	d;
+	SafePtr< LinearFuncX<cosmo_time> >	z2dFunc;
+	SafePtr< LinearFuncX<cosmo_time> >	d2zFunc;
+	SafePtr< LinearFuncX<cosmo_time> >	z2tFunc;
+	SafePtr< LinearFuncX<cosmo_time> >	t2zFunc;
+};
+
+inline cosmo_time Cosmology::t2z(cosmo_time _t) const
+{
+	 return t02z(_t-t0);
+}
+
+inline cosmo_time Cosmology::z2t0(cosmo_time _z) const//t0==0 at z=0
+{
+	ASSERT(z2tFunc->InTableRange(_z));
+	return -z2tFunc->f(_z);
+}
+
+inline cosmo_time Cosmology::t02z(cosmo_time _t0) const//t0==0 at z=0
+{
+	cosmo_time timeAgoFromNow = -_t0;
+	if(timeAgoFromNow<0)
+	{
+		ASSERT(-timeAgoFromNow/t0<1e-12);//roundoff error
+		timeAgoFromNow=0.;
+	}
+	ASSERT(t2zFunc->InTableRange(timeAgoFromNow));
+	return t2zFunc->f(timeAgoFromNow);
+}
+
+inline cosmo_time Cosmology::z2t(cosmo_time _z) const
+{
+	ASSERT(z2tFunc->InTableRange(_z));
+	return t0 - z2tFunc->f(_z);
+}
+
+inline cosmo_time Cosmology::eLossRate(cosmo_time aZ) const
+{
+	return 1./(1.+aZ)/diffT(aZ);
+}
+
+inline cosmo_time Cosmology::diffD(cosmo_time a) const
+{
+	return 1./( H*sqrt(a*(Lm+Lv*a*a*a)) );
+}
+
+inline cosmo_time Cosmology::z2d(cosmo_time _z) const
+{
+	ASSERT(z2dFunc->InTableRange(_z));
+	return z2dFunc->f(_z);
+}
+
+inline cosmo_time Cosmology::d2z(cosmo_time _d) const
+{
+	ASSERT(d2zFunc->InTableRange(_d));
+	return d2zFunc->f(_d);
+}
+
+inline cosmo_time Cosmology::diffT(cosmo_time aZ) const
+{
+	cosmo_time z1 = 1.+aZ;
+	return 1./H/z1/sqrt(Lm*z1*z1*z1+Lv);
+}
+
+inline cosmo_time Cosmology::Hubble(cosmo_time aZ) const
+{
+	return 1./((1.+aZ)*diffT(aZ));
+}
+
+extern Cosmology cosmology;
+
+class CosmoTime
+{
+public:
+	//construct with z=0
+	CosmoTime():m_z(0.),m_t(0.){}
+
+	//constructor with redshift
+	CosmoTime(cosmo_time aRedshift) {setZ(aRedshift);}
+
+	//copy constructor
+	CosmoTime(const CosmoTime& aTime):m_z(aTime.m_z), m_t(aTime.m_t){}
+
+	inline operator cosmo_time() const { return t(); }
+	inline CosmoTime& operator=(const CosmoTime& aTime) { m_z = aTime.m_z; m_t = aTime.m_t; return *this; }
+	inline CosmoTime& operator=(cosmo_time aT) { setT(aT); return *this; }
+	inline CosmoTime& operator += (cosmo_time aDt)
+	{
+		cosmo_time curT = t();
+		cosmo_time newT = curT+aDt;
+		if(newT>0){
+			ASSERT(newT/aDt<cosmology.RelError());
+			newT = 0.;//fix roundoff error
+		}
+		setT(newT); return *this;
+	}
+
+	inline bool operator<(const CosmoTime& aT) const
+	{
+		ASSERT(m_z>=0. || m_t<=0);
+		if(m_z<0 || aT.m_z<0)
+			return t()<aT.t();
+		else
+			return aT.m_z<m_z;
+	}
+
+	//redshift value
+	inline cosmo_time z() const
+	{
+		ASSERT(m_z>=0. || m_t<=0);
+		if(m_z<0)
+			m_z = cosmology.t02z(m_t);//lazy initialization
+		return m_z;
+	}
+
+	//time difference from z=0 (<0 for past moments)
+	inline cosmo_time t() const
+	{
+		ASSERT(m_z>=0. || m_t<=0);
+		if(m_t>0.)
+			m_t = cosmology.z2t0(m_z);//lazy initialization
+		return m_t;
+	}
+
+	//set redshift value z
+	inline void setZ(cosmo_time aZ)
+	{
+		ASSERT(aZ>=0.);
+		m_z = aZ;
+		m_t = 1;//enable lazy initialization
+	}
+
+	//set redshift using time difference from z=0
+	inline void setT(cosmo_time aT)
+	{
+		ASSERT(aT<=0.);
+		m_z = -1;//enable lazy initialization
+		m_t = aT;
+	}
+
+	std::string ToString() const {
+		std::ostringstream logStr;
+		logStr << "z=" << m_z << ", t=" <<m_t;
+		return logStr.str();
+	}
+private:
+	mutable cosmo_time m_z;
+	mutable cosmo_time m_t;//time difference between z=0 and this z (always < 0)
+};
+
+} /* namespace mcray */
+#endif /* COSMOLOGY_H_ */
diff --git a/src/lib/Debug.cpp b/src/lib/Debug.cpp
new file mode 100644
index 0000000..b652f32
--- /dev/null
+++ b/src/lib/Debug.cpp
@@ -0,0 +1,265 @@
+/*
+ * Debug.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Debug.h"
+#include "Utils.h"
+#include <iostream>
+#include <gsl/gsl_errno.h>
+#include <fstream>
+#include <omp.h>
+
+#include <execinfo.h>
+#include <errno.h>
+#include <cxxabi.h>
+#include <stdio.h>
+#include <signal.h>
+#include <stdlib.h>
+
+
+namespace Utils {
+
+    static inline void printStackTrace( FILE *out = stderr, unsigned int max_frames = 63 )
+    {
+        fprintf(out, "stack trace:\n");
+
+        // storage array for stack trace address data
+        void* addrlist[max_frames+1];
+
+        // retrieve current stack addresses
+        unsigned int addrlen = backtrace( addrlist, sizeof( addrlist ) / sizeof( void* ));
+
+        if ( addrlen == 0 )
+        {
+            fprintf( out, "  \n" );
+            return;
+        }
+
+        // resolve addresses into strings containing "filename(function+address)",
+        // Actually it will be ## program address function + offset
+        // this array must be free()-ed
+        char** symbollist = backtrace_symbols( addrlist, addrlen );
+
+        size_t funcnamesize = 1024;
+        char funcname[1024];
+
+        // iterate over the returned symbol lines. skip the first, it is the
+        // address of this function.
+        for ( unsigned int i = 4; i < addrlen; i++ )
+        {
+            char* begin_name   = NULL;
+            char* begin_offset = NULL;
+            char* end_offset   = NULL;
+
+            // find parentheses and +address offset surrounding the mangled name
+#ifdef DARWIN
+            // OSX style stack trace
+      for ( char *p = symbollist[i]; *p; ++p )
+      {
+         if (( *p == '_' ) && ( *(p-1) == ' ' ))
+            begin_name = p-1;
+         else if ( *p == '+' )
+            begin_offset = p-1;
+      }
+
+      if ( begin_name && begin_offset && ( begin_name < begin_offset ))
+      {
+         *begin_name++ = '\0';
+         *begin_offset++ = '\0';
+
+         // mangled name is now in [begin_name, begin_offset) and caller
+         // offset in [begin_offset, end_offset). now apply
+         // __cxa_demangle():
+         int status;
+         char* ret = abi::__cxa_demangle( begin_name, &funcname[0],
+                                          &funcnamesize, &status );
+         if ( status == 0 )
+         {
+            funcname = ret; // use possibly realloc()-ed string
+            fprintf( out, "  %-30s %-40s %s\n",
+                     symbollist[i], funcname, begin_offset );
+         } else {
+            // demangling failed. Output function name as a C function with
+            // no arguments.
+            fprintf( out, "  %-30s %-38s() %s\n",
+                     symbollist[i], begin_name, begin_offset );
+         }
+
+#else // !DARWIN - but is posix
+            // not OSX style
+            // ./module(function+0x15c) [0x8048a6d]
+            for ( char *p = symbollist[i]; *p; ++p )
+            {
+                if ( *p == '(' )
+                    begin_name = p;
+                else if ( *p == '+' )
+                    begin_offset = p;
+                else if ( *p == ')' && ( begin_offset || begin_name ))
+                    end_offset = p;
+            }
+
+            if ( begin_name && end_offset && ( begin_name < end_offset ))
+            {
+                *begin_name++   = '\0';
+                *end_offset++   = '\0';
+                if ( begin_offset )
+                    *begin_offset++ = '\0';
+
+                // mangled name is now in [begin_name, begin_offset) and caller
+                // offset in [begin_offset, end_offset). now apply
+                // __cxa_demangle():
+
+                int status = 0;
+                char* ret = abi::__cxa_demangle( begin_name, funcname,
+                                                 &funcnamesize, &status );
+                char* fname = begin_name;
+                if ( status == 0 )
+                    fname = ret;
+
+                if ( begin_offset )
+                {
+                    fprintf( out, "  %-30s ( %-40s  + %-6s) %s\n",
+                             symbollist[i], fname, begin_offset, end_offset );
+                } else {
+                    fprintf( out, "  %-30s ( %-40s    %-6s) %s\n",
+                             symbollist[i], fname, "", end_offset );
+                }
+#endif  // !DARWIN - but is posix
+            } else {
+                // couldn't parse the line? print the whole line.
+                fprintf(out, "  %-40s\n", symbollist[i]);
+            }
+        }
+
+        free(symbollist);
+    }
+
+    void abortHandler( int signum )
+    {
+        // associate each signal with a signal name string.
+        const char* name = NULL;
+        switch( signum )
+        {
+            case SIGABRT: name = "SIGABRT";  break;
+            case SIGSEGV: name = "SIGSEGV";  break;
+            case SIGBUS:  name = "SIGBUS";   break;
+            case SIGILL:  name = "SIGILL";   break;
+            case SIGFPE:  name = "SIGFPE";   break;
+        }
+
+        // Notify the user which signal was caught. We use printf, because this is the
+        // most basic output function. Once you get a crash, it is possible that more
+        // complex output systems like streams and the like may be corrupted. So we
+        // make the most basic call possible to the lowest level, most
+        // standard print function.
+        if ( name )
+            fprintf( stderr, "Caught signal %d (%s)\n", signum, name );
+        else
+            fprintf( stderr, "Caught signal %d\n", signum );
+
+        // Dump a stack trace.
+        // This is the function we will be implementing next.
+        printStackTrace();
+
+        // If you caught one of the above signals, it is likely you just
+        // want to quit your program right now.
+        exit( signum );
+    }
+
+Debug debug;
+
+bool Debug::fInitialized = false;
+
+Debug::Debug(LogLevel aLevel)
+{
+    fInitialized = false;
+    fTimestamp = false;
+    fStartTime = time(0);
+    fThreadFilter = -1;
+    fLevel = aLevel;
+
+	if(fInitialized) return;
+	gsl_set_error_handler (GslFrrorHandler);
+
+    signal( SIGABRT, abortHandler );
+    signal( SIGSEGV, abortHandler );
+    signal( SIGILL,  abortHandler );
+    signal( SIGFPE,  abortHandler );
+
+	fInitialized = true;
+}
+
+void Debug::SetOutputFile(std::string aFile)
+{
+#pragma omp critical (Debug)
+	{
+		if(fOut.is_open())
+			fOut.close();
+		fOut.open(aFile.c_str(),std::ios::out);
+	}
+}
+
+void Debug::PrintLine(std::string aMessage, LogLevel aLevel)
+{
+    int threadId = omp_get_thread_num();
+    if(fThreadFilter>=0 && threadId!=fThreadFilter)
+        return;
+
+#pragma omp critical (Debug)
+	{
+		if(IsLogEnabled(aLevel)) {
+                if (fTimestamp) {
+                    fOut << (time(NULL) - fStartTime) << "\t";
+                }
+                fOut << threadId << "\t" << aMessage << std::endl;
+        }
+		//else
+		//	std::cerr << aMessage << std::endl;
+	}
+}
+
+void Debug::GslFrrorHandler (const char * reason,
+        const char * file,
+        int line,
+        int gsl_errno)
+{
+	std::ostringstream message;
+	message << "GSL error " << gsl_errno << " occurred in " << file << "("<< line << ")\nreason:" << reason << "\n";
+	std::string msg = message.str();
+    LOG_ERROR(msg);
+#ifdef _DEBUG
+    DebugBreakpoint();
+#endif
+	Exception::Throw(msg);
+}
+
+void Debug::DebugBreakpoint()
+{
+/// add breakpoint here
+    std::cerr << "debug breakpoint reached\n";
+}
+
+}//end of namespace Utils
diff --git a/src/lib/Debug.h b/src/lib/Debug.h
new file mode 100644
index 0000000..bc94647
--- /dev/null
+++ b/src/lib/Debug.h
@@ -0,0 +1,140 @@
+/*
+ * Debug.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef DEBUG_H
+#define	DEBUG_H
+
+#include <string>
+#include <sstream>
+#include <fstream>
+
+namespace Utils {
+	
+template<class T> std::string ToString (const T& aValue)
+{
+	std::ostringstream logStr;
+	logStr << aValue;
+	return logStr.str();
+}
+
+#define NOT_IMPLEMENTED Exception::Throw(Utils::ToString("not implemented: " __FILE__ "(") + Utils::ToString(__LINE__) +  ")");
+
+    enum LogLevel{
+        ErrorLL=0,
+        WarningLL,
+        MessageLL,
+        VerboseLL
+    };
+	
+class Debug{
+public:
+	Debug(LogLevel aLevel = MessageLL);
+	static void DebugBreakpoint();
+	void SetOutputFile(std::string aFile);
+	void PrintLine(std::string aMessage, LogLevel aLogLevel = MessageLL);
+    void EnableTimestamp(bool aEnable = true){
+        fTimestamp = aEnable;
+    }
+    //set to negative to disable thread filtering
+    void SetThreadFilter(int aFilter=-1){
+        fThreadFilter = aFilter;
+    }
+
+    void SetLevel(LogLevel aLevel){
+        fLevel = aLevel;
+    }
+
+	static inline int IsInfinity(double _val) /* returns nonzero value if argument is Infinity */
+	{
+		if(_val>1.6e308)
+			return 1;
+		if(_val<(-1.6e308))
+			return -1;
+		return 0;
+	}
+	static int IsValid(double _val) /* returns zero if _val<0 or is Infinity */
+	{
+		if(_val>=0 && _val < 1.6e308)
+		{
+			return 1;
+		}
+		return 0;
+	}
+    inline bool IsLogEnabled(LogLevel aLevel) { return aLevel<=fLevel && fOut.is_open(); }
+private:
+	static void GslFrrorHandler (const char * reason,
+        const char * file,
+        int line,
+        int gsl_errno);
+	static bool fInitialized;
+    bool fTimestamp;
+    time_t fStartTime;
+    int fThreadFilter;
+    LogLevel fLevel;
+	std::ofstream fOut;
+};
+
+extern Debug debug;
+
+}//namespace Utils {
+
+//macro to ensure log output is calculated only if log is enabled
+#define LOG_OUTPUT(str,level) if(Utils::debug.IsLogEnabled(level)) { Utils::debug.PrintLine((str), level);};
+
+#define LOG_VERBOSE(str) LOG_OUTPUT(str,VerboseLL)
+#define LOG_MESSAGE(str) LOG_OUTPUT(str,MessageLL)
+#define LOG_WARNING(str) LOG_OUTPUT(str,WarningLL)
+#define LOG_ERROR(str) LOG_OUTPUT(str,ErrorLL)
+
+///////////////////////////////////////////////////////////////////////////
+// Preprocessor definitions for diagnostic support
+///////////////////////////////////////////////////////////////////////////
+#ifdef _DEBUG
+
+#define ASSERT(f) {if(!(f))\
+{\
+	Utils::Debug::DebugBreakpoint();\
+	Utils::Exception::Throw(Utils::ToString(__FILE__ "(") + Utils::ToString(__LINE__) + ") assertion failed: " #f );\
+};}
+#define VERIFY(f) ASSERT(f)
+#define DEBUG_ONLY(f)      (f)
+#define ASSERT_VALID_NO(f) ASSERT(Utils::Debug::IsValid(f))
+#define VERIFY_VALID_NO(f) ASSERT(Utils::Debug::IsValid(f))
+
+#else   // _DEBUG
+
+#define ASSERT(f)          ((void)0)
+#define VERIFY(f)          ((void)(f))
+#define DEBUG_ONLY(f)      ((void)0)
+#define ASSERT_VALID_NO(f)          ((void)0)
+#define VERIFY_VALID_NO(f)          ((void)(f))
+
+#endif  // _DEBUG
+
+
+#endif	/* DEBUG_H */
+
diff --git a/src/lib/Deflection1D.cpp b/src/lib/Deflection1D.cpp
new file mode 100644
index 0000000..e7ebc51
--- /dev/null
+++ b/src/lib/Deflection1D.cpp
@@ -0,0 +1,127 @@
+/*
+ * Deflection1D.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Deflection1D.h"
+
+namespace Interactions {
+
+Deflection1D::Deflection1D(Function* aB, Function* aLcor):
+		fB(aB),fLcor(aLcor)
+{
+}
+
+Deflection1D::~Deflection1D() {
+
+}
+
+bool Deflection1D::Propagate(cosmo_time aDeltaT, Particle& aParticle, Randomizer& aRandomizer) const
+{
+	double Lcor = fLcor->f(aParticle.Time.z());
+
+	if(aParticle.ElectricCharge()!=0)
+	{
+		if(aParticle.CorrelatedBpath>=Lcor)
+		{
+			aParticle.Deflection2 += (aParticle.CorrelatedDeflection*aParticle.CorrelatedDeflection);
+			aParticle.LastB = -1.;
+		}
+		if(aParticle.LastB < 0.)
+		{
+			aParticle.CorrelatedBpath = 0;
+			aParticle.CorrelatedDeflection = 0;
+		}
+		cosmo_time corDeflDt = Lcor - aParticle.CorrelatedBpath;
+
+		for(cosmo_time timeLeft = aDeltaT;  timeLeft>0;)
+		{
+			cosmo_time rate = Rate(aParticle);
+			if(corDeflDt>timeLeft)
+			{
+				aParticle.CorrelatedBpath += timeLeft;
+				aParticle.CorrelatedDeflection += rate*timeLeft;//Theta in radians
+				break;
+			}
+			else
+			{
+				aParticle.CorrelatedBpath = 0.;
+				double corDefl = aParticle.CorrelatedDeflection + rate*corDeflDt;//Theta in radians
+				aParticle.Deflection2 += corDefl*corDefl;
+				aParticle.CorrelatedDeflection = 0.;
+				timeLeft -= corDeflDt;
+				corDeflDt = Lcor;
+				aParticle.LastB = -1.;
+			}
+		}
+		aParticle.PropagateFreely(aDeltaT);//we don't modify Pdir in this class
+		return true;
+	}
+	else
+	{
+		aParticle.CorrelatedBpath += aDeltaT;
+		return false;
+	}
+}
+
+//deflection rate in radians per unit length (internal units) assuming constant field
+double Deflection1D::Rate(const Particle& aParticle) const
+{
+	int q = abs(aParticle.ElectricCharge());
+	if(q)
+	{
+		if(aParticle.LastB<0.)
+			aParticle.LastB = fabs(fB->f(aParticle.Time.z()));
+		double deflectionRate = ((double)q)*0.52*M_PI/180./(aParticle.Energy/units.TeV)/(10.*units.kpc)*aParticle.LastB*1e15;//formula (14) arxiv/1106.5508v2 or (9) from astro-ph/9604098
+		ASSERT_VALID_NO(deflectionRate);
+		return deflectionRate;
+	}
+	return 0.;
+}
+
+Function* Deflection1D::RandomDirectionB::Clone() const
+{
+	return new RandomDirectionB(fAbsBvalue, fRandomizer.CreateIndependent());
+}
+
+
+double Deflection1D::RandomDirectionB::f(double aZ) const
+{
+	double cos = fRandomizer.Rand()*2.0 - 1;
+	double sin = sqrt(1-cos*cos);
+	return fAbsBvalue*sin;
+}
+
+double Deflection1D::ConstComovingCorrelationLength::f(double aZ) const
+{
+	return fCorL/(1.+aZ);
+}
+
+Function* Deflection1D::ConstComovingCorrelationLength::Clone() const
+{
+	return new RandomDirectionB(fCorL);
+}
+
+} /* namespace mcray */
diff --git a/src/lib/Deflection1D.h b/src/lib/Deflection1D.h
new file mode 100644
index 0000000..84dcadd
--- /dev/null
+++ b/src/lib/Deflection1D.h
@@ -0,0 +1,97 @@
+/*
+ * Deflection1D.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef DEFLECTION1D_H_
+#define DEFLECTION1D_H_
+
+#include "Interaction.h"
+#include "MathUtils.h"
+
+namespace Interactions {
+using namespace mcray;
+using namespace Utils;
+
+//Small deflection approximation (assuming spherical symmetry)
+class Deflection1D : public DeflectionInteraction{
+    class RandomDirectionB : public Function
+    {
+    public:
+    	RandomDirectionB(double aAbsBvalue, unsigned long int aSeed=0) : fRandomizer(aSeed), fAbsBvalue(fabs(aAbsBvalue)){}
+    	double f(double aZ) const;//sign of B is disregarded since sum of squares is used
+    	virtual Function* Clone() const;
+    private:
+    	mutable Randomizer fRandomizer;
+    	double fAbsBvalue;
+    };
+    class ConstComovingCorrelationLength : public Function
+    {
+    public:
+    	ConstComovingCorrelationLength(double aCorL) : fCorL(aCorL){}
+    	double f(double aZ) const;
+    	virtual Function* Clone() const;
+    private:
+    	double fCorL;;
+    };
+public:
+	//Constructor with dependences of magnetic field abs value and correlation length on distance to the source in terms of redshift
+	Deflection1D(Function* aB, Function* aLcor);
+
+	Deflection1D(double aBgauss, double aLcorMpc, unsigned long int aRandomSeed)
+	{
+		fLcor = new ConstComovingCorrelationLength(aLcorMpc*units.Mpc);
+		fB = new RandomDirectionB(aBgauss, aRandomSeed);
+	}
+	virtual ~Deflection1D();
+
+	virtual bool Propagate(cosmo_time deltaT, Particle &aParticle, Randomizer &aRandomizer) const;
+    virtual double Rate(const Particle& aParticle) const;
+    virtual DeflectionInteraction* Clone() const
+    {
+    	return new Deflection1D(fB->Clone(), fLcor->Clone());
+    }
+//protected:
+	//Used for test purposes only (constant B projection value is not physical)
+    //This method can be used for comparison with kinetic equation based code
+    //with deflection taken into account in the form of effective electron decay
+	Deflection1D(double aBgauss, double aLcorMpc)
+	{
+		fLcor = new ConstComovingCorrelationLength(aLcorMpc*units.Mpc);
+		fB = new ConstFunction(aBgauss);
+	}
+private:
+    // fB->f(z) returns strength of (transverse) extragalactic B-field [Gauss] at redshift z at the distance from the observer
+    // corresponding to light travel time from redshift z to 0
+    // (spherical symmetry is assumed)
+    SafePtr<Function> fB;//sign of B is disregarded since sum of squares is used
+    // fLcor->f(z) returns B correlation length at redshift z at the distance from the
+    // observer corresponding to light travel time from redshift z to 0
+    // (spherical symmetry is assumed)
+    SafePtr<Function> fLcor;//sign of B is disregarded since sum of squares is used
+};
+
+} /* namespace mcray */
+#endif /* DEFLECTION1D_H_ */
diff --git a/src/lib/Deflection3D.cpp b/src/lib/Deflection3D.cpp
new file mode 100644
index 0000000..ef30f02
--- /dev/null
+++ b/src/lib/Deflection3D.cpp
@@ -0,0 +1,325 @@
+/*
+ * Deflection3D.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#include "Deflection3D.h"
+#include "Test.h"
+
+
+namespace Interactions {
+    using namespace mcray;
+    using namespace Utils;
+
+
+    Deflection3D::Deflection3D(MagneticField * aMagnField, u_int aAccuracy) :
+            fGlobalB(aMagnField),
+            fAccuracy(aAccuracy)
+    {
+    }
+
+    Deflection3D::Deflection3D(uint aAccuracy, int aUniqueMFslot):
+            fAccuracy(aAccuracy),
+            fMFslot(aUniqueMFslot)
+    {
+        if(fMFslot<0)
+            fMFslot = Particle::ReserveInteractionDataSlot();
+        if(fMFslot<0)
+            Exception::Throw("Deflection3D: Failed to reserve interaction slot");
+    }
+
+    void Deflection3D::GetRotationRate(const Particle &aParticle, const std::vector<double> &aBgauss, std::vector<double>& outRate)
+    {
+        //dn/dt = qeB/E, where q is charge in units of electron charge; e=sqrt(alpha)-electron charge
+        double mult = aParticle.ElectricCharge()*units.Gauss*units.e/aParticle.Energy;
+        const double* N = aParticle.Pdir;
+        ASSERT(outRate.size()==3);
+        outRate[0] = (N[1]* aBgauss[2]- aBgauss[1]*N[2])*mult;
+        outRate[1] = (N[2]* aBgauss[0]- aBgauss[2]*N[0])*mult;
+        outRate[2] = (N[0]* aBgauss[1]- aBgauss[0]*N[1])*mult;
+    }
+
+    double Deflection3D::Rate(const Particle &aParticle) const {
+        if(!aParticle.ElectricCharge())
+            return 0;
+        std::vector<double> deflRate(3);
+        std::vector<double> curB(3);
+        MagneticField* B = fGlobalB;
+        if(!B)
+            B = (MagneticField*)(aParticle.interactionData[fMFslot]);
+        ASSERT(B);
+        B->GetValueGauss(aParticle.X, aParticle.Time, curB);
+        GetRotationRate(aParticle, curB, deflRate);
+        double absRate = sqrt(deflRate[0]*deflRate[0]+deflRate[1]*deflRate[1]+deflRate[2]*deflRate[2]);
+        return absRate;
+    }
+
+    bool Deflection3D::Propagate(cosmo_time deltaT, Particle &aParticle, Randomizer &aRandomizer) const {
+        if(!aParticle.ElectricCharge())
+            return false;
+        cosmo_time initialT = aParticle.Time.t();
+        cosmo_time beta = aParticle.beta();//particle speed
+        MagneticField* pB = fGlobalB;
+        if(!pB)
+            pB = (MagneticField*)(aParticle.interactionData[fMFslot]);
+        ASSERT(pB);
+        cosmo_time dx = pB->MinVariabilityScale(aParticle.Time)/fAccuracy;
+        u_int nSteps = (u_int)(deltaT/dx + 1.);
+        dx = deltaT/nSteps;
+        std::vector<double> deflRate(3);
+        std::vector<double> curB(3);
+        double newPdir[3];
+        int totSteps=0;//debug info
+
+        for(u_int stepB = 0; stepB<nSteps; stepB++){
+            pB->GetValueGauss(aParticle.X, aParticle.Time, curB);
+            GetRotationRate(aParticle, curB, deflRate);
+            double absRate = sqrt(deflRate[0]*deflRate[0]+deflRate[1]*deflRate[1]+deflRate[2]*deflRate[2]);
+            cosmo_time maxStep = 0.05*M_PI/absRate/fAccuracy;//max step corresponds to 9/fAccuracy degrees turn
+            u_int nSubSteps = (u_int)(dx/maxStep+1.);
+            cosmo_time dXsubStep = dx/nSubSteps;
+            for(u_int substep=0; substep<nSubSteps; substep++){
+                if(substep)
+                    GetRotationRate(aParticle, curB, deflRate);
+                double absPdir = 0.;
+                for(int i=0; i<3; i++){
+                    double val = aParticle.Pdir[i] + deflRate[i]*dXsubStep;
+                    newPdir[i] = val;
+                    absPdir += val*val;
+                }
+                absPdir = sqrt(absPdir);//norm of new vector
+                for(int i=0; i<3; i++){
+                    double val = newPdir[i]/absPdir;//make sure rotated vector has unit norm
+                    aParticle.X[i] += 0.5*(aParticle.Pdir[i] + val)*beta*dXsubStep;
+                    aParticle.Pdir[i] = val;
+                }
+                aParticle.Time += dXsubStep;
+                totSteps++;
+            }
+        }
+        //double timeError = fabs((aParticle.Time.t()-initialT-deltaT)/deltaT)/totSteps;
+        //double eps = deltaT/initialT;
+        //char a[1024];
+        //sprintf(a, "eps=%e,timeError=%e", eps, timeError);
+        //ASSERT(timeError<1e-14); # adding small dt to large t leads to calculation error. We minimize the error by using long double for time
+        aParticle.Time.setT(initialT + deltaT);
+        //aParticle.Time.setT(finalT);//increase accuracy
+        return true;
+    }
+
+    DeflectionInteraction *Deflection3D::Clone() const {
+        return fGlobalB ? (new Deflection3D(fGlobalB, fAccuracy)) : (new Deflection3D(fAccuracy, fMFslot));
+    }
+
+    MonochromaticMF::MonochromaticMF(Randomizer &aRandomizer, double aLamdbaMpc,
+                                                       double aAmplitudeGauss):
+            fLambda(aLamdbaMpc*units.Mpc),
+            fK(2.*M_PI/fLambda),
+            fAbsBgauss(aAmplitudeGauss),
+            fBeta(aRandomizer.Rand()*M_PI*2.),
+            fAlpha(aRandomizer.Rand()*M_PI*2.),
+            fAmplitude(3)
+    {
+        /// Choosing random direction: cos(Theta) and phi are distributed uniformly
+        fCosTheta = 1.-(aRandomizer.Rand()*2);
+        fSinTheta = sqrt(1.- fCosTheta * fCosTheta);
+        double phiK(aRandomizer.Rand()*M_PI*2.);
+        fCosPhi = cos(phiK);
+        fSinPhi = sin(phiK);
+        init();
+    }
+
+    MonochromaticMF::MonochromaticMF(double aLamdbaMpc, double aAmplitudeGauss, double aBetaK,
+    double aAlphaK, double aTheta, double aPhi):
+            fLambda(aLamdbaMpc*units.Mpc),
+            fK(2.*M_PI/fLambda),
+            fAbsBgauss(aAmplitudeGauss),
+            fBeta(aBetaK),
+            fAlpha(aAlphaK),
+            fAmplitude(3),
+            fCosTheta(cos(aTheta)),
+            fSinTheta(sin(aTheta)),
+            fCosPhi(cos(aPhi)),
+            fSinPhi(sin(aPhi))
+    {
+        init();
+    }
+
+    void MonochromaticMF::init()
+    {
+        double cosAlpha = cos(fAlpha);
+        double sinAlpha = sin(fAlpha);
+
+        fAmplitude[0] = std::complex<double>(cosAlpha* fCosTheta * fCosPhi, -sinAlpha* fSinPhi)* fAbsBgauss;
+        fAmplitude[1] = std::complex<double>(cosAlpha* fCosTheta * fSinPhi, sinAlpha* fCosPhi)* fAbsBgauss;
+        fAmplitude[2] = std::complex<double>(-cosAlpha* fSinTheta, 0)* fAbsBgauss;
+    }
+
+    void MonochromaticMF::GetValueGauss(const double *x, const CosmoTime &aTime,
+                                               std::vector<double> &outValue) const
+    {
+        ASSERT(outValue.size()==3);
+        double zPrime = fSinTheta * fCosPhi *x[0] + fSinTheta * fSinPhi *x[1] + fCosTheta *x[2];
+        std::complex<double> phase(0,fK*zPrime+ fBeta);
+        std::complex<double> mult = exp(phase);
+        for(int i=0; i<3; i++){
+            outValue[i] = (fAmplitude[i]*mult).real();
+        }
+    }
+
+    double MonochromaticMF::MinVariabilityScale(const CosmoTime &aTime) const {
+        return fLambda;
+    }
+
+    int MagneticField::Print(double lambdaMpc, std::ostream& aOutput) {
+        std::vector<double> B(3);
+        CosmoTime t(0);
+        double step=lambdaMpc*units.Mpc/20.;
+        double L=lambdaMpc*units.Mpc*3;
+        for(double x=0.; x<=L; x+=step) {
+            for (double y = 0.; y <= L; y+=step) {
+                for (double z = 0.; z <= L; z += step) {
+                    const double r[] = {x, y, z};
+                    GetValueGauss(r, t, B);
+                    aOutput << x / units.Mpc << "\t" << y / units.Mpc << "\t" << z / units.Mpc << "\t"
+                              << B[0] << "\t" << B[1] << "\t" << B[2] << "\n";
+                }
+                aOutput << "\n";
+            }
+            aOutput << "\n\n";
+        }
+        aOutput << "# use gnuplot command \"set pm3d at b; splot 'B' i 0 u 2:3:col\"  where col=4,5 or 6 to view the data\n";
+        return 0;
+    }
+
+    int MonochromaticMF::UnitTest() {
+        std::vector<double> B(3);
+        CosmoTime t(0);
+        Randomizer r;
+        double lambdaMpc = 1.;
+        double Bgauss = 10.;
+        MonochromaticMF mf(r,lambdaMpc,Bgauss);
+        mf.Print(lambdaMpc);
+    }
+
+    int Deflection3D::UnitTest() {
+        if(!cosmology.IsInitialized())
+            cosmology.Init();
+        CosmoTime startTime(0.01);
+        Particle e(Electron, startTime.z());
+        e.Energy = 1e13*units.eV;
+        Randomizer r;
+        double lambdaMpc = 1;
+        double Bgauss = 1e-15;
+        double dT = units.Mpc*1.;
+
+        double Beta = 0.1;
+        double Alpha = M_PI/4;
+        double Theta = 0;
+        double Phi = 0;
+        SmartPtr<MagneticField> mf = new MonochromaticMF(lambdaMpc, Bgauss, Beta, Alpha, Theta, Phi);
+        std::vector<double> B0(3);
+        mf->GetValueGauss(e.X, e.Time, B0);
+        double Bperp = sqrt(B0[0]*B0[0]+B0[1]*B0[1]);
+        Deflection3D defl(mf);
+        //Deflection3D defl(new MonochromaticMF(r,lambdaMpc,Bgauss));
+        defl.Propagate(dT, e, r);
+        double L = sqrt(e.X[0]*e.X[0]+e.X[1]*e.X[1]+e.X[2]*e.X[2]);
+        double P = sqrt(e.Pdir[0]*e.Pdir[0]+e.Pdir[1]*e.Pdir[1]+e.Pdir[2]*e.Pdir[2]);
+        double theta = acos(e.Pdir[2])/M_PI*180.;
+
+        std::cout << e.X[0]/units.Mpc << "\t" << e.X[1]/units.Mpc << "\t" << e.X[2]/units.Mpc << "\n";
+        std::cout << "B(0)=" << Bperp << "\t(dt-dL)/dt = " << (dT-L)/dT << "\t|Pdir|=" << P << "\ttheta=" << theta << " deg.\n";
+        return 0;
+    }
+
+    void TurbulentMF::GetValueGauss(const double *x, const CosmoTime &aTime, std::vector<double> &outValue) const {
+        std::vector<double> wave(3, 0.);
+        outValue.assign(3, 0.);
+        for(std::vector<SafePtr<MonochromaticMF> >::const_iterator pw = fWaves.begin(); pw!=fWaves.end(); pw++){
+            (*pw)->GetValueGauss(x, aTime, wave);
+            for(uint i=0; i<3; i++)
+                outValue[i] += (wave[i]);
+        }
+    }
+
+    double TurbulentMF::MinVariabilityScale(const CosmoTime &aTime) const {
+        return fLc*0.1;
+    }
+
+    TurbulentMF::TurbulentMF(Randomizer &aRandomizer, double aLcor_Mpc, double aVariance_Gauss, double aMultK):
+            fLc(aLcor_Mpc*units.Mpc)
+    {
+        ASSERT(aMultK>1.);
+        const double gamma = 11./3.;
+        std::vector<double> norms;
+        std::vector<double> Ks;
+        double sumNorm = 0;
+        for(double k = 1./fLc/16.; k*fLc < 4e4; k*=aMultK){//here we limit the range of waves by condition normK>~1e-3
+            Ks.push_back(k);
+            double normK = k*k*k/(1.+pow(k*fLc,gamma));
+            norms.push_back(normK);
+            sumNorm += normK;
+            fWaves.push_back(0);
+        }
+        for(uint i=0; i<norms.size(); i++){
+            fWaves[i] = new MonochromaticMF(aRandomizer, 2.*M_PI/Ks[i]/units.Mpc, aVariance_Gauss*sqrt(norms[i]/sumNorm));
+        }
+    }
+
+    int TurbulentMF::UnitTest() {
+        if(!cosmology.IsInitialized())
+            cosmology.Init();
+        CosmoTime startTime(0.03);
+        Particle e(Electron, startTime.z());
+        e.Energy = 1e13*units.eV;
+        Randomizer r;
+        double lambdaMpc = 1;
+        double Bgauss = 1e-15;
+        SmartPtr<MagneticField> mf = new TurbulentMF(r, lambdaMpc, Bgauss);
+        Deflection3D defl(mf);
+        std::ofstream outB("TurbulentMF_B.txt");
+        mf->Print(lambdaMpc, outB);
+        std::vector<double> B0(3);
+        double t=0;
+        std::cout << "E/eV = " << e.Energy/units.eV << "\tB/G = " << Bgauss << "\tLc/Mpc = " << lambdaMpc << "\n";
+        mf->GetValueGauss(e.X, e.Time, B0);
+        double Bperp = sqrt(B0[0]*B0[0]+B0[1]*B0[1]);
+        std::cout << "t/Mpc=0\tB_xy/G=" << Bperp << "\tr = (" << e.X[0]/units.Mpc << "\t" << e.X[1]/units.Mpc << "\t" << e.X[2]/units.Mpc << ")\n";
+
+        for(double tMpc=1.; tMpc<100; tMpc*=3) {
+            defl.Propagate(units.Mpc * (tMpc - t), e, r);
+            mf->GetValueGauss(e.X, e.Time, B0);
+            double Bperp = sqrt(B0[0]*B0[0]+B0[1]*B0[1]);
+            std::cout << "t/Mpc=" << tMpc << "\tB_xy/G=" << Bperp << "\tr = (" << e.X[0]/units.Mpc << "\t" << e.X[1]/units.Mpc << "\t" << e.X[2]/units.Mpc << ")\n";
+            double L = sqrt(e.X[0]*e.X[0]+e.X[1]*e.X[1]+e.X[2]*e.X[2]);
+            double P = sqrt(e.Pdir[0]*e.Pdir[0]+e.Pdir[1]*e.Pdir[1]+e.Pdir[2]*e.Pdir[2]);
+            double theta = acos(e.Pdir[2])/M_PI*180.;
+            std::cout << "(dt-dL)/dt = " << (tMpc-L/units.Mpc)/tMpc << "\t|Pdir| = " << P << "\ttheta = " << theta << " deg.\n";
+            t=tMpc;
+        }
+
+        return 0;
+    }
+}
\ No newline at end of file
diff --git a/src/lib/Deflection3D.h b/src/lib/Deflection3D.h
new file mode 100644
index 0000000..97feefc
--- /dev/null
+++ b/src/lib/Deflection3D.h
@@ -0,0 +1,119 @@
+/*
+ * Deflection3D.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef MCRAY_DEFLECTION3D_H
+#define MCRAY_DEFLECTION3D_H
+
+#include <complex>
+#include "Interaction.h"
+#include "MathUtils.h"
+
+
+namespace Interactions {
+    using namespace mcray;
+    using namespace Utils;
+
+    class MagneticField : public SmartReferencedObj{
+    public:
+        virtual void GetValueGauss(const double *x, const CosmoTime &aTime, std::vector<double>& outValue) const = 0;
+
+        virtual double MinVariabilityScale(const CosmoTime &aTime) const = 0;
+
+        int Print(double lambdaMpc, std::ostream& aOutput=std::cout);
+    };
+
+    class Deflection3D : public DeflectionInteraction {
+    public:
+        //aMagnField should provide magnetic field in Gauss
+        Deflection3D(MagneticField * aMagnFieldGauss, uint aAccuracy=10);
+
+        //use custom magnetic field from each particle
+        //@param aUniqueMFslot custom data slot to use or -1 to reserve one
+        Deflection3D(uint aAccuracy = 10, int aUniqueMFslot = -1);
+
+        virtual double Rate(const Particle &aParticle) const;
+
+        virtual bool Propagate(cosmo_time deltaT, Particle &aParticle, Randomizer &aRandomizer) const;
+
+        virtual DeflectionInteraction *Clone() const;
+
+        //return 3D vector dn/dt === 1/beta dV/dt
+        static void GetRotationRate(const Particle &aParticle, const std::vector<double> &aBgauss, std::vector<double>& outRate);
+        static int UnitTest();
+        int MFSlot() const { return fMFslot; }
+    private:
+        SmartPtr<MagneticField> fGlobalB;
+        u_int fAccuracy;
+        int fMFslot;
+    };
+
+    //Randomly oriented plane wave with given k
+    //Generated as fixed k term in formula (3) of J. Giacalone, J.R. Jokipii, 1999, Ap.J., 520:204-214
+    class MonochromaticMF : public MagneticField{
+    public:
+        MonochromaticMF(Randomizer &aRandomizer, double aLamdbaMpc, double aAmplitudeGauss);
+        //constructor with specific orientation and phase
+        MonochromaticMF(double aLamdbaMpc, double aAmplitudeGauss, double aBetaK,
+                        double aAlphaK, double aTheta, double aPhi);
+
+        virtual void GetValueGauss(const double *x, const CosmoTime &aTime,
+                                   std::vector<double> &outValue) const;
+        virtual double MinVariabilityScale(const CosmoTime &aTime) const;
+
+        static int UnitTest();
+    private:
+        void init();
+    private:
+        double fLambda;
+        double fK;//2pi/lambda
+        double fAbsBgauss;
+        double fBeta;
+        double fAlpha;
+        double fCosTheta;
+        double fSinTheta;
+        double fCosPhi;
+        double fSinPhi;
+
+        std::vector<std::complex<double> >fAmplitude;
+    };
+
+    //Turbulent magnetic field defined as sum of randomly oriented plane waves
+    //as in J. Giacalone, J.R. Jokipii, 1999, Ap.J., 520:204-214 formulas (3)-(7)
+    class TurbulentMF : public MagneticField{
+
+    public:
+        TurbulentMF(Randomizer &aRandomizer, double aLcor_Mpc, double aVariance_Gauss, double aMultK=2./* steps in K */);
+        virtual void GetValueGauss(const double *x, const CosmoTime &aTime, std::vector<double> &outValue) const;
+        virtual double MinVariabilityScale(const CosmoTime &aTime) const;
+        static int UnitTest();
+    private:
+        double fLc;
+        std::vector<SafePtr<MonochromaticMF> > fWaves;
+    };
+
+}
+#endif //MCRAY_DEFLECTION3D_H
diff --git a/src/lib/ElmagTest.cpp b/src/lib/ElmagTest.cpp
new file mode 100644
index 0000000..e8c9678
--- /dev/null
+++ b/src/lib/ElmagTest.cpp
@@ -0,0 +1,350 @@
+/*
+ * ElmagTest.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "ElmagTest.h"
+#include "Units.h"
+#include "Utils.h"
+#include "ParticleStack.h"
+#include "PropagationEngine.h"
+#include "Thinning.h"
+#include "TableBackgrounds.h"
+#include "mpfrc++/mpreal.h"
+#include "Deflection1D.h"
+
+using namespace mcray;
+using namespace Utils;
+using namespace Backgrounds;
+using namespace Interactions;
+
+#ifdef NO_FORTRAN
+void elmag_init_test_()
+{
+	throw "Elmag integration is disabled";
+}
+
+#define rate_bb_(e0,zz,icq) ASSERT(FALSE)
+#define sample_photon_(e0,zz,sgam,ierr) ASSERT(FALSE)
+#define zpair_(sgam)_ ASSERT(FALSE)
+#define sample_electron_(e0,zz,sgam,ierr) ASSERT(FALSE)
+#define zics_(e0,sgam) ASSERT(FALSE)
+#define zloss_(e0,zz) ASSERT(FALSE)
+
+#else
+
+extern "C" void elmag_init_test_();
+
+extern "C" double rate_bb_(double* e0,double* zz,int* icq);
+
+extern "C" void sample_photon_(double* e0,double* zz,double* sgam,int* ierr);     // sample c.m. energy for PP interaction
+
+extern "C" double zpair_(double* sgam);  // sample secondary e+ or e- energy fraction for PP interaction
+
+extern "C" void sample_electron_(double* e0,double* zz,double* sgam,int* ierr);     // sample c.m. energy for ICS interaction
+
+extern "C" double zics_(double* e0,double* sgam);  // sample secondary electron(positron) energy fraction for ICS interaction
+
+extern "C" double zloss_(double* e0,double* zz); // relative energy loss (per cm) (due to under-threshold ICS photons)
+
+//extern "C" void get_zics_consts_(double* e0,double* sgam,double* mm,double* emin,double* zmin,double* zmax);
+
+#endif
+
+namespace Test {
+
+double ElmagProxy::Rate(const Particle& aParticle)
+{
+	if(!fEnabled)
+		Exception::Throw("ElmagTest is not initialized");
+	double e0 = aParticle.Energy*1e6*units.Eunit;
+	double zz = aParticle.Time.z();
+	int icq = ParticleTypeToInt(aParticle.Type);
+	double rate_cm = rate_bb_(&e0,&zz,&icq);
+	return rate_cm*units.Lunit;
+}
+
+double ElmagProxy::RateICScel(const Particle& aParticle)
+{
+	if(aParticle.Type != Electron && aParticle.Type != Positron)
+		return 0.;
+	if(!fEnabled)
+		Exception::Throw("ElmagTest is not initialized");
+
+	double e0 = aParticle.Energy*1e6*units.Eunit;
+	double zz = aParticle.Time.z();
+	double rate_cm = zloss_(&e0,&zz);
+	return rate_cm*units.Lunit;
+}
+
+int ElmagProxy::ParticleTypeToInt(ParticleType aType)
+{
+	switch(aType)
+	{
+	case Electron: return -1;
+	case Positron: return 1;
+	case Photon: return 0;
+	default:
+		Exception::Throw("ElmagTest:ParticleTypeToInt unexpected argument");
+		return -100;
+	}
+}
+
+bool ElmagProxy::SampleS(const Particle& aParticle, double& aS)
+{
+	if(!fEnabled)
+		Exception::Throw("ElmagTest is not initialized");
+	double e0 = aParticle.Energy*1e6*units.Eunit;
+	double zz = aParticle.Time.z();
+	double sgam = 0.;
+	int ierr = 0.;
+	if(aParticle.Type == Photon)
+	{
+		sample_photon_(&e0,&zz,&sgam,&ierr);
+		if(ierr)
+		{
+			std::cerr << "Elmag::sample_photon exited with error code " << ierr;
+			return false;
+		}
+	}
+	else if(aParticle.Type == Positron || aParticle.Type == Electron)
+	{
+		sample_electron_(&e0,&zz,&sgam,&ierr);
+		if(ierr)
+		{
+			std::cerr << "Elmag::sample_electron exited with error code " << ierr;
+			return false;
+		}
+	}
+	aS = sgam*1e-12/units.Eunit/units.Eunit;
+	Debug::PrintLine(ToString(aS));
+	return true;
+}
+
+bool ElmagProxy::GetSecondaries(const Particle& aParticle, std::vector<Particle>& aSecondaries, double aS)
+{
+	if(!fEnabled)
+		Exception::Throw("ElmagTest is not initialized");
+	double e0 = aParticle.Energy*1e6*units.Eunit;
+	double sgam = aS/(1e-12/units.Eunit/units.Eunit);
+	if(aParticle.Type == Photon)
+	{
+		double r = zpair_(&sgam);
+		Particle electron = aParticle;
+		electron.Type = Electron;
+		electron.Energy = aParticle.Energy*r;
+		aSecondaries.push_back(electron);
+		Particle positron = aParticle;
+		positron.Type = Positron;
+		positron.Energy = aParticle.Energy*(1.-r);
+		aSecondaries.push_back(positron);
+	}
+	else if(aParticle.Type == Positron || aParticle.Type == Electron)
+	{
+		/*
+		{
+		const double Esec_min = 1.;//MeV
+		double yMax = 1.-Esec_min/aParticle.Energy;
+		double yMin = aParticle.Mass()*aParticle.Mass()/aS;
+		ASSERT(yMin<=1);
+		ASSERT(yMax>=yMin);
+		}
+		{
+			double mm,emin,zmin,zmax;
+			get_zics_consts_(&e0,&sgam,&mm,&emin,&zmin,&zmax);
+			if(zmin>zmax)
+			{
+				mpfr::mpreal Esec_min = "1000000";//MeV
+				mpfr::mpreal E = e0;
+				mpfr::mpreal one = "1";
+				mpfr::mpreal m = aParticle.Mass()*1000000;
+				mpfr::mpreal s = sgam;
+
+				mpfr::mpreal yMax = one-Esec_min/E;
+				mpfr::mpreal yMin = m*m/s;
+				ASSERT(yMin<=one);
+				ASSERT(yMax>=yMin);
+			}
+		}
+		*/
+		double r = zics_(&e0,&sgam);
+		Particle lepton = aParticle;
+		lepton.Energy = aParticle.Energy*r;
+		aSecondaries.push_back(lepton);
+		Particle photon = aParticle;
+		photon.Type = Photon;
+		photon.Energy = aParticle.Energy*(1.-r);
+		aSecondaries.push_back(photon);
+	}
+	return true;
+}
+
+bool ElmagProxy::GetSecondaries(const Particle& aParticle, std::vector<Particle>& aSecondaries)
+{
+	double s=0;
+	if(!SampleS(aParticle, s))
+		return false;
+	return GetSecondaries(aParticle, aSecondaries, s);
+}
+
+void ElmagProxy::Enable()
+{
+	if(!fEnabled)
+	{
+		elmag_init_test_();
+		fEnabled = true;
+	}
+}
+
+bool ElmagProxy::fEnabled = false;
+
+int ElmagTests::Main(int argc, char** argv)
+{
+	if(argc==0)
+	{
+		RatesTest();
+		return 0;
+	}
+	const char* command = argv[0];
+	if(!strcmp(command,"Kachelries"))
+	{
+		int modePP = ElmagCalculateRate|ElmagSampleS|ElmagSampleSecondaryEnergy;
+		int modeICS = ElmagCalculateRate|ElmagSampleS|ElmagSampleSecondaryEnergy|ElmagCalculateCelRate;
+		if(argc>1)
+			sscanf(argv[1], "%d", &modePP);
+		if(argc>2)
+			sscanf(argv[2], "%d", &modeICS);
+		KachelriesTest(0.02, modePP, modeICS);
+		//KachelriesTest(0.15);
+	}
+	return 0;
+}
+
+std::string ElmagTests::HumanReadableMode(int aMode)
+{
+	std::string result = "elmag";
+	if(aMode == ElmagDoNotUse)
+		result += "None";
+	if(aMode & ElmagCalculateRate)
+		result += "Rate";
+	if(aMode & ElmagSampleS)
+		result += "S";
+	if(aMode & ElmagSampleSecondaryEnergy)
+		result += "E";
+	if(aMode & ElmagCalculateCelRate)
+		result += "Cel";
+	return result;
+}
+
+void ElmagTests::KachelriesTest(double aZmax, int aElmagUsageModePP, int aElmagUsageModeICS)
+{
+	if(!cosmology.IsInitialized())
+		cosmology.init(0.73, 71, 10);
+	double logStepK = pow(10,0.05);
+	double Emin = 1./units.Eunit;
+	double alphaThinning = 0.5;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	double Bgauss = 1e-17;//Gauss
+	double LcorMpc = 1.0;//Mpc
+	ParticleStack particles;
+	Result result(Emin);
+
+	result.AddOutput(new SpectrumOutput(
+			"Elmag_Kachelries_Zmax" + ToString(aZmax) + "_PP" + HumanReadableMode(aElmagUsageModePP) + "_ICS" + HumanReadableMode(aElmagUsageModeICS),
+			Emin, pow(10,0.05)));
+
+	CompoundBackground backgr;
+
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+	backgr.AddComponent(new Kneiske0309EBL());
+
+	double stepZ = aZmax<0.2 ? aZmax/2 : 0.1;
+	double epsRel = 1e-3;
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, aZmax, epsRel);
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(new RedShift());
+
+	if(aElmagUsageModeICS&ElmagCalculateCelRate)
+		pe.AddInteraction(new ElmagIcsCEL());
+	else
+		pe.AddInteraction(new Interactions::IcsCEL(backgrI,Emin));
+
+	pe.AddInteraction(new ElmagPP(aElmagUsageModePP, new Interactions::GammaPP(backgrI)));
+	pe.AddInteraction(new ElmagIcs(aElmagUsageModeICS, new Interactions::ICS(backgrI,Emin)));
+	pe.AddInteraction(new Deflection1D(Bgauss, LcorMpc));
+
+	int nParticles = 1000;
+	for(int i=0; i<nParticles; i++)
+	{
+		Particle gamma(Photon, aZmax);
+		gamma.Energy = 1e8/units.Eunit;//MeV
+		particles.Add(gamma);
+	}
+
+	pe.Run();
+	//pe.RunMultithread();
+}
+
+void ElmagTests::RatesTest()
+{
+	if(!cosmology.IsInitialized())
+		cosmology.init(0.73, 71, 10);
+
+	double logStepK = pow(10,0.05);
+
+	RedShift rs;
+	ElmagIcs ics;
+	ElmagIcsCEL icsCel;
+	ElmagPP pp;
+	double Emin=1;
+	double Emax=1e10;
+
+	std::ofstream ratesOut;
+	ratesOut.open("elmag_rates",std::ios::out);
+
+	ratesOut << "#E\tRedshiftRate\tIcsRate\tIcsCelRate\tPpRate\t[E]=1eV [Rate]=1/Mpc\n";
+	Particle particle(Electron, 0.);
+
+	for(double E=Emin; E<Emax; E*=logStepK)
+	{
+		particle.Energy = E/units.Eunit;//MeV
+		particle.Type = Electron;
+		double celRate = icsCel.Rate(particle);
+		double icsRate = ics.Rate(particle);
+		double redshiftRate = rs.Rate(particle);
+		particle.Type = Photon;
+		double ppRate = pp.Rate(particle);
+
+		double mult = 1./units.Mpc;
+		ratesOut << E*1e6 <<  "\t" << redshiftRate*mult <<  "\t" << icsRate*mult <<
+				"\t" << celRate*mult <<  "\t" << ppRate*mult <<	std::endl;
+	}
+}
+
+}//namespace Test {
diff --git a/src/lib/ElmagTest.h b/src/lib/ElmagTest.h
new file mode 100644
index 0000000..f9b5e13
--- /dev/null
+++ b/src/lib/ElmagTest.h
@@ -0,0 +1,162 @@
+/*
+ * ElmagTest.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef ELMAGTEST_H_
+#define ELMAGTEST_H_
+
+#include "Particle.h"
+#include "Interaction.h"
+#include "ICS.h"
+#include "GammaPP.h"
+
+namespace Test {
+using namespace mcray;
+
+/*!
+ * Propagation parameters such as background model, minimal photon energy etc. are defined in Elmag code,
+ * see user_sp102.f90 module user_variables)
+ */
+class ElmagProxy {
+public:
+	static double Rate(const Particle& aParticle);
+	static double RateICScel(const Particle& aParticle);
+	static bool GetSecondaries(const Particle& aParticle, std::vector<Particle>& aSecondaries);
+	static bool SampleS(const Particle& aParticle, double& aS);
+	static bool GetSecondaries(const Particle& aParticle, std::vector<Particle>& aSecondaries, double aS);
+	static void Enable();
+	static inline bool Enabled() { return fEnabled; }
+private:
+	static int ParticleTypeToInt(ParticleType aType);
+	static bool fEnabled;
+};
+
+enum ElmagUsageMode
+{
+	ElmagDoNotUse = 0,
+	ElmagCalculateRate = 1,
+	ElmagSampleS = 2,
+	ElmagSampleSecondaryEnergy = 4,
+	ElmagCalculateCelRate = 8
+};
+
+class ElmagIcsPP : public RandomInteraction{
+protected:
+	ElmagIcsPP():
+		fExternal(0),fElmagUsageMode(ElmagCalculateRate|ElmagSampleS|ElmagSampleSecondaryEnergy){}
+public:
+	//combination of ElmagUsageMode flags
+	ElmagIcsPP(int aElmagUsageMode, RandomInteractionS* pExternalInteraction):
+		fExternal(pExternalInteraction),fElmagUsageMode(aElmagUsageMode)
+	{
+		if(fElmagUsageMode!=ElmagDoNotUse)
+			ElmagProxy::Enable();
+	}
+	virtual bool Filter(ParticleType aType) const = 0;
+	double Rate(const Particle& aParticle) const
+	{
+		if(!Filter(aParticle.Type))
+			return 0.;
+		if(ElmagCalculateRate)
+			return ElmagProxy::Rate(aParticle);
+		else
+			return fExternal->Rate(aParticle);
+	}
+	bool GetSecondaries(const Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const
+	{
+		bool result = false;
+		if(!Filter(aParticle.Type))
+			return result;
+		double s = 0.;
+		result = (fElmagUsageMode&ElmagSampleS)?ElmagProxy::SampleS(aParticle, s) : fExternal->SampleS(aParticle, s, aRandomizer);
+		if(!result)
+			return false;
+		if(fElmagUsageMode&ElmagSampleSecondaryEnergy)
+			return ElmagProxy::GetSecondaries(aParticle, aSecondaries,s);
+		fExternal->SampleSecondaries(aParticle, aSecondaries, s, aRandomizer);
+		return true;
+	}
+protected:
+	SmartPtr<RandomInteractionS> fExternal;
+	int							 fElmagUsageMode;
+};
+
+class ElmagIcs : public ElmagIcsPP{
+public:
+	ElmagIcs(){}
+	ElmagIcs(int aElmagUsageMode, Interactions::ICS* pExternalInteraction):
+		ElmagIcsPP(aElmagUsageMode, pExternalInteraction){};
+
+	RandomInteraction* Clone() const { return new ElmagIcs(fElmagUsageMode, (Interactions::ICS*)fExternal->Clone()); }
+	bool Filter(ParticleType aType) const
+	{
+		return aType == Electron || aType == Positron;
+	}
+};
+
+class ElmagPP : public ElmagIcsPP{
+public:
+	ElmagPP(){}
+	ElmagPP(int aElmagUsageMode, Interactions::GammaPP* pExternalInteraction):
+		ElmagIcsPP(aElmagUsageMode, pExternalInteraction){};
+	RandomInteraction* Clone() const { return new ElmagPP(fElmagUsageMode,(Interactions::GammaPP*)fExternal->Clone()); }
+	virtual bool Filter(ParticleType aType) const
+	{
+		return aType == Photon;
+	}
+};
+
+/*!
+ * Continuous energy loss rate due to Inverse Compton Scattering (ICS) with soft secondary photons
+ * (energy lower than minimal value defined in Elmag code see user_sp102.f90 module user_variables) is calculated in this class
+ */
+class ElmagIcsCEL : public CELInteraction
+{
+public:
+	ElmagIcsCEL()
+	{
+		ElmagProxy::Enable();
+	}
+	double Rate(const Particle& aParticle) const
+	{
+		return ElmagProxy::RateICScel(aParticle);
+	}
+	CELInteraction* Clone() const { return new ElmagIcsCEL();}
+};
+
+class ElmagTests
+{
+public:
+	static int Main(int argc, char** argv);
+private:
+	static std::string HumanReadableMode(int aMode);
+	static void KachelriesTest(double aZmax, int aElmagUsageModePP, int aElmagUsageModeICS);
+	static void RatesTest();
+};
+
+} //namespace Test
+
+#endif /* ELMAGTEST_H_ */
diff --git a/src/lib/ExampleUserMain.cpp b/src/lib/ExampleUserMain.cpp
new file mode 100644
index 0000000..fbc702c
--- /dev/null
+++ b/src/lib/ExampleUserMain.cpp
@@ -0,0 +1,113 @@
+/*
+ * ExampleUserMain.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include <cstdlib>
+#include "ParticleStack.h"
+#include "Utils.h"
+#include "Cosmology.h"
+#include "PropagationEngine.h"
+#include <iostream>
+#include "Output.h"
+#include "TableBackgrounds.h"
+#include "ICS.h"
+#include "GammaPP.h"
+#include "Test.h"
+#include "PrecisionTests.h"
+
+using namespace std;
+using namespace mcray;
+using namespace Utils;
+using namespace Backgrounds;
+using namespace Interactions;
+
+void BuildInitialState(ParticleStack& particles, double aZmax)
+{
+	int nParticles = 10000;
+	for(int i=0; i<nParticles; i++)
+	{
+		Particle gamma(Photon, aZmax);
+		gamma.Energy = 1e7/units.Eunit;//MeV
+		gamma.Weight = 1;
+		particles.AddPrimary(gamma);
+	}
+}
+
+/*
+ * This is an example of user_main function performing user defined tasks
+ * the function should be provided by end user
+ */
+int user_main(int argc, char** argv) {
+	double Zmax = 0.01;
+	double Emin = 1./units.Eunit;
+	double alphaThinning = 1;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	result.AddOutput(new SpectrumOutput("out", Emin, pow(10, 0.05)));
+
+	cosmology.Init();
+
+	//CompoundBackground backgr;
+	//double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+	//backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+	//backgr.AddComponent(new Backgrounds::Kneiske1001EBL());
+
+	//backgr.AddComponent(new Backgrounds::GaussianBackground(1e-6/units.Eunit, 1e-7/units.Eunit, 1., 0.1*units.Vunit));
+	//double n = BackgroundUtils::CalcIntegralDensity(backgr)/units.Vunit;
+	//backgr.AddComponent(new Backgrounds::GaussianBackground(6.3e-10/units.Eunit, 1e-11/units.Eunit, 1., 413*units.Vunit));
+	//n = BackgroundUtils::CalcIntegralDensity(backgr)/units.Vunit;
+
+	GaussianBackground b1(1e-6/units.Eunit, 1e-7/units.Eunit, 1., 0.1*units.Vunit);
+	GaussianBackground b2(6.3e-10/units.Eunit, 1e-11/units.Eunit, 1., 413*units.Vunit);
+
+
+	double stepZ = Zmax<0.2 ? Zmax/2 : 0.1;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	//BackgroundIntegral backgrI(backgr, stepZ, logStepK, Zmax, epsRel);
+	PBackgroundIntegral backgrI1 = new ContinuousBackgroundIntegral(b1, stepZ, logStepK, Zmax, epsRel);
+	PBackgroundIntegral backgrI2 = new ContinuousBackgroundIntegral(b1, stepZ, logStepK, Zmax, epsRel);
+
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(new RedShift());
+	pe.AddInteraction(new Interactions::ICS(backgrI1,Emin));
+	pe.AddInteraction(new Interactions::IcsCEL(backgrI1,Emin));
+	pe.AddInteraction(new Interactions::GammaPP(backgrI1));
+	pe.AddInteraction(new Interactions::ICS(backgrI2,Emin));
+	pe.AddInteraction(new Interactions::IcsCEL(backgrI2,Emin));
+	pe.AddInteraction(new Interactions::GammaPP(backgrI2));
+
+	/// build initial state
+	BuildInitialState(particles, Zmax);
+
+	pe.Run();
+
+    return 0;
+}
diff --git a/src/lib/Filter.cpp b/src/lib/Filter.cpp
new file mode 100644
index 0000000..391e794
--- /dev/null
+++ b/src/lib/Filter.cpp
@@ -0,0 +1,104 @@
+/*
+ * Filter.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Filter.h"
+
+namespace mcray
+{
+
+	JetOutputFilter::JetOutputFilter(double aEmin, double aJetOpenningAngle, double aObservationAngle) :
+			fEmin(aEmin),
+			fJetOpenningAngle(aJetOpenningAngle),
+			fObservationAngle(aObservationAngle)
+	{
+	}
+
+	bool JetOutputFilter::Pass(const Particle& aParticle) const
+	{
+		if(aParticle.Energy<=fEmin)
+			return false;
+		if(fObservationAngle<M_PI_2 && aParticle.ObservationAngle() > fObservationAngle)
+			return false;
+		if(fJetOpenningAngle<M_PI && aParticle.JetOpenningAngle() > fJetOpenningAngle)
+			return false;
+		return true;
+	}
+
+	EnergyBasedFilter::EnergyBasedFilter(double aEmin, double aMaxDeflectionAngle) :
+			fEmin(aEmin),
+			fMaxDeflectionAngle(aMaxDeflectionAngle)
+	{
+	}
+
+	bool EnergyBasedFilter::Pass(const Particle& aParticle) const
+	{
+		if(aParticle.Energy<=fEmin)
+			return false;
+		if(fMaxDeflectionAngle<M_PI && aParticle.DeflectionAngle() > fMaxDeflectionAngle)
+			return false;
+		return true;
+	}
+
+	JetFilter::JetFilter(double aEmin, double aJetOpenningAngle, double aObservationAngle) :
+			fEmin(aEmin),
+			fJetOpenningAngleSin(aJetOpenningAngle<M_PI_2 ? sin(aJetOpenningAngle) : 1.),
+			fObservationAngleCos(cos(aObservationAngle)),
+			fObservationAngleSin(sin(aObservationAngle))
+	{
+	}
+
+	bool JetFilter::Pass(const Particle& aParticle) const
+	{
+		if(aParticle.Energy<=fEmin)
+			return false;
+		if(aParticle.DeflectionAngle()>0.5*M_PI && fJetOpenningAngleSin<1.)
+			return false;//exclude particles deflected by more than Pi/2
+		double sinBeta = sin(aParticle.DeflectionAngle());
+		double d = cosmology.z2d(aParticle.SourceParticle->Time.z());
+		double l = cosmology.z2d(aParticle.LastDeflectionTime().z());
+
+		if(sinBeta > d/l*fJetOpenningAngleSin)
+			return false;//particle can not reach observer from any angle
+
+		double r = sqrt(d*d+l*l-2.*d*l*fObservationAngleCos);//distance(at z=0) from the point of last deflection time to the source
+															//assuming that particle arrives at the edge of PSF
+		if(sinBeta > d/r*fObservationAngleSin)
+			return false;//particle will reach observer from outside PSF
+
+		return true;
+	}
+
+	ParticleTypeFilter::ParticleTypeFilter(const ParticleType aParticlesToDiscard[]) {
+		for(int i=0; i<ParticleTypeEOF; i++){
+			fFilter[i] = true;
+		}
+		if(aParticlesToDiscard)
+			for(int i=0; aParticlesToDiscard[i]!=ParticleTypeEOF; i++) {
+				fFilter[aParticlesToDiscard[i]] = false;
+			}
+	}
+}
diff --git a/src/lib/Filter.h b/src/lib/Filter.h
new file mode 100644
index 0000000..c011931
--- /dev/null
+++ b/src/lib/Filter.h
@@ -0,0 +1,98 @@
+/*
+ * Filter.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef FILTER_H_
+#define FILTER_H_
+#include <math.h>
+#include "Particle.h"
+#include "Utils.h"
+
+namespace mcray {
+
+class IFilter : public Utils::ISmartReferencedObj {
+public:
+	//must be thread-safe
+    virtual bool Pass(const Particle& aParticles) const = 0;
+};
+
+class EnergyBasedFilter : public  Utils::TSmartReferencedObj<IFilter>
+{
+public:
+	EnergyBasedFilter(double aEmin, double aMaxDeflectionAngle=M_PI);
+	bool Pass(const Particle& aParticle) const;
+    virtual ~EnergyBasedFilter(){}
+private:
+    double fEmin;
+    double fMaxDeflectionAngle;
+};
+
+class ParticleTypeFilter : public  Utils::TSmartReferencedObj<IFilter>
+{
+public:
+//aParticlesToRemove is array ending with ParticleTypeEOF
+    ParticleTypeFilter(const ParticleType aParticlesToDiscard[] = 0);
+    void DiscardParticle(ParticleType aType) {fFilter[aType] = false;}
+    bool Pass(const Particle& aParticle) const { return fFilter[aParticle.Type]; }
+    virtual ~ParticleTypeFilter(){}
+private:
+    bool fFilter[ParticleTypeEOF];
+};
+
+class JetOutputFilter : public  Utils::TSmartReferencedObj<IFilter>
+{
+public:
+	JetOutputFilter(double aEmin, double aJetOpenningAngle=M_PI, double aObservationAngle=M_PI_2);
+	bool Pass(const Particle& aParticle) const;
+    virtual ~JetOutputFilter(){}
+private:
+    double fEmin;
+    double fJetOpenningAngle;
+    double fObservationAngle;
+};
+
+class JetRuntimeFilter : public  EnergyBasedFilter
+{
+public:
+	JetRuntimeFilter(double aEmin, double aJetOpenningAngle=M_PI, double aObservationAngle=M_PI_2):
+		EnergyBasedFilter(aEmin, aJetOpenningAngle + aObservationAngle){}
+};
+
+class JetFilter : public  Utils::TSmartReferencedObj<IFilter>
+{
+public:
+	JetFilter(double aEmin, double aJetOpenningAngle=M_PI, double aObservationAngle=M_PI_2);
+	bool Pass(const Particle& aParticle) const;
+    virtual ~JetFilter(){}
+private:
+    double fEmin;
+    double fJetOpenningAngleSin;
+    double fObservationAngleCos;
+    double fObservationAngleSin;
+};
+
+} /* namespace mcray */
+#endif /* FILTER_H_ */
diff --git a/src/lib/GZK.cpp b/src/lib/GZK.cpp
new file mode 100644
index 0000000..04a115c
--- /dev/null
+++ b/src/lib/GZK.cpp
@@ -0,0 +1,189 @@
+/*
+ * GZK.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "GZK.h"
+#include "TableFunction.h"
+#include "Sophia.h"
+
+//used for unit test only
+#include "PropagationEngine.h"
+#include "Inoue12IROSpectrum.h"
+#include "TableBackgrounds.h"
+#include "NeutronDecay.h"
+#include "Test.h"
+
+using namespace mcray;
+namespace Interactions{
+
+GZK::GZK(BackgroundIntegral* aBackground, int aRandSeed):fBackground(aBackground)
+{
+    fMpSophia = SOPHIA::Mass(Proton);
+    fMnSophia = SOPHIA::Mass(Neutron);
+    fSigmaN = InitSigma(Neutron);
+    fSigmaP = InitSigma(Proton);
+    if(aRandSeed)
+    	SOPHIA::SetRandomSeed(aRandSeed);
+}
+
+Function* GZK::InitSigma(ParticleType aPrim)
+{
+    std::string tablesDir = "tables/sophia2/";
+    Utils::TableReader reader(tablesDir+(aPrim==Proton?"p":"n"), 2);
+    std::vector<double>& s = reader.getColumn(0);
+    std::vector<double>& sigma = reader.getColumn(1);
+    double M = aPrim==Proton?fMpSophia:fMnSophia;
+    for(int i=0; i<s.size(); i++)
+    {
+    	double val = M*(M+2.*s[i]*units.GeV);
+        s[i] = M*(M+2.*s[i]*units.GeV);
+        sigma[i] *= (units.barn*1e-6);
+    }
+    double rightVal = sigma[sigma.size()-1];
+    return new LinearFunc(s, sigma, new LogScale(), new LogScale(), 0, rightVal);
+}
+
+double GZK::Rate(const mcray::Particle &aParticle) const {
+    if(aParticle.Type!=Proton && aParticle.Type!=Neutron)
+        return 0.;
+    const Function* sigma = (aParticle.Type==Proton) ? fSigmaP : fSigmaN;
+    return fBackground->GetRateS(*sigma, aParticle);
+}
+
+RandomInteraction *GZK::Clone() const {
+    return new GZK(fBackground->Clone());
+}
+
+bool GZK::SampleS(const mcray::Particle &aParticle, double &aS,
+                                mcray::Randomizer &aRandomizer) const {
+    if(aParticle.Type!=Proton && aParticle.Type!=Neutron)
+        return false;
+    const Function* sigma = aParticle.Type==Proton?fSigmaP:fSigmaN;
+    if(fBackground->GetRateAndSampleS(*sigma, aParticle, aRandomizer, aS)!=0)
+    {
+    	const double sophiaThreshold=1.1646*units.GeV*units.GeV;
+    	if(aS<sophiaThreshold)
+    	{
+    		ASSERT(aS>=sophiaThreshold*0.999);
+    		aS = 1.001*sophiaThreshold;
+    	}
+    	return true;
+    }
+    else
+    	return false;
+}
+
+void GZK::SampleSecondaries(mcray::Particle &aParticle,
+                                          std::vector<mcray::Particle> &aSecondaries, double aS,
+                                          mcray::Randomizer &aRandomizer) const {
+    double M = aParticle.Type==Proton?fMpSophia:fMnSophia;
+    double epsPrimeGeV = 0.5*(aS/M-M)/units.GeV;
+    int noSecondaries = 0;
+    double secEfrac[250];
+    int secTypes[250];
+#pragma omp critical (SOPHIA)
+    {//SOPHIA class is not thread-safe
+        //todo: separately collect protons and neutrons in two queues and try to handle the queues one after another with critical section disabled
+        SOPHIA::SamplePhotopionRel(aParticle.Type, epsPrimeGeV, noSecondaries, secEfrac, secTypes);
+    }
+    while(--noSecondaries>=0)
+    {
+        Particle sec = aParticle;
+        sec.Type = (ParticleType)secTypes[noSecondaries];
+        sec.Energy *= secEfrac[noSecondaries];
+        sec.fCascadeProductionTime = aParticle.Time;
+        aSecondaries.push_back(sec);
+    }
+}
+
+    void GZK::UnitTest()
+    {
+        double Zmax = 1;
+        double Emin = 1e9*units.eV;
+        double Emax = 1e21*units.eV;
+        int Nparticles = 10000;
+        std::string outputDir = "testGZK";
+        unsigned int kAc = 1;
+        double epsRel = 1e-3;
+        double stepZ = Zmax<0.05 ? Zmax/2 : 0.025;
+        double logStepK = pow(10,0.05/kAc);
+        if(!cosmology.IsInitialized())
+            cosmology.Init(Zmax + 10);
+        double cmbTemp = 2.73*units.K;
+        double alphaThinning = 0.5;
+        CompoundBackground backgr;
+        backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp, 0., Zmax + 1.));
+        //IR/O component
+        backgr.AddComponent(new Backgrounds::Inoue12BaselineIROSpectrum());  //new GaussianBackground(0.1*units.eV, 0.01*units.eV, 5, 1./units.cm3, 0, Zmax + 1.));
+        PBackgroundIntegral backgrI(new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel));
+        int seed = 2015;
+        Result result(new EnergyBasedFilter(Emin, M_PI), true);
+
+        SmartPtr<RawOutput> pOutput = new RawOutput(outputDir, false);
+        result.AddOutput(pOutput);
+        ParticleStack particles;
+        PropagationEngine pe(particles, result, seed);
+        EnergyBasedThinning thinning(alphaThinning);
+        pe.SetThinning(&thinning);
+
+        Particle proton(Proton, Zmax);
+        int nSteps = log(Emax/Emin)/log(logStepK)+1.;
+        double mult = pow(Emax/Emin, 1./nSteps);
+        {
+            std::ofstream rateOut;
+            rateOut.open((outputDir + "/GZK").c_str(),std::ios::out);
+            PRandomInteraction i = new GZK(backgrI);
+            pe.AddInteraction(i);
+            int step=0;
+            for(proton.Energy=Emax/100; step<=nSteps; proton.Energy*=mult, step++)
+                rateOut << proton.Energy/units.eV << "\t" << i->Rate(proton)*units.Mpc << "\n";
+            rateOut.close();
+        }
+        Randomizer rand;
+        CosmoTime tEnd;
+        proton.Energy = Emax;
+        pOutput->SetOutputDir(outputDir + "/z0");
+        result.SetEndTime(tEnd);
+
+        for(int i=0; i<Nparticles; i++)
+        {
+            particles.AddPrimary(proton);
+        }
+        pe.RunMultithreadReleaseOnly();
+    }
+
+    void GZK::UnitTestEconserv(double aE, double aZ){
+        int nParticles = 100;
+        Test::EConservationTest test(aZ,1e6,2015);
+        test.alphaThinning = 0.0;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+        test.Engine().AddInteraction(new GZK(test.Backgr(),(int)(test.GetRandomizer().CreateIndependent())));
+        //test.Engine().AddInteraction(new NeutronDecay());
+        Particle p(Proton, aZ);
+        p.Energy = aE*units.eV;
+        test.SetPrimary(p, nParticles);
+        test.Run();
+    }
+}
diff --git a/src/lib/GZK.h b/src/lib/GZK.h
new file mode 100644
index 0000000..732f1f0
--- /dev/null
+++ b/src/lib/GZK.h
@@ -0,0 +1,61 @@
+/*
+ * GZK.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef MCRAY_GZK_H
+#define MCRAY_GZK_H
+
+#include "Interaction.h"
+#include "Background.h"
+
+namespace Interactions {
+    using namespace mcray;
+
+    class GZK : public RandomInteractionS {
+    public:
+        GZK(BackgroundIntegral* aBackground, int aRandSeed=0);
+
+        virtual double Rate(const Particle &aParticle) const;
+
+        virtual RandomInteraction *Clone() const;
+
+        virtual bool SampleS(const Particle &aParticle, double &aS, Randomizer &aRandomizer) const;
+
+        virtual void SampleSecondaries(Particle &aParticle, std::vector<Particle> &aSecondaries, double aS,
+                                       Randomizer &aRandomizer) const;
+        static void UnitTest();
+        static void UnitTestEconserv(double aE, double aZ);
+    private:
+        Function* InitSigma(ParticleType aPrim);
+        SmartPtr<BackgroundIntegral>	fBackground;
+        SafePtr<Function>               fSigmaN;
+        SafePtr<Function>               fSigmaP;
+        double                          fMpSophia;
+        double                          fMnSophia;
+    };
+}
+
+#endif //MCRAY_GZK_H
diff --git a/src/lib/GammaPP.cpp b/src/lib/GammaPP.cpp
new file mode 100644
index 0000000..a2dca97
--- /dev/null
+++ b/src/lib/GammaPP.cpp
@@ -0,0 +1,496 @@
+/*
+ * GammaPP.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "GammaPP.h"
+#include <math.h>
+#include "MathUtils.h"
+#include "Output.h"
+#include "ParticleStack.h"
+#include "TableBackgrounds.h"
+#include "PropagationEngine.h"
+
+namespace Interactions {
+using namespace mcray;
+
+const double EMInteraction::AlphaEM = 7.2973525698e-3;//~1/137
+const double EMInteraction::SigmaCoef = AlphaEM*AlphaEM*2*M_PI;
+
+GammaPP::GammaPP(BackgroundIntegral* aBackground):
+		fBackground(aBackground)
+{
+}
+
+RandomInteraction* GammaPP::Clone() const
+{
+	return new GammaPP(fBackground->Clone());
+}
+
+GammaPP::~GammaPP() {
+}
+
+double GammaPP::SampleSecondaryFracE(double aS, double aRand) const
+{
+	double mass = Particle::Mass(Electron);
+	double s = aS/(mass*mass);
+	SecondaryEnergySamplingEquationPars pars = {s, aRand*DifSigmaIntegral(0.5, s) };
+    double x_lo = 0.5*(1. - sqrt(1. - 4./s));
+    double x_hi = 0.5;
+    const double relError = 1e-3;
+    const int max_iter = 100;
+    double r = 0;
+    try{
+    	//TODO: use MathUtils::SampleLogDistribution sampling method
+    	r = MathUtils::SolveEquation(&SecondaryEnergySamplingEquation, x_lo, x_hi, &pars, relError, max_iter, gsl_root_fsolver_bisection);
+    }catch(Exception* ex)
+    {
+#ifdef _DEBUG
+    	fDebugOutput = true;
+    	double dx = 0.01*(x_hi-x_lo);
+    	for(double x=x_lo; x<=x_hi; x+=dx)
+    	{
+    		SecondaryEnergySamplingEquation(x, &pars);
+    	}
+    	ASSERT(0);
+#endif
+    	throw ex;
+    }
+    return r;
+}
+
+bool GammaPP::SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const
+{
+	ASSERT(aParticle.Type == Photon);
+	aS = 0.;
+	return (fBackground->GetRateAndSampleS(fSigma, aParticle, aRandomizer, aS)!=0);
+}
+
+void GammaPP::SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const
+{
+	ASSERT(aParticle.Type == Photon);
+	Particle sec = aParticle;
+	Particle sec2 = aParticle;
+	double rand = aRandomizer.Rand();
+	sec.Type = rand>0.5 ? Electron : Positron;
+	sec2.Type = rand>0.5 ? Positron : Electron;
+	double r = SampleSecondaryFracE(aS, aRandomizer.Rand());
+	sec.Energy = aParticle.Energy*r;
+	sec2.Energy = aParticle.Energy - sec.Energy;
+	aSecondaries.push_back(sec);
+	aSecondaries.push_back(sec2);
+}
+
+GammaPP::Sigma::Sigma()
+{
+	double m = Particle::Mass(Electron);
+	fThresholdS=4.*m*m;
+}
+
+double GammaPP::Sigma::f(double s) const
+{
+	double beta2 = 1. - fThresholdS/s;
+	if(beta2<=0)
+		return 0;
+	double beta = sqrt(beta2);
+
+	double result = SigmaCoef*((3.-beta2*beta2)*log((1.+beta)/(1.-beta))-2.*beta*(2.-beta2))/s;
+	ASSERT_VALID_NO(result);
+	return result;
+}
+
+double GammaPP::Rate(const Particle& aParticle) const
+{
+	if(aParticle.Type != Photon)
+		return 0.;
+	return fBackground->GetRateS(fSigma, aParticle);
+}
+
+bool GammaPP::fDebugOutput = false;
+
+double GammaPP::SecondaryEnergySamplingEquation(double r, void* aParams)
+{
+	SecondaryEnergySamplingEquationPars* p=(SecondaryEnergySamplingEquationPars*)aParams;
+	double result = DifSigmaIntegral(r, p->s) - p->Integral;
+#ifdef _DEBUG
+	if(GammaPP::fDebugOutput)
+	{
+		std::cerr << "f( " << r << " ) = " << result << std::endl;
+	}
+#endif
+	return result;
+}
+
+double GammaPP::DifSigmaIntegral(double r, double s)
+{
+	double beta = sqrt(1.-4./s);
+	double rMin = 0.5*(1.0 - beta);
+	if(r<=rMin)
+		return 0.;
+	return DifSigmaIntegralUnnorm(r, s) - DifSigmaIntegralUnnorm(rMin, s);
+}
+
+double GammaPP::DifSigmaIntegralUnnorm(double r, double s)
+{
+/*
+Formula for unnormalized diff cross section was taken from http://lanl.arxiv.org/abs/1106.5508v1 eq. (10)
+DifSigmaIntegral is integral of diff cross section from r_min = 1/2*(1 - (1 - 4/s)^(1/2)) to r
+Mathematica program used to produce the output:
+G[r_, s_] = 1/r*(r^2/(1 - r) + 1 - r + 4/s/(1 - r) - 4/s/s/r/(1 - r)/(1 - r))/(1 + (2 - 8/s)*4/s)
+F[r_, s_] = Integrate[G[t, s], {t, 1/2*(1 - (1 - 4/s)^(1/2)), r}, Assumptions -> r > 1/2*(1 - (1 - 4/s)^(1/2)) && r < 0.5 && s > 4]
+//the exact expression below has small (~1e-16 in mathematica) nonzero value for r=rMin (mathematica bug?)
+//therefore we subtract F[rMin, s] from F[r, s]
+*/
+	double s2 = s*s;
+	double r2 = r*r;
+	double r3 = r2*r;
+	double beta = sqrt(1.-4./s);
+	double log_r_minus = log(1 - r);
+	double log_r = log(r);
+	double result = 0.;
+	/*
+	///test (moved to DifSigmaIntegralTest)
+	double rMin = 0.5*(1.0 - beta);
+	if(r<=rMin)
+		return 0.;
+	double a=r-rMin;
+	if(a<1e-3)
+	{
+		double b_1=beta-1.;
+		double b_P1 = beta+1.;
+		result = (16*a*(-2 + s))/
+			    (b_1*b_1*b_P1*b_P1*
+			      (-32 + 8*s + s2)) +
+			   (32*a*a*(-4*beta + beta*s))/
+			    (b_1*b_1*b_1*b_P1*b_P1*b_P1*
+			      (-32 + 8*s + s2)) +
+			   (256*a*a*a*(28 - 11*s + s2))/
+			    (3.*b_1*b_1*b_1*b_1*b_P1*b_P1*b_P1*b_P1*s*
+			      (-32 + 8*s + s2));
+	}
+	else
+	{*/
+		result = -((4 - 8*r + r*s2 - 3*r2*s2 +
+	       2*r3*s2 - 4*r*beta +
+	       4*r2*beta - r*s2*beta +
+	       r2*s2*beta + 8*r*log_r_minus -
+	       8*r2*log_r_minus - 4*r*s*log_r_minus +
+	       4*r2*s*log_r_minus - r*s2*log_r_minus +
+	       r2*s2*log_r_minus - 8*r*log_r + 8*r2*log_r +
+	       4*r*s*log_r - 4*r2*s*log_r + r*s2*log_r -
+	       r2*s2*log_r +
+	       (-1 + r)*r*(-8 + 4*s + s2)*log(1 - beta) -
+	       (-1 + r)*r*(-8 + 4*s + s2)*log(1 + beta))/
+	     ((-1 + r)*r*(-32 + 8*s + s2)));
+	//}
+	ASSERT_VALID_NO(result);
+	return result;
+}
+
+double GammaPP::DifSigmaIntegralTest(double r, double s, bool aSeries, double& a)
+{
+/*
+Formula for unnormalized diff cross section was taken from http://lanl.arxiv.org/abs/1106.5508v1 eq. (10)
+DifSigmaIntegral is integral of diff cross section from r_min = 1/2*(1 - (1 - 4/s)^(1/2)) to r
+Mathematica program used to produce the output:
+G[r_, s_] = 1/r*(r^2/(1 - r) + 1 - r + 4/s/(1 - r) - 4/s/s/r/(1 - r)/(1 - r))/(1 + (2 - 8/s)*4/s)
+F[r_, s_] = Integrate[G[t, s], {t, 1/2*(1 - (1 - 4/s)^(1/2)), r}, Assumptions -> r > 1/2*(1 - (1 - 4/s)^(1/2)) && r < 0.5 && s > 4]
+//the exact expression below has small (~1e-16 in mathematica) nonzero value for r=rMin (mathematica bug?)
+*/
+	double s2 = s*s;
+	double r2 = r*r;
+	double r3 = r2*r;
+	double beta = sqrt(1.-4./s);
+	double log_r_minus = log(1 - r);
+	double log_r = log(r);
+	double rMin = 0.5*(1.0 - beta);
+	if(r<=rMin)
+		return 0.;
+	a=r-rMin;
+	double result = 0.;
+	if(aSeries)///Series[F[1/2*(1 - (1 - 4/s)^(1/2)) + a, s], {a, 0, 3}] excluding part depending on s only
+	{
+		double b_1=beta-1.;
+		double b_P1 = beta+1.;
+		result = (16*a*(-2 + s))/
+			    (b_1*b_1*b_P1*b_P1*
+			      (-32 + 8*s + s2)) +
+			   (32*a*a*(-4*beta + beta*s))/
+			    (b_1*b_1*b_1*b_P1*b_P1*b_P1*
+			      (-32 + 8*s + s2)) +
+			   (256*a*a*a*(28 - 11*s + s2))/
+			    (3.*b_1*b_1*b_1*b_1*b_P1*b_P1*b_P1*b_P1*s*
+			      (-32 + 8*s + s2));
+	}
+	else
+	{//F[r_, s_]
+		result = -((4 - 8*r + r*s2 - 3*r2*s2 +
+	       2*r3*s2 - 4*r*beta +
+	       4*r2*beta - r*s2*beta +
+	       r2*s2*beta + 8*r*log_r_minus -
+	       8*r2*log_r_minus - 4*r*s*log_r_minus +
+	       4*r2*s*log_r_minus - r*s2*log_r_minus +
+	       r2*s2*log_r_minus - 8*r*log_r + 8*r2*log_r +
+	       4*r*s*log_r - 4*r2*s*log_r + r*s2*log_r -
+	       r2*s2*log_r +
+	       (-1 + r)*r*(-8 + 4*s + s2)*log(1 - beta) -
+	       (-1 + r)*r*(-8 + 4*s + s2)*log(1 + beta))/
+	     ((-1 + r)*r*(-32 + 8*s + s2)));
+	}
+	ASSERT_VALID_NO(result);
+	return result;
+}
+
+void GammaPP::UnitTest()
+{
+	double Zmax = 2e-6;
+	double Emin = 10/units.Eunit;
+	//double alphaThinning = 0;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	result.AddOutput(new SpectrumOutput("out", Emin, pow(10, 0.05)));
+
+	cosmology.Init();
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-4*cmbTemp, 1e4*cmbTemp));
+	//backgr.AddComponent(new PlankBackground(cmbTemp, 6.2e-10/units.Eunit, 6.4e-10/units.Eunit));//only central value
+	//backgr.AddComponent(new Backgrounds::Kneiske1001EBL());
+	//backgr.AddComponent(new Backgrounds::GaussianBackground(6.3e-10/units.Eunit, 1e-11/units.Eunit, 1., 413*units.Vunit));
+	//double n = BackgroundUtils::CalcIntegralDensity(backgr)/units.Vunit;
+
+	double stepZ = 0.05;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel);
+	GammaPP* pp = new GammaPP(backgrI);
+	RedShift* rs = new RedShift();
+	double E = Emin;
+	std::cerr << "#E\tRedshiftRate\tPPRate\t\t[E]=1eV [Rate]=1/Mpc\n";
+
+	for(double E=1; E<1e14; E*=logStepK)
+	{
+		Particle photon(Photon, 0.);
+		photon.Energy = E/units.Eunit;//MeV
+		double rate = pp->Rate(photon);
+		double redshiftRate = rs->Rate(photon);
+		double mult = 1./units.Lunit*Units::Mpc_in_cm;
+		std::cerr << E*1e6 <<  "\t" << redshiftRate*mult <<  "\t" << rate*mult <<	std::endl;
+	}
+	double s = 8.;
+	double x_lo = 0.5*(1. - sqrt(1. - 4./s));
+	double dx = 0.01*(0.5-x_lo);
+	std::cerr << "\n\n\n\n\n";
+	for(double x=x_lo; x<=0.5; x+=dx)
+	{
+		double a;
+		double seriesRes = DifSigmaIntegralTest(x,s,true,a);
+		double origRes = DifSigmaIntegralTest(x,s,false,a);
+		std::cerr << a << "\t" << origRes << "\t" << seriesRes << std::endl;
+	}
+
+
+	PropagationEngine pe(particles, result, 2011);
+	//EnergyBasedThinning thinning(alphaThinning);
+	//pe.SetThinning(&thinning);
+	pe.AddInteraction(rs);
+	pe.AddInteraction(pp);
+
+	int nParticlesPerBin = 10000;
+	double Emax = 1e9/units.Eunit;
+	/// build initial state
+	//for(double E=Emin; E<Emax; E*=logStepK)
+	//{
+	E=Emax;
+		Particle photon(Photon, Zmax);
+		photon.Energy = E;//MeV
+		//electron.Weight = exp(-Emax/E);
+		//if(electron.Weight>0)
+		for(int i=0; i<nParticlesPerBin; i++)
+			particles.AddPrimary(photon);
+	//}
+	std::cerr << std::setprecision(15);
+	pe.Run();
+}
+
+void GammaPP::UnitTestSampling(double aE, double aZ)
+{
+	std::string out = "sample_ph_E";
+	out += ToString(aE*1e6);//eV
+	out += "_Z";
+	out += ToString(aZ);
+	std::ofstream secPPout;
+	secPPout.open("sample_sec_pp",std::ios::out);
+	std::ofstream secPPoutVarS;
+	secPPoutVarS.open("sample_sec_pp_varS",std::ios::out);
+
+	debug.SetOutputFile(out);
+
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+	//backgr.AddComponent(new PlankBackground(cmbTemp, 6.2e-10/units.Eunit, 6.4e-10/units.Eunit));//only central value
+
+	double stepZ = 0.05;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	Backgrounds::Kneiske0309EBL* kn0309 = new Backgrounds::Kneiske0309EBL();
+
+	backgr.AddComponent(kn0309);
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, aZ+1, epsRel);
+	Interactions::GammaPP pp(backgrI);
+	Particle photon(Photon, aZ);
+	photon.Energy = aE;//MeV
+	photon.Time.setZ(aZ);
+	photon.Type = Photon;
+	Randomizer rnd;
+	double S=2.;
+	for(int i=0; i<10000; i++)
+	{
+		std::vector<Particle> secondaries;
+		if(pp.GetSecondaries(photon, secondaries, rnd))//this outputs sampled s to debug file
+		{
+			ASSERT(secondaries.size()==2);
+			secPPoutVarS << secondaries[0].Energy/aE << "\n" << secondaries[1].Energy/aE << "\n";
+		}
+		double r = pp.SampleSecondaryFracE(S, rnd.Rand());
+		secPPout << r << '\n';
+		secPPout << 1.-r << std::endl;
+	}
+	secPPout.close();
+}
+
+void GammaPP::UnitTest(bool aPrintRates, bool aPropagate, bool aSplit, bool aBestFitEBL)
+{
+	double Zmax = 2e-4;
+	double Emax = 3e8/units.Eunit;
+	double Emin = 1/units.Eunit;
+	//double alphaThinning = 0;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	if(aPropagate)
+	{
+		std::string out = "out_pp_";
+		out += aSplit?"split" : "no_split";
+		result.AddOutput(new SpectrumOutput(out, Emin, pow(10, 0.05)));
+	}
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-5*cmbTemp, 1e3*cmbTemp));
+	//backgr.AddComponent(new PlankBackground(cmbTemp, 6.2e-10/units.Eunit, 6.4e-10/units.Eunit));//only central value
+
+	double stepZ = 0.05;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	TableBackground* ebl = aBestFitEBL ? ((TableBackground*)new Backgrounds::Kneiske0309EBL()) :
+			((TableBackground*)new Backgrounds::Kneiske1001EBL());
+	PBackgroundIntegral backgrIebl;
+	PRandomInteraction ppEbl;
+	if(!aSplit)
+	{
+		backgr.AddComponent(ebl);
+	}
+	else
+	{
+		backgrIebl = new ContinuousBackgroundIntegral(*ebl, stepZ, logStepK, Zmax, epsRel);
+		ppEbl = new GammaPP(backgrIebl);
+	}
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel);
+	PRandomInteraction pp = new GammaPP(backgrI);
+	PCELInteraction rs = new RedShift();
+	double E = Emin;
+	if(aPrintRates)
+	{
+		std::ofstream ratesOut;
+		std::string ratesFile = "pp_rates_";
+		ratesFile += aBestFitEBL ? "BestFitEBL_" : "MinimalEBL_";
+		ratesFile += aSplit ? "split" : "no_split";
+		ratesOut.open(ratesFile.c_str(),std::ios::out);
+
+		ratesOut << "#E\tRedshiftRate\tPPRate\t\t[E]=1eV [Rate]=1/Mpc\n";
+
+		for(double E=Emin; E<Emax; E*=logStepK)
+		{
+			Particle photon(Photon, 0.);
+			photon.Energy = E/units.Eunit;//MeV
+			double ppRate = pp->Rate(photon);
+			double redshiftRate = rs->Rate(photon);
+			if(aSplit)
+			{
+				ppRate += ppEbl->Rate(photon);
+			}
+			double mult = 1./units.Lunit*Units::Mpc_in_cm;
+			ratesOut << E*1e6 <<  "\t" << redshiftRate*mult <<  "\t" << ppRate*mult << std::endl;
+		}
+	}
+	if(!aPropagate)
+		return;
+
+	PropagationEngine pe(particles, result, 2011);
+	//EnergyBasedThinning thinning(alphaThinning);
+	//pe.SetThinning(&thinning);
+	//pe.AddInteraction(rs);
+	pe.AddInteraction(pp);
+	//ics->fDeleteElectron = true;//check in single interaction mode
+	if(aSplit)
+	{
+		pe.AddInteraction(ppEbl);
+	}
+
+	int nParticlesPerBin = 10000;
+	/// build initial state
+	//for(double E=Emin; E<Emax; E*=logStepK)
+	//{
+	E=Emax;
+		Particle photon(Photon, Zmax);
+		photon.Energy = E;//MeV
+		//electron.Weight = exp(-Emax/E);
+		//if(electron.Weight>0)
+		for(int i=0; i<nParticlesPerBin; i++)
+			particles.AddPrimary(photon);
+	//}
+
+	pe.RunMultithread();
+	if(aSplit)
+		delete ebl;
+}
+
+} /* namespace Interactions */
+
diff --git a/src/lib/GammaPP.h b/src/lib/GammaPP.h
new file mode 100644
index 0000000..05de267
--- /dev/null
+++ b/src/lib/GammaPP.h
@@ -0,0 +1,94 @@
+/*
+ * GammaPP.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef GAMMAPP_H_
+#define GAMMAPP_H_
+
+#include "Interaction.h"
+#include "Background.h"
+
+namespace Interactions {
+using namespace mcray;
+
+class EMInteraction
+{
+public:
+	static const double AlphaEM;//fine structure constant
+	static const double SigmaCoef;//AlphaEM*AlphaEM*2*M_PI used by PP and ICS
+};
+
+class GammaPP : public RandomInteractionS, EMInteraction{
+	class Sigma : public Function
+	{
+	public:
+		Sigma();
+		double f(double s) const;
+		double Xmin() const { return fThresholdS; }
+	private:
+		double fThresholdS;
+	};
+public:
+	GammaPP(BackgroundIntegral* aBackground);
+	RandomInteraction* Clone() const;
+	static void UnitTest();
+	static void UnitTest(bool aPrintRates, bool aPropagate, bool aSplit, bool aBestFitEBL);
+	static void UnitTestSampling(double aE, double aZ);
+	virtual ~GammaPP();
+	double Rate(const Particle& aParticle) const;
+	bool SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const;
+	void SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const;
+
+private:
+	double SampleSecondaryFracE(double aS, double aRand) const;
+	struct SecondaryEnergySamplingEquationPars
+	{
+		double s;
+		double Integral;
+	};
+	/*!
+	@param[in]  s center of mass energy in units of electron mass squared. s>4
+	@param[in]  r ratio of lower energy secondary lepton energy to primary photon energy  1/2*(1 - (1 - 4/s)^(1/2)) < r < 1/2
+	*/
+	static double DifSigmaIntegral(double r, double s);
+	static double DifSigmaIntegralUnnorm(double r, double s);
+	static double DifSigmaIntegralTest(double r, double s, bool aSeries, double& a);
+
+	/*!
+	@param[in]  r ratio of lower energy secondary lepton energy to primary photon energy
+	@param[in]  aParams pointer to SecondaryEnergySamplingEquationPars struct
+	@return DifSigmaIntegral(r, aParams->s) - aParams->Integral
+	*/
+	static double SecondaryEnergySamplingEquation(double r, void* aParams);
+
+	SmartPtr<BackgroundIntegral>	fBackground;
+	Sigma							fSigma;
+	static bool						fDebugOutput;
+};
+
+} /* namespace Interactions */
+
+#endif /* GAMMAPP_H_ */
diff --git a/src/lib/ICS.cpp b/src/lib/ICS.cpp
new file mode 100644
index 0000000..6973276
--- /dev/null
+++ b/src/lib/ICS.cpp
@@ -0,0 +1,665 @@
+/*
+ * ICS.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#include "ICS.h"
+#include <math.h>
+#include "MathUtils.h"
+#include "Output.h"
+#include "ParticleStack.h"
+#include "TableBackgrounds.h"
+#include "PropagationEngine.h"
+#include "PrecisionTests.h"
+
+namespace Interactions {
+using namespace mcray;
+
+ICS::ICS(BackgroundIntegral* aBackground, double aGammaEmin):
+		fBackground(aBackground),
+		fGammaEmin(aGammaEmin),
+		fDeleteElectron(false)
+{
+}
+
+ICS::~ICS() {
+}
+
+double ICS::SampleSecondaryGammaFracE(double aS, double rMin, double rand) const
+{
+	double mass = Particle::Mass(Electron);
+	double s = aS/(mass*mass);
+	SecondaryEnergySamplingEquationPars pars;
+	pars.s = s;
+	pars.Integral = rand*PartialSigma(rMin, s);
+    double x_lo = rMin;
+    double x_hi = 1.-1./s;
+    const double relError = 1e-3;
+    const int max_iter = 100;
+    double r = MathUtils::SolveEquation(&SecondaryEnergySamplingEquation, x_lo, x_hi, &pars, relError, max_iter, gsl_root_fsolver_bisection);
+    return r;
+}
+
+bool ICS::SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const
+{
+	ASSERT(aParticle.Type == Electron || aParticle.Type == Positron);
+	double rMin = fGammaEmin/aParticle.Energy;
+	Sigma sigma(rMin);
+	aS = 0.;
+	return (fBackground->GetRateAndSampleS(sigma, aParticle, aRandomizer, aS)!=0);
+}
+
+void ICS::SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const
+{
+	double rMin = fGammaEmin/aParticle.Energy;
+	double r = SampleSecondaryGammaFracE(aS, rMin, aRandomizer.Rand());
+	Particle secLepton = aParticle;
+	Particle secPhoton = aParticle;
+	secPhoton.Type = Photon;
+    secPhoton.Energy = aParticle.Energy*r;
+    secLepton.Energy = aParticle.Energy - secPhoton.Energy;
+	aSecondaries.push_back(secPhoton);
+	if(!fDeleteElectron)
+		aSecondaries.push_back(secLepton);
+}
+
+ICS::Sigma::Sigma(double rMin) : f_rMin(rMin)
+{
+	double mass = Particle::Mass(Electron);
+	fM2 = mass*mass;
+	fXmin = fM2/(1.-rMin);
+}
+
+double ICS::Sigma::f(double s) const
+{
+	return PartialSigma(f_rMin, s/fM2);
+}
+
+double ICS::Rate(const Particle& aParticle) const
+{
+	if(aParticle.Type != Electron && aParticle.Type != Positron)
+		return 0.;
+	double rMin = fGammaEmin/aParticle.Energy;
+	if(rMin>=1)
+		return 0.;
+	Sigma sigma(rMin);
+	return fBackground->GetRateS(sigma, aParticle);
+}
+
+double ICS::SecondaryEnergySamplingEquation(double r, void* aParams)
+{
+	SecondaryEnergySamplingEquationPars* p=(SecondaryEnergySamplingEquationPars*)aParams;
+	return PartialSigma(r, p->s) - p->Integral;
+}
+
+double ICS::PartialSigma(double rMin, double s)
+{
+/*
+Formula for partial cross section was taken from http://lanl.arxiv.org/abs/1106.5508v1 eq. (9)
+*/
+	double m=Particle::Mass(Electron);
+
+	//return SigmaCoef/m/m*Test::PrecisionTests::PartialSigmaAccurate(rMin,s);
+
+
+	double yMin=1./s;//s here is in units of m^2
+	double yMax = 1-rMin;
+	if(yMin>=yMax)
+	{
+		ASSERT((yMax-yMin)/(yMax+yMin)<1e-10);
+		return 0.;
+	}
+
+	double y_1 = 1.-yMin;
+	double y_1_2 = y_1*y_1;
+	double a=yMax-yMin;
+	double result = 0;
+	if(a>1e-3)
+	{
+		result = yMin*(yMax-yMin)/y_1*(log(yMax/yMin)/(yMax-yMin)*(1.-4.*yMin*(1.+yMin)/y_1_2)+4.*(yMin/yMax+yMin)/y_1_2+0.5*(yMax+yMin));
+		ASSERT_VALID_NO(result);
+	}
+	else
+	{
+		double x=yMin;
+		double x2=x*x;
+		double x3=x2*x;
+		double x4=x3*x;
+		double x_1=x-1.;//negative
+		double x_1_2=x_1*x_1;
+		double x_1_3=x_1_2*x_1;
+		double a2=a*a;
+		double a3=a2*a;
+		double a4=a3*a;
+		double a5=a4*a;
+
+		//result = a/x_1*(-1.-x2+a/x/x_1*(-1.-3.*x+x2-x3+a/x/x_1*(-1./3.-2.*x+x2+a/x*(0.25+2.5*x-0.75*x2+0.2*a/x*(-1.-14.*x+3*x2)))));
+
+		result = (a4*(1 + 10*x - 3*x2))/(4.*x_1_3*x3) +
+		   (a*(1 + x2))/(1 - x) +
+		   (a5*(-1 - 14*x + 3*x2))/(5.*x_1_3*x4) +
+		   (a3*(-1 - 6*x + 3*x2))/(3.*x_1_3*x2) +
+		   (a2*(-1 - 3*x + x2 - x3))/(2.*x_1_2*x);
+
+		ASSERT_VALID_NO(result);
+	}
+
+	return SigmaCoef/m/m*result;
+}
+
+double PartialSigmaSeries(double rMin, double s)
+{
+/*
+Formula for partial cross section was taken from http://lanl.arxiv.org/abs/1106.5508v1 eq. (9)
+*/
+	double yMin=1./s;//s here is in units of m^2
+	double yMax = 1-rMin;
+	if(yMin>=yMax)
+	{
+		ASSERT((yMax-yMin)/(yMax+yMin)<1e-10);
+		return 0.;
+	}
+
+	double a=yMax-yMin;
+	double result = 0;
+	{
+		double x=yMin;
+		double x2=x*x;
+		double x3=x2*x;
+		double x4=x3*x;
+		double x_1=x-1.;//negative
+		double x_1_2=x_1*x_1;
+		double x_1_3=x_1_2*x_1;
+		double a2=a*a;
+		double a3=a2*a;
+		double a4=a3*a;
+		double a5=a4*a;
+
+		//result = a/x_1*(-1.-x2+a/x/x_1*(-1.-3.*x+x2-x3+a/x/x_1*(-1./3.-2.*x+x2+a/x*(0.25+2.5*x-0.75*x2+0.2*a/x*(-1.-14.*x+3*x2)))));
+
+		result = (a4*(1 + 10*x - 3*x2))/(4.*x_1_3*x3) +
+		   (a*(1 + x2))/(1 - x) +
+		   (a5*(-1 - 14*x + 3*x2))/(5.*x_1_3*x4) +
+		   (a3*(-1 - 6*x + 3*x2))/(3.*x_1_3*x2) +
+		   (a2*(-1 - 3*x + x2 - x3))/(2.*x_1_2*x);
+
+		ASSERT_VALID_NO(result);
+	}
+	return result;
+}
+
+double PartialSigmaExact(double rMin, double s)
+{
+/*
+Formula for partial cross section was taken from http://lanl.arxiv.org/abs/1106.5508v1 eq. (9)
+*/
+	double yMin=1./s;//s here is in units of m^2
+	double yMax = 1-rMin;
+	if(yMin>=yMax)
+	{
+		ASSERT((yMax-yMin)/(yMax+yMin)<1e-10);
+		return 0.;
+	}
+
+	double y_1 = 1.-yMin;
+	double y_1_2 = y_1*y_1;
+	//double a=yMax-yMin;
+	double result = 0;
+
+	result = yMin*(yMax-yMin)/y_1*(log(yMax/yMin)/(yMax-yMin)*(1.-4.*yMin*(1.+yMin)/y_1_2)+4.*(yMin/yMax+yMin)/y_1_2+0.5*(yMax+yMin));
+	ASSERT_VALID_NO(result);
+
+	return result;
+}
+
+void ICS::UnitTestDistribution(double aEmin, double aE, double aZ)
+{
+	std::string out = "distrib_s_ics_Emin";
+	out += ToString(aEmin*1e6);//eV
+	out += "_E";
+	out += ToString(aE*1e6);//eV
+	out += "_Z";
+	out += ToString(aZ);
+
+	std::ofstream distribOut;
+	distribOut.open(out.c_str(),std::ios::out);
+	FunctionCallLoggerX<double> logger(distribOut);
+
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+	//backgr.AddComponent(new PlankBackground(cmbTemp, 6.2e-10/units.Eunit, 6.4e-10/units.Eunit));//only central value
+
+	double stepZ = 0.05;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	Backgrounds::Kneiske0309EBL* kn0309 = new Backgrounds::Kneiske0309EBL();
+	PBackgroundIntegral backgrIebl;
+	PCELInteraction icsCelEbl,icsCelFullEbl;
+	PRandomInteraction icsEbl,icsFullEbl;
+
+	backgr.AddComponent(kn0309);
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, aZ+1, epsRel);
+	Interactions::ICS ics(backgrI,aEmin);
+	Particle electron(Electron, aZ);
+	electron.Energy = aE;//MeV
+	Randomizer rnd;
+
+	std::vector<Particle> secondaries;
+	MathUtils::SetLogger(&logger);
+	ics.GetSecondaries(electron, secondaries, rnd);//this outputs distribution
+
+	distribOut.close();
+}
+
+void ICS::UnitTestSampling(double aEmin, double aE, double aZ)
+{
+	std::string out = "sample_e_Emin";
+	out += ToString(aEmin*1e6);//eV
+	out += "_E";
+	out += ToString(aE*1e6);//eV
+	out += "_Z";
+	out += ToString(aZ);
+
+    debug.SetOutputFile(out);
+
+	std::ofstream secICSout;
+	secICSout.open("sample_sec_gamma_ics",std::ios::out);
+
+	std::ofstream secICSoutVarS;
+	secICSoutVarS.open("sample_sec_gamma_ics_varS",std::ios::out);
+
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+	//backgr.AddComponent(new PlankBackground(cmbTemp, 6.2e-10/units.Eunit, 6.4e-10/units.Eunit));//only central value
+
+	double stepZ = 0.05;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	Backgrounds::Kneiske0309EBL* kn0309 = new Backgrounds::Kneiske0309EBL();
+	PBackgroundIntegral backgrIebl;
+	PCELInteraction icsCelEbl,icsCelFullEbl;
+	PRandomInteraction icsEbl,icsFullEbl;
+
+	backgr.AddComponent(kn0309);
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, aZ+1, epsRel);
+	Interactions::ICS ics(backgrI,aEmin);
+	Particle electron(Electron,aZ);
+
+	electron.Energy = aE;//MeV
+
+	Randomizer rnd;
+	double S=2.;
+	for(int i=0; i<10000; i++)
+	{
+		std::vector<Particle> secondaries;
+		if(ics.GetSecondaries(electron, secondaries, rnd))//this outputs sampled s to debug file
+		{
+			ASSERT(secondaries.size()==2);
+			ASSERT(secondaries[0].Type == Photon);
+			secICSoutVarS << secondaries[0].Energy/aE << "\n";
+		}
+		double r = ics.SampleSecondaryGammaFracE(S, aEmin/aE, rnd.Rand());
+		secICSout << r << "\n";
+	}
+	secICSout.close();
+}
+
+void ICS::UnitTest(bool aPrintRates, bool aPropagate, bool aSplit, double Emin, double Emax, double Zmax)
+{
+	//double alphaThinning = 0;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	if(aPropagate)
+	{
+		std::string out = "ics_";
+		out += aSplit?"split" : "no_split";
+		out += "_Emin";
+		out += ToString(Emin);
+		out += "_Emax";
+		out += ToString(Emax);
+		out += "_Zmax";
+		out += ToString(Zmax);
+		result.AddOutput(new SpectrumOutput("out_" + out, Emin, pow(10, 0.05)));
+        debug.SetOutputFile("debug_" + out);
+	}
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+	//backgr.AddComponent(new PlankBackground(cmbTemp, 6.2e-10/units.Eunit, 6.4e-10/units.Eunit));//only central value
+
+	double stepZ = 0.05;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	Backgrounds::Kneiske0309EBL* kn0309 = new Backgrounds::Kneiske0309EBL();
+	PBackgroundIntegral backgrIebl;
+	PCELInteraction icsCelEbl,icsCelFullEbl;
+	PRandomInteraction icsEbl,icsFullEbl;
+	if(!aSplit)
+	{
+		backgr.AddComponent(kn0309);
+	}
+	else
+	{
+		backgrIebl = new ContinuousBackgroundIntegral(*kn0309, stepZ, logStepK, Zmax, epsRel);
+		icsCelEbl = new Interactions::IcsCEL(backgrIebl,Emin);
+		icsEbl = new Interactions::ICS(backgrIebl,Emin);
+		icsFullEbl = new Interactions::ICS(backgrIebl,0);
+		icsCelFullEbl = new Interactions::IcsCEL(backgrIebl,1e20);
+	}
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel);
+	PCELInteraction icsCel = new Interactions::IcsCEL(backgrI,Emin);
+	PRandomInteraction ics = new Interactions::ICS(backgrI,Emin);
+	PRandomInteraction icsFull = new Interactions::ICS(backgrI,0);
+	PCELInteraction icsCelFull = new Interactions::IcsCEL(backgrI,1e20);
+	PCELInteraction rs = new RedShift();
+	double E = Emin;
+	if(aPrintRates)
+	{
+		std::ofstream ratesOut;
+		std::string ratesFile = "ics_rates_";
+		ratesFile += aSplit ? "split" : "no_split";
+		ratesOut.open(ratesFile.c_str(),std::ios::out);
+
+		ratesOut << "#E\tRedshiftRate\tIcsRate\tIcsCelRate\tIcsFullRate\tIcsCelFull\t\t[E]=1eV [Rate]=1/Mpc\n";
+
+		for(double E=Emin; E<Emax; E*=logStepK)
+		{
+			Particle electron(Electron, 0.);
+			electron.Energy = E/units.Eunit;//MeV
+			double celRate = icsCel->Rate(electron);
+			double icsRate = ics->Rate(electron);
+			double redshiftRate = rs->Rate(electron);
+			double icsFullRate = icsFull->Rate(electron);
+			double celFull = icsCelFull->Rate(electron);
+			if(aSplit)
+			{
+				celRate += icsCelEbl->Rate(electron);
+				icsRate += icsEbl->Rate(electron);
+				icsFullRate += icsFullEbl->Rate(electron);
+				celFull += icsCelFullEbl->Rate(electron);
+			}
+			double mult = 1./units.Lunit*Units::Mpc_in_cm;
+			ratesOut << E*1e6 <<  "\t" << redshiftRate*mult <<  "\t" << icsRate*mult <<
+					"\t" << celRate*mult <<  "\t" << icsFullRate*mult <<
+					"\t" << celFull*mult <<	std::endl;
+		}
+	}
+	if(!aPropagate)
+		return;
+
+	PropagationEngine pe(particles, result);
+	//EnergyBasedThinning thinning(alphaThinning);
+	//pe.SetThinning(&thinning);
+	//pe.AddInteraction(rs);
+	pe.AddInteraction(ics);
+	//ics->fDeleteElectron = true;//check in single interaction mode
+	//pe.AddInteraction(icsCel);
+	if(aSplit)
+	{
+		//pe.AddInteraction(icsCelEbl);
+		pe.AddInteraction(icsEbl);
+	}
+
+	int nParticlesPerBin = 100000;
+
+	/// build initial state
+	//for(double E=Emin; E<Emax; E*=logStepK)
+	//{
+	E=Emax;
+		Particle electron(Electron, Zmax);
+		electron.Energy = E;//MeV
+		//electron.Weight = exp(-Emax/E);
+		//if(electron.Weight>0)
+		for(int i=0; i<nParticlesPerBin; i++)
+			particles.AddPrimary(electron);
+	//}
+
+	pe.RunMultithread();
+	if(aSplit)
+		delete kn0309;
+}
+
+void ICS::UnitTestMono(bool aMono, int aAc, bool aRatesOnly)
+{
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+	//double rMin = 0.00031827314821827185;//3.18e-4;
+	//double s=1.0003183787358734;//1./(0.999-rMin);
+	//double exactSigma=PartialSigmaExact(rMin,s);
+	//double seriesSigma=PartialSigmaSeries(rMin,s);
+	//double sigmaAc = Test::PrecisionTests::PartialSigmaAccurate(rMin,s);
+
+	double Zmax = 1e-6;
+	double Emin = 10/units.Eunit;
+	double alphaThinning = 0;//1 conserves number of particles on the average; alpha = 0 disables thinning
+	double logStepK = pow(10,0.05/aAc);
+	double epsRel = 1e-3;
+
+	double conc = 413*units.Vunit;//413 cm^{-3}
+	double centralE = 6.3e-10/units.Eunit;
+	ConstFunction k(centralE);
+	ConstFunction c(conc);
+
+	GaussianBackground backgr(centralE, 0.1*centralE, 1., conc);
+
+	//double concTest = BackgroundUtils::CalcIntegralDensity(backgr, epsRel)/units.Vunit;
+
+
+	double stepZ = Zmax<0.2 ? Zmax/2 : 0.1;
+
+	PBackgroundIntegral backgrM = new MonochromaticBackgroundIntegral(c, k, logStepK, epsRel);
+	PBackgroundIntegral backgrC = new ContinuousBackgroundIntegral(backgr, stepZ, pow(logStepK,0.1), Zmax, epsRel);
+	BackgroundIntegral* backgrI = aMono ? backgrM : backgrC;
+
+	Interactions::IcsCEL* icsCel = new Interactions::IcsCEL(backgrI,Emin);
+	Interactions::ICS* ics = new Interactions::ICS(backgrI,Emin);
+	Interactions::ICS icsFull(backgrI,0);
+	Interactions::IcsCEL icsCelFull(backgrI,1e20);
+	RedShift* rs = new RedShift();
+	double E = Emin;
+	std::ofstream ratesOut;
+	std::string ratesFile = "ics_rates_";
+	ratesFile += aMono ? "mono" : "gauss";
+	ratesFile += "_ac" + ToString(aAc);
+	ratesOut.open(ratesFile.c_str(),std::ios::out);
+	ratesOut << "#E\tRedshiftRate\tIcsRate\tIcsCelRate\tIcsFullRate\tIcsCelFull\t\t[E]=1eV [Rate]=1/Mpc\n";
+
+	for(double E=Emin; E<1e8*Emin; E*=logStepK)
+	{
+		Particle electron(Electron, 0.);
+		electron.Energy = E/units.Eunit;//MeV
+		double celRate = icsCel->Rate(electron);
+		double icsRate = ics->Rate(electron);
+		double redshiftRate = rs->Rate(electron);
+		double icsFullRate = icsFull.Rate(electron);
+		double celFull = icsCelFull.Rate(electron);
+		double mult = 1./units.Lunit*Units::Mpc_in_cm;
+		ratesOut << E*1e6 <<  "\t" << redshiftRate*mult <<  "\t" << icsRate*mult <<
+				"\t" << celRate*mult <<  "\t" << icsFullRate*mult <<
+				"\t" << celFull*mult <<	std::endl;
+	}
+	ratesOut.close();
+	if(aRatesOnly)
+		return;
+
+	ParticleStack particles;
+	Result result(Emin);
+	result.AddOutput(new SpectrumOutput(aMono ? "out_ics_mono" : "out_ics_gauss", Emin, pow(10, 0.05)));
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(rs);
+	pe.AddInteraction(ics);
+	//ics->fDeleteElectron = true;//check in single interaction mode
+	pe.AddInteraction(icsCel);
+	//pe.AddInteraction(new Interactions::GammaPP(backgrI));
+
+	int nParticlesPerBin = 1000;
+	double Emax = 1e6/units.Eunit;
+	/// build initial state
+	//for(double E=Emin; E<Emax; E*=logStepK)
+	//{
+	E=Emax;
+		Particle electron(Electron, Zmax);
+		electron.Energy = E;//MeV
+		//electron.Weight = exp(-Emax/E);
+		//if(electron.Weight>0)
+		for(int i=0; i<nParticlesPerBin; i++)
+			particles.AddPrimary(electron);
+	//}
+
+	pe.Run();
+}
+
+void IcsCEL::UnitTest()
+{
+	double Emin=1;
+	double step = pow(10.,0.1);
+	for(double E = Emin; E<10*Emin; E*=step)
+	{
+		double rMaxGamma = Emin/E;
+		SigmaE sigma(rMaxGamma);
+		std::cout << "# Emin = " << Emin << " ; E = " << E << "\n";
+		sigma.Print(std::cout, 1000, true, 0, 1e5*E);
+		std::cout << "\n\n";
+	}
+}
+
+IcsCEL::IcsCEL(BackgroundIntegral* aBackground, double aMaxGammaE):
+		fBackground(aBackground),
+		fMaxGammaE(aMaxGammaE)
+{
+}
+
+IcsCEL::~IcsCEL()
+{
+
+}
+
+CELInteraction* IcsCEL::Clone() const
+{
+	return new IcsCEL(fBackground->Clone(), fMaxGammaE);
+}
+
+RandomInteraction* ICS::Clone() const
+{
+	return new ICS(fBackground->Clone(), fGammaEmin);
+}
+
+IcsCEL::SigmaE::SigmaE(double rGammaMax)
+: f_rGammaMax(rGammaMax)
+{
+	f_m2 = Particle::Mass(Electron);
+	f_m2*=f_m2;
+}
+
+double IcsCEL::SigmaE::f(double sDim) const
+{
+	double result = 0;
+	double s=sDim/f_m2;//s in units of m^2
+	double s_1 = s-1.;
+	if(s_1<=0)
+		return 0.;
+	double a = 1.-1./s;
+	if(a>f_rGammaMax)
+	{
+		a = f_rGammaMax;
+		double a2=a*a;
+		double s_1_2 = s_1*s_1;
+// Expression below is obtained from output of Mathematica:
+// F[s_, r_] := (r + 1/r + 4/(s - 1)*(1 - 1/r) + 4/(s - 1)^2*(1 - 1/r)^2)
+// Integrate[F[s, r]*(1 - r), {r, 1 - a, 1}, Assumptions -> a < 1 && a > 0]
+// F[s,r] is expression inside brackets in formula (23) of astro-ph/9604098
+		if(a<1e-3)
+		{
+			double s2=s*s;
+			result = a2*(1.+a/s_1*(-4./3.+a/s_1*(0.25*(9.-6.*s+s2)+0.2*a*(13.-6.*s+s2)+a2/6.*(17.-6.*s+s2))));
+		}
+		else
+		{
+			result = -((a*(2.*a2*a*s_1_2 - 6.*(-7. + s)*(1 + s) +
+				3.*a*(-5. + s*(-10. + 3.*s)) - a2*(5. + s*(2. + 5.*s))) +
+				6.*(-1 + a)*(-7. + s)*(1 + s)*log(1. - a))/(6.*(-1 + a)*s_1_2));
+		}
+		ASSERT_VALID_NO(result);
+	}
+	else
+	{
+		// Expression below is obtained from output of Mathematica:
+		// F[s_, r_] := (r + 1/r + 4/(s - 1)*(1 - 1/r) + 4/(s - 1)^2*(1 - 1/r)^2)
+		// Integrate[F[s, r]*(1 - r), {r, 1/s, 1}, Assumptions -> s > 1]
+		// F[s,r] is expression inside brackets in formula (23) of astro-ph/9604098
+		if(s_1<1e-3)
+		{
+			result = s_1*s_1*(2./3. + s_1*(-7./5.+s_1*(49./20.-404./105.*s_1)));
+		}
+		else
+		{
+			result = -1.*(((s_1*(2. + s*(-5. + s*(-3. + s*(-71. + 5.*s)))))/(6.*s*s*s) -
+			       (-7 + s)*(1 + s)*log(s))/s_1/s_1);
+		}
+		ASSERT_VALID_NO(result);
+	}
+	result *= (SigmaCoef/(sDim-f_m2));
+	ASSERT_VALID_NO(result);
+	return result;
+}
+
+double IcsCEL::SigmaE::Xmin() const
+{
+	return f_m2;
+}
+
+double IcsCEL::Rate(const Particle& aParticle) const
+{
+	if(aParticle.Type != Electron && aParticle.Type != Positron)
+		return 0.;
+
+	double rMaxGamma = fMaxGammaE/aParticle.Energy;
+	SigmaE sigma(rMaxGamma);
+	return fBackground->GetRateS(sigma, aParticle);
+}
+
+} /* namespace Interactions */
+
diff --git a/src/lib/ICS.h b/src/lib/ICS.h
new file mode 100644
index 0000000..30b7769
--- /dev/null
+++ b/src/lib/ICS.h
@@ -0,0 +1,121 @@
+/*
+ * ICS.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+#ifndef ICS_H_
+#define ICS_H_
+#include "Interaction.h"
+#include "Background.h"
+#include "GammaPP.h"
+
+namespace Interactions {
+using namespace mcray;
+
+/*!
+ * Inverse Compton Scattering (ICS) with secondary photon energy higher than minimal is calculated in this class.
+ * ICS part with soft secondary photons is approximated by continuous energy loss in class IcsCEL
+ */
+class ICS : public RandomInteractionS, EMInteraction{
+	class Sigma : public Function
+	{
+	public:
+		Sigma(double rMin);
+		double f(double s) const;
+		double Xmin() const {return fXmin;}
+	private:
+		double f_rMin;//minimal fraction of energy lost in single interaction
+		double fXmin;
+		double fM2;
+	};
+public:
+	ICS(BackgroundIntegral* aBackground, double aGammaEmin);
+	RandomInteraction* Clone() const;
+	virtual ~ICS();
+	double Rate(const Particle& aParticle) const;
+	static void UnitTest(bool aPrintRates, bool aPropagate, bool aSplit, double aEmin, double aEmax, double aZmax);
+	static void UnitTestSampling(double aEmin, double aE, double aZ);
+	static void UnitTestDistribution(double aEmin, double aE, double aZ);
+	static void UnitTestMono(bool aMono, int aAc, bool aRatesOnly);
+	bool SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const;
+	void SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const;
+
+private:
+	double SampleSecondaryGammaFracE(double aS, double rMin, double aRand) const;
+	struct SecondaryEnergySamplingEquationPars
+	{
+		double s;
+		double Integral;
+	};
+	/*!
+	@param[in]  rMin ratio of minimal secondary photon energy to primary electron energy
+	@param[in]  s center of mass energy in units of electron mass squared
+	*/
+	static double PartialSigma(double rMin, double s);
+
+	/*!
+	@param[in]  r ratio of lower energy secondary lepton energy to primary photon energy
+	@param[in]  aParams pointer to SecondaryEnergySamplingEquationPars struct
+	@return DifSigmaIntegral(r, aParams->s) - aParams->Integral
+	*/
+	static double SecondaryEnergySamplingEquation(double r, void* aParams);
+
+	SmartPtr<BackgroundIntegral>	fBackground;
+	double							fGammaEmin;//minimal energy of secondary photon
+	bool							fDeleteElectron;//used for unit test
+};
+
+/*!
+ * Continuous energy loss rate due to Inverse Compton Scattering (ICS) with soft secondary photons
+ * (energy lower than minimal value provided in constructor) is calculated in this class
+ */
+class IcsCEL : public CELInteraction, EMInteraction
+{
+	/*!
+	 * 1/E * Integrate[E_gamma * dSigma/dE_gamma,{E_gamma,0,E*f_rGammaMax}]
+	 * */
+	class SigmaE : public Function
+	{
+	public:
+		SigmaE(double rGammaMax);
+		double f(double s) const;
+		double Xmin() const;
+	private:
+		double f_rGammaMax;//maximal fraction of energy lost in single interaction
+		double f_m2;
+	};
+public:
+	static void UnitTest();
+	IcsCEL(BackgroundIntegral* aBackground, double aMaxGammaE);
+	virtual ~IcsCEL();
+	double Rate(const Particle& aParticle) const;
+	CELInteraction* Clone() const;
+private:
+	SmartPtr<BackgroundIntegral>	fBackground;
+	double							fMaxGammaE;//maximal energy of secondary photons
+};
+
+} /* namespace Interactions */
+
+#endif /* ICS_H_ */
diff --git a/src/lib/Inoue12IROSpectrum.cpp b/src/lib/Inoue12IROSpectrum.cpp
new file mode 100644
index 0000000..838475a
--- /dev/null
+++ b/src/lib/Inoue12IROSpectrum.cpp
@@ -0,0 +1,60 @@
+/*
+ * Inoue12IROSpectrum.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Inoue12IROSpectrum.h"
+
+namespace Backgrounds
+{
+using namespace mcray;
+
+Inoue12IROSpectrum::Inoue12IROSpectrum(std::string aName, std::string aDataFile):
+MatrixBackground(aName, aDataFile, false, true)
+{
+}
+
+double Inoue12IROSpectrum::n(double aE, double z) const
+{
+	double E_eV = aE/units.eV;
+	return MatrixBackground::n(aE,z)/E_eV;
+}
+
+Inoue12BaselineIROSpectrum::Inoue12BaselineIROSpectrum():
+Inoue12IROSpectrum("Inoue12Baseline", "EBL_Inoue/EBL_proper_baseline.dat")
+{
+}
+
+Inoue12LowPop3IROSpectrum::Inoue12LowPop3IROSpectrum():
+Inoue12IROSpectrum("Inoue12low", "EBL_Inoue/EBL_proper_low_pop3.dat")
+{
+}
+
+Inoue12UpperPop3IROSpectrum::Inoue12UpperPop3IROSpectrum():
+Inoue12IROSpectrum("Inoue12hi", "EBL_Inoue/EBL_proper_up_pop3.dat")
+{
+}
+
+}
diff --git a/src/lib/Inoue12IROSpectrum.h b/src/lib/Inoue12IROSpectrum.h
new file mode 100644
index 0000000..753c65c
--- /dev/null
+++ b/src/lib/Inoue12IROSpectrum.h
@@ -0,0 +1,60 @@
+/*
+ * Inoue12IROSpectrum.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef INOUE12IROSPECTRUM_H_
+#define INOUE12IROSPECTRUM_H_
+
+#include "Background.h"
+
+namespace Backgrounds {
+
+using namespace mcray;
+
+class Inoue12IROSpectrum  : public MatrixBackground{
+public:
+	Inoue12IROSpectrum(std::string aName, std::string aDataFile);
+	virtual double n(double E, double z) const;
+};
+
+class Inoue12BaselineIROSpectrum  : public Inoue12IROSpectrum{
+public:
+	Inoue12BaselineIROSpectrum();
+};
+
+class Inoue12LowPop3IROSpectrum  : public Inoue12IROSpectrum{
+public:
+	Inoue12LowPop3IROSpectrum();
+};
+
+class Inoue12UpperPop3IROSpectrum  : public Inoue12IROSpectrum{
+public:
+	Inoue12UpperPop3IROSpectrum();
+};
+
+}
+
+#endif /* INOUE12IROSPECTRUM_H_ */
diff --git a/src/lib/Interaction.cpp b/src/lib/Interaction.cpp
new file mode 100644
index 0000000..125efde
--- /dev/null
+++ b/src/lib/Interaction.cpp
@@ -0,0 +1,66 @@
+/*
+ * Interaction.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Interaction.h"
+#include "Cosmology.h"
+
+namespace mcray
+{
+double RedShift::Rate(const Particle& aParticle) const
+{
+	return cosmology.eLossRate(aParticle.Time.z());
+}
+
+bool RandomInteractionS::GetSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const
+{
+	double s = 0.;
+	if(!SampleS(aParticle, s, aRandomizer))
+		return false;
+	debug.PrintLine(ToString(s));
+	SampleSecondaries(aParticle, aSecondaries, s, aRandomizer);
+	return true;
+}
+
+TypeChange::TypeChange(ParticleType aPrimary, ParticleType aSecondary, double aRate):
+fPrimary(aPrimary),
+fSecondary(aSecondary),
+fRate(aRate){};
+double TypeChange::Rate(const Particle& aParticle) const{
+	return aParticle.Type == fPrimary ? fRate : 0.;
+}
+bool TypeChange::GetSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const{
+	Particle sec = aParticle;
+	sec.Type = fSecondary;
+	sec.fCascadeProductionTime = aParticle.Time;
+    aSecondaries.push_back(sec);
+    return true;
+}
+RandomInteraction* TypeChange::Clone() const{
+	return new TypeChange(fPrimary,fSecondary,fRate);
+}
+
+}
diff --git a/src/lib/Interaction.h b/src/lib/Interaction.h
new file mode 100644
index 0000000..14b34d2
--- /dev/null
+++ b/src/lib/Interaction.h
@@ -0,0 +1,125 @@
+/*
+ * Interaction.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef INTERACTION_H
+#define	INTERACTION_H
+
+#include <vector>
+#include "Particle.h"
+#include "Randomizer.h"
+#include "Utils.h"
+
+namespace mcray
+{
+
+///must be thread-safe
+class Interaction : public Utils::SmartReferencedObj {
+public:
+    enum InteractionType
+    {
+        IntRandom,
+        IntContEnergyLoss,
+        IntDeflections
+    };
+    virtual ~Interaction(){};
+    virtual InteractionType Type() const { return fType; };
+    
+    ///Mean interaction rate for random processes
+    ///Energy loss rate 1/E dE/dt for continuous energy loss processes
+    ///Characteristic time^-1 for deflection process
+    virtual double Rate(const Particle& aParticle) const = 0;
+protected:
+    Interaction(InteractionType aType){fType=aType;}
+private:
+    InteractionType fType;
+};
+typedef Utils::SmartPtr<Interaction> PInteraction;
+
+///must be thread-safe
+class CELInteraction : public Interaction {
+public:
+	CELInteraction() : Interaction(IntContEnergyLoss) { }
+	virtual void GetSecondaries(const Particle& aParticle, std::vector<Particle>& aSecondaries,
+								cosmo_time aDeltaT, Randomizer& aRandomizer) const {};
+	virtual CELInteraction* Clone() const = 0;
+};
+typedef Utils::SmartPtr<CELInteraction> PCELInteraction;
+
+///must be thread-safe
+class RandomInteraction : public Interaction {
+public:
+    RandomInteraction() : Interaction(IntRandom) { }
+    //returns false if the process rate is zero
+    virtual bool GetSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const = 0;
+    virtual bool KillsPrimary() const
+    {
+    	return true;
+    }
+    virtual RandomInteraction* Clone() const = 0;
+};
+typedef Utils::SmartPtr<RandomInteraction> PRandomInteraction;
+
+///must be thread-safe
+class DeflectionInteraction : public Interaction {
+public:
+    DeflectionInteraction() : Interaction(IntDeflections) { }
+    //increases aParticle.Time by deltaT and adjust particle position and momentum direction
+    // Return false and leave aParticle fields unchanged if particle is subjected to the interaction
+    virtual bool Propagate(cosmo_time deltaT, Particle &aParticle, Randomizer &aRandomizer) const = 0;
+    virtual DeflectionInteraction* Clone() const = 0;
+};
+typedef Utils::SmartPtr<DeflectionInteraction> PDeflectionInteraction;
+
+class RedShift : public CELInteraction
+{
+public:
+	double Rate(const Particle& aParticle) const;
+	CELInteraction* Clone() const { return new RedShift();}
+};
+
+class RandomInteractionS : public RandomInteraction{
+public:
+	bool GetSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const;
+	virtual bool SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const = 0;
+	virtual void SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const = 0;
+};
+
+//Auxiliary class for debugging etc.
+class TypeChange : public RandomInteraction{
+	ParticleType fPrimary;
+	ParticleType fSecondary;
+	double fRate;
+public:
+	TypeChange(ParticleType aPrimary, ParticleType aSecondary, double aRate);
+	virtual double Rate(const Particle& aParticle) const;
+	bool GetSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const;
+	virtual RandomInteraction* Clone() const;
+};
+
+}
+#endif	/* INTERACTION_H */
+
diff --git a/src/lib/Logger.cpp b/src/lib/Logger.cpp
new file mode 100644
index 0000000..79de9be
--- /dev/null
+++ b/src/lib/Logger.cpp
@@ -0,0 +1,72 @@
+/*
+ * Logger.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "Logger.h"
+#include <iomanip>
+
+namespace mcray {
+    Logger::Logger(std::ostream& aOut) :
+            fLog(aOut),
+            fHeaderPrinted(false)
+    {
+    }
+
+    void Logger::log(const Particle &p) {
+#pragma omp critical (ParticleStack)
+        {
+            if(!fHeaderPrinted){
+                fLog << "# id\tparent_id\t";
+                print_header(fLog);
+                fLog << std::endl;
+                fHeaderPrinted = true;
+            }
+            fLog << p.id << "\t" << p.fPrevId << "\t";
+            print_data(p, fLog);
+            fLog << std::endl;
+        }
+    }
+
+    void Logger::print_data(const Particle &p, std::ostream& aOut){
+        double t = p.Time.t()-p.SourceParticle->Time.t();
+        double t_1 = 1.;
+        double zDif = 0.;
+        if(t>0){
+            t_1 = 1./t;
+            zDif = 1.-t_1*p.X[2];
+        }
+        aOut << p.Type << "\t";
+        std::streamsize orig_pr = aOut.precision(12);
+        aOut << t/units.Mpc << std::setprecision(orig_pr) << "\t" << p.ElectricCharge()
+             << "\t" << p.Energy/units.eV << "\t" << p.Pdir[0] << "\t" << p.Pdir[1] << "\t" << t_1*p.X[0]
+             << "\t" << t_1*p.X[1]  << "\t" << zDif;
+    }
+
+    void Logger::print_header(std::ostream& aOut){
+        aOut << "type\tt/Mpc\tcharge\tE/eV\tp[0]/p\tp[1]/p\tx/t\ty/t\t1-z/t";
+    }
+}
\ No newline at end of file
diff --git a/src/lib/Logger.h b/src/lib/Logger.h
new file mode 100644
index 0000000..4a7749b
--- /dev/null
+++ b/src/lib/Logger.h
@@ -0,0 +1,55 @@
+/*
+ * Logger.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef CRBEAM_LOGGER_H
+#define CRBEAM_LOGGER_H
+#include <iostream>
+#include "Particle.h"
+
+namespace mcray {
+
+    class ILogger {
+    public:
+        // should be thread-safe function
+        virtual void log(const Particle &aParticle) = 0;
+    };
+
+    class Logger : public ILogger {
+        std::ostream& fLog;
+        bool fHeaderPrinted;
+    public:
+        Logger(std::ostream& aLog);
+        // should be thread-safe function
+        virtual void log(const Particle &aParticle);
+        virtual void print_data(const Particle &p, std::ostream& aOut);
+        virtual void print_header(std::ostream& aOut);
+    };
+
+}
+
+#endif //CRBEAM_LOGGER_H
diff --git a/src/lib/MathUtils.cpp b/src/lib/MathUtils.cpp
new file mode 100644
index 0000000..c1bf7be
--- /dev/null
+++ b/src/lib/MathUtils.cpp
@@ -0,0 +1,1015 @@
+/*
+ * MathUtils.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifdef USE_BOOST
+
+#include <boost/numeric/odeint/config.hpp>
+
+#include <boost/numeric/odeint.hpp>
+#include <boost/numeric/odeint/stepper/bulirsch_stoer.hpp>
+#include <boost/numeric/odeint/stepper/bulirsch_stoer_dense_out.hpp>
+
+#endif
+
+#include "Utils.h"
+#include "MathUtils.h"
+#include <gsl/gsl_errno.h>
+#include "gsl/gsl_sf_erf.h"
+#include <gsl/gsl_math.h>
+#include "TableFunction.h"
+#include "nr/odeint.h"
+#include "nr/stepperdopr5.h"
+
+
+namespace Utils {
+#ifdef USE_BOOST
+	using namespace boost::numeric::odeint;
+
+	template< class Obj , class Mem >
+	class ode_wrapper
+	{
+		Obj* m_pObj;
+		Mem m_mem;
+
+	public:
+
+		ode_wrapper( Obj* obj , Mem mem ) : m_pObj(obj ) , m_mem(mem ) { }
+
+		template< class State , class Deriv , class Time >
+		void operator()( const State &x , Deriv &dxdt , Time t )
+		{
+			((*m_pObj).*m_mem)(x , dxdt , t );
+		}
+	};
+
+	template< class Obj , class Mem >
+	ode_wrapper< Obj , Mem > make_ode_wrapper( Obj* obj , Mem mem )
+	{
+		return ode_wrapper< Obj , Mem >( obj , mem );
+	}
+
+
+	template< class Obj , class Mem >
+	class observer_wrapper
+	{
+		Obj* m_pObj;
+		Mem m_mem;
+
+	public:
+
+		observer_wrapper( Obj* obj , Mem mem ) : m_pObj(obj ) , m_mem(mem ) { }
+
+		template< class State , class Time >
+		void operator()( const State &x , Time t )
+		{
+			((*m_pObj).*m_mem)(x , t );
+		}
+	};
+
+	template< class Obj , class Mem >
+	observer_wrapper< Obj , Mem > make_observer_wrapper( Obj* obj , Mem mem )
+	{
+		return observer_wrapper< Obj , Mem >( obj , mem );
+	}
+
+	template< class X=double >
+	class Sampler4 : public ISampler<X> {
+		//typedef runge_kutta_dopri5<X> dopri5_type;
+		//typedef controlled_runge_kutta< dopri5_type > controlled_dopri5_type;
+		//typedef dense_output_runge_kutta< controlled_dopri5_type > dense_output_dopri5_type;
+		//typedef bulirsch_stoer_dense_out<X> dopri5_type;
+	public:
+		Sampler4(){
+			fIntermediateVals=new std::vector<X>;
+			fIntermediateTs=new std::vector<X>;
+			fIntermediateTs->reserve(256);
+			fIntermediateVals->reserve(256);
+		}
+
+		~Sampler4(){
+			delete fIntermediateVals;
+			delete fIntermediateTs;
+		}
+
+		void System(const X &x , X &dxdt , X t ){
+			dxdt=fFunc->f(t);
+		}
+
+		void Output(const X &x , X t ){
+			if(fMaxX>0 && x>=fMaxX){
+				X lastT = *(fIntermediateTs->end()-1);
+				X lastX = *(fIntermediateVals->end()-1);
+				if((t-lastT)/(t+lastT)<fRelErr && t-lastT<fAbsErr){
+					X result = lastT + (t-lastT)/(x-lastX)*(fMaxX-lastX);
+					throw result;
+				}
+				X totRate = x-lastX;
+				throw sample(*fFunc, lastT, t, (fMaxX-lastX)/(x-lastX), totRate, fRelErr, fAbsErr);
+			}
+			else {
+				fIntermediateVals->push_back(x);
+				fIntermediateTs->push_back(t);
+			}
+		}
+
+		/// If total rate is known it should be passed via aTotRate argument
+		/// Otherwize aTotRate should be set to 0 (it will be calculated by function)
+		X sample(const FunctionX<X>& f, X aTmin, X aTmax, X aRand,
+				 X & aTotRate, X aRelErr, X aAbsErr = 1e300){
+			fMaxX = aTotRate*aRand;
+			fRelErr = aRelErr;
+			fAbsErr = aAbsErr;
+
+			size_t nPoints = (size_t)((aTmax-aTmin)/aRelErr+0.5)/2+1;
+			X step= (aTmax-aTmin)/nPoints;
+
+			fIntermediateTs->resize(0);//this will not decrease capacity of vectors
+			fIntermediateVals->resize(0);
+
+			X curT=aTmin;
+			fFunc = &f;
+			aTotRate = 0.;
+
+			X dt = (aTmax - aTmin) * aRelErr;
+
+			try {
+				//dense_output_dopri5_type dopri5 = make_dense_output( aAbsErr , aRelErr , dopri5_type() );
+				bulirsch_stoer_dense_out< X > stepper( aAbsErr , aRelErr);
+				integrate_adaptive( stepper , make_ode_wrapper(this,&Sampler4::System) , aTotRate , aTmin , aTmax , dt , make_observer_wrapper(this, &Sampler4::Output) );
+				//integrate_const
+				//integrate_adaptive(dopri5, make_ode_wrapper(this,&Sampler3::System), aTotRate, aTmin, aTmax, dt,
+								   //make_observer_wrapper(this, &Sampler3::Output));
+			}catch (X aValue){
+				return aValue;
+			}
+			if(aTotRate<=0){
+				return aTmax;
+			}
+			std::vector<X>& intermediateVals = *fIntermediateVals;
+			std::vector<X>& intermediateTs = *fIntermediateTs;
+			X searchVal = aTotRate * aRand;
+			size_t i2= intermediateVals.size();
+			size_t i1=0;
+			for(size_t i=(i2+i1)/2; i2-i1>1; i=(i2+i1)/2)
+			{
+				if(intermediateVals[i] < searchVal)
+					i1 = i;
+				else
+					i2 = i;
+			}
+			X t1 = intermediateTs[i1];
+			X t2 = intermediateTs[i2];
+			X x1 = intermediateVals[i1];
+			X x2 = intermediateVals[i2];
+			if((t2-t1)/(t2+t1)<fRelErr && t2-t1<fAbsErr){
+				return t1 + (t2-t1)/(x2-x1)*(searchVal-x1);
+			}
+			X totRate=x2-x1;
+			return sample(*fFunc, t1, t2, (searchVal-x1)/(x2-x1), totRate, fRelErr, fAbsErr);
+		}
+	private:
+		const FunctionX<X>* fFunc;
+		std::vector<X>* fIntermediateVals;
+		std::vector<X>* fIntermediateTs;
+		X      fMaxX;
+		X      fRelErr;
+		X      fAbsErr;
+	};
+
+	template< class X=double >
+	class Sampler3 : public ISampler<X> {
+		typedef runge_kutta_dopri5<X> dopri5_type;
+		typedef controlled_runge_kutta< dopri5_type > controlled_dopri5_type;
+		typedef dense_output_runge_kutta< controlled_dopri5_type > dense_output_dopri5_type;
+	public:
+		Sampler3(){
+			fIntermediateVals=new std::vector<X>;
+			fIntermediateTs=new std::vector<X>;
+			fIntermediateTs->reserve(256);
+			fIntermediateVals->reserve(256);
+		}
+
+		~Sampler3(){
+			delete fIntermediateVals;
+			delete fIntermediateTs;
+		}
+
+		void System(const X &x , X &dxdt , X t ){
+			dxdt=fFunc->f(t);
+		}
+
+		void Output(const X &x , X t ){
+			if(fMaxX>0 && x>=fMaxX){
+				X lastT = *(fIntermediateTs->end()-1);
+				X lastX = *(fIntermediateVals->end()-1);
+				if((t-lastT)/(t+lastT)<fRelErr && t-lastT<fAbsErr){
+					X result = lastT + (t-lastT)/(x-lastX)*(fMaxX-lastX);
+					throw result;
+				}
+				X totRate = x-lastX;
+				throw sample(*fFunc, lastT, t, (fMaxX-lastX)/(x-lastX), totRate, fRelErr, fAbsErr);
+			}
+			else {
+				fIntermediateVals->push_back(x);
+				fIntermediateTs->push_back(t);
+			}
+		}
+
+		/// If total rate is known it should be passed via aTotRate argument
+		/// Otherwize aTotRate should be set to 0 (it will be calculated by function)
+		X sample(const FunctionX<X>& f, X aTmin, X aTmax, X aRand,
+				 X & aTotRate, X aRelErr, X aAbsErr = 1e300){
+			fMaxX = aTotRate*aRand;
+			fRelErr = aRelErr;
+			fAbsErr = aAbsErr;
+
+			size_t nPoints = (size_t)((aTmax-aTmin)/aRelErr+0.5)/2+1;
+			X step= (aTmax-aTmin)/nPoints;
+
+			fIntermediateTs->resize(0);//this will not decrease capacity of vectors
+			fIntermediateVals->resize(0);
+
+			X curT=aTmin;
+			fFunc = &f;
+			aTotRate = 0.;
+			dense_output_dopri5_type dopri5 = make_dense_output( aAbsErr , aRelErr , dopri5_type() );
+			X dt = (aTmax - aTmin) * aRelErr;
+
+			try {
+				integrate_adaptive(dopri5, make_ode_wrapper(this,&Sampler3::System), aTotRate, aTmin, aTmax, dt,
+								   make_observer_wrapper(this, &Sampler3::Output));
+			}catch (X aValue){
+				return aValue;
+			}
+			if(aTotRate<=0){
+				return aTmax;
+			}
+			std::vector<X>& intermediateVals = *fIntermediateVals;
+			std::vector<X>& intermediateTs = *fIntermediateTs;
+			X searchVal = aTotRate * aRand;
+			size_t i2= intermediateVals.size();
+			size_t i1=0;
+			for(size_t i=(i2+i1)/2; i2-i1>1; i=(i2+i1)/2)
+			{
+				if(intermediateVals[i] < searchVal)
+					i1 = i;
+				else
+					i2 = i;
+			}
+			X t1 = intermediateTs[i1];
+			X t2 = intermediateTs[i2];
+			X x1 = intermediateVals[i1];
+			X x2 = intermediateVals[i2];
+			if((t2-t1)/(t2+t1)<fRelErr && t2-t1<fAbsErr){
+				return t1 + (t2-t1)/(x2-x1)*(searchVal-x1);
+			}
+			X totRate=x2-x1;
+			return sample(*fFunc, t1, t2, (searchVal-x1)/(x2-x1), totRate, fRelErr, fAbsErr);
+		}
+	private:
+		const FunctionX<X>* fFunc;
+		std::vector<X>* fIntermediateVals;
+		std::vector<X>* fIntermediateTs;
+		X      fMaxX;
+		X      fRelErr;
+		X      fAbsErr;
+	};
+
+	template<typename X = double >
+	class Sampler : public ISampler<X> {
+		typedef runge_kutta_dopri5<X> dopri5_type;
+		typedef controlled_runge_kutta< dopri5_type > controlled_dopri5_type;
+		typedef dense_output_runge_kutta< controlled_dopri5_type > dense_output_dopri5_type;
+	public:
+		Sampler(){
+			fIntermediateVals=0;
+			fIntermediateTs=0;
+		}
+		void operator()(const X &x , X &dxdt , const X t ){
+			dxdt=fFunc->f(t);
+		}
+		void operator()(const X &x , const X t ){
+			(*fIntermediateVals)[fLastStep++]=x;
+			double a=(*fIntermediateVals)[fLastStep-1];
+			a=a+1;
+		}
+
+		X sample(const FunctionX<X>& f, X aTmin, X aTmax, X aRand,
+				 X & aTotRate, X aRelErr, X aAbsErr = 1e300){
+			// create a vector with observation time points
+			size_t nPoints = (size_t)((aTmax-aTmin)/aRelErr/2+0.5);
+			X step= (aTmax-aTmin)/nPoints;
+			std::vector<X> intermediateVals(nPoints + 1);
+			std::vector<X> intermediateTs(nPoints + 1);
+			fIntermediateVals=&intermediateVals;
+			fIntermediateTs=&intermediateTs;
+			X curT=aTmin;
+			for( size_t i=0 ; i<=nPoints ; ++i, curT+=step )
+				intermediateTs[i] = curT;
+
+			fFunc = &f;
+			aTotRate = 0.;
+			dense_output_dopri5_type dopri5 = make_dense_output( aAbsErr , aRelErr , dopri5_type() );
+			X dt = (aTmax - aTmin) * aRelErr;
+
+			fLastStep=0;
+			integrate_times(dopri5 , (*this) , aTotRate , intermediateTs, dt , (*this) );
+			if(aTotRate==0.){
+				//aTotRate=0.;
+				return aTmax;
+			}
+
+			X searchVal = aTotRate * aRand;
+			size_t i2= intermediateVals.size();
+			size_t i1=0;
+			for(size_t i=(i2+i1)/2; i2-i1>1; i=(i2+i1)/2)
+			{
+				if(intermediateVals[i] < searchVal)
+					i1 = i;
+				else
+					i2 = i;
+			}
+			X t1 = intermediateTs[i1];
+			X t2 = intermediateTs[i2];
+			X x1 = intermediateVals[i1];
+			X x2 = intermediateVals[i2];
+			return t1 + (t2 - t1) / (x2 - x1) * (searchVal - x1);
+		}
+
+		const FunctionX<X>* fFunc;
+		std::vector<X>* fIntermediateVals;
+		std::vector<X>* fIntermediateTs;
+
+		size_t      fLastStep;
+	};
+
+	template<typename X = double >
+	class LogSampler : public ISampler<X> {
+		typedef runge_kutta_dopri5<X> dopri5_type;
+		typedef controlled_runge_kutta< dopri5_type > controlled_dopri5_type;
+		typedef dense_output_runge_kutta< controlled_dopri5_type > dense_output_dopri5_type;
+	public:
+		LogSampler(){
+			fIntermediateVals=0;
+			fIntermediateTs=0;
+		}
+		void operator()(const X &x , X &dxdt , const X t ){
+			dxdt=fFunc->f(t);
+		}
+		void operator()(const X &x , const X t ){
+			(*fIntermediateVals)[fLastStep++]=x;
+			double a=(*fIntermediateVals)[fLastStep-1];
+			a=a+1;
+		}
+
+		X sample(const FunctionX<X>& f, X aTmin, X aTmax, X aRand,
+				 X & aTotRate, X aRelErr, X aAbsErr = 1e300){
+			// create a vector with observation time points
+			size_t nPoints = (size_t)(log(aTmax/aTmin)/log(1.0+aRelErr)/2+0.5);
+			X step= pow(aTmax/aTmin, 1./nPoints);
+			std::vector<X> intermediateVals(nPoints + 1);
+			std::vector<X> intermediateTs(nPoints + 1);
+			fIntermediateVals=&intermediateVals;
+			fIntermediateTs=&intermediateTs;
+			X curT=aTmin;
+			for( size_t i=0 ; i<=nPoints ; ++i, curT*=step )
+				intermediateTs[i] = curT;
+
+			fFunc = &f;
+			aTotRate = 0.;
+			dense_output_dopri5_type dopri5 = make_dense_output( aAbsErr , aRelErr , dopri5_type() );
+			X dt = (aTmax - aTmin) * aRelErr;
+
+			fLastStep=0;
+			integrate_times(dopri5 , (*this) , aTotRate , intermediateTs, dt , (*this) );
+			if(aTotRate==0.){
+				//aTotRate=0.;
+				return aTmax;
+			}
+
+			X searchVal = aTotRate * aRand;
+			size_t i2= intermediateVals.size();
+			size_t i1=0;
+			for(size_t i=(i2+i1)/2; i2-i1>1; i=(i2+i1)/2)
+			{
+				if(intermediateVals[i] < searchVal)
+					i1 = i;
+				else
+					i2 = i;
+			}
+			X t1 = intermediateTs[i1];
+			X t2 = intermediateTs[i2];
+			X x1 = intermediateVals[i1];
+			X x2 = intermediateVals[i2];
+			return t1 + (t2 - t1) / (x2 - x1) * (searchVal - x1);
+		}
+
+		const FunctionX<X>* fFunc;
+		std::vector<X>* fIntermediateVals;
+		std::vector<X>* fIntermediateTs;
+
+		size_t      fLastStep;
+	};
+
+	template<typename X = double >
+	class Sampler2 : public ISampler<X> {
+		typedef runge_kutta_dopri5<X> dopri5_type;
+		typedef controlled_runge_kutta< dopri5_type > controlled_dopri5_type;
+		typedef dense_output_runge_kutta< controlled_dopri5_type > dense_output_dopri5_type;
+	public:
+		Sampler2(){
+			fIntermediateVals=0;
+			fIntermediateTs=0;
+		}
+		~Sampler2()
+		{
+			fIntermediateVals=0;
+			fIntermediateTs=0;
+		}
+		void operator()(const X &x , X &dxdt , const double t ){
+			dxdt=fFunc->f(t);
+		}
+		void operator()(const X &x , const X t ){
+			if(fMaxX>0 && x>=fMaxX){
+				X lastT = *(fIntermediateTs->end()-1);
+				X lastX = *(fIntermediateVals->end()-1);
+				if((t-lastT)/(t+lastT)<fRelErr && t-lastT<fAbsErr){
+					X result = lastT + (t-lastT)/(x-lastX)*(fMaxX-lastX);
+					throw result;
+				}
+				X totRate = x-lastX;
+				throw sample(*fFunc, lastT, t, (fMaxX-lastX)/(x-lastX), totRate, fRelErr, fAbsErr);
+			}
+			else {
+				fIntermediateVals->push_back(x);
+				fIntermediateTs->push_back(t);
+			}
+		}
+
+		/// If total rate is known it should be passed via aTotRate argument
+		/// Otherwize aTotRate should be set to 0 (it will be calculated by function)
+		X sample(const FunctionX<X>& f, X aTmin, X aTmax, X aRand,
+				 X & aTotRate, X aRelErr, X aAbsErr = 1e300){
+			fMaxX = aTotRate*aRand;
+			//fTmin = aTmin;
+			//fTmax = aTmax;
+			//fTotRate = aTotRate;
+			fRelErr = aRelErr;
+			fAbsErr = aAbsErr;
+
+			size_t nPoints = 128;//(size_t)((aTmax-aTmin)/aRelErr+0.5)/2+1;
+			//X step= (aTmax-aTmin)/nPoints;
+			std::vector<X> intermediateVals;
+			std::vector<X> intermediateTs;
+			intermediateTs.reserve(nPoints);
+			intermediateVals.reserve(nPoints);
+			fIntermediateVals=&intermediateVals;
+			fIntermediateTs=&intermediateTs;
+			X curT=aTmin;
+			fFunc = &f;
+			aTotRate = 0.;
+
+			///TODO: try different steppers
+
+			dense_output_dopri5_type dopri5 = make_dense_output( aAbsErr , aRelErr , dopri5_type() );
+			X dt = (aTmax - aTmin) * aRelErr;
+
+			try {
+				integrate_adaptive(dopri5, (*this), aTotRate, aTmin, aTmax, dt,
+								   (*this));//this will create two copies of (*this)
+				/* //this one is a bit slower than dense_output_dopri5_type
+				typedef runge_kutta_cash_karp54< X > error_stepper_type;
+				integrate_adaptive( make_controlled< error_stepper_type >( aAbsErr , aRelErr ) ,
+									(*this) , aTotRate , aTmin, aTmax, dt,
+									(*this));*/
+
+			}catch (X aValue){
+				return aValue;
+			}
+			if(aTotRate<=0){
+				return aTmax;
+			}
+
+			X searchVal = aTotRate * aRand;
+			size_t i2= intermediateVals.size();
+			size_t i1=0;
+			for(size_t i=(i2+i1)/2; i2-i1>1; i=(i2+i1)/2)
+			{
+				if(intermediateVals[i] < searchVal)
+					i1 = i;
+				else
+					i2 = i;
+			}
+			X t1 = intermediateTs[i1];
+			X t2 = intermediateTs[i2];
+			X x1 = intermediateVals[i1];
+			X x2 = intermediateVals[i2];
+			if((t2-t1)/(t2+t1)<fRelErr && t2-t1<fAbsErr){
+				return t1 + (t2-t1)/(x2-x1)*(searchVal-x1);
+			}
+			X totRate=x2-x1;
+			return sample(*fFunc, t1, t2, (searchVal-x1)/(x2-x1), totRate, fRelErr, fAbsErr);
+		}
+
+		const FunctionX<X>* fFunc;
+		std::vector<X>* fIntermediateVals;
+		std::vector<X>* fIntermediateTs;
+		X      fMaxX;
+		X      fRelErr;
+		X      fAbsErr;
+	};
+
+#endif //#ifdef USE_BOOST
+
+	template<typename X = double >
+	class NRSampler : public ISampler<X> {
+		X sample(const FunctionX<X>& f, X aTmin, X aTmax, X aRand,
+				 X & aTotRate, X aRelErr, X aAbsErr = 1e300){
+			X x;
+			MathUtils::SampleLogDistributionNR(f,aRand,x,aTotRate,aTmin,aTmax,aRelErr);
+		}
+	};
+
+	double MathUtils::SolveEquation(
+			gsl_function F,	double x_lo, double x_hi,
+			const double relError, const int max_iter,
+			const gsl_root_fsolver_type *T)
+{
+    double r = 0;
+	int status;
+	int iter = 0;
+
+	gsl_root_fsolver *solver = gsl_root_fsolver_alloc (T);
+	gsl_root_fsolver_set (solver, &F, x_lo, x_hi);
+	do
+	{
+	 	iter++;
+	    status = gsl_root_fsolver_iterate (solver);
+	    r = gsl_root_fsolver_root (solver);
+	    x_lo = gsl_root_fsolver_x_lower (solver);
+	    x_hi = gsl_root_fsolver_x_upper (solver);
+	    status = gsl_root_test_interval (x_lo, x_hi, 0, relError);
+	}
+	while (status == GSL_CONTINUE && iter < max_iter);
+	ASSERT(status == GSL_SUCCESS);
+	gsl_root_fsolver_free (solver);
+	if (status != GSL_SUCCESS)
+	{
+		if(iter >= max_iter)
+			Exception::Throw("Failed to solve equation: maximal number of iterations achieved");
+	  	else
+	  		Exception::Throw("Failed to solve equation: GSL status " + ToString(status));
+	}
+	return r;
+}
+
+double MathUtils::SolveEquation(
+		double (*aEquation) (double, void*),
+		double x_lo, double x_hi, void* aEquationPars,
+		const double relError, const int max_iter,
+		const gsl_root_fsolver_type *T)
+{
+	gsl_function F;
+	F.function = aEquation;
+	F.params = aEquationPars;
+	return SolveEquation(F,	x_lo, x_hi, relError, max_iter, T);
+}
+
+MathUtils::MathUtils():
+gslQAGintegrator(0)
+{
+
+}
+
+MathUtils::~MathUtils()
+{
+	if(gslQAGintegrator)
+		gsl_integration_workspace_free (gslQAGintegrator);
+}
+
+	template<class X> class GaussDisr : public FunctionX<X>{
+	public:
+		X mean;
+		X sigma;
+		GaussDisr(X aMean, X aSigma):mean(aMean),sigma(aSigma){}
+		virtual X f(X _x) const{
+			X diff = (_x-mean)/sigma;
+			return exp(-diff*diff*0.5)/sigma*0.398942280401433;
+		}
+	};
+
+	template<class X> void ISampler<X>::UnitTest()
+	{
+
+		X mean = 1e10;
+		X sigma = 1e9;
+		X minX=1e9;
+		X maxX=1e11;
+		X minI = 0.5*(1+gsl_sf_erf((minX-mean)/sigma/sqrt(2.0)));
+		X maxI = 0.5*(1+gsl_sf_erf((maxX-mean)/sigma/sqrt(2.0)));
+		X totI=maxI-minI;
+
+		GaussDisr<X> gd(mean,sigma);
+		X relError = 1e-6;
+		int nSteps = 10000;
+//gnuplot command: 0.5*(1+erf((x-mean)/sigma/sqrt(2.0))) w l
+
+		for(int i=1; i<nSteps; i++){
+			X rand = 1./nSteps*i;
+			X totRate = 0;
+			X curX = sample(gd, minX, maxX, rand, totRate, relError);
+			X exactFrac = (0.5*(1+gsl_sf_erf((curX-mean)/sigma/sqrt(2.0)))-minI)/totI;
+			std::cout << curX << "\t" << rand << "\t"  << exactFrac << std::endl;
+		}
+	}
+
+	template void ISampler<double>::UnitTest();
+	template void ISampler<long double>::UnitTest();
+
+	template<typename X> bool MathUtils::SampleLogscaleDistribution(const Function& aDistrib, double aRand, X& aOutputX, X& aOutputIntegral, int nStepsS, X xMin, X xMax, double aRelError)
+{
+	std::vector<X> sArray,ratesArray;
+	double distrXmin = aDistrib.Xmin();
+	if(xMin < distrXmin)
+		xMin = distrXmin;
+	double distrXmax = aDistrib.Xmax();
+	if(xMax > distrXmax)
+		xMax = distrXmax;
+	ASSERT(xMin>0. && xMin<xMax);
+	double stepS = pow(xMax/xMin,1./nStepsS);
+
+	sArray.push_back(0);
+	ratesArray.push_back(0);
+
+	X taleAccLimit;
+	RelAccuracy<X>(taleAccLimit);
+	taleAccLimit*=10;
+	int maxIntervals = (int)(0.1/aRelError + 10.5);
+	aOutputIntegral=0.;
+	double deltaLogS = log(stepS);
+	double s=xMin;
+	for(int iS=1; iS<=nStepsS; iS++)
+	{
+		double s2=s*stepS;
+		X rate = Integration_qag(aDistrib,s,s2,1e-300,aRelError,maxIntervals);
+		ASSERT_VALID_NO(rate);
+		if(rate==0. && aOutputIntegral==0.)
+		{//move Smax
+			sArray[0] = deltaLogS*iS;
+		}
+		else
+		{
+			if(rate==0 || (aOutputIntegral>0 && rate/aOutputIntegral < taleAccLimit))
+				rate = aOutputIntegral*taleAccLimit;//avoiding zero derivative (inverse function must be defined everywhere)
+			aOutputIntegral += rate;
+			sArray.push_back(deltaLogS*iS);
+			ratesArray.push_back(aOutputIntegral);
+		}
+		s=s2;
+	}
+	if(aOutputIntegral>0)
+	{//sampling s
+		ASSERT(sArray.size()>1);
+		double randRate = aRand*aOutputIntegral;
+		for(int i = ratesArray.size()-1;i>=0; i--)
+			ratesArray[i]-=randRate;
+		GSLTableFunc func(sArray, ratesArray, 0, 0, gsl_interp_cspline);
+		func.SetAutoLimits();
+		double logError = aRelError/(nStepsS*deltaLogS);
+		if(logError>aRelError)
+			logError = aRelError;
+		aOutputX = SolveEquation(func, 0, nStepsS*deltaLogS, logError);
+		aOutputX = xMin*exp(aOutputX);
+		ASSERT(aOutputX>=xMin && aOutputX<=xMax);
+		return true;
+	}
+	return false;
+}
+
+class MathUtilsODE
+{
+public:
+	MathUtilsODE(const Function& aDistrib):fDistrib(aDistrib)
+	{}
+	void operator() (const nr::Doub x, nr::VecDoub_I &y, nr::VecDoub_O &dydx) {
+		double val = fDistrib(x);
+		if(val!=0)
+			dydx[0]= val;
+		else
+			dydx[0]= 0.;
+		ASSERT(val > -1.6e308 && val < 1.6e308);
+	}
+private:
+	const Function& fDistrib;
+};
+
+IFunctionCallHandlerX<double>* MathUtils::fLogger = 0;
+
+bool MathUtils::SampleDistribution(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double xMin, double xMax, double aRelError)
+{
+	const Function* distrib = &aDistrib;
+	SafePtr<DebugFunctionX<double> > func;
+	if(fLogger)
+	{
+		func = new DebugFunctionX<double>(aDistrib, *fLogger);
+		distrib = func;
+	}
+
+	double distrXmin = distrib->Xmin();
+	if(xMin < distrXmin)
+		xMin = distrXmin;
+	double distrXmax = distrib->Xmax();
+	if(xMax > distrXmax)
+		xMax = distrXmax;
+	ASSERT(xMax>xMin);
+
+	double initialStep = aRelError*(xMax-xMin);
+
+	MathUtilsODE d(*distrib);
+
+	const nr::Doub atol=0.;//1.0e-3;
+	const nr::Doub hmin=0.0;//minimal step (can be zero)
+	nr::VecDoub ystart(1);
+	ystart[0]=0.;
+	nr::Output out(-1); //output is saved at every integration step
+	nr::Odeint<nr::StepperDopr5<MathUtilsODE> > ode(ystart,xMin,xMax,atol,aRelError,initialStep,hmin,out,d);
+	ode.integrate();
+	aOutputIntegral = ystart[0];
+	if(aOutputIntegral<=0)
+		return false;
+	aRand *= aOutputIntegral;
+	int i1=0;
+	int i2=out.count-1;
+	double* ysave = out.ysave[0];
+	for(int i=(i1+i2)/2; i2-i1>1; i=(i1+i2)/2)
+	{
+		double y = ysave[i];
+		if(y<aRand)
+			i1 = i;
+		else
+			i2 = i;
+	}
+	double y1=ysave[i1];
+	double y2=ysave[i2];
+	double x1=out.xsave[i1];
+	double x2=out.xsave[i2];
+	aRand -= y1;
+	aOutputX = x1 + (x2-x1)/(y2-y1)*aRand;//make a linear estimate of X
+	if(fabs((x2-x1)/aOutputX)<=aRelError)
+		return true;
+	ystart[0]=0.;
+	initialStep = aRelError*(aOutputX>0 ? aOutputX : (x2-x1));
+	nr::Output out2(2+(int)fabs((x2-x1)/initialStep));
+
+	nr::Odeint<nr::StepperDopr5<MathUtilsODE> > ode2(ystart,x1,x2,atol,aRelError,initialStep,hmin,out2,d);
+	ode2.integrate();
+	i1=0;
+	i2=out2.count-1;
+	ysave = out2.ysave[0];
+	for(int i=(i1+i2)/2; i2-i1>1; i=(i1+i2)/2)
+	{
+		double y = ysave[i];
+		if(y<aRand)
+			i1 = i;
+		else
+			i2 = i;
+	}
+	y1=ysave[i1];
+	y2=ysave[i2];
+	x1=out2.xsave[i1];
+	x2=out2.xsave[i2];
+	aOutputX = x1 + (x2-x1)/(y2-y1)*(aRand-y1);//make a linear estimate of X
+	return true;
+}
+
+class MathUtilsLogODE
+{
+public:
+	MathUtilsLogODE(const Function& aDistrib):fDistrib(aDistrib)
+	{}
+	void operator() (const nr::Doub x, nr::VecDoub_I &y, nr::VecDoub_O &dydx) {
+		double xx = exp(x);
+		double val = xx*fDistrib(xx);
+		if(val!=0)
+			dydx[0]= val;
+		else
+			dydx[0]= 0.;
+		ASSERT(val > -1.6e308 && val < 1.6e308);
+	}
+private:
+	const Function& fDistrib;
+};
+
+bool MathUtils::SampleLogDistribution(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double xMin, double xMax, double aRelError)
+{
+#ifdef USE_BOOST
+    return SampleLogDistributionBoost(aDistrib, aRand, aOutputX, aOutputIntegral, xMin, xMax, aRelError);
+#else
+	return SampleLogDistributionNR(aDistrib, aRand, aOutputX, aOutputIntegral, xMin, xMax, aRelError);
+#endif
+}
+
+bool MathUtils::SampleLogDistributionNR(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double xMin, double xMax, double aRelError)
+{
+	ASSERT(aRelError>0 && aRelError<=0.1);
+
+	const Function* distrib = &aDistrib;
+	SafePtr<DebugFunctionX<double> > func;
+	if(fLogger)
+	{
+		func = new DebugFunctionX<double>(aDistrib, *fLogger);
+		distrib = func;
+	}
+
+	double distrXmin = distrib->Xmin();
+	if(xMin < distrXmin)
+		xMin = distrXmin;
+	double distrXmax = distrib->Xmax();
+	if(xMax > distrXmax)
+		xMax = distrXmax;
+	ASSERT(xMax>xMin && xMin>0);
+	xMin=log(xMin);
+	xMax=log(xMax);
+
+	double initialStep = 0.5*(xMax-xMin);
+//	if(aRelError<initialStep)
+//		initialStep=aRelError;
+
+	MathUtilsLogODE d(*distrib);
+
+	const nr::Doub atol=0;
+	const nr::Doub hmin=0.0;//minimal step (can be zero)
+	nr::VecDoub ystart(1);
+	ystart[0]=0.;
+	nr::Output out(-1); //output is saved at every integration step
+	nr::Odeint<nr::StepperDopr5<MathUtilsLogODE> > ode(ystart,xMin,xMax,atol,aRelError,initialStep,hmin,out,d);
+	ode.integrate();
+	aOutputIntegral = ystart[0];
+	if(aOutputIntegral<=0)
+		return false;
+	double yRand = aRand*aOutputIntegral;
+	int i1=0;
+	int i2=out.count-1;
+	double* ysave = out.ysave[0];
+	for(int i=(i1+i2)/2; i2-i1>1; i=(i1+i2)/2)
+	{
+		double y = ysave[i];
+		if(y<yRand)
+			i1 = i;
+		else
+			i2 = i;
+	}
+	double y1=ysave[i1];
+	double y2=ysave[i2];
+	double x1=out.xsave[i1];
+	double x2=out.xsave[i2];
+	double yFrac = (yRand-y1)/(y2-y1);
+	if((x2-x1)<aRelError)
+	{
+		aOutputX = x1 + (x2-x1)*yFrac;//make a linear estimate of X in log scale
+		ASSERT(aOutputX>=xMin && aOutputX<=xMax);
+	}
+	else
+	{
+		ystart[0]=0.;
+		initialStep = 0.5*(x2-x1);//aRelError;
+		nr::Output out2(2+(int)fabs((x2-x1)/initialStep));
+		nr::Odeint<nr::StepperDopr5<MathUtilsLogODE> > ode2(ystart,x1,x2,atol,aRelError,initialStep,hmin,out2,d);
+		ode2.integrate();
+		yRand = ystart[0]*yFrac;
+		i1=0;
+		i2=out2.count-1;
+		ysave = out2.ysave[0];
+		for(int i=(i1+i2)/2; i2-i1>1; i=(i1+i2)/2)
+		{
+			double y = ysave[i];
+			if(y<yRand)
+				i1 = i;
+			else
+				i2 = i;
+		}
+		y1=ysave[i1];
+		y2=ysave[i2];
+		x1=out2.xsave[i1];
+		x2=out2.xsave[i2];
+		aOutputX = x1 + (x2-x1)/(y2-y1)*(yRand-y1);//make a linear estimate of X in log scale
+		ASSERT(aOutputX>=xMin && aOutputX<=xMax);
+	}
+	aOutputX = exp(aOutputX);
+	ASSERT_VALID_NO(aOutputX);
+	return true;
+}
+
+
+bool MathUtils::SampleLogDistributionBoost(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double xMin, double xMax, double aRelError)
+	{
+#ifdef USE_BOOST
+		ASSERT(aRelError>0 && aRelError<=0.1);
+		ASSERT(xMin>0 && xMax>xMin);
+		LogSampler<double> dil;//slower 25 sec
+		//Sampler2<double> dil;// 20 sec (todo: fix memory leaks)
+		//Sampler3<double> dil;// 20 sec (todo: fix memory leaks)
+		aOutputIntegral=0.;
+
+		aOutputX=dil.sample(aDistrib, xMin, xMax, aRand, aOutputIntegral, aRelError);
+
+		ASSERT_VALID_NO(aOutputX);
+		return true;
+#else
+	Exception::Throw("MathUtils::SampleLogDistributionBoost boostlib support is disabled");
+	return false;//avoid compiler warning
+#endif
+	}
+
+template<typename X> void MathUtils::RelAccuracy(X& aOutput)
+{
+	NOT_IMPLEMENTED
+}
+
+template<> void MathUtils::RelAccuracy<double>(double& aOutput)
+{
+	aOutput = 1e-15;
+}
+
+template<> void MathUtils::RelAccuracy<long double>(long double& aOutput)
+{
+	aOutput = 1e-18L;
+}
+
+template bool MathUtils::SampleLogscaleDistribution<double>(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, int nStepsS, double xMin, double xMax, double aRelError);
+//template bool MathUtils::SampleLogscaleDistribution<long double>(const Function& aDistrib, double aRand, long double& aOutput, int nStepsS, long double xMin, long double xMax, double aRelError);
+
+	int MathUtils::UnitTest(){
+		//LogSampler<double> dil;//slower 25 sec
+		//Sampler2<double> dil;// 20 sec (todo: fix memory leaks)
+		//Sampler3<double> dil;// 20 sec (todo: fix memory leaks)
+		//Sampler4<double> dil;
+		NRSampler<double> dil;
+		dil.UnitTest();
+	}
+
+double MathUtils::Integration_qag (
+		gsl_function aFunction,
+		double aXmin,
+		double aXmax,
+		double epsabs,
+		double epsrel,
+		size_t limit,
+		int key)
+{
+	if(gslQAGintegrator==0)
+		gslQAGintegrator = gsl_integration_workspace_alloc (limit);
+	else if(gslQAGintegrator->limit < limit)
+	{
+		gsl_integration_workspace_free (gslQAGintegrator);
+		gslQAGintegrator = gsl_integration_workspace_alloc (limit);
+	}
+	double result, abserr;
+	try{
+		if(epsabs==0)
+			epsabs = std::numeric_limits<double>::min();
+		int failed = gsl_integration_qag (&aFunction, aXmin, aXmax, epsabs, epsrel, limit, key, gslQAGintegrator, &result, &abserr);
+		if(failed)
+		{
+			ASSERT(0);
+			Exception::Throw("Integration failed with code " + ToString(failed));
+		}
+	}catch(Exception* ex)
+	{
+#ifdef _DEBUG
+		GslProxyFunction f(aFunction,aXmin,aXmax);
+		std::cerr << "\n\n#Integration_qag debug output:" << std::endl;
+		bool logscale = aXmin>0 && aXmax/aXmin > 100;
+		f.Print(std::cerr, 100, logscale, aXmin, aXmax);
+		std::cerr << "\n\n#end of Integration_qag debug output" << std::endl;
+#endif
+		throw ex;
+	}
+	return result;
+}
+
+} /* namespace Utils */
diff --git a/src/lib/MathUtils.h b/src/lib/MathUtils.h
new file mode 100644
index 0000000..f13823e
--- /dev/null
+++ b/src/lib/MathUtils.h
@@ -0,0 +1,263 @@
+/*
+ * MathUtils.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef MATHUTILS_H_
+#define MATHUTILS_H_
+
+#include <gsl/gsl_roots.h>
+#include <gsl/gsl_integration.h>
+#include <iostream>
+#include "Utils.h"
+#include <limits>
+
+namespace Utils {
+
+
+template<typename X = double >
+class FunctionX
+{
+public:
+	virtual X f(X _x) const = 0;
+	virtual X Xmin() const {return -std::numeric_limits<X>::max();}
+	virtual X Xmax() const {return std::numeric_limits<X>::max();}
+	inline X operator()(X _x) const {return f(_x);};
+	virtual ~FunctionX(){};
+
+	virtual FunctionX<X>* Clone() const { NOT_IMPLEMENTED; return 0; }
+
+	inline operator gsl_function() const
+	{
+		gsl_function result = {GslProxyFunc, (void*)this};
+		return result;
+	}
+	void Print(std::ostream& aOut, int nIntervals, bool aLogScale, double aXmin=-DBL_MAX, double aXmax=DBL_MAX) const
+	{
+		if(aXmin<Xmin())
+			aXmin = Xmin();
+		if(aXmax>Xmax())
+			aXmax = Xmax();
+		ASSERT(aXmin<=aXmax);
+		ASSERT(aXmax<DBL_MAX);
+		ASSERT(aXmin>-DBL_MAX);
+		ASSERT(nIntervals>=1);
+		ASSERT((!aLogScale) || aXmin>0);
+		double step = aLogScale?pow(aXmax/aXmin,1./nIntervals) : (aXmax-aXmin)/nIntervals;
+		double x = aXmin;
+		for(int i=0; i<=nIntervals; i++, (x = aLogScale ? x*step : x+step) )
+			aOut << x << "\t" << f(x) << "\n";
+	}
+private:
+	static double GslProxyFunc (double x, void * params)
+	{
+		const FunctionX<X>* f = (const FunctionX<X>*)params;
+		return f->f(x);
+	}
+};
+
+template<typename X = double >
+class Function2X
+{
+public:
+	virtual X f(X x, X y) const = 0;
+	virtual X MinArg(int aArgNo) const {return std::numeric_limits<X>::min();}
+	virtual X MaxArg(int aArgNo) const  {return std::numeric_limits<X>::max();}
+
+	virtual inline X operator()(X _x, X _y) const {return f(_x,_y);};
+	virtual ~Function2X(){};
+};
+
+template<typename X = double >
+class IFunctionCallHandlerX
+{
+public:
+	virtual void OnCall(const FunctionX<X>& aFunc, X aX, X aY) const = 0;
+};
+
+template<typename X = double >
+class DebugFunctionX : public FunctionX<X>
+{
+public:
+	DebugFunctionX(const FunctionX<X>& aOrigFunc, const IFunctionCallHandlerX<X>& aDebugger):fOrigFunc(aOrigFunc),fDebugger(aDebugger){};
+	virtual X f(X _x) const
+	{
+		X y = fOrigFunc.f(_x);
+		fDebugger.OnCall(fOrigFunc, _x, y);
+		return y;
+	}
+	virtual ~DebugFunctionX(){};
+	virtual FunctionX<X>* Clone() const { return new DebugFunctionX<X>(fOrigFunc,fDebugger); }
+private:
+	const FunctionX<X>&				fOrigFunc;
+	const IFunctionCallHandlerX<X>&	fDebugger;
+};
+
+template<typename X = double >
+class FunctionCallLoggerX : public IFunctionCallHandlerX<X>
+{
+public:
+	FunctionCallLoggerX(std::ostream& aOutput) : fOutput(aOutput){}
+	virtual void OnCall(const FunctionX<X>& aFunc, X aX, X aY) const
+	{
+		((std::ostream&)fOutput) << aX << "\t" << aY << "\n";
+	}
+	virtual ~FunctionCallLoggerX()
+	{
+		fOutput << std::endl;
+	}
+private:
+	std::ostream& fOutput;
+};
+
+typedef FunctionX<double> Function;
+typedef Function2X<double> Function2;
+
+template<typename X = double >
+class ParamlessFunctionX : public FunctionX<X>
+{
+public:
+	ParamlessFunctionX(X (*aFunction)(X)):fFunction(aFunction){}
+	X f(X _x) const { return fFunction(_x); }
+private:
+	X (*fFunction)(X);
+};
+
+typedef ParamlessFunctionX<double> ParamlessFunction;
+
+	template<typename X = double >
+	class ISampler {
+	public:
+		virtual X sample(const FunctionX<X> &f, X aTmin, X aTmax, X aRand,
+				 X &aTotRate, X aRelErr, X aAbsErr = 1e300) = 0;
+		void UnitTest();
+	};
+
+/// Mathematical functions
+/// Static methods are thread-safe
+/// Non-static methods are not guaranteed to be thread-safe
+	///TODO: make implementation using boost odeint and dense output and get rid of nr library
+class MathUtils {
+public:
+	MathUtils();
+	~MathUtils();
+    static inline double RelDifference(double aVal1, double aVal2){
+        double meanNorm = 0.5*fabs(aVal1)+fabs(aVal2);
+        return meanNorm==0. ? 0.:fabs(aVal1-aVal2)/meanNorm;
+    }
+
+	static double SolveEquation(
+			double (*aEquation) (double, void*),
+			double x_lo, double x_hi, void* aEquationPars = 0,
+			const double relError=1e-3, const int max_iter=100,
+			const gsl_root_fsolver_type *T = gsl_root_fsolver_bisection);
+
+	static double SolveEquation(
+			gsl_function f,	double x_lo, double x_hi,
+			const double relError=1e-3, const int max_iter=100,
+			const gsl_root_fsolver_type *T = gsl_root_fsolver_bisection);
+
+	double Integration_qag (
+			gsl_function aFunction,
+			double aXmin,
+			double aXmax,
+			double epsabs,
+			double epsrel,
+			size_t limit,
+			int key=GSL_INTEG_GAUSS15);
+	template<typename X> bool SampleLogscaleDistribution(const Function& aDistrib, double aRand, X& aOutputX, X& aOutputIntegral, int nStepsS, X xMin, X xMax, double aRelError);
+	static bool SampleDistribution(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double xMin, double xMax, double aRelError);
+	static bool SampleLogDistribution(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double xMin, double xMax, double aRelError);
+
+	static bool SampleLogDistributionBoost(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double xMin, double xMax, double aRelError);
+	static bool SampleLogDistributionNR(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double xMin, double xMax, double aRelError);
+
+	template<typename X> static void RelAccuracy(X& aOutput);
+	static void SetLogger(IFunctionCallHandlerX<double>* aLogger) { fLogger = aLogger; }
+	static int UnitTest();
+private:
+	static double GslProxySampleLogscaleDistributionFunc (double x, void * params)
+	{
+		x=exp(x);
+		const Function* f = (const Function*)params;
+		return x*f->f(x);
+	}
+	gsl_integration_workspace* gslQAGintegrator;
+	static IFunctionCallHandlerX<double>* fLogger;
+};
+
+class GslProxyFunction : public Function
+{
+public:
+	GslProxyFunction(gsl_function aFunction, double aXmin=-DBL_MAX, double aXmax=DBL_MAX) :
+		fFunction(aFunction),
+		fXmin(aXmin),
+		fXmax(aXmax){};
+	virtual double f(double _x) const
+	{
+		return fFunction.function(_x,fFunction.params);
+	}
+	virtual double Xmin() const {return fXmin;}
+	virtual double Xmax() const {return fXmax;}
+	Function* Clone() const
+	{//There is no way to clone fFunction.params
+		ASSERT(0);
+		Exception::Throw("GslProxyFunction doesn't support Clone");
+		return 0;
+	}
+private:
+	gsl_function fFunction;
+	double fXmin;
+	double fXmax;
+};
+
+template<typename X = double >
+class ConstFunctionX : public FunctionX<X>
+{
+public:
+	ConstFunctionX(X aValue, X aXmin=-std::numeric_limits<X>::max(), X aXmax=std::numeric_limits<X>::max()) :
+		fValue(aValue),
+		fXmin(aXmin),
+		fXmax(aXmax){};
+	virtual X f(X _x) const
+	{
+		return fValue;
+	}
+	virtual X Xmin() const {return fXmin;}
+	virtual X Xmax() const {return fXmax;}
+	FunctionX<X>* Clone() const
+	{
+		return new ConstFunctionX<X>(fValue, fXmin, fXmax);
+	}
+private:
+	X fValue;
+	X fXmin;
+	X fXmax;
+};
+
+typedef ConstFunctionX<double> ConstFunction;
+
+} /* namespace Utils */
+#endif /* MATHUTILS_H_ */
diff --git a/src/lib/NeutronDecay.cpp b/src/lib/NeutronDecay.cpp
new file mode 100644
index 0000000..a84933c
--- /dev/null
+++ b/src/lib/NeutronDecay.cpp
@@ -0,0 +1,101 @@
+/*
+ * NeutronDecay.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "NeutronDecay.h"
+#include "Test.h"
+
+namespace Interactions {
+    using namespace mcray;
+
+    const double NeutronDecay::Neutron_lifetime=881.5; // (sec)
+    NeutronDecay::NeutronDecay():
+            n_decayM(Particle::Mass(Neutron)/Neutron_lifetime/units.second)
+    {
+        double Me = Particle::Mass(Electron);
+        double delta_m = Particle::Mass(Neutron)-Particle::Mass(Proton);
+        n_decay_k=0.5*(delta_m-Me*Me/delta_m);
+        n_decay_E=sqrt(n_decay_k*n_decay_k+Me*Me);
+    }
+
+    double NeutronDecay::Rate(const Particle &aParticle) const {
+        if(aParticle.Type!=Neutron)
+            return 0;
+        return n_decayM/aParticle.Energy;
+    }
+
+    bool NeutronDecay::GetSecondaries(Particle &aParticle, std::vector<Particle> &aSecondaries,
+                                      Randomizer &aRandomizer) const {
+        if(aParticle.Type!=Neutron)
+            return false;
+
+        //proton
+        Particle proton = aParticle;
+        proton.Type = Proton;
+        proton.Energy *= Particle::Mass(Proton)/Particle::Mass(Neutron);
+        double gamma = aParticle.Energy/aParticle.Mass();
+        double beta = aParticle.beta();
+
+        //electron
+        //sample electron momentum direction in CM frame
+        double angle_e = aRandomizer.Rand()*M_PI;
+        double cos_e = cos(angle_e);
+        Particle electron = aParticle;
+        electron.Type = Electron;
+        electron.Energy = gamma*(n_decay_E-beta*cos_e*n_decay_k);
+
+        //neutrino
+        Particle nu = aParticle;
+        nu.Type = NeutrinoAE;
+        nu.Energy = gamma*n_decay_k*(1.0+beta*cos_e);//cos_nu=-cos_e
+
+        aSecondaries.push_back(proton);
+        aSecondaries.push_back(electron);
+        aSecondaries.push_back(nu);
+
+        return true;
+    }
+
+    int NeutronDecay::UnitTestEconserv(){
+        double aZ=0.1;
+        double aE=1e19;
+        int nParticles = 100;
+        Test::EConservationTest test(aZ,1e6,2015);
+        test.alphaThinning = 0.0;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+        //test.Engine().AddInteraction(new GZK(test.Backgr(),(int)(test.GetRandomizer().CreateIndependent())));
+        test.Engine().AddInteraction(new NeutronDecay());
+        Particle p(Neutron, aZ);
+        p.Energy = aE*units.eV;
+        test.SetPrimary(p, nParticles);
+        test.Run();
+        return 0;
+    }
+
+    RandomInteraction *NeutronDecay::Clone() const {
+        return new NeutronDecay();
+    }
+}
diff --git a/src/lib/NeutronDecay.h b/src/lib/NeutronDecay.h
new file mode 100644
index 0000000..93e9a99
--- /dev/null
+++ b/src/lib/NeutronDecay.h
@@ -0,0 +1,56 @@
+/*
+ * NeutronDecay.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef MCRAY_NEUTRONDECAY_H
+#define MCRAY_NEUTRONDECAY_H
+
+#include "Interaction.h"
+
+namespace Interactions {
+    using namespace mcray;
+
+    class NeutronDecay : public RandomInteraction {
+
+    public:
+        NeutronDecay();
+
+        virtual double Rate(const Particle &aParticle) const;
+
+        virtual bool GetSecondaries(Particle &aParticle, std::vector<Particle> &aSecondaries,
+                                    Randomizer &aRandomizer) const;
+
+        virtual RandomInteraction* Clone() const;
+        static int UnitTestEconserv();
+    private:
+        static const double Neutron_lifetime; // neutron lifetime in it's rest frame (sec)
+        const double n_decayM; // neutron decay probability it's rest frame times neutron mass
+        double n_decay_k; // secondary leptons momenta in CM frame (assuming zero nu mass)
+        double n_decay_E; // electron energy in CM frame
+    };
+}
+#endif //MCRAY_NEUTRONDECAY_H
diff --git a/src/lib/Nucleus.cpp b/src/lib/Nucleus.cpp
new file mode 100644
index 0000000..6967ce4
--- /dev/null
+++ b/src/lib/Nucleus.cpp
@@ -0,0 +1,420 @@
+/*
+ * Nucleus.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+//
+// Photodisintegration cross sections as defined in F.W. Stecker, M.H. Salamon, Astrophys.J. 512 (1999) 521-526 (astro-ph/9808110v3)
+//
+#include "Nucleus.h"
+#include "gsl/gsl_sf_erf.h"
+#include "GammaPP.h"
+
+#define AUTO -1
+
+namespace Interactions {
+	using namespace Utils;
+	using namespace mcray;
+
+	const double portionsHe4[] = {0.8, 0.2};
+	static const TBranching branchingHe4 = {2, portionsHe4, AUTO, AUTO};
+
+	const double portionsBe8[] = {0, 0, 0, 2.};
+	// a trick to make Be9 decay to 2He4 + n only
+	static const TBranching branchingBe8 = {4, portionsBe8, 0., 0.};
+
+	const double portionsBe9[] = {0, 0, 0, 0, 2.};
+	static const TBranching branchingBe9 = {5, portionsBe9, 0., 1.};
+
+	const double portionsB10[] = {0.1, 0.3, 0.1, 0.1, 0.2, 0.2};
+	static const TBranching branchingB10 = {6, portionsB10, AUTO, AUTO};//3.6
+
+	const double portionsNa23[] = {0.10, 0.35, 0.10, 0.05, 0.15, 0.045, 0.04, 0.035, 0.03, 0.025, 0.02, 0.018, 0.015,
+								   0.012, 0.01};
+	static const TBranching branchingNa23 = {15, portionsNa23, AUTO, AUTO};//4.349
+
+	static const int EUnstable = -1;
+
+#define NON 0
+#define INF 1000
+#define UNKNOWN_K 5
+#define UNKNOWN_DELTA 5,25
+#define FAST_DECAY {1, UNKNOWN_K, INF , 5,25}
+#define NO_REACTION {2,15,0.,25,25}
+
+// Photodisintegration cross sections as defined in Puget, J.L., F .W. Stecker & J.H. Bredekamp 1976, ApJ 205, 638
+// with threshold energies from F.W. Stecker, M.H. Salamon, Astrophys.J. 512 (1999) 521-526 (astro-ph/9808110v3)
+	static const TIsotope stableIsotopes[] = {
+			{1,  0,  {NO_REACTION,                          NO_REACTION},    NON, NULL}, //  neutron
+			{1,  1,  {NO_REACTION,                          NO_REACTION},    NON, NULL}, //  proton
+			{2,  1,  {{2.2,  5,  0.97, 3,  15},             NO_REACTION},    NON, NULL}, //  H2
+			{3,  2,  {{5.5,  13, 0.33, 18, 18}, /* NO_REACTION */{2,    15,   0.33, 13, 13}}, NON, NULL},//No threshold data for double nucleon decay of A03 in astro-ph/9808110v3
+			// -> one might either use 2 MeV threshold as in Puget, J.L., F .W. Stecker & J.H. Bredekamp 1976, ApJ 205, 638
+			// or just suppress the chanel. We choose the former option
+			{4,  2,  {{19.8, 27, 0.47, 12, 12}, {26.1, 45,   0.11, 40, 40}}, 1.11, &branchingHe4},
+			{5,  3,  {FAST_DECAY,                           NO_REACTION},    NON, NULL},
+			{6,  3,  {{4.6, UNKNOWN_K, INF, UNKNOWN_DELTA}, FAST_DECAY},     NON, NULL},
+			{7,  4,  {{7.3, UNKNOWN_K, INF, UNKNOWN_DELTA}, FAST_DECAY},     NON, NULL},
+			{8,  4,  {NO_REACTION,                          NO_REACTION},    INF,  &branchingBe8},// a trick to make Be9 decay to 2He4 + n only
+			{9,  4,  {{1.7,  26, 0.67, 20, 20}, {18.9, NON, NON, NON, NON}}, 1.00, &branchingBe9},
+			{10, 5,  {{6.6,  25, 0.54, 11, 11}, {8.3,  25,   0.15, 11, 11}}, 1.03, &branchingB10},
+			{11, 5,  {{11.2, 26, 0.85, 11, 11}, {18,   26,   0.15, 11, 11}}, 1.03, &branchingB10},
+			{12, 6,  {{16,   23, 0.76, 6,  6},  {27.2, NON, NON, NON, NON}}, 1.06, &branchingB10},
+			{13, 6,  {{4.9,  23, 0.71, 8,  8},  {20.9, 27,   0.05, 8,  8}},  1.06, &branchingB10},
+			{14, 7,  {{7.6,  23, 0.46, 10, 10}, {12.5, 23,   0.37, 10, 10}}, 1.07, &branchingB10},
+			{15, 7,  {{10.2, 23, 0.73, 10, 10}, {18.4, 23,   0.10, 10, 10}}, 1.07, &branchingB10},
+			{16, 8,  {{12.1, 24, 0.83, 9,  9},  {22.3, 30,   0.04, 10, 10}}, 1.10, &branchingB10},
+			{17, 8,  {{4.1,  24, 0.77, 9,  9},  {16.3, 29,   0.20, 10, 10}}, 1.10, &branchingB10},
+			{18, 8,  {{8,    24, 0.67, 9,  9},  {21.8, 29,   0.20, 10, 10}}, 1.10, &branchingB10},
+			{19, 9,  {{8,    23, 0.76, 14, 14}, {16,   29,   0.14, 14, 14}}, 1.10, &branchingB10},
+			{20, 10, {{12.8, 22, 0.87, 12, 12}, {20.8, 26,   0.05, 8,  8}},  1.09, &branchingB10},
+			{21, 10, {{6.8,  22, 0.84, 12, 12}, {19.6, 25,   0.08, 6,  6}},  1.09, &branchingB10},
+			{22, 10, {{10.4, 22, 0.81, 12, 12}, {23.4, 21,   0.11, 4,  4}},  1.09, &branchingB10},
+			{23, 11, {{8.8,  22, 0.83, 12, 12}, {19.2, 25,   0.12, 10, 10}}, 1.09, &branchingNa23},
+			{24, 12, {{11.7, 19, 0.94, 11, 11}, {20.5, 29,   0.03, 6,  6}},  1.08, &branchingNa23},
+			{25, 12, {{7.3,  23, 0.77, 9,  9},  {19,   28,   0.20, 7,  7}},  1.08, &branchingNa23},
+			{26, 12, {{11.1, 18, 0.77, 8,  8},  {23.2, 26,   0.20, 8,  8}},  1.08, &branchingNa23},
+			{27, 13, {{8.3,  21, 0.80, 8,  8},  {19.4, 29,   0.20, 12, 12}}, 1.05, &branchingNa23},
+			{28, 14, {{11.6, 21, 1.01, 8,  8},  {19.9, 30,   0.02, 8,  8}},  1.04, &branchingNa23},
+			{29, 14, {{8.5,  20, 0.83, 7,  7},  {20.1, 26,   0.20, 8,  8}},  1.04, &branchingNa23},
+			{30, 14, {{10.6, 20, 0.83, 7,  7},  {22.9, 26,   0.20, 8,  8}},  1.04, &branchingNa23},
+			{31, 15, {{7.3,  21, 0.85, 8,  8},  {17.9, 29,   0.20, 12, 12}}, 1.02, &branchingNa23},
+			{32, 16, {{8.9,  22, 0.97, 12, 12}, {16.2, 30,   0.10, 12, 12}}, 1.00, &branchingNa23},
+			{33, 16, {{8.6,  22, 0.82, 12, 12}, {17.5, 22,   0.25, 12, 12}}, 1.00, &branchingNa23},
+			{34, 16, {{10.9, 22, 0.87, 12, 12}, {20.4, 22,   0.20, 12, 12}}, 1.00, &branchingNa23},
+			{35, 17, {{6.4,  20, 0.87, 7,  7},  {17.3, 26,   0.22, 10, 10}}, 1.00, &branchingNa23},
+			{36, 16, {{9.9,  22, 0.82, 12, 12}, {21.5, 22,   0.25, 12, 12}}, 1.00, &branchingNa23},
+			{37, 17, {{8.4,  20, 0.81, 7,  7},  {18.3, 24,   0.28, 7,  7}},  1.00, &branchingNa23},
+			{38, 18, {{10.2, 18, 0.86, 8,  8},  {18.6, 22,   0.24, 8,  8}},  0.98, &branchingNa23},
+			{39, 19, {{6.4,  20, 0.73, 7,  7},  {16.6, 25,   0.38, 12, 12}}, 0.98, &branchingNa23},
+			{40, 20, {{8.3,  20, 0.84, 6,  6},  {14.7, 26,   0.28, 10, 10}}, 0.96, &branchingNa23},
+			{41, 20, {{7.8,  20, 0.92, 6,  6},  {17.7, 26,   0.20, 8,  8}},  0.96, &branchingNa23},
+			{42, 20, {{10.3, 20, 1.02, 7,  7},  {18.1, 26,   0.10, 8,  8}},  0.96, &branchingNa23},
+			{43, 20, {{7.9,  20, 0.97, 8,  8},  {18.2, 26,   0.15, 8,  8}},  0.96, &branchingNa23},
+			{44, 20, {{11.1, 20, 0.92, 9,  9},  {21.6, 26,   0.20, 8,  8}},  0.96, &branchingNa23},
+			{45, 21, {{6.9,  19, 0.97, 9,  9},  {18,   26,   0.15, 8,  8}},  0.95, &branchingNa23},
+			{46, 22, {{10.3, 19, 1.03, 8,  8},  {17.2, 25,   0.10, 6,  6}},  0.95, &branchingNa23},
+			{47, 22, {{8.9,  19, 1.03, 8,  8},  {18.7, 25,   0.10, 6,  6}},  0.95, &branchingNa23},
+			{48, 22, {{9.9,  19, 1.03, 8,  8},  {24.2, 25,   0.10, 6,  6}},  0.95, &branchingNa23},
+			{49, 22, {{8.1,  19, 1.03, 8,  8},  {19.6, 25,   0.10, 6,  6}},  0.95, &branchingNa23},
+			{50, 22, {{10.9, 19, 1.03, 8,  8},  {21.8, 25,   0.10, 6,  6}},  0.95, &branchingNa23},
+			{51, 23, {{8.1,  19, 1.02, 7,  7},  {19,   25,   0.11, 6,  6}},  0.95, &branchingNa23},
+			{52, 24, {{10.5, 18, 1.08, 7,  7},  {18.6, 24,   0.05, 8,  8}},  0.95, &branchingNa23},
+			{53, 24, {{7.9,  18, 1.03, 7,  7},  {18.4, 24,   0.10, 8,  8}},  0.95, &branchingNa23},
+			{54, 24, {{9.7,  18, 0.93, 7,  7},  {20.9, 24,   0.20, 8,  8}},  0.95, &branchingNa23},
+			{55, 25, {{8.1,  18, 0.93, 7,  7},  {17.8, 23.5, 0.20, 8,  8}},  0.95, &branchingNa23},
+			{56, 26, {{10.2, 18, 0.98, 8,  8},  {18.3, 22,   0.15, 7,  7}},  0.95, &branchingNa23},
+	};
+
+	void CNucleus::getPhotopionMultipliers(int aA, double &aRn, double &aRp, double &aRN) {
+		ASSERT(aA >= 0 && aA <= 56);
+		const TIsotope &theIsotope = stableIsotopes[aA];
+		aRN = pow((double) aA, -1. / 3.);//	= A^{2/3}(cross section)  / A(energy loss suppression)
+		aRn = ((double) (aA - theIsotope.Z)) * aRN / ((double) aA);//  * N/A (fraction of neutrons)
+		aRp = ((double) theIsotope.Z) * aRN / ((double) aA);//  * Z/A (fraction of protons)
+	}
+
+	double CNucleus::PPPmultiplierR(int aA) {
+		const TIsotope &theIsotope = stableIsotopes[aA];
+		double result = theIsotope.Z * theIsotope.Z;
+		result /= (double) aA;
+		return result;
+	}
+
+	int CNucleus::getZ(ParticleType aNucleus) {
+		ASSERT(aNucleus >= StartNuclei - 2 && aNucleus < EndNuclei);
+		int no = 2 + (int) aNucleus - StartNuclei;
+		return stableIsotopes[no].Z;
+	}
+
+	int CNucleus::getA(ParticleType aNucleus) {
+		ASSERT(aNucleus >= StartNuclei - 2 && aNucleus < EndNuclei);
+		int no = 2 + (int) aNucleus - StartNuclei;
+		return stableIsotopes[no].A;
+	}
+
+	CPhotoDisintegrationMap::CPhotoDisintegrationMap() {
+		int i;
+		fCanals.setIndexShift(-2);//
+		const int NucleiCount = EndNuclei - StartNuclei;
+
+		for (i = 0; i < NucleiCount + 2; i++) {
+			fSecondaryCanals.add(new CPDMSecLine());
+		}
+		int curParticle = StartNuclei;
+		for (i = 2; i < NucleiCount + 2; i++, curParticle++) {
+
+			const TIsotope &isotope = stableIsotopes[i];
+			CPDMLine *line = new CPDMLine;
+			//if (IsEnabled(curParticle)) {
+				if (isotope.pd[ESingle].sigma > 0.) {
+					CPhotoDisintegrationCanal *c = new CPhotoDisintegrationCanal(isotope, ESingle);
+					line->add(c);
+					initSecMap(c);
+				}
+				if (isotope.pd[EDouble].sigma > 0.) {
+					CPhotoDisintegrationCanal *c = new CPhotoDisintegrationCanal(isotope, EDouble);
+					line->add(c);
+					initSecMap(c);
+				}
+				if (isotope.pmn > 0) {
+					CPhotoDisintegrationCanal *c = new CPhotoDisintegrationCanal(isotope, EMultiple);
+					line->add(c);
+					initSecMap(c);
+				}
+			//}
+			fCanals.add(line);
+		}
+	}
+
+	void CPhotoDisintegrationMap::initSecMap(CPhotoDisintegrationCanal *aCanal) {
+		const TIsotope &isotope = aCanal->isotope();
+		int maxDeltaA = aCanal->getMaxDeltaA();
+		int A = isotope.A;
+		ASSERT(maxDeltaA < A);
+		double autoN = 0.;
+		double autoP = 0.;
+		for (int deltaA = 1; deltaA <= maxDeltaA; deltaA++) {
+			const TIsotope &aProduct = stableIsotopes[A - deltaA];//in case A=2,3 secondary canal for proton (H1) may be duplicated, but it is ok, since summary rate is correct
+			double rate = aCanal->getRate(deltaA);
+			int deltaZ = isotope.Z - aProduct.Z;
+			if (deltaZ < 0) {//processes with neutron emission followed by beta decay
+				deltaZ = 0;
+			}
+			int deltaN = deltaA - deltaZ;
+			autoN += deltaN * rate;
+			autoP += deltaZ * rate;
+			if (rate > 0.) {
+				fSecondaryCanals[A - deltaA]->add(new TPhotoDisintegrationMapEntry(aCanal, rate));
+			}
+		}
+		double rateP = aCanal->getRateP();
+		double rateN = aCanal->getRateN();
+		if (rateP == AUTO) {
+			rateP = autoP;
+		};
+		if (rateN == AUTO) {
+			rateN = autoN;
+		};
+		if (rateN > 0.) {
+			fSecondaryCanals[0]->add(new TPhotoDisintegrationMapEntry(aCanal, rateN));
+		}
+		if (rateP > 0.) {
+			fSecondaryCanals[1]->add(new TPhotoDisintegrationMapEntry(aCanal, rateP));
+		}
+	}
+/*
+//reset all entries
+//used to recalculate R when background changes
+	void CPhotoDisintegrationMap::recalculate(const CBackgroundIntegral *aBackground) {
+		const int NucleiCount = EndNuclei - StartNuclei;
+		for (int i = 2; i < NucleiCount + 2; i++) {
+			CPDMLine &line = *(fCanals[i]);
+			int jMax = line.size();
+			for (int j = 0; j < jMax; j++)
+				line[j]->recalculate(aBackground);
+		}
+		//printValidity(56);
+	}
+
+	void CPhotoDisintegrationMap::approximate(CPhotoDisintegrationMap &aMap1, CPhotoDisintegrationMap &aMap2,
+											  double aPart) {
+		const int NucleiCount = EndNuclei - StartNuclei;
+		for (int i = 2; i < NucleiCount + 2; i++) {
+			CPDMLine &line = *(fCanals[i]);
+			CPDMLine &line1 = *(aMap1.fCanals[i]);
+			CPDMLine &line2 = *(aMap2.fCanals[i]);
+			int jMax = line.size();
+			for (int j = 0; j < jMax; j++)
+				line[j]->approximate(*(line1[j]), *(line2[j]), aPart);
+		}
+	}*/
+
+
+// get list of canals contributing to nucleus A
+// A=1 proton
+// A=0 neutron
+	const CPDMSecLine *CPhotoDisintegrationMap::getIncome(int A) const {
+		return fSecondaryCanals[A];
+	}
+
+// get all suppression canals for nucleus A
+	const CPDMLine *CPhotoDisintegrationMap::getOutcome(int A) const {
+		return fCanals[A];
+	}
+
+	CPhotoDisintegrationCanal::CPhotoDisintegrationCanal(const TIsotope &aIsotope, TPhotoDisintegrationChanalType aType)
+			:
+			iIsotope(aIsotope),
+			iType(aType),
+			iMeanDeltaA(0) {
+		double meanNucleonMassMeV = 0.5*(Particle::Mass(Proton)+Particle::Mass(Neutron));
+		//const double alpha = 0.00729735307639648;//fine structure constant
+		iSumTRK = 2. * M_PI * M_PI * EMInteraction::AlphaEM / meanNucleonMassMeV * units.Eunit;
+		//double test = iSumTRK*Lunit*Lunit*Eunit*1e27;// (should be ~ 59.8 MeV * mb)
+		iSumTRK *= ((double) ((iIsotope.A - iIsotope.Z) * iIsotope.Z)) / ((double) iIsotope.A);
+		//iSumTRK *= 0.001;//test
+		ASSERT(iSumTRK > 0);
+
+		if (iType == EMultiple)
+			iSigma = new TLinearSigma(iIsotope.pmn, iSumTRK);
+		else
+			iSigma = new TGaussianSigma(iIsotope.pd[iType], iSumTRK);
+	}
+
+
+	int    CPhotoDisintegrationCanal::getMaxDeltaA() {
+		switch (iType) {
+			case ESingle:
+				return 1;
+			case EDouble:
+				return 2;
+			case EMultiple:
+				return iIsotope.branching->noOfModes;
+		}
+		return 0;//never goes here
+	}
+
+	double    CPhotoDisintegrationCanal::getMeanDeltaA() {
+		switch (iType) {
+			case ESingle:
+				return 1;
+			case EDouble:
+				return 2;
+			case EMultiple: {
+				if (iMeanDeltaA) return iMeanDeltaA;
+				int maxDeltaA = iIsotope.branching->noOfModes;
+				for (int i = 1; i <= maxDeltaA; i++)
+					iMeanDeltaA += iIsotope.branching->nucleiRates[i - 1] * ((double) i);
+				return iMeanDeltaA;
+			}
+		}
+		return 0;//never goes here
+	}
+
+	const double CPhotoDisintegrationCanal::AutoRate = AUTO;
+
+	void CPhotoDisintegrationCanal::getEnergyLoss(std::vector<double> &aOutput) {
+		/* //TODO implement if needed
+		aOutput.copy(iR.ptr());
+		aOutput *= (getMeanDeltaA() / ((double) iIsotope.A));
+		 */
+	}
+
+	double    CPhotoDisintegrationCanal::getRate(int aDeltaA) {
+		switch (iType) {
+			case ESingle:
+				return (aDeltaA == 1) ? 1. : 0.;
+			case EDouble:
+				return (aDeltaA == 2) ? 1. : 0.;
+			case EMultiple:
+				return (aDeltaA > 0 || aDeltaA <= iIsotope.branching->noOfModes) ? iIsotope.branching->nucleiRates[
+						aDeltaA - 1] : 0.;
+		}
+		return 0.;//never goes here
+	}
+
+	double    CPhotoDisintegrationCanal::getRateP() {
+		return (iType != EMultiple) ? AUTO : iIsotope.branching->pRate;
+	}
+
+	double    CPhotoDisintegrationCanal::getRateN() {
+		return (iType != EMultiple) ? AUTO : iIsotope.branching->nRate;
+	}
+
+	/*
+	void CPhotoDisintegrationCanal::recalculate(const CBackgroundIntegral *aBackground) {
+		const int nn = iR.size();//Ranges().nE();
+		Function *sigma = NULL;
+		if (iType == EMultiple)
+			sigma = new TLinearSigma(iIsotope.pmn);
+		else
+			sigma = new TGaussianSigma(iIsotope.pd[iType]);
+
+		double minK = sigma->Xmin();
+		double maxK = sigma->Xmax();
+
+		for (int i = 0; i < nn; i++) {
+			double R = 0;
+			//TODO: implement if needed
+			//double R = iSumTRK * aBackground->calculateR(Ranges().midNucleonGamma()[i], sigma, minK, maxK);
+			ASSERT_VALID_NO(R);
+			iR[i] = R;
+		}
+	}*/
+
+	const double CPhotoDisintegrationCanal::TGaussianSigma::iDefaultMaxK = 30.;
+	//30 MeV
+	const double CPhotoDisintegrationCanal::TLinearSigma::iMaxK = 150.;//150MeV
+
+//temporary
+//double gsl_sf_erf(double x){return x;}
+
+	CPhotoDisintegrationCanal::TGaussianSigma::TGaussianSigma(const TGaussCrossSection &aCS, double aMult, bool aCompatibilityMode) : iCS(aCS) {
+		if(aCompatibilityMode) {//this version was originally used in propagation program
+			iMaxK = iCS.k +
+					0.5 *
+					iCS.deltaPlus;//this adjustment of iMaxK is done to take into account wide deltas for light nuclei
+			if (iMaxK <
+				iDefaultMaxK)// the way it is done need to be checked, the only unclear reference is page 646 of ApJ 205 (Puget at al 1976)
+				iMaxK = iDefaultMaxK;
+		}
+		else
+			iMaxK = iDefaultMaxK;
+
+
+		double leftIntegral =
+				iCS.deltaMinus * sqrt(M_PI / 8.) * gsl_sf_erf(sqrt(2.) * (iCS.k - iCS.kTh) / iCS.deltaMinus);
+		double rightIntegral = iCS.deltaPlus * sqrt(M_PI / 8.) * gsl_sf_erf(sqrt(2.) * (iMaxK - iCS.k) / iCS.deltaPlus);
+		iNorm = iCS.sigma / (leftIntegral + rightIntegral)*aMult;
+	}
+
+
+	double CPhotoDisintegrationCanal::TGaussianSigma::f(double _x) const {
+		double power = (_x * units.Eunit - iCS.k);
+		power /= ((power < 0.) ? iCS.deltaMinus : iCS.deltaPlus);
+		return iNorm * exp(-2. * power * power);
+	}
+
+	double CPhotoDisintegrationCanal::TGaussianSigma::Xmin() const {
+		return iCS.kTh / units.Eunit;
+	}
+
+	double CPhotoDisintegrationCanal::TGaussianSigma::Xmax() const {
+		return TGaussianSigma::iMaxK / units.Eunit;
+	}
+
+	CPhotoDisintegrationCanal::TLinearSigma::TLinearSigma(double aSigmaIntegral, double aMult) {
+		iNorm = aMult*aSigmaIntegral / (iMaxK - TGaussianSigma::iDefaultMaxK);
+	}
+
+	double CPhotoDisintegrationCanal::TLinearSigma::Xmin() const {
+		return CPhotoDisintegrationCanal::TGaussianSigma::iDefaultMaxK / units.Eunit;
+	}
+
+	double CPhotoDisintegrationCanal::TLinearSigma::Xmax() const {
+		return TLinearSigma::iMaxK / units.Eunit;
+	}
+}//end of namespace Interactions
\ No newline at end of file
diff --git a/src/lib/Nucleus.h b/src/lib/Nucleus.h
new file mode 100644
index 0000000..0535ec3
--- /dev/null
+++ b/src/lib/Nucleus.h
@@ -0,0 +1,215 @@
+/*
+ * Nucleus.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#if !defined(NUCLEUS_H__INCLUDED_)
+#define NUCLEUS_H__INCLUDED_
+
+
+#include "Particle.h"
+#include "MathUtils.h"
+
+namespace Interactions {
+	using namespace Utils;
+	using namespace mcray;
+
+	class CBackgroundIntegral;
+
+	struct TGaussCrossSection {
+		double kTh;
+		//threshold energy (MeV)
+		double k; //middle energy (MeV)
+		double sigma; //total sigma integrated
+		double deltaMinus; //energy range (MeV)
+		double deltaPlus; //energy range (MeV)
+	};
+
+
+	struct TBranching {
+		int noOfModes;
+		const double *nucleiRates;
+		double pRate;
+		double nRate;
+	};
+
+	struct TIsotope {
+		int A; //mass
+		int Z; //electric charge
+
+		// photodisintegration with single and double nucleon emission
+		TGaussCrossSection pd[2];
+
+		// photodisintegration with multiple nucleon emission
+		double pmn;
+
+		// branching rates for multiple nucleon emission
+		const TBranching *branching;
+	};
+
+	enum TPhotoDisintegrationChanalType {
+		ESingle = 0, //order and numbers are important (see for ex. CPhotoDisintegrationCanal::recalculate)
+				EDouble,
+		EMultiple
+	};
+
+	class CPhotoDisintegrationCanal {
+	public:
+		CPhotoDisintegrationCanal(const TIsotope &aIsotope, TPhotoDisintegrationChanalType aType);
+
+		Function* Sigma(){return iSigma;}
+
+		//forces recalculation of R next time R(int) is called
+		//used to recalculate R when background changes
+		//void recalculate(const CBackgroundIntegral *aBackground);
+
+		//void approximate(CPhotoDisintegrationCanal &aCanal1, CPhotoDisintegrationCanal &aCanal2, double aPart);
+
+		int getMaxDeltaA();
+
+		double getMeanDeltaA();
+
+		//needed for energy loss outputs
+		double getRate(int aDeltaA);
+
+		double getRateP();
+
+		double getRateN();
+
+		void getEnergyLoss(std::vector<double> &aOutput);
+
+		inline ParticleType getParticle() const { return (ParticleType)(StartNuclei + iIsotope.A - 2); };
+
+		inline TPhotoDisintegrationChanalType type() { return iType; };
+
+		inline const TIsotope &isotope() { return iIsotope; };
+
+		static const double AutoRate;
+
+
+		class TGaussianSigma : public Function {
+		public:
+			TGaussianSigma(const TGaussCrossSection &aCS, double aMult, bool aCompatibilityMode=false);
+
+			double f(double _x) const;
+
+			//sigma devided by TRK sum
+			double Xmin() const;
+
+			double Xmax() const;
+
+		private:
+			double iNorm;
+			const TGaussCrossSection &iCS;
+			double iMaxK;
+		public:
+			static const double iDefaultMaxK;//30MeV
+		};
+
+		class TLinearSigma : public Function {
+		public:
+			TLinearSigma(double aSigmaIntegral, double aMult);
+
+			double f(double _x) const { return iNorm; };
+
+			//sigma devided by TRK sum
+			double Xmin() const;
+
+			double Xmax() const;
+
+		private:
+			double iNorm;
+		public:
+			static const double iMaxK;//150MeV
+		};
+
+		const TIsotope &iIsotope;
+		TPhotoDisintegrationChanalType iType;
+		double iSumTRK;
+		double iMeanDeltaA;
+		SafePtr<Function> iSigma;
+	};
+
+	struct TPhotoDisintegrationMapEntry {
+		CPhotoDisintegrationCanal *iCanal;
+		double iRate;
+
+		TPhotoDisintegrationMapEntry(CPhotoDisintegrationCanal *aCanal, double aRate) : iCanal(aCanal),
+																						iRate(aRate) { };
+	};
+
+	typedef AutoDeletePtrArray <TPhotoDisintegrationMapEntry> CPDMSecLine;
+	typedef AutoDeletePtrArray <CPhotoDisintegrationCanal> CPDMLine;//canals for single isotope
+
+
+	class CPhotoDisintegrationMap {
+	public:
+		CPhotoDisintegrationMap();
+
+		//reset all entries
+		//used to recalculate R when background changes
+		//void recalculate(const CBackgroundIntegral *aBackground);
+
+		//void approximate(CPhotoDisintegrationMap &aMap1, CPhotoDisintegrationMap &aMap2, double aPart);
+
+		// get list of canals contributing to nucleus A
+		// A=1 proton
+		// A=0 neutron
+		const CPDMSecLine *getIncome(int A) const;
+
+		// get all suppression canals for nucleus A
+		const CPDMLine *getOutcome(int A) const;
+
+		void printValidity(int aA);
+
+//debug
+		void printEnergyLoss(int aA, std::string &aFileName);
+
+	private:
+
+		void initSecMap(CPhotoDisintegrationCanal *aCanal);
+
+		typedef AutoDeletePtrArray <CPDMLine> CPDMTable;
+		typedef AutoDeletePtrArray <CPDMSecLine> CPDMSecTable;
+
+		CPDMTable fCanals;
+		CPDMSecTable fSecondaryCanals;
+	};
+
+	class CNucleus {
+	public:
+		static double PPPmultiplierR(int aA);
+
+		static int getZ(ParticleType aNucleus);
+
+		static int getA(ParticleType aNucleus);
+
+		static void getPhotopionMultipliers(int aA, double &aRn, double &aRp, double &aRN);
+
+	};
+}
+
+#endif // !defined(NUCLEUS_H__INCLUDED_)
diff --git a/src/lib/Output.cpp b/src/lib/Output.cpp
new file mode 100644
index 0000000..3eddf84
--- /dev/null
+++ b/src/lib/Output.cpp
@@ -0,0 +1,279 @@
+/*
+ * Output.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "Output.h"
+#include<sys/stat.h>
+#include <map>
+#include <utility>
+#include "ParticleStack.h"
+
+namespace mcray {
+
+SpectrumOutput::SpectrumOutput(std::string aFile, double aEmin, double aStep):
+fEmin(aEmin),
+fLogStep(log(aStep)),
+fFile(aFile)
+{
+	fEmin /= sqrt(aStep);//center bins in Emin*aStep^N
+	//std::ofstream	fileOut;
+	//fileOut.open(fFile.c_str());//make sure file can be created and delete the contents of old file if it exists
+	//if(!fileOut)
+		//Exception::Throw("Failed to open " + fFile + " for writing");
+	//fileOut.close();
+}
+
+SpectrumOutput::~SpectrumOutput() {
+
+}
+
+void SpectrumOutput::AddParticle(const Particle& aParticle)
+{
+	int iBin = log(aParticle.Energy/fEmin)/fLogStep;
+	if(iBin<0)
+		return;//skip output of particles with E<Emin
+	Vector& spec = fSpectra[aParticle.Type];
+	std::vector<int>& numbers = fNumberOfParticles[aParticle.Type];
+	while((int)spec.size()<=iBin)
+	{
+		spec.push_back(0);
+		numbers.push_back(0);
+	}
+	spec[iBin] += aParticle.Weight;
+	numbers[iBin] += 1;
+}
+
+void SpectrumOutput::Flush(bool aFinal)
+{
+    if(!aFinal)
+        return;//intermedeate output is not supported
+	std::ofstream	fileOut;
+	fileOut.open(fFile.c_str());//replace previous content
+	//fileOut.open(fFile.c_str(), std::ios::app);//don't remove previous contents
+	if(!fileOut)
+		Exception::Throw("Failed to open " + fFile + " for writing");
+	//fileOut.seekp(std::ios::beg);//every next spectrum output is saved in the beginning of the file
+	unsigned int nBins = 0;
+	for(int p=0; p<ParticleTypeEOF; p++)
+	{
+		unsigned int size = fSpectra[p].size();
+		if(size>nBins)
+			nBins = size;
+	}
+	double mult = exp(fLogStep);
+	double E=fEmin*sqrt(mult);
+	double deltaEmult = sqrt(mult) - 1./sqrt(mult);
+	for(unsigned int iE=0; iE<nBins; iE++, E*=mult)
+	{
+		fileOut << E*units.Eunit*1e6;//eV
+		for(unsigned int p=0; p<ParticleTypeEOF; p++)
+		{
+			double intFlux = 0;
+			int	nParticles = 0;
+			if(fSpectra[p].size()>iE)
+			{
+				intFlux = fSpectra[p][iE];
+				nParticles = fNumberOfParticles[p][iE];
+			}
+			fileOut << "\t" << intFlux*deltaEmult*E << "\t" << nParticles;//print dN(E)/dE * E^2
+		}
+		fileOut << '\n';
+	}
+	fileOut << "\n\n";/// delimiters between consequent spectra outputs
+	fileOut.close();
+}
+
+RawOutput::RawOutput(std::string aDir, bool aCheckRepetitions, bool aOverwriteExisting):
+		fParticles(0),
+		fDir(aDir),
+		fCheckRepetitions(aCheckRepetitions),
+        fHeaderWritten(ParticleTypeEOF,false),
+        fFlushInProgress(false)
+{
+	SetOutputDir(aDir, aOverwriteExisting);
+	fParticles = new std::vector<Particle>[ParticleTypeEOF];
+}
+
+RawOutput::~RawOutput()
+{
+	delete[] fParticles;
+}
+
+void RawOutput::SetOutputDir(std::string aDir, bool aOverwriteExisting)
+{
+	fDir = aDir;
+	if(mkdir(fDir.c_str(),0777))
+	{
+		if(aOverwriteExisting)
+		{
+			system(("rm -r " + aDir).c_str());
+			if(!mkdir(fDir.c_str(),0777))
+				return;
+		}
+		Exception::Throw("Failed to create directory " + aDir);
+	}
+}
+
+void RawOutput::CopyToStack(ParticleStack& aStack)
+{
+	for(int p=0; p<ParticleTypeEOF; p++)
+	{
+		std::vector<Particle>& particles = fParticles[p];
+		if(particles.size())
+			aStack.AddSecondaries(particles);
+	}
+}
+
+void RawOutput::AddParticle(const Particle& aParticle)
+{
+	fParticles[aParticle.Type].push_back(aParticle);
+}
+
+void RawOutput::Flush(bool aFinal)
+{
+	if(fFlushInProgress)
+		Exception::Throw("RawOutput::Flush(): was invoked from more than one thread");
+    fFlushInProgress = true;
+    if(fCheckRepetitions && aFinal)
+        LookForRepetitions(fParticles);
+
+
+	for(int p=0; p<ParticleTypeEOF; p++)
+	{
+		std::vector<Particle>& particles = fParticles[p];
+		if(particles.size()==0)
+			continue;
+		std::string fileName = fDir + "/" + Particle::Name((ParticleType)p);
+		std::ofstream file;
+		file.open(fileName.c_str(), std::ios::app);
+		if(!file) {
+            fFlushInProgress = false;
+            Exception::Throw("Failed to open " + fileName + " for writing");
+        }
+		std::vector<Particle>::const_iterator pit = particles.begin();
+        if(!fHeaderWritten[p]) {
+            WriteHeader(file, *pit);
+            file << "\n";
+            fHeaderWritten[p] = true;
+        }
+		for(; pit != particles.end(); pit++)
+		{
+			Write(file, *pit);
+			file << "\n";
+		}
+		file.close();
+		particles.clear();
+	}
+    fFlushInProgress = false;
+}
+
+void RawOutput::WriteHeader(std::ostream& aOut, const Particle& aFirstParticle){
+	aOut << "# E/eV\tweight\tNinteractions\tz_fin\tsinBeta\tD_lastDeflection/Mpc\tD_source\tz_source";
+}
+
+void RawOutput::Write(std::ostream& aOut, const Particle& aParticle){
+	double beta = aParticle.DeflectionAngle();
+	double sinBeta = (beta<0.5*M_PI)?sin(aParticle.DeflectionAngle()):1.;//to enable correct filtering based on sin(beta) set sin=1 for particles deflected by angles larger than Pi/2
+	double d = cosmology.z2d(aParticle.SourceParticle->Time.z())/units.Mpc;
+	double l = cosmology.z2d(aParticle.LastDeflectionTime().z())/units.Mpc;
+	aOut << aParticle.Energy*units.Eunit*1e6 << "\t" << aParticle.Weight << "\t"
+	<< aParticle.Ninteractions << "\t" << aParticle.Time.z() <<  "\t" //used for debugging
+	<< sinBeta << "\t" << l << "\t" << d << "\t" << aParticle.SourceParticle->Time.z(); //used to filter on base of PSF and jet angle or on base of source Z
+}
+
+void RawOutput::LookForRepetitions(std::vector<Particle>* aParticles)
+{
+	std::map<float, int> energies;
+	for(int p=0; p<ParticleTypeEOF; p++)
+	{
+		std::vector<Particle>& particles = aParticles[p];
+		for(std::vector<Particle>::const_iterator pit = particles.begin(); pit != particles.end(); pit++)
+		{
+			if(!pit->Ninteractions)//skipping particles which did not interact
+				continue;
+			float log10E = (float)log10(pit->Energy*units.Eunit*1e6);
+			energies[log10E]++;
+		}
+	}
+
+	std::ofstream file;
+	for (std::map<float,int>::iterator it = energies.begin(); it != energies.end(); it++)
+	{
+		if(it->second > 1)
+		{
+			if(!file.is_open())
+			{
+				std::cerr << "Repetitions found" << std::endl;
+				std::string fileName = fDir + "/repetitions";
+				file.open(fileName.c_str(),std::ios::out);
+				if(!file.is_open())
+					Exception::Throw("Failed to create " + fileName);
+			}
+			file << it->first << "\t" << it->second << "\n";
+		}
+	}
+	if(file.is_open())
+		file.close();
+}
+
+	void RawOutput3D::Write(std::ostream& aOut, const Particle& aParticle){
+		double T = aParticle.Time.t()-aParticle.SourceParticle->Time.t();
+		aOut << aParticle.Energy/units.eV << "\t" << aParticle.Weight << "\t"
+		<< aParticle.X[0]/T << "\t" << aParticle.X[1]/T << "\t" << 1.-aParticle.X[2]/T << "\t"
+		<< aParticle.Pdir[0] << "\t" << aParticle.Pdir[1] << "\t" << 1.-aParticle.Pdir[2] << "\t"
+		<< aParticle.fCascadeProductionTime.z() << "\t" << aParticle.Ninteractions;
+		if(fSaveSourceZ)
+			aOut << "\t" << aParticle.SourceParticle->Time.z();
+		if(fSaveSourceE)
+			aOut << "\t" << aParticle.SourceParticle->Energy/units.eV;
+		if(fSaveId){
+			aOut << "\t" << aParticle.id;
+		}
+	}
+
+	void RawOutput3D::WriteHeader(std::ostream& aOut, const Particle& aFirstParticle){
+		aOut << "# " << Particle::Name(aFirstParticle.Type);
+		if(!fSaveSourceZ){
+			aOut << "\tz_src = "<< aFirstParticle.SourceParticle->Time.z();
+		}
+		if(!fSaveSourceE){
+			aOut << "\tE_src/eV = "<< aFirstParticle.SourceParticle->Energy/units.eV;
+		}
+		aOut << "\t" << "T/Mpc = " << (aFirstParticle.Time.t()-aFirstParticle.SourceParticle->Time.t())/units.Mpc << "\n#\n";
+		aOut << "# E/eV\tweight\tx/T\ty/T\t1-(z/T)\tPx/P\tPy/P\t1-(Pz/P)\tz_cascade_production\tN_interactions";
+		if(fSaveSourceZ){
+			aOut << "\tz_src";
+		}
+		if(fSaveSourceE){
+			aOut << "\tE_src/eV";
+		}
+		if(fSaveId){
+			aOut << "\tId";
+		}
+	}
+
+} /* namespace mcray */
diff --git a/src/lib/Output.h b/src/lib/Output.h
new file mode 100644
index 0000000..851c024
--- /dev/null
+++ b/src/lib/Output.h
@@ -0,0 +1,108 @@
+/*
+ * Output.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef OUTPUT_H_
+#define OUTPUT_H_
+
+#include "Particle.h"
+#include <string>
+#include "Utils.h"
+#include <fstream>
+
+namespace mcray {
+
+class IOutput : public ISmartReferencedObj{
+public:
+	virtual void AddParticle(const Particle& aParticle) = 0;
+	virtual void Flush(bool aFinal) = 0;
+};
+
+class SpectrumOutput : public TSmartReferencedObj<IOutput> {
+public:
+	SpectrumOutput(std::string aFile, double aEmin, double aStep);
+	virtual ~SpectrumOutput();
+	void AddParticle(const Particle& aParticle);
+	void Flush(bool aFinal);
+private:
+	Vector			fSpectra[ParticleTypeEOF];
+	std::vector<int> fNumberOfParticles[ParticleTypeEOF];
+	double			fEmin;
+	double			fLogStep;
+	std::string 	fFile;
+};
+
+class RawOutput : public TSmartReferencedObj<IOutput> {
+public:
+	RawOutput(std::string aDir, bool aCheckRepetitions, bool aOverwriteExisting = false);
+	virtual ~RawOutput();
+	void AddParticle(const Particle& aParticle);
+	void Flush(bool aFinal);
+	void SetOutputDir(std::string aDir, bool aOverwriteExisting = false);
+	virtual void WriteHeader(std::ostream& aOut, const Particle& aFirstParticle);
+	virtual void Write(std::ostream& aOut, const Particle& aParticle);
+
+	//TODO: delete this method when intermediate output is implemented via multiple IResult instances in PropagEngine
+	void CopyToStack(class ParticleStack& aStack);
+private:
+	void LookForRepetitions(std::vector<Particle>* aParticles);
+	std::vector<Particle>*	fParticles;
+    std::vector<bool> 	    fHeaderWritten;
+	std::string				fDir;
+	bool					fCheckRepetitions;
+    bool                    fFlushInProgress;
+};
+
+	class RawOutput3D : public RawOutput{
+	public:
+		RawOutput3D(std::string aDir, bool aOverwriteExisting = false, bool aSaveSourceZ=false, bool aSaveSourceE=false, bool aSaveId=false):
+				RawOutput(aDir, false, aOverwriteExisting),fSaveSourceZ(aSaveSourceZ), fSaveSourceE(aSaveSourceE), fSaveId(aSaveId){ }
+		void WriteHeader(std::ostream& aOut, const Particle& aFirstParticle);
+		void Write(std::ostream& aOut, const Particle& aParticle);
+	private:
+		bool fSaveSourceZ;
+		bool fSaveSourceE;
+		bool fSaveId;
+	};
+
+	class TotalEnergyOutput : public TSmartReferencedObj<IOutput> {
+	public:
+		TotalEnergyOutput(){memset(fE,0,ParticleTypeEOF*sizeof(double));fTotE=0.;}
+		void AddParticle(const Particle& aParticle) {
+			fTotE += aParticle.Energy*aParticle.Weight;
+			fE[aParticle.Type] += aParticle.Energy*aParticle.Weight;
+		}
+		void Flush(bool aFinal){}
+		double TotalE() const { return fTotE; }
+		double TotalE(ParticleType aType) const { return fE[aType]; }
+	private:
+		double	fTotE;
+		double	fE[ParticleTypeEOF];
+	};
+
+} /* namespace mcray */
+#endif /* OUTPUT_H_ */
diff --git a/src/lib/PPP.cpp b/src/lib/PPP.cpp
new file mode 100644
index 0000000..3d2d413
--- /dev/null
+++ b/src/lib/PPP.cpp
@@ -0,0 +1,485 @@
+/*
+ * PPP.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "PPP.h"
+#include "MathUtils.h"
+#include "Output.h"
+#include "ParticleStack.h"
+#include "TableBackgrounds.h"
+#include "PropagationEngine.h"
+#include "ProtonPP.h"
+#include "Test.h"
+
+namespace Interactions {
+using namespace mcray;
+using namespace Utils;
+
+PPP::PPP(BackgroundIntegral* aBackground, double aScaleFactor):
+fBackground(aBackground),
+f_ScaleFactor(aScaleFactor),
+fMp(Particle::Mass(Proton)),
+fMe(Particle::Mass(Electron))
+{
+	ASSERT(aScaleFactor>0);
+}
+
+RandomInteraction* PPP::Clone() const
+{
+	return new PPP(fBackground->Clone(), f_ScaleFactor);
+}
+
+PPP::~PPP() {
+}
+
+PPP::SigmaTot::SigmaTot():
+fMp(Particle::Mass(Proton)),
+fMe(Particle::Mass(Electron)),
+fThresholdS(fMp*(4.*fMe+fMp)),
+fMaxS(1e300),//1e2*fMp*fMp
+fSigmaConst(AlphaEM*AlphaEM*AlphaEM/fMe/fMe)//alpha*r_0
+{
+
+}
+
+double PPP::SigmaTot::f(double s) const
+{
+	double k = 0.5*(s/fMp-fMp)/fMe;//photon energy in proton rest frame in units of electron mass
+	if(k<=2)
+		return 0;
+	double result = 0.;
+	if(k<4)
+	{//formula (A1) from M. J. Chodorowski, A. A. Zdziarski, and M. Sikora, Astrophys. J. 400, 181 (1992)
+		double eta = (k-2.)/(k+2.);
+		result = (k-2.)/k;
+		result *= (result*result*2./3.*M_PI*(1.+eta*(0.5+eta*(23./40.+eta*(37./120.+eta*61./192.)))));
+	}
+	else
+	{//formula (A2) from M. J. Chodorowski, A. A. Zdziarski, and M. Sikora, Astrophys. J. 400, 181 (1992)
+		double ln2k = log(2.*k);
+		double _kk = 4./k/k;
+		double _kk2 = _kk*_kk;
+		result = 28./9.*ln2k-218./27.+_kk*(ln2k*(6.-M_PI*M_PI/3.+ln2k*(-1+2./3.*ln2k))+0.549054066848226);
+		result -= (_kk2*(0.1875*ln2k+0.125+_kk*(0.0125868055555556*ln2k-0.00557002314814815)));
+	}
+	result *= fSigmaConst;
+	ASSERT_VALID_NO(result);
+	return result;
+}
+
+PPP::DifSigma_Eprime::DifSigma_Eprime():
+fE(-1),
+fK(-1),
+fCoef(-1)
+{
+	fKern_Theta.function = sigma;
+	fKern_Theta.params = this;
+}
+
+double PPP::s_epsRel = 1e-4;
+
+double PPP::DifSigma_Eprime::f(double E) const
+{
+	((PPP::DifSigma_Eprime*)this)->fE = E;
+	double result = ((Utils::MathUtils)fMath).Integration_qag(fKern_Theta,-1.,1.,0, s_epsRel, (size_t)(1./s_epsRel));
+	ASSERT_VALID_NO(result);
+	return result*fCoef;
+}
+
+void PPP::DifSigma_Eprime::SetK(double aK)
+{
+	fK = aK;
+	fCoef = 0.5*fSigmaConst/(aK*aK*aK);
+}
+
+
+double PPP::DifSigma_Eprime::sigma(double aCosTheta, void* data)
+{
+	const PPP::DifSigma_Eprime* sig = (const PPP::DifSigma_Eprime*)data;
+	return PPP::DiffSigma(sig->fK, sig->fE, aCosTheta);
+}
+
+//d(sigma)/d(e)/d(eps)
+double PPP::DiffSigma(double k, double em, double cosT)
+{//formula (10) of Blumenthal, Phys Rev D (1970) vol 1, number 6, page 1596
+// all quantities are given in units of electron mass
+// em - electron energy in proton rest frame
+// k - photon energy in proton rest frame
+// cosT - angle between electron and photon in proton rest frame
+// constant multiplier alpha*r_0^2/2k^3 is omitted
+	if(em<=1.)
+		return 0.;
+	double em2 = em*em;
+	double ep = k-em;//positron energy in proton rest frame (we neglect proton recoiling momentum)
+	if(ep<=1.)
+		return 0.;
+	double ep2 = ep*ep;
+	double pm2 = em2-1.;
+	double pm = sqrt(pm2);//electron momentum in proton rest frame
+	double pp = sqrt(ep2-1.);//positron momentum in proton rest frame
+	double e=em-cosT*pm;//delta_ - electron energy in the LAB (related to observer on Earth) frame divided by proton gamma factor
+	double e2 = e*e;
+	double e4 = e2*e2;
+	double k2 = k*k;
+	double sin2T = 1.-cosT*cosT;
+	double T2 = k2+pm2-2.*k*pm*cosT;
+	double T = sqrt(T2);
+	double Y = 2./pm2*log((ep*em+pp*pm+1)/k);
+	double ep_pp = (ep-pp<1e-8)? 0.5/ep/ep : (ep-pp);
+	double yp =	log((ep+pp)/ep_pp)/pp;
+	double deltaTp = log((T+pp)/(T-pp));
+	double result = -4.*sin2T*(2.*em2+1.)/(pm2*e4) + (5.*em2-2.*ep*em+3.)/(pm2*e2) + (pm2-k2)/(T2*e2) + 2.*ep/(pm2*e);
+	result += (Y/(pm*pp)*(2.*em*sin2T*(3.*k+pm2*ep)/e4 + (2.*em2*(em2+ep2)-7.*em2-3.*ep*em-ep2+1.)/e2 + k*(em2-em*ep-1.)/e));
+	result -= (deltaTp/(pp*T)*(2./e2-3.*k/e-k*(pm2-k2)/(T2*e)) + 2.*yp/e);
+	result *= (pp*pm);
+	if(result<0)
+		return 0.;
+	ASSERT_VALID_NO(result);
+	return result;
+}
+
+//d(sigma)/d(e)/d(eps)
+double PPP::DifSigma_r::Kern::f(double eps) const
+{//formula (10) of Blumenthal, Phys Rev D (1970) vol 1, number 6, page 1596
+ // all quantities are given in units of electron mass
+ // eps = E_ - electron energy in proton rest frame
+ // k - photon energy in proton rest frame
+ // e=delta_ - electron energy in the LAB (related to observer on Earth) frame divided by proton gamma factor
+ // cos(Theta)=(E_-e)/p_; constant multiplier alpha*r_0^2/2k^3 is omitted
+ // and extra multiplier dcosTheta/de = 1/p_ is introduced to take into account change of integration variables cosTheta -> e
+	double em = eps;//electron energy in proton rest frame
+	ASSERT(em>1);
+	double em2 = em*em;
+	double ep = k-em;//positron energy in proton rest frame (we neglect proton recoiling momentum)
+	ASSERT(ep>1);
+	double ep2 = ep*ep;
+	double pm2 = em2-1.;
+	double pm = sqrt(pm2);//electron momentum in proton rest frame
+	double pp = sqrt(ep2-1.);//positron momentum in proton rest frame
+	double cosT = (em-e)/pm; //cos(Theta) the angle between photon and electron in p-rest frame
+	double sin2T = 1.-cosT*cosT;
+	if(cosT>=1.)
+	{
+		sin2T = (2.*e*pm-1.)/pm2;
+		if(sin2T<0)
+			return 0;
+		ASSERT(sin2T<1e-3);
+		cosT = 1.-0.5*sin2T;
+	}
+	double T2 = k2+pm2-2.*k*pm*cosT;
+	double T = sqrt(T2);
+	double Y = 2./pm2*log((ep*em+pp*pm+1)/k);
+	double ep_pp = (ep-pp<1e-8)? 0.5/ep/ep : (ep-pp);
+	double yp =	log((ep+pp)/ep_pp)/pp;
+	double deltaTp = log((T+pp)/(T-pp));
+	double result = -4.*sin2T*(2.*em2+1.)/(pm2*e4) + (5.*em2-2.*ep*em+3.)/(pm2*e2) + (pm2-k2)/(T2*e2) + 2.*ep/(pm2*e);
+	result += (Y/(pm*pp)*(2.*em*sin2T*(3.*k+pm2*ep)/e4 + (2.*em2*(em2+ep2)-7.*em2-3.*ep*em-ep2+1.)/e2 + k*(em2-em*ep-1.)/e));
+	result -= (deltaTp/(pp*T)*(2./e2-3.*k/e-k*(pm2-k2)/(T2*e)) + 2.*yp/e);
+	result *= pp;
+	if(result<0)
+		return 0.;//TODO find the reason
+	//ASSERT_VALID_NO(result);
+	return result;
+}
+
+void PPP::DifSigma_r::Kern::SetParams(double _k, double _e)
+{
+	k = _k;
+	k2 = k*k;
+	e = _e;//electron energy in lab frame divided by proton gamma factor
+	e2 = e*e;
+	e4 = e2*e2;
+	fEpsMin = 0.5*(e + 1./e);
+	//ASSERT(Xmin()<=Xmax());//if Xmin>Xmax PPP::DifSigma::f(double r) will return 0
+	//(this may occure for large k (k>m_p, since approximation of zero proton kinetic energy is not valid anymore)
+}
+
+void PPP::DifSigma_r::SetS(double aS)
+{
+	fK = 0.5*(aS/fMp-fMp)/fMe;//photon energy in proton rest frame in units of electron mass
+	double m = fMp/fMe;//proton mass in units of electron mass
+	double tmp = fK + m - 1.;
+	tmp *= tmp;
+	//maximal momentum of electron directed opposite to photon momentum
+	//in proton rest frame in units of electron mass
+	double maxP = (sqrt(fK*(fK*(-1 + m) - 2*m)*(-1 + m)*tmp) +
+				   fK*(-1 + fK + m - fK*m))/((-1 + m)*(-1 + 2*fK + m));
+	f_rMax = (sqrt(maxP*maxP+1.)+maxP)/m;//converting to lab frame and dividing by proton energy
+	ASSERT(f_rMax<1);
+	//maximal momentum of electron directed along photon momentum
+	//in proton rest frame in units of electron mass
+	double minP = ((-1 + fK)*fK*(-1 + m) + sqrt(fK*(fK*(-1 + m) - 2*m)*(-1 + m)*
+												tmp))/((-1 + m)*(-1 + 2*fK + m));
+
+	//converting to lab frame and dividing by proton energy
+	f_rMin = (minP<1e4 ? (sqrt(minP*minP+1.)-minP) : 0.5/minP/minP)/m;
+}
+
+double PPP::DifSigma_r::f(double r) const
+{
+	double e = fMp/fMe*r;//  E_e/gamma_p in units of electron mass
+	Kern fKern;
+	fKern.SetParams(fK, e);
+	double epsMin = fKern.Xmin();
+	double epsMax = fKern.Xmax();
+	double relDiff = (epsMax-epsMin)/epsMax;
+	if(relDiff<1e-9)
+		return 0.;
+	const size_t limit = (size_t)(1./s_epsRel);
+	double relError = ( relDiff > 1e-5)? s_epsRel : (relDiff<3e-7?0.1:0.01);
+	double result = ((MathUtils)fMath).Integration_qag((gsl_function)fKern, epsMin, epsMax, 0, relError, limit);
+	ASSERT_VALID_NO(result);
+	result *= (0.5*fSigmaConst*fMp/fMe/fK/fK/fK);
+	return result;
+}
+
+double PPP::Rate(const Particle& aParticle) const
+{
+	if(aParticle.Type != Proton)
+		return 0.;
+	return fBackground->GetRateS(fSigma, aParticle) / f_ScaleFactor;
+}
+
+bool PPP::SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const
+{
+	ASSERT(aParticle.Type == Proton);
+	aS = 0.;
+	return (fBackground->GetRateAndSampleS(fSigma, aParticle, aRandomizer, aS)!=0);
+}
+
+void PPP::SampleSecondariesMean(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const
+{//// sampling secondary electrons/positrons is expensive so thinning is performed before sampling
+ /// via usage of scale factor parameter and generating either electron or positron (not both)
+	ASSERT(aParticle.Type == Proton);
+
+	Particle sec = aParticle;
+	sec.Type = aRandomizer.Rand()>0.5 ? Electron : Positron;
+	DifSigma_r difSigma;
+	double r = difSigma.SampleR(aS, s_epsRel, math, aRandomizer.Rand());
+	sec.Energy *= r;
+	sec.Weight *= (f_ScaleFactor*2);//2 takes into account that pair is produced
+	sec.fCascadeProductionTime = aParticle.Time;
+	aSecondaries.push_back(sec);
+
+	//don't add proton to the list of secondaries since this.KillsPrimary == false
+}
+
+	double PPP::DifSigma_r::SampleR(double aS, double aEpsRel, const Utils::MathUtils& aMath, double aRand)
+	{
+		double r = 0.;
+		SetS(aS);
+		if(fK<20000)
+		{
+			double sigmaTot;
+			aMath.SampleLogDistribution(*this, aRand, r, sigmaTot, Xmin(), Xmax(), aEpsRel);
+			ASSERT(r>=f_rMin && r<=f_rMax);
+		}
+		else
+		{ //to avoid rounding error using analytical estimate dSigma/dr ~ r^{-7/4} for fK>>1
+			// < r > = m_e/m_p //this is clear from plotting r^2 dSigma/dr in log scale (try e.g. PPP::UnitTest)
+			double rMinAdj = f_rMin;
+			for(int nIt=1; true; nIt++)//approximately solving eq.
+			{
+				double rMinNew = pow(3.*fMp/fMe*(pow(f_rMax,0.25)-pow(rMinAdj,0.25))+pow(f_rMax,-0.75),-4./3.);
+				ASSERT(rMinNew>0 && rMinNew<f_rMax);
+				double relErr = 2.*fabs(rMinNew-rMinAdj)/(rMinNew+rMinAdj);
+				rMinAdj = rMinNew;
+				if(relErr<aEpsRel)
+					break;
+				ASSERT(nIt<50);
+			}
+			//double rMaxAdj = fMe/fMp/3./pow(f_rMin,0.75) + pow(f_rMin,0.25);   //adjust rMin to have < r > = m_e/m_p (assuming rMin<<rMax)
+			//ASSERT(rMaxAdj<1.);
+			//ASSERT(rMaxAdj/f_rMin<1e-3);
+			r = pow(pow(rMinAdj,-0.75)*(1.-aRand)+aRand*pow(f_rMax,-0.75) , -4./3.);
+			ASSERT(r>=rMinAdj && r<=f_rMax);
+		}
+		return r;
+	}
+
+
+void PPP::SampleSecondariesExact(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const
+{
+	ASSERT(aParticle.Type == Proton);
+	double m = aParticle.Mass();
+	double k = 0.5*(aS/m-m)/fMe;//photon energy in proton rest frame in units of electron mass
+	ASSERT(k>2);
+	((PPP::DifSigma_Eprime&)fDifSigma_Eprime).SetK(k);
+
+	double Eprime, cosTheta, sigmaTot;
+	((MathUtils&)math).SampleLogDistribution(fDifSigma_Eprime, aRandomizer.Rand(), Eprime, sigmaTot, fDifSigma_Eprime.Xmin(), fDifSigma_Eprime.Xmax(), 10.*s_epsRel);
+	CosThetaSamplingEq cosProb(Eprime, k);
+	((MathUtils&)math).SampleDistribution(cosProb, aRandomizer.Rand(), cosTheta, sigmaTot, cosProb.Xmin(), cosProb.Xmax(), s_epsRel);
+	Particle electron = aParticle;
+	electron.Type = Electron;
+	electron.Energy *= ((Eprime-cosTheta*sqrt(Eprime-1.))*fMe/m);
+	Particle positron = aParticle;
+	positron.Type = Positron;
+	Eprime = k-Eprime;//we neglect proton kinetic energy
+	positron.Energy *= ((Eprime+cosTheta*sqrt(Eprime-1.))*fMe/m);//we assume that positron is directed against electron??? TODO: this is probably wrong
+	aParticle.Energy -= (electron.Energy + positron.Energy);
+	if(aParticle.Energy < m)
+		Exception::Throw("PPP::SampleSecondariesExact error: Ep<m");
+	aParticle.fCascadeProductionTime = aParticle.Time;
+	electron.Weight *= f_ScaleFactor;
+	positron.Weight *= f_ScaleFactor;
+	aSecondaries.push_back(electron);
+	aSecondaries.push_back(positron);
+}
+
+void PPP::SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const
+{
+	//SampleSecondariesExact(aParticle, aSecondaries, aS, aRandomizer); //wrong calculation (inelasticity growing with S instead of dropping)
+	//also in SampleSecondariesExact we assume that positron is directed against electron which is probably wrong
+
+	//this should to be correct on the average although not precise for individual interactions
+	//since electron and positron energies are sampled independently of each other for individual interactions
+	SampleSecondariesMean(aParticle, aSecondaries, aS, aRandomizer);
+}
+
+double PPP::CosThetaSamplingEq::f(double aCosT) const
+{
+	return PPP::DiffSigma(fK,fE,aCosT);
+}
+
+void PPP::UnitTest()
+{
+	Utils::MathUtils math;
+	SigmaTot sigma;
+	DifSigma_r difSigma;
+	double Mp(Particle::Mass(Proton));
+	double Me(Particle::Mass(Electron));
+	std::ofstream sigmaPPout;
+	sigmaPPout.open("ppp_diff_sigma",std::ios::out);
+	std::cout << "k/m_e\tsigTot/mbarn\t2.*(sigTot-sigIntTot)/(sigTot+sigIntTot)" << std::endl;
+	for(double k=2.2; k<3e4; k*=10.)
+	{
+		double s = Mp + k*Me;
+		s*=s;
+		//if(s>sigma.Xmax())
+		//	continue;
+		double sigTot = sigma(s);
+		difSigma.SetS(s);
+		double sigIntTot = math.Integration_qag((gsl_function)difSigma, difSigma.Xmin(), difSigma.Xmax(), 0, 1e-5, 10000);
+		std::cout << k << "\t" << sigTot/units.barn*1e3 << "\t" << 2.*(sigTot-sigIntTot)/(sigTot+sigIntTot) << std::endl;
+		sigmaPPout << "# k=" << k << std::endl;
+		difSigma.Print(sigmaPPout,100,true);
+		sigmaPPout << "\n" << std::endl;
+	}
+	UnitTestSampling(1e12, 0);
+	UnitTestEconserv(1e20, 0.1);
+}
+
+void PPP::UnitTestSampling(double aE, double aZ)
+{
+	std::string out = "debug_sample_ppp_E";
+	out += ToString(aE*1e6);//eV
+	out += "_Z";
+	out += ToString(aZ);
+	std::ofstream secPPout;
+	secPPout.open("sample_sec_ppp",std::ios::out);
+	//std::ofstream secPPoutVarS;
+	//secPPoutVarS.open("sample_sec_ppp_varS",std::ios::out);
+
+    debug.SetOutputFile(out);
+
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+	//backgr.AddComponent(new PlankBackground(cmbTemp, 6.2e-10/units.Eunit, 6.4e-10/units.Eunit));//only central value
+
+	double stepZ = 0.05;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	Backgrounds::Kneiske0309EBL* kn0309 = new Backgrounds::Kneiske0309EBL();
+
+	backgr.AddComponent(kn0309);
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, aZ+1, epsRel);
+	Interactions::PPP pp(backgrI);
+	Particle proton(Proton, aZ);
+	proton.Energy = aE;//MeV
+	proton.Time.setZ(aZ);
+	proton.Type = Proton;
+	Randomizer rnd;
+	/*
+	double S=2.;
+	for(int i=0; i<10000; i++)
+	{
+		std::vector<Particle> secondaries;
+		if(pp.GetSecondaries(proton, secondaries, rnd))//this outputs sampled s to debug file
+		{		std::vector<Particle> secondariesS;
+		pp.SampleSecondaries(proton, secondariesS, S rnd);
+			ASSERT(secondaries.size()==3);
+			//print electron and proton energies
+			secPPoutVarS << secondaries[0].Energy/aE << "\n" << secondaries[1].Energy/aE << "\n";
+		}
+	}*/
+
+	double Mp(Particle::Mass(Proton));
+	double Me(Particle::Mass(Electron));
+	SigmaTot sigma;
+	for(double k=2.2; k<3e5; k*=10.)
+	{
+		double s = Mp + k*Me;
+		s*=s;
+		if(s>sigma.Xmax())
+			continue;
+		for(int i=0; i<100; i++)
+		{
+			std::vector<Particle> secondaries;
+			pp.SampleSecondaries(proton, secondaries, s, rnd);
+			ASSERT(secondaries.size()==1);//only electron or positron is sampled
+			secPPout << k << "\t" << secondaries[0].Energy/aE << "\n";
+			//std::cout << k << "\t" << secondaries[0].Energy/aE << "\n";
+		}
+		secPPout << "\n" << std::endl;
+		//std::cout << std::endl;
+	}
+
+	secPPout.close();
+}
+
+void PPP::UnitTestEconserv(double aE, double aZ){
+	uint scaleFactorPPP = 10;
+	int nParticles = 100;
+	Test::EConservationTest test(aZ,1e6,2015);
+	test.alphaThinning = 0.9;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	test.Engine().AddInteraction(new PPP(test.Backgr(),scaleFactorPPP));
+	test.Engine().AddInteraction(new ProtonPPcel(test.Backgr()));
+	Particle p(Proton, aZ);
+	p.Energy = aE*units.eV;
+	test.SetPrimary(p, nParticles);
+	test.Run();
+}
+
+} /* namespace Interactions */
diff --git a/src/lib/PPP.h b/src/lib/PPP.h
new file mode 100644
index 0000000..d7320df
--- /dev/null
+++ b/src/lib/PPP.h
@@ -0,0 +1,149 @@
+/*
+ * PPP.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef PPP_H_
+#define PPP_H_
+
+#include "GammaPP.h"
+#include "Interaction.h"
+
+namespace Interactions {
+using namespace mcray;
+
+/// This class samples secondaries from pair production by protons
+/// The energy loss by protons due to pair production is implemented
+/// by class ProtonPPcel
+class PPP: public RandomInteractionS, public EMInteraction {
+	class SigmaTot : public Function
+	{
+	public:
+		SigmaTot();
+		double f(double s) const;
+		double Xmin() const { return fThresholdS; }
+		double Xmax() const { return fMaxS; }
+	protected:
+		const double fMp;
+		const double fMe;
+		const double fThresholdS;
+		const double fMaxS;//introduced to avoid computational problems (with differential cross section)
+							//for extremely large S (sqrt(S)>10 m_p,  where PPP is much less important than pion production anyway)
+		const double fSigmaConst;
+	};
+	class DifSigma_r : public SigmaTot
+	{
+		class Kern : public Function
+			{
+			public:
+				double f(double eps) const;
+				void SetParams(double _k, double _e);
+				double Xmin() const { return fEpsMin; }
+				double Xmax() const { return k - 1.; }
+			private:
+				double fEpsMin;
+				double k;
+				double k2;//k^2
+				double e;
+				double e2;
+				double e4;
+			};
+	public:
+		double f(double r) const;
+		void SetS(double aS);
+		double SampleR(double aS, double aEpsRel, const Utils::MathUtils& aMath, double aRand);
+		double Xmin() const { return f_rMin; }
+		double Xmax() const { return f_rMax; }
+	private:
+		//Kern fKern;
+		double fK;//photon energy in proton rest frame in units of electron mass
+		double f_rMin;
+		double f_rMax;
+		Utils::MathUtils fMath;
+	};
+
+	class DifSigma_Eprime : public SigmaTot
+	{// dSigma/dE_, where E_ electron gamma factor in proton rest frame
+	public:
+		DifSigma_Eprime();
+		void SetK(double aK);//k-photon energy in proton rest frame in units of electron mass
+		double f(double E) const;
+		static double sigma(double aCosTheta, void* data);
+		double Xmin() const { return 1.; }
+		double Xmax() const { return fK-1.; }
+	private:
+		double fE;//electron gamma factor in proton rest frame
+		double fK;//photon energy in proton rest frame in units of electron mass
+		gsl_function fKern_Theta;
+		Utils::MathUtils fMath;
+		double fCoef;
+	};
+
+	class CosThetaSamplingEq : public Function
+	{
+		double fE;
+		double fK;
+	public:
+		CosThetaSamplingEq(double aEprime, double aK):fE(aEprime),fK(aK){}
+		double Xmin() const { return -1.; }
+		double Xmax() const { return 1.; }
+		double f(double aCosT) const;
+	};
+
+public:
+	PPP(BackgroundIntegral* aBackground, double aScaleFactor = 1.);
+	static void UnitTest();
+	static void UnitTestSampling(double aE, double aZ);
+	static void UnitTestEconserv(double aE, double aZ);
+	static double DiffSigma(double aK, double aE, double aCosTheta);
+	virtual ~PPP();
+	RandomInteraction* Clone() const;
+	virtual bool KillsPrimary() const
+	    {
+	    	return false;
+	    }
+	double Rate(const Particle& aParticle) const;
+	bool SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const;
+	void SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const;
+	void SampleSecondariesMean(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const;
+	void SampleSecondariesExact(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const;
+
+
+private:
+	SmartPtr<BackgroundIntegral>	fBackground;
+	SigmaTot						fSigma;
+	DifSigma_r						fDifSigma;
+	DifSigma_Eprime					fDifSigma_Eprime;
+	double f_ScaleFactor;//>=0 divide interaction rate by f_ScaleFactor and
+	// multiply weight of secondary electrons and positrons by f_ScaleFactor
+	double fMp;
+	double fMe;
+	Utils::MathUtils math;
+	static double s_epsRel;//Desired relative error of integration
+};
+
+} /* namespace Interactions */
+
+#endif /* PPP_H_ */
diff --git a/src/lib/Particle.cpp b/src/lib/Particle.cpp
new file mode 100644
index 0000000..62f28a0
--- /dev/null
+++ b/src/lib/Particle.cpp
@@ -0,0 +1,195 @@
+/*
+ * Particle.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "Particle.h"
+#include "Nucleus.h"
+
+namespace mcray
+{
+
+const double Particle::MassesMeV[EndLightParticle] =
+{// order of masses in this array must follow the order of particles in the enumeration ParticleType
+//#ifdef ELMAG_TEST
+		0.511, //Electron,
+		0.511,//Positron,
+//#else
+		//0.510998928, //Electron,
+		//0.510998928,//Positron,
+//#endif
+        0,//Photon,
+        0,//NeutrinoE,
+        0,//NeutrinoM,
+        0,//NeutrinoT,
+        0,//NeutrinoAE,
+        0,//NeutrinoAM,
+        0,//NeutrinoAT,
+        939.565378,//Neutron,
+        938.272046//Proton,
+
+};
+
+const int Particle::ElectricCharges[EndLightParticle] =
+{// order of masses in this array must follow the order of particles in the enumeration ParticleType
+		-1, //Electron,
+		1,//Positron,
+        0,//Photon,
+        0,//NeutrinoE,
+        0,//NeutrinoM,
+        0,//NeutrinoT,
+        0,//NeutrinoAE,
+        0,//NeutrinoAM,
+        0,//NeutrinoAT,
+        0,//Neutron,
+        1//Proton,
+};
+
+const char* Particle::ParticleNames[ParticleTypeEOF] =
+{
+	    "electron",
+	    "positron",
+	    "photon",
+	    "neutrinoE",
+	    "neutrinoM",
+	    "neutrinoT",
+	    "neutrinoAE",
+	    "neutrinoAM",
+	    "neutrinoAT",
+	    "neutron",
+	    "proton",
+        "A2",
+        "A3",
+        "A4",
+        "A5",
+        "A6",
+        "A7",
+        "A8",
+        "A9",
+        "A10",
+        "A11",
+        "A12",
+        "A13",
+        "A14",
+        "A15",
+        "A16",
+        "A17",
+        "A18",
+        "A19",
+        "A20",
+        "A21",
+        "A22",
+        "A23",
+        "A24",
+        "A25",
+        "A26",
+        "A27",
+        "A28",
+        "A29",
+        "A30",
+        "A31",
+        "A32",
+        "A33",
+        "A34",
+        "A35",
+        "A36",
+        "A37",
+        "A38",
+        "A39",
+        "A40",
+        "A41",
+        "A42",
+        "A43",
+        "A44",
+        "A45",
+        "A46",
+        "A47",
+        "A48",
+        "A49",
+        "A50",
+        "A51",
+        "A52",
+        "A53",
+        "A54",
+        "A55",
+        "A56"
+};
+
+    int Particle::fNextCustomDataIndex = 0;
+    unsigned long long Particle::fMaxId = 0;
+
+    void Particle::PropagateFreely(cosmo_time dt)
+    {
+        Time += dt;
+        double b=beta();
+        for(int i=0; i<3; i++)
+            X[i]+=(b*Pdir[i]*dt);
+    }
+
+    void Particle::GenerateId(){
+#pragma omp critical (Particle)
+        id = ++fMaxId;
+    }
+
+    int Particle::ReserveInteractionDataSlot()//returns index to access and store interaction data or -1 if no space left
+    {
+        int result = -1;
+#pragma omp critical (Particle)
+        {
+            if(fNextCustomDataIndex < ParticleCustomDataSize)
+                result = fNextCustomDataIndex;
+            fNextCustomDataIndex++;
+        }
+        return result;
+    }
+
+    std::string Particle::ToString(){
+        std::ostringstream logStr;
+        logStr << ParticleNames[Type] << "(E=" << Energy/units.eV << "eV, " << Time.ToString() << ")";
+        return logStr.str();
+    }
+
+    double Particle::Mass(ParticleType aType){
+        if(aType<EndLightParticle)
+            return MassesMeV[aType]/units.Eunit;
+        if(aType<EndNuclei){
+            double meanNucleonMassMeV = 0.5*(Particle::Mass(Proton)+Particle::Mass(Neutron));
+            return meanNucleonMassMeV*Interactions::CNucleus::getA(aType);
+        }
+        NOT_IMPLEMENTED // treat other particles when added
+        return 0.;
+    }
+
+    double Particle::ElectricCharge(ParticleType aType){
+        if(aType<EndLightParticle)
+            return ElectricCharges[aType];
+        if(aType<EndNuclei)
+            return ElectricCharges[Proton]*Interactions::CNucleus::getZ(aType);
+        NOT_IMPLEMENTED // treat other particles when added
+        return 0.;
+    }
+
+}
diff --git a/src/lib/Particle.h b/src/lib/Particle.h
new file mode 100644
index 0000000..54a5700
--- /dev/null
+++ b/src/lib/Particle.h
@@ -0,0 +1,223 @@
+/*
+ * Particle.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef PARTICLE_H
+#define	PARTICLE_H
+
+#include "Cosmology.h"
+#include <cstring>
+#include "Units.h"
+#include <math.h>
+
+namespace mcray
+{
+    enum ParticleType
+    {// order of masses in the array Particle::massesMeV must follow the order of particles below
+        Electron = 0,
+        Positron,
+        Photon,
+    	NeutrinoE,
+    	NeutrinoM,
+    	NeutrinoT,
+    	NeutrinoAE,
+    	NeutrinoAM,
+    	NeutrinoAT,
+    	Neutron,
+    	Proton,
+        EndLightParticle,
+        StartNuclei = EndLightParticle,
+        A02 = StartNuclei,
+        A03,
+        A04,
+        A05,
+        A06,
+        A07,
+        A08,
+        A09,
+        A10,
+        A11,
+        A12,
+        A13,
+        A14,
+        A15,
+        A16,
+        A17,
+        A18,
+        A19,
+        A20,
+        A21,
+        A22,
+        A23,
+        A24,
+        A25,
+        A26,
+        A27,
+        A28,
+        A29,
+        A30,
+        A31,
+        A32,
+        A33,
+        A34,
+        A35,
+        A36,
+        A37,
+        A38,
+        A39,
+        A40,
+        A41,
+        A42,
+        A43,
+        A44,
+        A45,
+        A46,
+        A47,
+        A48,
+        A49,
+        A50,
+        A51,
+        A52,
+        A53,
+        A54,
+        A55,
+        A56,
+        EndNuclei,
+        ParticleTypeEOF = EndNuclei //must be the last
+    };
+#define ParticleCustomDataSize 16
+    class Particle
+    {
+    private:
+    	inline void Reset() {
+            memset(this,0,sizeof(Particle));Weight=1.; LastB = -1.; Pdir[2]=1.;
+        }
+        void GenerateId();
+    public:
+    	inline double Mass() const {
+            return Mass(Type);
+        }
+    	inline int ElectricCharge() const {
+            return ElectricCharge(Type);
+        }
+        //Construct primary particle
+    	inline Particle(ParticleType aType, cosmo_time aZsource){
+            Reset(); Type=aType; fProductionTime.setZ(aZsource) ; Time.setZ(aZsource); SourceParticle = this;
+            GenerateId();
+        }
+
+    	inline Particle& operator=(const Particle& aParticle) {
+            memcpy(this, &aParticle, sizeof(Particle)); return *this;
+        }
+    	inline double beta() const {
+            double m=Mass(); return sqrt(1.-m*m/Energy/Energy);
+        }
+
+        inline void SetParent(const Particle& aParticle){
+            GenerateId();
+            if(id != aParticle.id){
+                fPrevId = aParticle.id;
+                fProductionTime = Time;
+            }
+        }
+
+    	static double Mass(ParticleType aType);
+    	static double ElectricCharge(ParticleType aType);
+        void PropagateFreely(cosmo_time dt);
+        ParticleType Type;
+        double Weight;
+        CosmoTime Time;
+        double Energy;
+        long Ninteractions;//number of interactions in the interaction chain
+        double X[3];//comoving coordinates, source location: (0,0,0)
+        double Pdir[3];//momentum direction, initial value: (0,0,1)
+        unsigned long long id;
+        unsigned long long fPrevId;
+
+    // TODO: interaction specific attributes should be stored separately in custom structures which can
+    // be accessed via pointers stored in interactionData field
+        void* interactionData[ParticleCustomDataSize];//used to store interaction specific attributes as pointers
+        static int ReserveInteractionDataSlot();//returns index to access and store interaction data or -1 if no space left
+
+        const Particle* SourceParticle;
+        double Deflection2;//uncorrelated deflection angle squared
+        double CorrelatedDeflection;//current value of correlated deflection
+        mutable double CorrelatedBpath;// travel distance within B-field coherence length
+        CosmoTime fProductionTime; // time when the particle was produced either in source or on the way as secondary from interaction;
+        CosmoTime fCascadeProductionTime; // time when primary EM cascade particle was produced by hadron
+        double dt; // particle time delay;
+        mutable double LastB; //last transverce magnetic field within B-field coherence length
+
+        inline double JetOpenningAngle() const
+        {
+        	return DeflectionAngle()-ObservationAngle();
+        }
+
+        inline CosmoTime LastDeflectionTime() const
+        {
+        	return ElectricCharge() ? Time : fProductionTime;
+        }
+
+        inline double ObservationAngle() const
+        {
+        	double beta = DeflectionAngle();
+        	if(beta==0.)
+        		return 0.;
+            double sinBeta=sin(beta);
+            double pathToLastDeflection = LastDeflectionTime().t()-SourceParticle->Time.t();//distance from the source for the parent electron/positron (valid for small z source and deflection)
+            double distanceToSource = Time.t()-SourceParticle->Time.t();// distance between the source and the observer  (valid for small z source)
+            return asin(pathToLastDeflection/distanceToSource*sinBeta);// observation angle
+        }
+
+        inline double DeflectionAngle() const
+        {
+        	return sqrt(Deflection2 + CorrelatedDeflection*CorrelatedDeflection);
+        }
+
+        inline double TimeDelay() const
+        {
+        	if(ElectricCharge())
+        		return 0.;//the approximation used below works only for neutral particles ( LastDeflectionTime() > Time.t() )
+            double sinBeta=sin(DeflectionAngle());
+            double pathToLastDeflection = LastDeflectionTime().t()-SourceParticle->Time.t();//xx - distance from the source for the parent electron/positron (valid for small z source and deflection)
+            double distanceToSource = Time.t()-SourceParticle->Time.t();// distance between the source and the observer  (valid for small z source)
+            return 2*pathToLastDeflection*(1.0-pathToLastDeflection/distanceToSource)*sinBeta*sinBeta;//  time delay
+        }
+        inline static const char* Name(ParticleType aType)
+        {
+            return ParticleNames[aType];
+        }
+        std::string ToString();
+    private:
+        static const double MassesMeV[EndLightParticle];
+        static const int ElectricCharges[EndLightParticle];
+        static const char* ParticleNames[ParticleTypeEOF];
+        static int fNextCustomDataIndex;
+        static unsigned long long fMaxId;
+    };
+}
+#endif	/* PARTICLE_H */
+
diff --git a/src/lib/ParticleStack.cpp b/src/lib/ParticleStack.cpp
new file mode 100644
index 0000000..7871d7e
--- /dev/null
+++ b/src/lib/ParticleStack.cpp
@@ -0,0 +1,210 @@
+/*
+ * ParticleStack.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "ParticleStack.h"
+#include <signal.h>
+#include "Utils.h"
+#include <time.h>
+#include <algorithm>
+
+namespace mcray
+{   
+
+ParticleStack::ParticleStack(int aDebugOutputPeriod):fLastDebugOutputTime(0),fDebugOutputPeriod(aDebugOutputPeriod) {
+}
+
+ParticleStack::~ParticleStack() {
+	for(std::vector<Particle*>::const_iterator  it = fPrimaryParticles.begin(); it != fPrimaryParticles.end(); it++)
+		delete (*it);
+	fPrimaryParticles.clear();
+}
+
+
+void ParticleStack::AddSecondaries(const std::vector<Particle> &aParticles)
+{
+#pragma omp critical (ParticleStack)
+	{
+	for(std::vector<Particle>::const_iterator  it = aParticles.begin(); it != aParticles.end(); it++)
+		fParticles.push_back(*it);
+//#ifdef _DEBUG
+	time_t t = time(0);
+	if(fDebugOutputPeriod>=0 && t>fLastDebugOutputTime + fDebugOutputPeriod)
+	{
+		std::cerr << fOutputPrefix << "particle stack: " << fParticles.size() << std::endl;
+		fLastDebugOutputTime = t;
+	}
+//#endif
+	}
+}
+
+void ParticleStack::AddPrimary(const Particle &aParticle)
+{
+#pragma omp critical (ParticleStack)
+	{
+		Particle* primary = new Particle(aParticle);
+		primary->SourceParticle = 0;
+		fPrimaryParticles.push_back(primary);
+		fParticles.push_back(aParticle);
+		//make sure SourceParticle field is valid until ParticleStack object is destroyed
+		fParticles.back().SourceParticle = primary;
+//#ifdef _DEBUG
+		time_t t = time(0);
+		if(fDebugOutputPeriod>=0 && t>fLastDebugOutputTime + fDebugOutputPeriod)
+		{
+			std::cerr << fOutputPrefix << "particle stack: " << fParticles.size() << std::endl;
+			fLastDebugOutputTime = t;
+		}
+//#endif
+	}
+}
+
+bool ParticleStack::Pop(Particle& aParticle)
+{
+	bool result = false;
+#pragma omp critical (ParticleStack)
+	{
+	if(fParticles.size())
+	{
+		aParticle = fParticles.back();//*(fParticles.end()-1);
+		fParticles.pop_back();//fParticles.erase(fParticles.end()-1);
+//#ifdef _DEBUG
+	time_t t = time(0);
+	if(fDebugOutputPeriod>=0 && t>fLastDebugOutputTime + fDebugOutputPeriod)
+	{
+		std::cerr << fOutputPrefix << "particle stack: " << fParticles.size() << std::endl;
+		fLastDebugOutputTime = t;
+	}
+//#endif
+		result = true;
+	}
+	}
+	return result;
+}
+
+void Result::Add(std::vector<Particle> aParticles)
+{
+	if(fFilter)///! filtering outside critical section
+		for(int i=aParticles.size()-1; i>=0; i--)
+			if (aParticles[i].Time < fT || (fFilter && !fFilter->Pass(aParticles[i])))
+				aParticles.erase(aParticles.begin()+i);
+
+#pragma omp critical (Result_Add)
+	{
+		for(std::vector<Particle>::const_iterator pit = aParticles.begin(); pit != aParticles.end(); pit++) {
+			for (std::vector<SmartPtr<IOutput> >::iterator it = fOutputs.begin(); it != fOutputs.end(); it++) {
+				(*it)->AddParticle(*pit);
+			}
+		}
+        fParticleCounter++;//we only count number of Result::Add calls which should correspond to number of primary particles successfully treated
+		if(fAutoFlushInterval>0 && fParticleCounter>=fAutoFlushInterval){//flush only when all particles from current cascade are added
+			//to insure output is always selfconsistent
+			fParticleCounter = 0;
+			Flush(false);
+		}
+	}
+}
+
+void Result::Add(const Particle& aParticle)
+{
+	if(aParticle.Time < fT || (fFilter && !fFilter->Pass(aParticle)))///! filtering outside critical section
+		return;
+#pragma omp critical (Result_Add)
+	{
+        for(std::vector<SmartPtr<IOutput> >::iterator it = fOutputs.begin(); it != fOutputs.end(); it++)
+        {
+            (*it)->AddParticle(aParticle);
+        }
+        fParticleCounter++;
+        if(fAutoFlushInterval>0 && fParticleCounter>=fAutoFlushInterval){
+            fParticleCounter = 0;
+            Flush(false);
+        }
+	}
+}
+
+void Result::SetAutoFlushInterval(uint aNumberOfPrimaryParticles){
+#pragma omp critical (Result_Add)
+    {
+        fAutoFlushInterval = aNumberOfPrimaryParticles;
+    }
+}
+
+void Result::Flush(bool aFinal)
+{
+#pragma omp critical (Result_Flush)
+	{
+		for (std::vector<SmartPtr<IOutput> >::iterator it = fOutputs.begin(); it != fOutputs.end(); it++) {
+			(*it)->Flush(aFinal);
+		}
+	}
+}
+
+void Result::SetEndTime(const CosmoTime& aT)
+{
+	fT = aT;
+}
+
+cosmo_time Result::GetMinTravelTimeLeft(const Particle& aParticle) const
+{
+	return fT-aParticle.Time;
+}
+
+Result::~Result()
+{
+	Flush();
+	fOutputs.clear();
+
+	std::vector<Result *>::iterator pos = std::find(fCtrlCFlushList.begin(), fCtrlCFlushList.end(), this);
+	if (pos != fCtrlCFlushList.end())
+		fCtrlCFlushList.erase(pos);
+
+}
+
+std::vector<Result*> Result::fCtrlCFlushList;
+
+void Result::EnableFlushOnCtrlC() {
+    if (!fCtrlCFlushList.size()) {
+        struct sigaction act;
+        act.sa_handler = Result::SIGINT_handler;
+        sigaction(SIGINT, &act, NULL);//register Ctrl-C handler
+    }
+    fCtrlCFlushList.push_back(this);
+}
+
+void Result::SIGINT_handler(int) {
+#pragma omp critical (Result_Add)
+    {
+        for (std::vector<Result *>::iterator it = fCtrlCFlushList.begin(); it != fCtrlCFlushList.end(); it++) {
+            (*it)->fAutoFlushInterval = 0;//disable autoflush
+            (*it)->Flush(true);
+            (*it)->fStopRequested = true;
+        }
+    }
+}
+
+}//namespace mcray
+
diff --git a/src/lib/ParticleStack.h b/src/lib/ParticleStack.h
new file mode 100644
index 0000000..d393896
--- /dev/null
+++ b/src/lib/ParticleStack.h
@@ -0,0 +1,122 @@
+/*
+ * ParticleStack.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef PARTICLESTACK_H
+#define	PARTICLESTACK_H
+
+#include "Particle.h"
+#include "Output.h"
+#include "Utils.h"
+#include "Filter.h"
+#include <vector>
+#include <set>
+
+namespace mcray
+{
+
+//Thread-safe stack
+class ParticleStack {
+public:
+    ParticleStack(int aDebugOutputPeriod=-1);
+	//TODO: remove this method and RawOutput::CopyToStack() when intermediate output is implemented via multiple IResult instances in PropagEngine
+	//Currently used in OmegaCascade application
+    void AddSecondaries(const std::vector<Particle> &aParticles);
+    //void AddSecondary(const Particle &aParticle);
+	void AddPrimary(const Particle &aParticle);
+    bool Pop(Particle& aParticle);
+    virtual ~ParticleStack();
+    inline void SetOutputPrefix(std::string aPrefix) { fOutputPrefix = aPrefix; }
+private:
+    /*
+    struct Less : public std::binary_function<Particle, Particle, bool>
+        {
+          bool
+          operator()(const Particle& __x, const Particle& __y) const
+          {
+        	  return __x.Energy < __y.Energy;
+          }
+        };
+    std::multiset<Particle,Less> fParticles;*/
+    std::vector<Particle> fParticles;
+	std::vector<Particle*> fPrimaryParticles;//stored here to guarantee validity of Particle::SourceParticle field
+    time_t fLastDebugOutputTime;
+    int fDebugOutputPeriod;
+    std::string fOutputPrefix;
+};
+
+class IResult {
+public:
+	virtual bool AbortRequested() const = 0;
+	virtual bool ContinuePropagation(const Particle& aParticle) const = 0;
+	virtual void Add(std::vector<Particle> aParticles) = 0;
+	virtual cosmo_time GetMinTravelTimeLeft(const Particle& aParticles) const = 0;
+};
+
+class Result : public IResult{
+public:
+	Result() : fFilter(0), fIncludeFilterInPropagation(false),fAutoFlushInterval(0),fParticleCounter(0), fStopRequested(false){};
+	Result(IFilter*	aOutputFilter, bool aIsPropagationFilter = false) :
+		fFilter(aOutputFilter), fIncludeFilterInPropagation(aIsPropagationFilter),fAutoFlushInterval(0),fParticleCounter(0), fStopRequested(false){};
+	Result(double aEmin):
+		fFilter(new EnergyBasedFilter(aEmin)), fIncludeFilterInPropagation(true),fAutoFlushInterval(0),fParticleCounter(0), fStopRequested(false){}
+    //set to 0 to disable auto flush
+    void SetAutoFlushInterval(uint aNumberOfPrimaryParticles);
+	/*!
+	@param[in]  aParticle particle
+	@return true if farther propagation simulation is required for the particle, false otherwise
+	*/
+	bool ContinuePropagation(const Particle& aParticle) const
+	{
+		return GetMinTravelTimeLeft(aParticle)>0 && !(fIncludeFilterInPropagation &&  fFilter && !fFilter->Pass(aParticle));
+	}
+	inline const CosmoTime& T() const {return fT;}
+	cosmo_time GetMinTravelTimeLeft(const Particle& aParticles) const;
+	~Result();
+	void SetEndTime(const CosmoTime& aT);
+    void Add(std::vector<Particle> aParticles);
+    void Add(const Particle& aParticle);
+    void Flush(bool aFinal=true);
+    void AddOutput(IOutput* aOutput) { fOutputs.push_back(aOutput); }
+    inline void SetOutputFilter(IFilter* aFilter) {fFilter = aFilter;}
+	void EnableFlushOnCtrlC();
+	bool AbortRequested() const { return fStopRequested; }
+private:
+	static void SIGINT_handler(int);
+	static std::vector<Result*> fCtrlCFlushList;
+    CosmoTime			fT;//default end time is z=0 (see default constructor of CosmoTime)
+    SmartPtr<IFilter>	fFilter;
+    std::vector<SmartPtr<IOutput> >  fOutputs;
+    bool fIncludeFilterInPropagation;
+    uint fAutoFlushInterval;
+    uint fParticleCounter;
+	bool fStopRequested;
+};
+
+}
+#endif	/* PARTICLESTACK_H */
+
diff --git a/src/lib/PhotoDisintegration.cpp b/src/lib/PhotoDisintegration.cpp
new file mode 100644
index 0000000..0345798
--- /dev/null
+++ b/src/lib/PhotoDisintegration.cpp
@@ -0,0 +1,384 @@
+/*
+ * PhotoDisintegration.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "PhotoDisintegration.h"
+#include "ParticleStack.h"
+#include "TableBackgrounds.h"
+#include "PropagationEngine.h"
+
+Interactions::PhotoDisintegration::PhotoDisintegration(mcray::BackgroundIntegral *aBackground, bool aSampleK):
+        fBackground(aBackground),
+        fSampleK(aSampleK)
+{
+}
+
+mcray::RandomInteraction *Interactions::PhotoDisintegration::Clone() const {
+    return new PhotoDisintegration(fBackground);
+}
+
+Interactions::PhotoDisintegration::~PhotoDisintegration() {
+
+}
+
+double Interactions::PhotoDisintegration::Rate(const mcray::Particle &aParticle) const {
+    if(aParticle.Type<A02 || aParticle.Type>=EndNuclei)
+        return 0.;
+    const CPDMLine* canals = fMap.getOutcome(CNucleus::getA(aParticle.Type));
+    int noOfCanals = canals?canals->size():0;
+    if(!noOfCanals)
+        return 0.;
+    if(fSampleK){
+        SigmaK sigma(canals);
+        return fBackground->GetRateK(sigma, aParticle);
+    }else{
+        SigmaS sigma(canals);
+        return fBackground->GetRateS(sigma, aParticle);
+    }
+}
+
+int Interactions::PhotoDisintegration::UnitTest()
+{
+    UnitTest(A04, true, true, false, 5e18*units.eV, 1e20*units.eV, 1, 10000);
+    return 0;
+}
+
+void Interactions::PhotoDisintegration::UnitTest(ParticleType aPrimary, bool aPrintRates, bool aPropagate, bool aSplit, double Emin, double Emax, double Zmax, int nParticles)
+{
+    //double alphaThinning = 0;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+    ParticleStack particles;
+    Result result(Emin/CNucleus::getA(aPrimary));
+    if(aPropagate)
+    {
+        std::string out = Particle::Name(aPrimary);
+        out += aSplit?"_split" : "";
+        out += "_Emin";
+        out += ToString(Emin);
+        out += "_Emax";
+        out += ToString(Emax);
+        out += "_Zmax";
+        out += ToString(Zmax);
+        //result.AddOutput(new SpectrumOutput("out_" + out, Emin, pow(10, 0.05)));
+        result.AddOutput(new RawOutput(out, false, true));
+        debug.SetOutputFile("debug_" + out);
+    }
+    if(!cosmology.IsInitialized())
+        cosmology.Init();
+
+    CompoundBackground backgr;
+    double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+    backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+    //backgr.AddComponent(new PlankBackground(cmbTemp, 6.2e-10/units.Eunit, 6.4e-10/units.Eunit));//only central value
+
+    double stepZ = 0.05;
+    double logStepK = pow(10,0.05);
+    double epsRel = 1e-3;
+
+    Backgrounds::Kneiske0309EBL* kn0309 = new Backgrounds::Kneiske0309EBL();
+    PBackgroundIntegral backgrIebl;
+    PRandomInteraction interactionEbl;
+    if(!aSplit)
+    {
+        backgr.AddComponent(kn0309);
+    }
+    else
+    {
+        backgrIebl = new ContinuousBackgroundIntegral(*kn0309, stepZ, logStepK, Zmax, epsRel);
+        interactionEbl = new Interactions::PhotoDisintegration(backgrIebl);
+    }
+
+    PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel);
+    PRandomInteraction interaction = new Interactions::PhotoDisintegration(backgrI);
+
+    PCELInteraction rs = new RedShift();
+    double E = Emin;
+    if(aPrintRates)
+    {
+        std::ofstream ratesOut;
+        std::string ratesFile = ToString(Particle::Name(aPrimary)) + "_rates";
+        ratesFile += aSplit ? "_split" : "";
+        ratesOut.open(ratesFile.c_str(),std::ios::out);
+
+        ratesOut << "#E\tRedshiftRate\tRate\t\t[E]=1eV [Rate]=1/Mpc\n";
+
+        for(double E=Emin; E<Emax; E*=logStepK)
+        {
+            Particle particle(aPrimary, 0.);
+            particle.Energy = E / units.Eunit;//MeV
+            double rate = interaction->Rate(particle);
+            double redshiftRate = rs->Rate(particle);
+            if(aSplit)
+            {
+                rate += interactionEbl->Rate(particle);
+            }
+            double mult = 1./units.Lunit*Units::Mpc_in_cm;
+            ratesOut << E*1e6 << "\t" << redshiftRate*mult << "\t" << rate * mult << std::endl;
+        }
+    }
+    if(!aPropagate)
+        return;
+
+    PropagationEngine pe(particles, result);
+
+    pe.AddInteraction(interaction);
+
+    if(aSplit)
+    {
+        pe.AddInteraction(interactionEbl);
+    }
+
+    E=Emax;
+    Particle primary(aPrimary, Zmax);
+    primary.Energy = E;//MeV
+    //primary.Weight = exp(-Emax/E);
+    //if(primary.Weight>0)
+    for(int i=0; i < nParticles; i++)
+        particles.AddPrimary(primary);
+    //}
+
+    pe.RunMultithreadReleaseOnly();
+    if(aSplit)
+        delete kn0309;
+}
+
+
+bool Interactions::PhotoDisintegration::SampleS(const mcray::Particle &aParticle, double &aS,
+                                                mcray::Randomizer &aRandomizer) const {
+    ASSERT(aParticle.Type>=A02 && aParticle.Type<EndNuclei);
+    const CPDMLine* canals = fMap.getOutcome(CNucleus::getA(aParticle.Type));
+    int noOfCanals = canals?canals->size():0;
+    ASSERT(noOfCanals>0);
+    SigmaS sigma(canals);
+    return (fBackground->GetRateAndSampleS(sigma, aParticle, aRandomizer, aS)!=0);
+}
+
+bool Interactions::PhotoDisintegration::SampleK(const mcray::Particle &aParticle, double &aK,
+                                                mcray::Randomizer &aRandomizer) const {
+    ASSERT(aParticle.Type>=A02 && aParticle.Type<EndNuclei);
+    const CPDMLine* canals = fMap.getOutcome(CNucleus::getA(aParticle.Type));
+    int noOfCanals = canals?canals->size():0;
+    ASSERT(noOfCanals>0);
+    SigmaK sigma(canals);
+    return (fBackground->GetRateAndSampleK(sigma, aParticle, aRandomizer, aK)!=0);
+}
+
+bool Interactions::PhotoDisintegration::GetSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const
+{
+    if(fSampleK){
+        double k = 0.;
+        if(!SampleK(aParticle, k, aRandomizer))
+            return false;
+        SampleSecondariesWithK(aParticle, aSecondaries, k, aRandomizer);
+    }
+    else{
+        double s = 0.;
+        if(!SampleS(aParticle, s, aRandomizer))
+            return false;
+        SampleSecondariesWithS(aParticle, aSecondaries, s, aRandomizer);
+    }
+    return true;
+}
+
+void Interactions::PhotoDisintegration::SampleSecondariesWithS(mcray::Particle &aParticle,
+                                                               std::vector<mcray::Particle> &aSecondaries, double aS,
+                                                               mcray::Randomizer &aRandomizer) const {
+    ASSERT(aParticle.Type>=A02 && aParticle.Type<EndNuclei);
+    double m=aParticle.Mass();
+    double k = 0.5*(aS/m-m);
+    SampleSecondariesWithK(aParticle, aSecondaries, k, aRandomizer);
+}
+
+void Interactions::PhotoDisintegration::SampleSecondariesWithK(mcray::Particle &aParticle,
+                                                               std::vector<mcray::Particle> &aSecondaries, double k,
+                                                               mcray::Randomizer &aRandomizer) const {
+    ASSERT(aParticle.Type>=A02 && aParticle.Type<EndNuclei);
+    const CPDMLine* canals = fMap.getOutcome(CNucleus::getA(aParticle.Type));
+    int noOfCanals = canals?canals->size():0;
+    ASSERT(noOfCanals>0);
+
+    //select canal
+    double totSigma = 0.;
+    std::vector<double> sigma(canals->size(),0.);
+    size_t iCanal=0;
+    for (std::vector<CPhotoDisintegrationCanal *>::const_iterator it = canals->begin();
+         it != canals->end(); it++)
+    {
+        Function* curSigma = (*it)->Sigma();
+        double kMinCur = curSigma->Xmin();
+        if (kMinCur < k)
+            totSigma += curSigma->f(k);
+        sigma[iCanal]=totSigma;
+        iCanal++;
+    }
+    double randSigma = aRandomizer.Rand()*totSigma;
+    CPhotoDisintegrationCanal* selectedCanal=0;
+    for (iCanal=0; iCanal<canals->size(); iCanal++)
+    {
+        if(sigma[iCanal]>=randSigma) {
+            selectedCanal = canals->at(iCanal);
+            break;
+        }
+    }
+
+    double secE=0;// energy conservation test (debug only)
+
+    const TIsotope &isotope = selectedCanal->isotope();
+    int maxDeltaA = selectedCanal->getMaxDeltaA();
+    int A = isotope.A;
+    ASSERT(CNucleus::getA(aParticle.Type)==A);
+    ASSERT(maxDeltaA < A);
+    double autoN = 0.;
+    double autoP = 0.;
+    for (int deltaA = 1; deltaA <= maxDeltaA; deltaA++) {
+        double rate = selectedCanal->getRate(deltaA);
+        ParticleType prodType=(ParticleType)(aParticle.Type-deltaA);//in case A=2,3 secondary canal for proton (H1) may be duplicated, but it is ok, since summary rate is correct
+        int deltaZ = isotope.Z - CNucleus::getZ(prodType);
+        if (deltaZ < 0) {//processes with neutron emission followed by beta decay
+            deltaZ = 0;
+        }
+        int deltaN = deltaA - deltaZ;
+        autoN += deltaN * rate;
+        autoP += deltaZ * rate;
+        if (rate > 0.) {
+            Particle product = aParticle;
+            product.Type = prodType;
+            product.Weight *= rate;
+            product.Energy *= (product.Mass()/aParticle.Mass());//same gamma factor
+            aSecondaries.push_back(product);
+            secE += (product.Energy * product.Weight);// energy conservation test (debug only)
+        }
+    }
+    double rateP = selectedCanal->getRateP();
+    double rateN = selectedCanal->getRateN();
+    if (rateP == CPhotoDisintegrationCanal::AutoRate) {
+        rateP = autoP;
+    };
+    if (rateN == CPhotoDisintegrationCanal::AutoRate) {
+        rateN = autoN;
+    };
+    if (rateN > 0.) {
+        Particle product = aParticle;
+        product.Type = Neutron;
+        product.Weight *= rateN;
+        product.Energy *= (product.Mass()/aParticle.Mass());//same gamma factor
+        aSecondaries.push_back(product);
+        secE += (product.Energy * product.Weight);// energy conservation test (debug only)
+    }
+    if (rateP > 0.) {
+        Particle product = aParticle;
+        product.Type = Proton;
+        product.Weight *= rateP;
+        product.Energy *= (product.Mass()/aParticle.Mass());//same gamma factor
+        aSecondaries.push_back(product);
+        secE += (product.Energy * product.Weight);
+    }
+    double primE = aParticle.Energy * aParticle.Weight;// energy conservation test (debug only)
+    ASSERT(MathUtils::RelDifference(secE,primE)<1e-3);// verify energy conservation
+}
+
+Interactions::PhotoDisintegration::SigmaS::SigmaS(const CPDMLine* canals) :
+fCanals(canals),
+fXmin(1e300),
+fXmax(0),
+fM(-1.)
+{
+    if(fCanals->size()) {
+        double kMin = 1e300;
+        double kMax = 0;
+        fM = Particle::Mass(fCanals->at(0)->getParticle());
+        for (std::vector<CPhotoDisintegrationCanal *>::const_iterator it = fCanals->begin();
+             it != fCanals->end(); it++)
+        {
+            double kMinCur = (*it)->Sigma()->Xmin();
+            if (kMinCur < kMin)
+                kMin = kMinCur;
+            double kMaxCur = (*it)->Sigma()->Xmax();
+            if (kMaxCur > kMax)
+                kMax = kMaxCur;
+        }
+        fXmin = fM * (fM + 2. * kMin);
+        fXmax = fM * (fM + 2. * kMax);
+    }
+}
+
+double Interactions::PhotoDisintegration::SigmaS::f(double s) const {
+    if(s<=fXmin)
+        return 0.;
+    double k = 0.5*(s/fM-fM);
+    double sigma = 0.;
+    for (std::vector<CPhotoDisintegrationCanal *>::const_iterator it = fCanals->begin();
+         it != fCanals->end(); it++)
+    {
+        Function* curSigma = (*it)->Sigma();
+        if (curSigma->Xmin() <= k && curSigma->Xmax() >= k) {
+            sigma += curSigma->f(k);
+        }
+    }
+    return sigma;
+}
+
+
+Interactions::PhotoDisintegration::SigmaK::SigmaK(const CPDMLine* canals) :
+        fCanals(canals),
+        fXmin(1e300),
+        fXmax(0)
+{
+    if(fCanals->size()) {
+        double kMin = 1e300;
+        double kMax = 0;
+        for (std::vector<CPhotoDisintegrationCanal *>::const_iterator it = fCanals->begin();
+             it != fCanals->end(); it++)
+        {
+            double kMinCur = (*it)->Sigma()->Xmin();
+            if (kMinCur < kMin)
+                kMin = kMinCur;
+            double kMaxCur = (*it)->Sigma()->Xmax();
+            if (kMaxCur > kMax)
+                kMax = kMaxCur;
+        }
+        fXmin = kMin;
+        fXmax = kMax;
+        ASSERT(kMin>0);
+    }
+}
+
+double Interactions::PhotoDisintegration::SigmaK::f(double k) const {
+    if(k<=fXmin || k>fXmax)
+        return 0.;
+
+    double sigma = 0.;
+    for (std::vector<CPhotoDisintegrationCanal *>::const_iterator it = fCanals->begin();
+         it != fCanals->end(); it++)
+    {
+        Function* curSigma = (*it)->Sigma();
+        if (curSigma->Xmin() <= k && curSigma->Xmax() >= k) {
+            sigma += curSigma->f(k);
+        }
+    }
+    return sigma;
+}
\ No newline at end of file
diff --git a/src/lib/PhotoDisintegration.h b/src/lib/PhotoDisintegration.h
new file mode 100644
index 0000000..b34ef13
--- /dev/null
+++ b/src/lib/PhotoDisintegration.h
@@ -0,0 +1,86 @@
+/*
+ * PhotoDisintegration.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef MCRAY_PHOTODISINTEGRATION_H
+#define MCRAY_PHOTODISINTEGRATION_H
+
+#include "Interaction.h"
+#include "Background.h"
+#include "Nucleus.h"
+
+namespace Interactions {
+    using namespace mcray;
+
+    class PhotoDisintegration : public RandomInteraction {
+        class SigmaS : public Function
+        {
+        public:
+            SigmaS(const CPDMLine* canals);
+            double f(double s) const;
+            double Xmin() const {return fXmin;}
+            double Xmax() const {return fXmax;}
+        private:
+            const CPDMLine* fCanals;
+            double fXmin;
+            double fXmax;
+            double fM;
+        };
+        class SigmaK : public Function
+        {
+        public:
+            SigmaK(const CPDMLine* canals);
+            double f(double s) const;
+            double Xmin() const {return fXmin;}
+            double Xmax() const {return fXmax;}
+        private:
+            const CPDMLine* fCanals;
+            double fXmin;
+            double fXmax;
+        };
+    public:
+        PhotoDisintegration(BackgroundIntegral* aBackground, bool aSampleK=true);
+        RandomInteraction* Clone() const;
+        virtual ~PhotoDisintegration();
+        double Rate(const Particle& aParticle) const;
+        static void UnitTest(ParticleType aPrimary, bool aPrintRates, bool aPropagate, bool aSplit, double aEmin, double aEmax, double aZmax, int nParticles=1000);
+        static int UnitTest();
+        bool SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const;
+        bool GetSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const;
+        void SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aL, Randomizer& aRandomizer) const;
+        bool SampleK(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const;
+        void SampleSecondariesWithS(Particle &aParticle, std::vector<Particle> &aSecondaries, double aS, Randomizer &aRandomizer) const;
+        void SampleSecondariesWithK(Particle &aParticle, std::vector<Particle> &aSecondaries, double aK, Randomizer &aRandomizer) const;
+
+    private:
+        CPhotoDisintegrationMap         fMap;
+        SmartPtr<BackgroundIntegral>	fBackground;
+        bool                            fSampleK;
+    };
+}
+
+#endif //MCRAY_PHOTODISINTEGRATION_H
diff --git a/src/lib/PrecisionTests.cpp b/src/lib/PrecisionTests.cpp
new file mode 100644
index 0000000..4168e41
--- /dev/null
+++ b/src/lib/PrecisionTests.cpp
@@ -0,0 +1,72 @@
+/*
+ * PrecisionTests.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/*
+ * PrecisionTests.cpp
+ *
+ *  Created on: Dec 25, 2011
+ *      Author: ok
+ */
+
+#include "PrecisionTests.h"
+#include "../external/mpfrc++/mpreal.h"
+#include "Utils.h"
+
+using namespace mpfr;
+
+namespace Test{
+
+void PrecisionTests::SetPrecision(int aPrecision)
+{
+	mpreal::set_default_prec(aPrecision);
+}
+
+double PrecisionTests::PartialSigmaAccurate(double rMin, double s)
+{
+/*
+Formula for partial cross section was taken from http://lanl.arxiv.org/abs/1106.5508v1 eq. (9)
+*/
+	mpreal yMin=1./s;//s here is in units of m^2
+	mpreal yMax = 1-rMin;
+	if(yMin>=yMax)
+	{
+		ASSERT((yMax-yMin)/(yMax+yMin)<1e-10);
+		return 0.;
+	}
+
+	mpreal y_1 = 1.-yMin;
+	mpreal y_1_2 = y_1*y_1;
+	mpreal a=yMax-yMin;
+	mpreal result = 0;
+
+	result = yMin*(yMax-yMin)/y_1*(log(yMax/yMin)/(yMax-yMin)*(1.-4.*yMin*(1.+yMin)/y_1_2)+4.*(yMin/yMax+yMin)/y_1_2+0.5*(yMax+yMin));
+	ASSERT_VALID_NO(result.toDouble());
+
+	return result.toDouble();
+}
+
+}//namespace Test
diff --git a/src/lib/PrecisionTests.h b/src/lib/PrecisionTests.h
new file mode 100644
index 0000000..d5a6147
--- /dev/null
+++ b/src/lib/PrecisionTests.h
@@ -0,0 +1,49 @@
+/*
+ * PrecisionTests.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef PRECISIONTESTS_H_
+#define PRECISIONTESTS_H_
+
+#ifdef USE_MPFR
+
+#include "mpfrc++/mpreal.h"
+
+namespace Test
+{
+
+class PrecisionTests {
+public:
+	static void SetPrecision(int aPrecision);
+	static double PartialSigmaAccurate(double rMin, double s);
+};
+
+}
+#endif //#ifdef USE_MPFR
+
+
+#endif /* PRECISIONTESTS_H_ */
diff --git a/src/lib/PropagationEngine.cpp b/src/lib/PropagationEngine.cpp
new file mode 100644
index 0000000..7daaead
--- /dev/null
+++ b/src/lib/PropagationEngine.cpp
@@ -0,0 +1,521 @@
+/*
+ * PropagationEngine.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include <math.h>
+#include <numeric>
+#include "PropagationEngine.h"
+#include <omp.h>
+
+using namespace std;
+
+namespace mcray
+{
+
+unsigned long int PropagationEngine::fLastRandSeed = 0;
+
+PropagationEngine::PropagationEngine(ParticleStack &aStack, IResult &aResult, unsigned long int aRandSeed) :
+		fThinning(0),
+		fFilter(0),
+        fStack(aStack),
+        fResult(aResult),
+        fRandomizer(aRandSeed ? aRandSeed:fLastRandSeed),
+        fAccuracy(0.01),
+        fMinStep(0.),
+        fInfoPrinted(false),
+        fLogger(0)
+{
+	if(!aRandSeed)
+		fLastRandSeed++;
+
+}
+
+PropagationEngine::PropagationEngine(PropagationEngine &aEngine, unsigned long int aRandSeed) :
+		fThinning(aEngine.fThinning),
+		fFilter(aEngine.fFilter),
+		fStack(aEngine.fStack),
+		fResult(aEngine.fResult),
+		fRandomizer(aRandSeed ? aRandSeed:fLastRandSeed),
+		fAccuracy(aEngine.fAccuracy),
+        fMinStep(aEngine.fMinStep),
+        fLogger(aEngine.fLogger)
+{
+	if(!aRandSeed)
+		fLastRandSeed++;
+    for(vector<PCELInteraction>::iterator it = aEngine.fContinuousEnergyLosses.begin(); it != aEngine.fContinuousEnergyLosses.end(); it++)
+    {
+    	fContinuousEnergyLosses.push_back((*it)->Clone());
+    }
+    for(vector<PDeflectionInteraction>::iterator it = aEngine.fDeflectionInteractions.begin(); it != aEngine.fDeflectionInteractions.end(); it++)
+    {
+    	fDeflectionInteractions.push_back((*it)->Clone());
+    }
+    for(vector<PRandomInteraction>::iterator it = aEngine.fRandomInteractions.begin(); it != aEngine.fRandomInteractions.end(); it++)
+    {
+    	fRandomInteractions.push_back((*it)->Clone());
+    }
+    for(vector<double>::iterator it = aEngine.fRandomInteractionRateMultipliers.begin(); it != aEngine.fRandomInteractionRateMultipliers.end(); it++)
+    {
+        fRandomInteractionRateMultipliers.push_back(*it);
+    }
+}
+
+void PropagationEngine::Run(double aMaxRateVariation) {
+    try {
+        Particle p(Electron, 0.);//will be reset by fStack.Pop
+        while (fStack.Pop(p)) {
+            try {
+                while (true) {
+                    if(fLogger)
+                        fLogger->log(p); // record initial state
+                    if (aMaxRateVariation > 0)
+                        RunParticleWithVariableRate(p, aMaxRateVariation);
+                    else
+                        RunParticle(p);
+                    if(fResult.AbortRequested())
+                        return;
+                    if (!fSecondaryParticles.size())
+                        break;
+                    p = fSecondaryParticles.back();
+                    fSecondaryParticles.pop_back();
+                }
+
+            }catch(Exception* ex){
+                fFinalParticles.clear();//don't save current paricle cascade
+                std::string message = ex->Message() + "\tprimary " + p.ToString();
+                LOG_ERROR(message);
+                std::cerr << message << std::endl;
+            }
+            if(fFinalParticles.size()){
+                fResult.Add(fFinalParticles);
+                fFinalParticles.clear();
+            }
+        }
+    }catch(Exception* ex)
+    {
+        LOG_ERROR(ex->Message());
+        std::cerr << ex->Message() << std::endl;
+    }
+}
+
+void PropagationEngine::RunMultithreadReleaseOnly(double aMaxRateVariation, int aNumberOfThreads)
+{
+#ifdef _DEBUG
+	Run(aMaxRateVariation);
+#else
+	RunMultithread(aMaxRateVariation, aNumberOfThreads);
+#endif
+}
+
+void PropagationEngine::RunMultithread(double aMaxRateVariation, int aNumberOfThreads)
+{
+    if(aNumberOfThreads>1)
+        omp_set_num_threads(aNumberOfThreads);
+    else if(aNumberOfThreads==1){
+        Run(aMaxRateVariation);
+        return;
+    }
+#pragma omp parallel
+{
+  SafePtr<PropagationEngine> pe;
+#pragma omp critical (PropagationEngine)
+  {
+	   pe = new PropagationEngine(*this, fRandomizer.CreateIndependent());
+  }
+
+#pragma omp barrier
+
+#pragma omp master
+{
+    if(!fInfoPrinted){
+        int nthreads = omp_get_num_threads();
+        cout << "Running in " << nthreads << " threads" << '\n';
+        fInfoPrinted = true;
+    }
+}
+
+  pe->Run(aMaxRateVariation);
+}
+}
+
+void PropagationEngine::move(Particle &aParticle, cosmo_time aDt, double aElossRate)
+{
+    cosmo_time eLossHalf = exp(-0.5 * aElossRate * aDt);//relative energy loss during dt/2
+    aParticle.Energy = aParticle.Energy * eLossHalf;//mean energy during period [t, t+dt]
+    bool deflected = false;
+    for(vector<PDeflectionInteraction>::iterator it = fDeflectionInteractions.begin(); it != fDeflectionInteractions.end(); it++)
+    {
+        if((*it)->Propagate(aDt, aParticle, fRandomizer))//should not modify aParticle.Energy and aParticle.Time
+        {
+            if(deflected)
+                Exception::Throw("Particle treated by more than one deflection interaction");
+            deflected = true;
+        }
+    }
+    if(!deflected)
+        aParticle.PropagateFreely(aDt);
+    aParticle.Energy = aParticle.Energy * eLossHalf;
+    if(fLogger)
+        fLogger->log(aParticle);
+}
+
+double PropagationEngine::GetInteractionRate(const Particle &aParticle) const {
+    double totRate = 0.;
+    for(vector<PRandomInteraction>::const_iterator it = fRandomInteractions.begin(); it != fRandomInteractions.end(); it++)
+    {
+        double rate = (*it)->Rate(aParticle);
+        totRate += rate;
+    }
+    return totRate;
+}
+
+void PropagationEngine::RunParticle(Particle& aParticle) 
+{
+    while((!fResult.AbortRequested()) && fResult.ContinuePropagation(aParticle))
+    {
+    	if(fFilter && !fFilter->Pass(aParticle))
+    		return;
+        double eLossRate = 0.;
+        //double bRate = 0.;
+        double randRate = 0.;
+        cosmo_time maxDt = fResult.GetMinTravelTimeLeft(aParticle);
+        ASSERT(maxDt>0);
+        for(vector<PCELInteraction>::iterator it = fContinuousEnergyLosses.begin(); it != fContinuousEnergyLosses.end(); it++)
+        {
+            eLossRate += (*it)->Rate(aParticle);
+        }
+        //for(vector<PDeflectionInteraction>::iterator it = fDeflectionInteractions.begin(); it != fDeflectionInteractions.end(); it++)
+        //{
+        //    bRate += (*it)->Rate(aParticle);
+        //}
+        vector<double> rates;
+        for(vector<PRandomInteraction>::iterator it = fRandomInteractions.begin(); it != fRandomInteractions.end(); it++)
+        {
+            double rate = (*it)->Rate(aParticle);
+            randRate += rate;
+            rates.push_back(rate);
+        }
+        cosmo_time dt = eLossRate>0 ? fAccuracy/eLossRate : maxDt;//fAccuracy<1
+        if(dt>maxDt)
+            dt = maxDt;
+
+        double rand = -1;
+        bool hasInteraction = false;
+        if(randRate>0)
+        {
+			double freePath = fRandomizer.GetFreePath(1./randRate, rand);
+			if(freePath<=dt)
+			{
+				dt = freePath;
+				hasInteraction = true;
+			}
+        }
+
+        move(aParticle, dt, eLossRate);
+
+
+        for(vector<PCELInteraction>::iterator it = fContinuousEnergyLosses.begin(); it != fContinuousEnergyLosses.end(); it++)
+        {
+        	vector<Particle> secondaries;
+            (*it)->GetSecondaries(aParticle,secondaries,dt,fRandomizer);
+            HandleSecondaries(aParticle, secondaries, 1.);
+        }
+
+        if(hasInteraction)
+        {
+            //select interaction
+            double r = fRandomizer.Rand() * randRate;
+            double totRate = 0.;
+            RandomInteraction* interaction = 0;
+            for(unsigned int i=0; i<rates.size(); i++)
+            {
+                totRate += rates[i];
+                if(totRate>=r)
+                {
+                    interaction = fRandomInteractions[i];
+                    break;
+                }
+            }
+            uint maxError_count = 3;
+            bool completed = false;
+            vector<Particle> secondaries;
+            bool interactionOccurred = false;
+            for(uint i=0; !completed; i++)
+            {
+                try {
+
+                    interactionOccurred = interaction->GetSecondaries(aParticle, secondaries,
+                                                    fRandomizer);//may return false if particle energy is close to process threshold
+
+                    completed = true;
+                }catch(Exception* ex)
+                {
+                    if(i==maxError_count) {
+                        std::cerr << "recovery failed after " << maxError_count-1 << "attempts" << std::endl;
+                        throw ex;
+                    }
+                    std::cerr << ex->Message() << "\n\tattempting to recover" << std::endl;
+                    secondaries.clear();
+                }
+            }
+            if(interactionOccurred) {
+                HandleSecondaries(aParticle, secondaries, 1.);//todo: implement calculation speedup for rare processes using rate multiplier in RunParticle (see implementation in RunParticleWithVariableRate)
+                if (interaction->KillsPrimary())
+                    return;
+            }
+        }
+    }
+    fFinalParticles.push_back(aParticle);
+}
+
+void PropagationEngine::RunParticleWithVariableRate(Particle& aParticle, double aMaxRelRateError){
+    const int maxNumberOfStepAdjustments = 50;//2e-50 ~ 1e-15 which is double type accuracy
+    LOG_VERBOSE("RPWVR:0\t" + aParticle.ToString());
+    while((!fResult.AbortRequested()) && fResult.ContinuePropagation(aParticle))
+    {
+        LOG_VERBOSE("RPWVR:1\t" + aParticle.ToString());
+        if(fFilter && !fFilter->Pass(aParticle))
+            return;
+
+        double eLossRate1 = 0.;//total continuous energy loss rate at start point
+
+        cosmo_time maxDt = fResult.GetMinTravelTimeLeft(aParticle);//maximal step
+        ASSERT(maxDt>0);
+
+        vector<double> celRates1(fContinuousEnergyLosses.size(),0.);// continuous energy loss rates for separate processes at start point
+        int iRate = 0;
+        for(vector<PCELInteraction>::iterator it = fContinuousEnergyLosses.begin(); it != fContinuousEnergyLosses.end(); it++)
+        {
+            double rate = (*it)->Rate(aParticle);
+            celRates1[iRate++] = rate;
+            eLossRate1 += rate;
+        }
+
+        cosmo_time dtCEL = eLossRate1 >0 ? fAccuracy/ eLossRate1 : maxDt;//fAccuracy<1
+        if(dtCEL >maxDt)
+            dtCEL = maxDt;
+        vector<double> celRates2(fContinuousEnergyLosses.size(), 0.);
+        //find optimal step for CEL interaction
+        Particle part2 = aParticle;
+        LOG_VERBOSE("RPWVR:2\t" + aParticle.ToString() + "\tdtCel = " + ToString(dtCEL/units.Mpc));
+        for(int nSteps=0; true; nSteps++) {
+            move(part2, dtCEL, eLossRate1);
+            LOG_VERBOSE("RPWVR:2.1\t" + part2.ToString() + "\tdtCel = " + ToString(dtCEL/units.Mpc));
+            double eLossRate2 = 0.;
+            iRate = 0;
+            for (vector<PCELInteraction>::iterator it = fContinuousEnergyLosses.begin();
+                 it != fContinuousEnergyLosses.end(); it++) {
+                celRates2[iRate] = (*it)->Rate(part2);
+                eLossRate2 += celRates2[iRate];
+                LOG_VERBOSE("RPWVR:2.2\t" + part2.ToString() + "\tRelDiffCel[" + ToString(iRate) + "] = " + ToString(MathUtils::RelDifference(celRates2[iRate],celRates1[iRate])));
+                iRate++;
+            }
+            double relDif = MathUtils::RelDifference(eLossRate1,eLossRate2);
+            LOG_VERBOSE("RPWVR:2.2\t" + part2.ToString() + "\tRelDiffCel = " + ToString(relDif));
+            if(relDif<aMaxRelRateError)
+                break;
+            if(dtCEL<=fMinStep){
+                LOG_WARNING("CEL rate variance " + ToString(relDif) + " (limitted by step size)")
+                break;
+            }
+            if(nSteps==maxNumberOfStepAdjustments){
+                Exception::Throw("Unable to achieve desired CEL rate variance. Minimal value " + ToString(relDif) + "\nSet minimal step and/or decrease desired accuracy");
+            }
+            else// adjust step and repeat
+            {
+                dtCEL *= (0.5 * aMaxRelRateError / relDif);
+                part2 = aParticle;
+            }
+        };
+        LOG_VERBOSE("RPWVR:3\t" + aParticle.ToString() + "\tdtCel = " + ToString(dtCEL/units.Mpc));
+        double randRate1 = 0.;
+        vector<double> randRates1(fRandomInteractions.size());
+        iRate = 0;
+        for(vector<PRandomInteraction>::iterator it = fRandomInteractions.begin(); it != fRandomInteractions.end(); it++)
+        {
+            double rate = (*it)->Rate(aParticle)*fRandomInteractionRateMultipliers[iRate];
+            randRate1 += rate;
+            randRates1[iRate++] = rate;
+        }
+
+        vector<double> randRates2(fRandomInteractions.size(),0);
+        //double randRate2 = 0.;
+        double dtRand = dtCEL;
+        //vector<double> maxValidStep(fRandomInteractions.size(), dtCEL);
+        iRate = 0;
+        double relDif = -1.;
+        for(int nSteps=0; true; nSteps++) {
+            LOG_VERBOSE("RPWVR:4\t" + part2.ToString() + "\tdtRand/dtCEL = " + ToString(dtRand/dtCEL));
+            for(; iRate < fRandomInteractions.size(); iRate++)
+            {
+                randRates2[iRate] = fRandomInteractions[iRate]->Rate(part2)*fRandomInteractionRateMultipliers[iRate];
+                relDif = MathUtils::RelDifference(randRates1[iRate],randRates2[iRate]);
+                if(relDif>aMaxRelRateError){
+                    LOG_VERBOSE("RPWVR:4.1\tRelDiffCel[" + ToString(iRate) + "] = " + ToString(relDif));
+                    double dtMult = 0.5*aMaxRelRateError/relDif;
+                    dtRand *= dtMult;
+                    for(int iPrevIntr=0; iPrevIntr <iRate; iPrevIntr++){// (using linear interpolation to avoid recalculation of well known rates)
+                        randRates2[iPrevIntr] = randRates1[iPrevIntr] + (randRates2[iPrevIntr]-randRates1[iPrevIntr])*dtMult;
+                    }
+                    break;
+                }
+            }
+            if(iRate==fRandomInteractions.size())//all tests passed
+                break;
+
+            if(dtRand<=fMinStep){
+                LOG_WARNING("Rate variance " + ToString(relDif) + " (limitted by step size)")
+                break;
+            }
+            if(nSteps == maxNumberOfStepAdjustments){
+                Exception::Throw("Unable to achieve desired rate variance. Minimal value " + ToString(relDif) + "\nSet minimal step and/or decrease desired accuracy");
+            }
+            //else, adjust step and repeat
+            part2 = aParticle;
+            move(part2, dtRand, eLossRate1);
+        };
+        LOG_VERBOSE("RPWVR:5\t" + aParticle.ToString() + "\tdtRand = " + ToString(dtRand/units.Mpc));
+        double randRate2 = std::accumulate(randRates2.begin(), randRates2.end(), 0.);
+
+        double rand = -1;
+        RandomInteraction* interaction = 0;
+        double sec_weight = 1.;
+        double sqrt_noninteracting_weight = 1.;
+        double meanRandRate = 0.5*(randRate1+randRate2);
+        double dt = dtRand;
+        if(meanRandRate > 0)
+        {
+            double freePath = fRandomizer.GetFreePath(1./ meanRandRate, rand);
+            if(freePath <= dtRand)
+            {
+                dt = freePath;
+                //calculate mean rate and save it to randRates1 vector
+                double sumRates = 0.;
+                for(iRate = fRandomInteractions.size()-1; iRate>=0; iRate--){
+                    randRates1[iRate] += (dt/dtRand*(randRates2[iRate]-randRates1[iRate]));
+                    sumRates += randRates1[iRate];
+                }
+                //select interaction
+                double r = fRandomizer.Rand() * sumRates;
+                double totRate = 0.;
+                for(size_t i=0; i< randRates1.size(); i++)
+                {
+                    totRate += randRates1[i];
+                    if(totRate>=r)
+                    {
+                        interaction = fRandomInteractions[i];
+                        sec_weight = 1./fRandomInteractionRateMultipliers[i];
+                        break;
+                    }
+                }
+            }
+            else{
+                double ln_noninteracting_weight = 0.;
+                for(iRate = fRandomInteractions.size()-1; iRate>=0; iRate--){
+                    if(fRandomInteractionRateMultipliers[iRate]!=1.){
+                        ln_noninteracting_weight += (0.5*(randRates2[iRate]+randRates1[iRate])*(1.-1./fRandomInteractionRateMultipliers[iRate]));
+                    }
+                }
+                sqrt_noninteracting_weight = exp(0.5*ln_noninteracting_weight);
+            }
+
+        }
+        LOG_VERBOSE("RPWVR:6\t" + aParticle.ToString()+ "\tdt = " + ToString(dt/units.Mpc));
+        aParticle.Weight *= sqrt_noninteracting_weight;
+        move(aParticle, 0.5*dt, eLossRate1);
+        for(vector<PCELInteraction>::iterator it = fContinuousEnergyLosses.begin(); it != fContinuousEnergyLosses.end(); it++)
+        {
+            vector<Particle> secondaries;
+            (*it)->GetSecondaries(aParticle,secondaries, dt,fRandomizer);
+            HandleSecondaries(aParticle, secondaries, 1.);//currently continuous energy loss weighting is not implemented
+        }
+        move(aParticle, 0.5*dt, eLossRate1);
+        aParticle.Weight *= sqrt_noninteracting_weight;
+        LOG_VERBOSE("RPWVR:6.1\t" + aParticle.ToString());
+
+        if(interaction)
+        {
+            uint maxError_count = 3;
+            bool completed = false;
+            vector<Particle> secondaries;
+            bool interactionOccurred = false;
+            for(uint i=0; !completed; i++)
+            {
+                LOG_VERBOSE("RPWVR:7\t" + aParticle.ToString());
+                try {
+
+                    interactionOccurred = interaction->GetSecondaries(aParticle, secondaries,
+                                                                      fRandomizer);//may return false if particle energy is close to process threshold
+
+                    completed = true;
+                }catch(Exception* ex)
+                {
+                    if(i==maxError_count) {
+                        LOG_ERROR("recovery failed after " + ToString(maxError_count-1) + " attempts");
+                        throw ex;
+                    }
+                    LOG_WARNING(ex->Message() + "\tattempting to recover");
+                    secondaries.clear();
+                }
+            }
+            if(interactionOccurred) {
+                LOG_VERBOSE("RPWVR:8\t" + aParticle.ToString());
+                HandleSecondaries(aParticle, secondaries, sec_weight);
+                if (interaction->KillsPrimary()){
+                    LOG_VERBOSE("RPWVR:-2\t" + aParticle.ToString());
+                    return;
+                }
+            }
+        }
+        LOG_VERBOSE("RPWVR:9\t" + aParticle.ToString());
+    }
+    LOG_VERBOSE("RPWVR:10\t" + aParticle.ToString());
+    fFinalParticles.push_back(aParticle);
+    LOG_VERBOSE("RPWVR:-1\t" + aParticle.ToString());
+}
+
+void PropagationEngine::HandleSecondaries(Particle& aPrimary, vector<Particle>& aSecondaries, double aWeightMultiplier)
+{
+	long numberOfInteractions = aPrimary.Ninteractions + 1;
+	if(fFilter)
+		for(int i=aSecondaries.size()-1; i>=0; i--) {
+            aSecondaries[i].Weight *= aWeightMultiplier;
+            if (!fFilter->Pass(aSecondaries[i])) {
+                LOG_VERBOSE("HS:filter\t" + (aSecondaries.begin() + i)->ToString());
+                aSecondaries.erase(aSecondaries.begin() + i);
+            }
+		}
+	if(fThinning)
+		fThinning->Run(aSecondaries, fRandomizer);
+	for(std::vector<Particle>::iterator  it = aSecondaries.begin(); it != aSecondaries.end(); it++)
+	{
+        it->SetParent(aPrimary);
+        it->Ninteractions = numberOfInteractions;
+        fSecondaryParticles.push_back(*it);
+	}
+}
+
+}
diff --git a/src/lib/PropagationEngine.h b/src/lib/PropagationEngine.h
new file mode 100644
index 0000000..1fa1179
--- /dev/null
+++ b/src/lib/PropagationEngine.h
@@ -0,0 +1,92 @@
+/*
+ * PropagationEngine.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef PROPAGATIONENGINE_H
+#define	PROPAGATIONENGINE_H
+
+#include <vector>
+#include "Interaction.h"
+#include "Thinning.h"
+#include "Filter.h"
+#include "ParticleStack.h"
+#include "Randomizer.h"
+#include "Utils.h"
+#include "Logger.h"
+
+namespace mcray
+{
+
+class PropagationEngine {
+public:
+    PropagationEngine(ParticleStack &aStack, IResult &aResult, unsigned long int aRandSeed=0);
+    PropagationEngine(PropagationEngine &aCloneFrom, unsigned long int aRandSeed=0);
+    void Run(double aMaxRateVariation=0);
+    void RunMultithread(double aMaxRateVariation=0, int aNumberOfThreads = 0);
+    void RunMultithreadReleaseOnly(double aMaxRateVariation=0, int aNumberOfThreads = 0);
+    inline void SetThinning(IThinning* aThinning) {fThinning = aThinning;}
+
+    /// Set propagation filter (particles not passing the filter will be erased)
+    inline void SetPropagationFilter(IFilter* aFilter) {fFilter = aFilter;}
+    inline void AddInteraction(PCELInteraction aInt) {fContinuousEnergyLosses.push_back(aInt);}
+    inline void AddInteraction(PDeflectionInteraction aInt) {fDeflectionInteractions.push_back(aInt);}
+    inline void AddInteraction(PRandomInteraction aInt, double aRateMultiplier=1.) {//todo: implement calculation speedup for rare processes using rate multiplier in RunParticle (see implementation in RunParticleWithVariableRate)
+        fRandomInteractions.push_back(aInt);
+        fRandomInteractionRateMultipliers.push_back(aRateMultiplier);
+    }
+    //set minimal step for variable rate mode
+    inline void SetMinimalStep(double aMinStep) { fMinStep = aMinStep; }
+    inline void SetLogger(ILogger* aLogger){fLogger=aLogger;}
+    double GetInteractionRate(const Particle &aParticle) const;
+private:
+    void RunParticle(Particle& aParticle);
+    void move(Particle &aParticle, cosmo_time aDt, double aElossRate);
+
+    //special mode for highly variable rates
+    void RunParticleWithVariableRate(Particle& aParticle, double aMaxRelRateError);
+    void HandleSecondaries(Particle& aPrimary, std::vector<Particle>& aSecondaries, double aWeightMultiplier);
+    std::vector<PRandomInteraction>     fRandomInteractions;
+    std::vector<PDeflectionInteraction> fDeflectionInteractions;
+    std::vector<PCELInteraction>        fContinuousEnergyLosses;
+    std::vector<double>                 fRandomInteractionRateMultipliers;//useful for very rare interactions
+    IThinning                           *fThinning;
+    SmartPtr<IFilter>					fFilter;
+    ParticleStack                       &fStack;
+    IResult                             &fResult;
+    Randomizer                          fRandomizer;
+    double                              fAccuracy;
+    double                              fMinStep;
+    std::vector<Particle>               fSecondaryParticles;
+    std::vector<Particle>               fFinalParticles;
+    static unsigned long int			fLastRandSeed;
+    bool                                fInfoPrinted;
+    ILogger*                            fLogger;
+};
+
+}
+#endif	/* PROPAGATIONENGINE_H */
+
diff --git a/src/lib/ProtonPP.cpp b/src/lib/ProtonPP.cpp
new file mode 100644
index 0000000..5c8791d
--- /dev/null
+++ b/src/lib/ProtonPP.cpp
@@ -0,0 +1,341 @@
+/*
+ * ProtonPP.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "ProtonPP.h"
+#include <fstream>
+
+//used for unit test only
+#include "PropagationEngine.h"
+
+namespace Interactions {
+using namespace mcray;
+
+ProtonPPSigma::ProtonPPSigma()
+{
+	double Me = Particle::Mass(Electron);
+	double Mp = Particle::Mass(Proton);
+	fMpMe_1 = 1./(Me*Mp);
+	fThresholdS=2.*Me + Mp;
+	fThresholdS *= fThresholdS;
+	fMp2 = Mp*Mp;
+	fCoef = AlphaEM*AlphaEM*AlphaEM/Me/Me; //alpha*Z^2*r_0^2 == alpha^3*Z^2/m_e^2
+}
+
+double ProtonPPSigma::sigmaLab(double k) const
+{
+	double result = 0;
+	if(k<4)//using formula (A1) from M. J. Chodorowski, A. A. Zdziarski, and M. Sikora, Astrophys. J. 400, 181 (1992)
+	{
+		double eta = (k - 2.)/(k+2);
+		double eta2 = eta*eta;
+		double kk = (k - 2.)/k;
+		//2pi/3*fCoef * kk^3 * (1+1/2 eta + 23/40 eta^2 + 37/120 eta^3 + 61/192 eta^4)
+		result = 2.0943951023932*fCoef*kk*kk*kk*(1.+0.5*eta+0.575*eta2+
+				0.308333333333333*eta2*eta+0.317708333333333*eta2*eta2);
+	}
+	else
+	{//using formula (A2) from M. J. Chodorowski, A. A. Zdziarski, and M. Sikora, Astrophys. J. 400, 181 (1992)
+		double ln2k = log(2.*k);
+		double ln2k2 = ln2k*ln2k;
+		double coef2 = 6.*ln2k + 0.666666666666667*ln2k2*ln2k-ln2k2-3.28986813369645*ln2k + 0.549047873167415;
+		double coef4 = -(0.1875*ln2k+0.125);
+		double coef6 = -(0.0125868055555556*ln2k-0.00557002314814815);
+		double r2 = 4./k/k;
+		double r4 = r2*r2;
+		result = fCoef*(3.11111111111111*ln2k-8.07407407407407+r2*coef2+r4*coef4+r2*r4*coef6);
+	}
+	ASSERT_VALID_NO(result);
+	return result;
+}
+
+double ProtonPPSigma::f(double s) const
+{
+	if(s<=fThresholdS)
+		return 0.;
+
+	double k = 0.5*(s - fMp2)*fMpMe_1; // photon energy in proton rest frame in units of electron mass
+	return sigmaLab(k);
+}
+
+ProtonPP::ProtonPP(BackgroundIntegral* aBackground):
+fBackground(aBackground)
+{
+}
+
+ProtonPP::~ProtonPP() {
+}
+
+double ProtonPP::Rate(const Particle& aParticle) const
+{
+	if(aParticle.Type != Proton)
+		return 0.;
+	return fBackground->GetRateS(fSigma, aParticle);
+}
+
+bool ProtonPP::SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const
+{
+	ASSERT(aParticle.Type == Proton);
+	aS = 0.;
+	return (fBackground->GetRateAndSampleS(fSigma, aParticle, aRandomizer, aS)!=0);
+}
+
+double ProtonPP::maxPPPemaxError = 0.;
+
+void ProtonPP::SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const
+{
+	ASSERT(aParticle.Type == Proton);
+	Particle secProton = aParticle;
+	Particle secElectron = aParticle;
+	Particle secPositron = aParticle;
+	secElectron.Type = Electron;
+	secPositron.Type = Positron;
+	double E = aParticle.Energy;
+	double Mp = Particle::Mass(Proton);
+	double k = 0.5*(aS/Mp-Mp);
+	double Me = Particle::Mass(Electron);
+	double betaP = aParticle.beta();
+	double gammaP = E / Mp;
+	double mFrac = Mp/Me;
+	double km = k/Me;
+	double c1 = mFrac*km-2.;
+
+	///formula (3.28) J. W. Motz, H. A. Olsen, and H. W. Koch, Rev. Mod. Phys. 41, 581 (1969).
+	double pMaxProtonLab = (km*(mFrac*(km+mFrac)-2)+(km+mFrac)*sqrt(c1*c1-4.*mFrac*mFrac))/mFrac/(2.*km+mFrac)*Me;
+	ASSERT_VALID_NO(pMaxProtonLab);
+	double EmaxProtonLab = sqrt(pMaxProtonLab*pMaxProtonLab+Mp*Mp);
+	double EminProton = gammaP*(EmaxProtonLab - betaP*pMaxProtonLab);
+
+	///formula (3.43a) J. W. Motz, H. A. Olsen, and H. W. Koch, Rev. Mod. Phys. 41, 581 (1969).
+	double c2 = km+mFrac;
+	double c3 = mFrac*c2*(km-1.);
+	double c4 = km*sqrt(km*km + mFrac*mFrac*km*(km-2.));
+	double c5 = (c2*c2-km*km);
+
+	double EeLabMaxAntiparallelToK = (c3-c4)/c5*Me;
+	double EeLabMaxParallelToK = (c3+c4)/c5*Me;
+
+	double PeLabMaxAntiparallelToK = (EeLabMaxAntiparallelToK>1.001*Me) ? sqrt(EeLabMaxAntiparallelToK*EeLabMaxAntiparallelToK - Me*Me) : 0.;
+	double PeLabMaxParallelToK = (EeLabMaxParallelToK>1.001*Me) ? sqrt(EeLabMaxParallelToK*EeLabMaxParallelToK - Me*Me) : 0.;
+	double Emin = gammaP*(EeLabMaxParallelToK - betaP*PeLabMaxParallelToK);
+	double Emax = gammaP*(EeLabMaxAntiparallelToK + betaP*PeLabMaxAntiparallelToK);
+	if(Emin>Emax)
+	{//this may occur if EeLabMaxAntiparallelToK << Me, i.e. close to threshold
+		ASSERT(EeLabMaxAntiparallelToK<1.001*Me);
+		double tmp = Emin;
+		Emin = Emax;
+		Emax = tmp;
+	}
+
+	ASSERT(Emin>Me);
+	ASSERT(Emax<E);
+
+	if(E < (EminProton+Emax+Emin))
+	{
+		//ASSERT(EminProton+Emax+Emin<E);
+		double correctedEmax = E - EminProton - Emin;
+		double relError = (Emax-correctedEmax)/Emax;
+		if(relError>maxPPPemaxError)
+		{
+			std::cerr << "PPP Emax error " << relError << std::endl;
+			maxPPPemaxError = relError;
+		}
+		Emax = correctedEmax;
+	}
+
+	double E1 = SampleE(Emin, Emax, aRandomizer);
+	double E2max = E - E1 - EminProton;
+	if(E2max>Emax)
+		E2max = Emax;
+	double E2 = SampleE(Emin, E2max, aRandomizer);
+	if(aRandomizer.Rand()>0.5)
+	{//Randomly choose which particle has energy E1 and which E2
+		double tmp = E1;
+		E1 = E2; E2 = tmp;
+	}
+	secElectron.Energy = E1;
+	secPositron.Energy = E2;
+	secProton.Energy = E - E1 - E2;
+	aSecondaries.push_back(secElectron);
+	aSecondaries.push_back(secPositron);
+	aSecondaries.push_back(secProton);
+}
+
+void ProtonPP::UnitTest()
+{
+	double Zmax = 1;
+	double Emin = 1e6*units.eV;
+	double Emax = 1e19*units.eV;
+	int Nparticles = 1000;
+	std::string outputDir = "testProtonPP";
+	unsigned int kAc = 1;
+	double epsRel = 1e-3;
+	double stepZ = Zmax<0.05 ? Zmax/2 : 0.025;
+	double logStepK = pow(10,0.05/kAc);
+	if(!cosmology.IsInitialized())
+		cosmology.Init(Zmax + 10);
+	double cmbTemp = 2.73*units.K;
+	double alphaThinning = 0.5;
+	CompoundBackground backgr;
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp, 0., Zmax + 1.));
+	//IR/O component
+	backgr.AddComponent(new GaussianBackground(0.1*units.eV, 0.01*units.eV, 5, 1./units.cm3, 0, Zmax + 1.));
+	PBackgroundIntegral backgrI(new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel));
+	int seed = 2015;
+	Result result(new EnergyBasedFilter(Emin, M_PI), true);
+
+	SmartPtr<RawOutput> pOutput = new RawOutput(outputDir, false);
+	result.AddOutput(pOutput);
+	ParticleStack particles;
+	PropagationEngine pe(particles, result, seed);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+
+	Particle proton(Proton, Zmax);
+	int nSteps = log(Emax/Emin)/log(logStepK)+1.;
+	double mult = pow(Emax/Emin, 1./nSteps);
+	{
+		std::ofstream rateOut;
+		rateOut.open((outputDir + "/PPP").c_str(),std::ios::out);
+		PRandomInteraction i = new ProtonPP(backgrI);
+		pe.AddInteraction(i);
+		int step=0;
+		for(proton.Energy=Emax/100; step<=nSteps; proton.Energy*=mult, step++)
+			rateOut << proton.Energy/units.eV << "\t" << i->Rate(proton)*units.Mpc << "\n";
+		rateOut.close();
+	}
+	Randomizer rand;
+	CosmoTime tEnd;
+	proton.Energy = Emax;
+	pOutput->SetOutputDir(outputDir + "/z0");
+	result.SetEndTime(tEnd);
+
+	for(int i=0; i<Nparticles; i++)
+	{
+		particles.AddPrimary(proton);
+	}
+	pe.RunMultithreadReleaseOnly();
+}
+
+double ProtonPP::SampleE(double aEmin, double aEmax, Randomizer& aRandomizer) const
+{//assuming dif sigma is proportional to E^{-7/4}
+	double r = aRandomizer.Rand();
+	double E = pow(pow(aEmin,-0.75)*(1.-r) + pow(aEmax,-0.75)*r, -4./3.);
+	ASSERT(E>=aEmin && E<=aEmax);
+	return E;
+}
+
+RandomInteraction* ProtonPP::Clone() const
+{
+	return new ProtonPP(fBackground->Clone());
+}
+
+ProtonPPcel::SigmaE::SigmaE()
+{
+	fInelCoef = 4.*Particle::Mass(Electron)/Particle::Mass(Proton);
+}
+
+double ProtonPPcel::SigmaE::f(double s) const
+{
+	double k = 0.5*(s - fMp2)*fMpMe_1; // photon energy in p-rest frame in units of electron mass
+	if(k<=2.)
+		return 0.;
+
+	//using formulas (3.7)-(3.10) from M. J. Chodorowski, A. A. Zdziarski, and M. Sikora, Astrophys. J. 400, 181 (1992)
+	double inelasticity = 0.;
+	if(k<1000.)
+	{
+		double ln = log(k-1);
+		double lnI = 1.;
+		for(int i=0; i<4; i++, lnI *= ln)
+			inelasticity += lnI*a[i];
+	}
+	else
+	{
+		double ln = log(k);
+		double lnI = 1.;
+		for(int i=0; i<4; i++, lnI *= ln)
+			inelasticity += lnI*b[i];
+		inelasticity /= (3.11111111111111*(ln + M_LN2)-8.07407407407407);
+	}
+	inelasticity *= (fInelCoef/k);
+	ASSERT_VALID_NO(inelasticity);
+	return ProtonPPSigma::sigmaLab(k)*inelasticity;
+}
+
+//constants given by (3.8) from M. J. Chodorowski, A. A. Zdziarski, and M. Sikora, Astrophys. J. 400, 181 (1992)
+const double ProtonPPcel::SigmaE::a[] = {1, 0.3958, 0.1, 0.00781};
+
+//constants given by (3.10) from M. J. Chodorowski, A. A. Zdziarski, and M. Sikora, Astrophys. J. 400, 181 (1992)
+const double ProtonPPcel::SigmaE::b[] = {-8.778, 5.512, -1.614, 0.666666666666667};
+
+ProtonPPcel::ProtonPPcel(BackgroundIntegral* aBackground):
+fBackground(aBackground)
+{
+}
+
+ProtonPPcel::~ProtonPPcel() {
+}
+
+void ProtonPPcel::UnitTest()
+{
+	std::ofstream rateOut;
+	rateOut.open("ppp_test",std::ios::out);
+	SigmaE sigmaE;
+	ProtonPPSigma sigma;
+	double Mp = Particle::Mass(Proton);
+	double Me = Particle::Mass(Electron);
+	double coef = 3.88593900935001e-07/Mp/Me;
+	for(double k = 2.; k<=1e4; k *= 1.58489319246111)
+	{//compare sigK to dashed curve from figure 2 of M. J. Chodorowski, A. A. Zdziarski, and M. Sikora, Astrophys. J. 400, 181 (1992)
+		double s = Mp*(Mp + 2.*k*Me);
+		double sig = sigma(s)/coef;
+		double sigK = sigmaE(s)/coef;
+		rateOut << k << "\t" << sig << "\t" << sigK << "\n";
+	}
+	rateOut.close();
+}
+
+double ProtonPPcel::Rate(const Particle& aParticle) const
+{
+	if(aParticle.Type != Proton)
+		return 0.;
+
+	return fBackground->GetRateS(fSigma, aParticle);
+}
+
+void ProtonPPcel::GetSecondaries(const Particle& aParticle, std::vector<Particle>& aSecondaries,
+				cosmo_time aDeltaT, Randomizer& aRandomizer) const
+{
+
+}
+
+CELInteraction* ProtonPPcel::Clone() const
+{
+	return new ProtonPPcel(fBackground->Clone());
+}
+
+} /* namespace Interactions */
diff --git a/src/lib/ProtonPP.h b/src/lib/ProtonPP.h
new file mode 100644
index 0000000..a1078ec
--- /dev/null
+++ b/src/lib/ProtonPP.h
@@ -0,0 +1,102 @@
+/*
+ * ProtonPP.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef PROTONPP_H_
+#define PROTONPP_H_
+
+#include "Interaction.h"
+#include "GammaPP.h"
+
+namespace Interactions {
+
+using namespace mcray;
+
+class ProtonPPSigma : public Utils::Function, EMInteraction
+{
+public:
+	ProtonPPSigma();
+	double f(double s) const;
+	double sigmaLab(double k) const;
+	double Xmin() const { return fThresholdS; }
+protected:
+	double fThresholdS;
+	double fMpMe_1;
+	double fMp2;
+	double fCoef;
+};
+
+	/// This is outdated implementation of pair production by protons
+	/// Use PPP class instead unless you are sure what you are doing
+class ProtonPP : public RandomInteractionS{
+public:
+	ProtonPP(BackgroundIntegral* aBackground);
+	virtual double Rate(const Particle& aParticle) const;
+	virtual ~ProtonPP();
+	virtual bool SampleS(const Particle& aParticle, double& aS, Randomizer& aRandomizer) const;
+	virtual void SampleSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, double aS, Randomizer& aRandomizer) const;
+	virtual RandomInteraction* Clone() const;
+	static void UnitTest();
+private:
+	double SampleE(double aEmin, double aEmax, Randomizer& aRandomizer) const;
+	SmartPtr<BackgroundIntegral>	fBackground;
+	ProtonPPSigma 			fSigma;
+	static double 					maxPPPemaxError;
+};
+
+///Used from PPP class to speed-up simulations
+class ProtonPPcel : public CELInteraction
+{
+	/*!
+	 * 2/E * Integrate[E_e * dSigma/dE_e,{E_e,Emin,Emax}]
+	 * */
+	class SigmaE : public ProtonPPSigma
+	{
+	public:
+		SigmaE();
+		double f(double s) const;
+	private:
+		static const double a[];
+		static const double b[];
+		double fInelCoef;
+	};
+public:
+	static void UnitTest();
+	ProtonPPcel(BackgroundIntegral* aBackground);
+	virtual ~ProtonPPcel();
+	double Rate(const Particle& aParticle) const;
+	virtual void GetSecondaries(const Particle& aParticle, std::vector<Particle>& aSecondaries,
+								cosmo_time aDeltaT, Randomizer& aRandomizer) const;
+
+	CELInteraction* Clone() const;
+private:
+	SmartPtr<BackgroundIntegral>	fBackground;
+	SigmaE							fSigma;
+};
+
+} /* namespace Interactions */
+#endif /* PROTONPP_H_ */
diff --git a/src/lib/Randomizer.cpp b/src/lib/Randomizer.cpp
new file mode 100644
index 0000000..3792adc
--- /dev/null
+++ b/src/lib/Randomizer.cpp
@@ -0,0 +1,97 @@
+/*
+ * Randomizer.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "Randomizer.h"
+#include <math.h>
+#include "Utils.h"
+#include "gsl/gsl_errno.h"
+#include "gsl/gsl_math.h"
+#include "gsl/gsl_rng.h"
+
+namespace mcray
+{
+Randomizer::Randomizer(unsigned long int aSeed):
+		fRand2(0)
+{
+	/*
+The following table shows the relative performance of a selection the available random number generators.
+The fastest simulation quality generators are taus, gfsr4 and mt19937.
+The generators which offer the best mathematically-proven quality are those based on the ranlux algorithm.
+
+     1754 k ints/sec,    870 k doubles/sec, taus
+     1613 k ints/sec,    855 k doubles/sec, gfsr4
+     1370 k ints/sec,    769 k doubles/sec, mt19937
+      565 k ints/sec,    571 k doubles/sec, ranlxs0
+      400 k ints/sec,    405 k doubles/sec, ranlxs1
+      490 k ints/sec,    389 k doubles/sec, mrg
+      407 k ints/sec,    297 k doubles/sec, ranlux ---- buggy one, don't use
+      243 k ints/sec,    254 k doubles/sec, ranlxd1
+      251 k ints/sec,    253 k doubles/sec, ranlxs2
+      238 k ints/sec,    215 k doubles/sec, cmrg
+      247 k ints/sec,    198 k doubles/sec, ranlux389
+      141 k ints/sec,    140 k doubles/sec, ranlxd2
+	*/
+	fRand = gsl_rng_alloc (gsl_rng_ranlxd2);//gsl_rng*
+	if(aSeed>0)//use aSeed=0 and GSL_RNG_SEED environment variable to overwrite seed at runtime
+		gsl_rng_set ((gsl_rng*)fRand, aSeed);
+}
+
+Randomizer::~Randomizer() {
+	gsl_rng_free((gsl_rng*)fRand);
+	if(fRand2)
+		gsl_rng_free((gsl_rng*)fRand2);
+}
+
+double Randomizer::GetFreePath(double aMeanFreePath, double& aRand)
+{
+	if(aRand<0 || aRand>1)
+		aRand = Rand();
+    if(aRand==0)
+    	return 1e300;
+    return aMeanFreePath * log(1./aRand);
+}
+
+double Randomizer::Rand()
+{
+	return gsl_rng_uniform_pos ((gsl_rng*)fRand);
+}
+
+unsigned long int Randomizer::CreateIndependent()
+{
+	if(!fRand2)
+	{
+		fRand2 = gsl_rng_alloc (gsl_rng_ranlux389);//must be different type from fRand
+		unsigned long int max2 = gsl_rng_max ((gsl_rng*)fRand2);
+		unsigned long int max1 = gsl_rng_max ((gsl_rng*)fRand);
+		fRand2Max = max1<max2?max1:max2;
+		gsl_rng_set ((gsl_rng*)fRand2, gsl_rng_uniform_int((gsl_rng*)fRand, fRand2Max));
+	}
+	return gsl_rng_uniform_int((gsl_rng*)fRand2, fRand2Max);
+}
+
+}
diff --git a/src/lib/Randomizer.h b/src/lib/Randomizer.h
new file mode 100644
index 0000000..69f8ce5
--- /dev/null
+++ b/src/lib/Randomizer.h
@@ -0,0 +1,58 @@
+/*
+ * Randomizer.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef RANDOMIZER_H
+#define	RANDOMIZER_H
+
+namespace mcray
+{
+///not expected to be thread-safe
+class Randomizer {
+public:
+    Randomizer(unsigned long int aSeed=0);
+    virtual ~Randomizer();
+    double Rand();//generate random number in the range [0,1]
+
+	/*!
+	@param[in]  aMeanFreePath mean free path of a particle
+	@param[in,out]  aRand random number used for sampling. If 0<aRand<1 than input value is used for sampling,
+	otherwise randomly generated number is used and returned as output parameter
+	@return sampled value of the free path
+	*/
+    double GetFreePath(double aMeanFreePath, double& aRand);
+
+    //generate seed for independent Randomizer
+    unsigned long int CreateIndependent();
+private:
+    void* fRand;
+    void* fRand2;//used in CreateIndependent() only
+    unsigned long int fRand2Max;
+};
+}
+#endif	/* RANDOMIZER_H */
+
diff --git a/src/lib/Sophia.cpp b/src/lib/Sophia.cpp
new file mode 100644
index 0000000..375e9af
--- /dev/null
+++ b/src/lib/Sophia.cpp
@@ -0,0 +1,180 @@
+/*
+ * Sophia.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Sophia.h"
+
+extern "C"
+   {
+		void sample_photopion_(int& aPrimary, double& aEnergyGeV, double& aEpsilonGeV, double& aThetaDeg, int& aNoSecondaries, double aSecEnergies[], int aSecTypes[]);
+
+		void sample_photopion_rel_(int& aPrimary, double& aEpsPrimeGeV, int& aNoSecondaries, double aSecEfrac[], int aSecTypes[]);
+
+
+		//initialize SOPHIA
+		//L0 - primary particle type (13 = proton, 14 = neutron)
+		void initial_(int& L0);
+
+		//double crossection_(double x, double NDIR, int NL0);
+
+		void crossec_(int& L0, double& eps_prime, double& sigma);
+
+		void get_mass_(int& L0,double& m);
+
+		void set_random_seed_(int& aSeed);
+   }
+
+namespace Interactions {
+	using namespace mcray;
+	using namespace Utils;
+
+	int SOPHIA::toSOPHIA(ParticleType aType) {
+		switch (aType) {
+			case Proton:
+				return 13;
+			case Neutron:
+				return 14;
+			case Photon:
+				return 1;
+			case Positron:
+				return 2;
+			case Electron:
+				return 3;
+			case NeutrinoE:
+				return 15;
+			case NeutrinoAE:
+				return 16;
+			case NeutrinoM:
+				return 17;
+			case NeutrinoAM:
+				return 18;
+			default:
+				Exception::Throw("unsupported particle type");
+				break;
+		}
+		return -1;
+	}
+
+	ParticleType SOPHIA::fromSOPHIA(int aType) {
+		switch (aType) {
+			case 1:
+				return Photon;
+			case 2:
+				return Positron;
+			case 3:
+				return Electron;
+			case 13:
+				return Proton;
+			case 14:
+				return Neutron;
+			case 15:
+				return NeutrinoE;
+			case 16:
+				return NeutrinoAE;
+			case 17:
+				return NeutrinoM;
+			case 18:
+				return NeutrinoAM;
+		}
+		return ParticleTypeEOF;
+	}
+
+	void SOPHIA::Init(int aPrimary) {
+		ASSERT(aPrimary == 13 || aPrimary == 14);
+		if (aPrimary != LastInit) {
+			initial_(aPrimary);
+			LastInit = aPrimary;
+		}
+	}
+
+	void SOPHIA::SamplePhotopion(ParticleType aPrimary, double aEnergyGeV, double aEpsilonGeV, double aThetaDeg,
+								 int &aNoSecondaries, double aSecEnergies[], int aSecTypes[]) {
+		ASSERT(aPrimary == Proton || aPrimary == Neutron);
+		int primary = toSOPHIA(aPrimary);
+		Init(primary);
+		sample_photopion_(primary, aEnergyGeV, aEpsilonGeV, aThetaDeg, aNoSecondaries, aSecEnergies, aSecTypes);
+		for (int i = 0; i < aNoSecondaries; i++)
+			aSecTypes[i] = fromSOPHIA(aSecTypes[i]);
+	}
+
+	double SOPHIA::CrossSection(int aPrimary, double aEpsPrimeGeV) {
+		Init(aPrimary);
+		double result = 0;
+		crossec_(aPrimary, aEpsPrimeGeV, result);
+		return result;
+	}
+
+	const int SOPHIA::MaxNumberOfProducts = 2000;
+	int SOPHIA::LastInit = -1;
+
+	SophiaCS::SophiaCS(ParticleType aPrimary) :
+			fPrimary(SOPHIA::toSOPHIA(aPrimary)) {
+		const double Smin = 1.1646;
+		ASSERT(fPrimary == 13 || fPrimary == 14);
+		double pm = fPrimary == 13 ? 0.93827 : 0.93957;
+		fXmin = 0.5 * (Smin / pm - pm);
+	}
+
+	double SophiaCS::f(double _x) const {
+		return SOPHIA::CrossSection(fPrimary, _x);
+	}
+
+	double SophiaCS::Xmin() const {
+		return fXmin;
+	}
+
+	void SOPHIA::SamplePhotopionRel(mcray::ParticleType aPrimary, double aEpsPrimeGeV, int &aNoSecondaries,
+									double aSecEfrac[], int aSecTypes[]) {
+		ASSERT(aPrimary == Proton || aPrimary == Neutron);
+		int primary = toSOPHIA(aPrimary);
+		Init(primary);
+		sample_photopion_rel_(primary, aEpsPrimeGeV, aNoSecondaries, aSecEfrac, aSecTypes);
+		for (int i = 0; i < aNoSecondaries; i++)
+			aSecTypes[i] = fromSOPHIA(aSecTypes[i]);
+	}
+
+	bool SOPHIA::RandomSeedSet = false;
+
+	void SOPHIA::SetRandomSeed(int aSeed) {
+		bool doJob = true;
+#pragma omp critical (SOPHIA)
+		{
+			if (RandomSeedSet)
+				doJob = false;//initialize only once
+			else
+				RandomSeedSet = true;
+		}
+		if(doJob)
+			set_random_seed_(aSeed);
+	}
+
+	double SOPHIA::Mass(mcray::ParticleType aParticle) {
+		int particle = toSOPHIA(aParticle);
+		double mGeV = 0.;
+		get_mass_(particle, mGeV);
+		return mGeV * units.GeV;
+	}
+}
diff --git a/src/lib/Sophia.h b/src/lib/Sophia.h
new file mode 100644
index 0000000..f558c85
--- /dev/null
+++ b/src/lib/Sophia.h
@@ -0,0 +1,82 @@
+/*
+ * Sophia.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef SOPHIA_H_
+#define SOPHIA_H_
+
+#include "Particle.h"
+#include "MathUtils.h"
+
+namespace Interactions {
+	using namespace mcray;
+
+///stub for calls to SOPHIA
+///the class is not thread-safe
+	class SOPHIA {
+		static int LastInit;
+		static bool RandomSeedSet;
+
+		static void Init(int aPrimary);
+
+	public:
+		static const int MaxNumberOfProducts;
+
+		static int toSOPHIA(mcray::ParticleType aType);
+
+		static mcray::ParticleType fromSOPHIA(int aType);
+
+		//this method is not thread-safe!!!
+		static void SamplePhotopion(mcray::ParticleType aPrimary, double aEnergyGeV, double aEpsilonGeV,
+									double aThetaDeg, int &aNoSecondaries, double aSecEnergies[], int aSecTypes[]);
+
+		//this method is not thread-safe!!!
+		//sample energies of secondaries in units of primary energy in ultrarelativistic limit
+		static void SamplePhotopionRel(mcray::ParticleType aPrimary, double aEpsPrimeGeV, int &aNoSecondaries,
+									   double aSecEfrac[], int aSecTypes[]);
+
+		//this method is not thread-safe
+		static double CrossSection(int aPrimary, double aEpsPrimeGeV);
+
+		static double Mass(mcray::ParticleType aParticle);
+
+		static void SetRandomSeed(int aSeed);
+	};
+
+	class SophiaCS : public Utils::Function {
+	public:
+		SophiaCS(mcray::ParticleType aPrimary);
+
+		virtual double f(double _x) const;
+
+		virtual double Xmin() const;
+
+	private:
+		double fXmin;
+		int fPrimary;
+	};
+}
+#endif /* SOPHIA_H_ */
diff --git a/src/lib/Stecker16Background.cpp b/src/lib/Stecker16Background.cpp
new file mode 100644
index 0000000..6f27e15
--- /dev/null
+++ b/src/lib/Stecker16Background.cpp
@@ -0,0 +1,60 @@
+/*
+ * Stecker16Background.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "Stecker16Background.h"
+#include "Units.h"
+
+namespace Backgrounds {
+
+    Stecker16LowerBackground::Stecker16LowerBackground() :
+            Stecker16Background(TABLES_DIR  "stecker_ebl16/comoving_enerdens_lo.csv", "Stecker_16_lower") {
+}
+
+Stecker16UpperBackground::Stecker16UpperBackground() :
+        Stecker16Background(TABLES_DIR "stecker_ebl16/comoving_enerdens_up.csv", "Stecker_16_upper")
+{
+}
+
+
+double Stecker16Background::n(double E, double z) const {
+    // matrix function returns the spectrum E*dn/dE in sm^-3 [E]=eV in comoving volume
+    double result = fMatrixFunction.f(z, E/units.eV) / units.Hz_photon * units.erg;
+    double dl = (1.+z);
+    //convert to physical density in internal units
+    return dl*dl*dl/units.cm3*result;
+}
+
+int Stecker16Background::UnitTest()
+{
+    Stecker16UpperBackground backgrUpper;
+    BackgroundUtils::UnitTest(backgrUpper);
+    Stecker16LowerBackground backgrLower;
+    BackgroundUtils::UnitTest(backgrLower);
+}
+
+}
\ No newline at end of file
diff --git a/src/lib/Stecker16Background.h b/src/lib/Stecker16Background.h
new file mode 100644
index 0000000..b996e01
--- /dev/null
+++ b/src/lib/Stecker16Background.h
@@ -0,0 +1,82 @@
+/*
+ * Stecker16Background.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef PROPAGATION_STECKER16BACKGROUND_H
+#define PROPAGATION_STECKER16BACKGROUND_H
+
+#include "Background.h"
+//#include "Vector.h"
+//#include "TableFunc.h"
+#include <string>
+#include <vector>
+//#include "DataReader.h"
+using namespace std;
+
+#include "Background.h"
+
+namespace Backgrounds {
+
+    using namespace mcray;
+
+    class Stecker16Background : public IBackground {
+    public:
+        Stecker16Background(string aTableFile, std::string aName) :
+                fMatrixFunction(aTableFile),
+                fName(aName)
+        {}
+
+        static int UnitTest();
+/*!
+	 Calculate density E*dn(E)/dE at given redshift in internal units. Physical density should be returned, not a comoving one
+	 @param[in]  aE  background photon energy (in internal units)
+	 @param[in]  aZ  red shift
+	 @return background photon density E*dn(E)/dE in internal units
+*/
+        virtual double n(double aE, double aZ) const;
+        virtual double MinE() const { return fMatrixFunction.MinArg(2)*units.eV; }
+        virtual double MaxE() const { return fMatrixFunction.MaxArg(2)*units.eV; }
+        virtual double MinZ() const { return fMatrixFunction.MinArg(1); }
+        virtual double MaxZ() const { return fMatrixFunction.MaxArg(1); }
+        virtual std::string Name() const { return fName; };
+
+    private:
+        MatrixFunction fMatrixFunction;
+        std::string fName;
+    };
+
+    class Stecker16LowerBackground : public Stecker16Background {
+    public:
+        Stecker16LowerBackground();
+    };
+
+    class Stecker16UpperBackground : public Stecker16Background {
+    public:
+        Stecker16UpperBackground();
+    };
+}
+#endif //PROPAGATION_STECKER16BACKGROUND_H
diff --git a/src/lib/SteckerEBL.cpp b/src/lib/SteckerEBL.cpp
new file mode 100644
index 0000000..4064ead
--- /dev/null
+++ b/src/lib/SteckerEBL.cpp
@@ -0,0 +1,166 @@
+/*
+ * SteckerEBL.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include <algorithm>
+#include "SteckerEBL.h"
+#include "TableBackgrounds.h"
+
+using namespace mcray;
+
+namespace Backgrounds {
+
+#define IRO_DATA_FILE "iro_stecker2005"
+    Stecker2005EBL::Stecker2005EBL():
+            CTableWithHeaderReader(TABLES_DIR IRO_DATA_FILE),
+    m_accuracyE(0),
+    m_accuracyZ(0),
+    m_curE(0)
+{
+    read();
+}
+
+Stecker2005EBL::~Stecker2005EBL()
+{
+
+}
+
+double Stecker2005EBL::n(double aE, double aZ) const
+{
+    aE /= units.eV;//convert internal units to eV
+    int iZ=0;
+    //	int iE=-1;
+    if(aZ < 1e-4)//z=0
+    {
+        return m_fE[0]->f(aE);// logarithmic approximation on E
+    }
+    //iZ=m_zArray.findLeftX(z);
+    //std::vector<double>::iterator it=std::find_if(m_zArray.begin(),m_zArray.end(),[](double x){return (x > z);});
+    iZ=TableFunction::FindLeftX(m_zArray, aZ);
+
+    if(iZ<0)
+        return 0.;
+
+    double n1 = m_fE[iZ]->f(aE);// logarithmic approximation on E
+    double n2 = m_fE[iZ+1]->f(aE);
+    double z1 = m_zArray[iZ];
+    double z2 = m_zArray[iZ+1];
+
+    double result = n1 +  (n2-n1)/(z2-z1)*(aZ - z1);  // simple linear approximation on z
+
+    return result;
+}
+
+std::string Stecker2005EBL::Name() const
+{
+    return "Stecker05";
+}
+
+double Stecker2005EBL::MinE() const
+{
+    return m_eArray[0]*units.eV;
+}
+
+double Stecker2005EBL::MaxE() const
+{
+    return m_eArray[m_accuracyE-1]*units.eV;
+}
+
+double Stecker2005EBL::MaxZ() const
+{
+    return m_zArray[m_accuracyZ-1];
+}
+
+double Stecker2005EBL::MinZ() const
+{
+    return 0;
+}
+
+CTableWithHeaderReader::ECompleteStatus Stecker2005EBL::readHeaderLine(const char* theString)
+{
+    if (m_accuracyE<=0)
+    {
+        return readDataLength(m_accuracyE,theString)?notCompletedE:failedE;
+    }
+
+    if (!readVector(m_zArray, theString))
+    {
+        return failedE;
+    }
+
+    m_eArray.insert(m_eArray.begin(),m_accuracyE,0.0);
+    m_accuracyZ = m_zArray.size();
+    std::vector<double> v;
+    m_nArray.insert(m_nArray.begin(),m_accuracyZ,v);
+    for(size_t i=0; i<m_accuracyZ; i++)
+        m_nArray[i].insert(m_nArray[i].begin(), m_accuracyE, 0.0);
+
+    SafePtr<TableFunction> ptr;
+    m_fE.insert(m_fE.begin(),m_accuracyZ, ptr);
+
+    return completedE;
+}
+
+bool Stecker2005EBL::readDataLine(const char* theString)
+{
+    if (m_curE>=m_accuracyE)
+    {
+        return false;
+    }
+    std::vector<double> v;
+    if(!readVector(v, theString))
+        return false;
+    ASSERT(v.size()==m_accuracyZ+1);
+    double E = pow(10.,v[0]-9.)/241803.; // first column contains log10(E/Hz), converting to eV
+    m_eArray[m_curE] = E;
+    for(int i=0; i< m_accuracyZ; i++)
+        //m_nArray[i][m_curE] = v[i+1]*1e-6*E*pow(1.+m_zArray[i],-3); // multiply by energy and convert m^-3 to cm^-3, multiplied by (1+z)^{-3} to make it in comoving volume
+        m_nArray[i][m_curE] = v[i+1]*1e-6/units.cm3; // convert from m^-3 to internal units in physical volume
+    m_curE++;
+    return true;
+}
+
+bool Stecker2005EBL::testData()
+{
+    return true;
+}
+
+void Stecker2005EBL::processData()
+{
+    for(int i=0; i<m_accuracyZ; i++)
+        m_fE[i] = new LinearFunc(m_eArray, m_nArray[i]);
+}
+
+int Stecker2005EBL::UnitTest()
+{
+    Stecker2005EBL backgr;
+    //Kneiske1001EBL backgr;
+    //CuttedBackground backgr(new Stecker2005EBL(), 0, 0.5*units.eV);
+    BackgroundUtils::UnitTest(backgr);
+}
+
+}
\ No newline at end of file
diff --git a/src/lib/SteckerEBL.h b/src/lib/SteckerEBL.h
new file mode 100644
index 0000000..8d00a02
--- /dev/null
+++ b/src/lib/SteckerEBL.h
@@ -0,0 +1,75 @@
+/*
+ * SteckerEBL.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef MCRAY_STECKEREBL_H
+#define MCRAY_STECKEREBL_H
+
+#include "Background.h"
+
+namespace Backgrounds {
+    using namespace mcray;
+
+
+// IR/O spectrum from Stecker at al. astro-ph/0510449
+//(received from Matt Malkan <malkan@astro.UCLA.EDU> on the 6th of Jan 2006)
+
+    class Stecker2005EBL : public CTableWithHeaderReader, public IBackground
+    {
+    public:
+        Stecker2005EBL();
+        virtual ~Stecker2005EBL();
+
+
+        virtual double n(double aE, double aZ) const;
+        virtual double MinE() const;
+        virtual double MaxE() const;
+        virtual double MinZ() const;
+        virtual double MaxZ() const;
+        virtual std::string Name() const;
+
+        static int UnitTest();
+
+    private:
+// from CDataReader
+        ECompleteStatus readHeaderLine(const char* theString);
+        bool readDataLine(const char* theString);
+        bool testData();
+        void processData();
+
+// data
+        int m_accuracyE;
+        int m_accuracyZ;
+        int m_curE;
+        std::vector<double> m_zArray;// redshifts
+        std::vector<double> m_eArray;// energies
+        std::vector<std::vector<double> > m_nArray;// photon concentrations
+        std::vector<SafePtr<TableFunction> > m_fE; /// F(E,z_i)
+    };
+}
+
+#endif //MCRAY_STECKEREBL_H
diff --git a/src/lib/TableBackgrounds.cpp b/src/lib/TableBackgrounds.cpp
new file mode 100644
index 0000000..af2a48e
--- /dev/null
+++ b/src/lib/TableBackgrounds.cpp
@@ -0,0 +1,184 @@
+/*
+ * TableBackgrounds.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "TableBackgrounds.h"
+#include "Units.h"
+#include <math.h>
+
+namespace Backgrounds {
+
+
+Kneiske1001EBL::Kneiske1001EBL():
+		TableBackground("Kneiske1001 lower-limit")
+{
+	const char* files[] = {"0","0.1","0.3","0.8","2",0};
+	Init("Kneiske1001", files, true);
+}
+
+Kneiske1001EBL::~Kneiske1001EBL() {
+}
+
+//convert energy in internal units to table x scale
+double Kneiske1001EBL::EtoRecordX(double aE, double aZ) const
+{
+	double lambda = 2.*M_PI/aE;//wavelength in internal units
+	lambda *= units.Lunit*1e4;//wavelength in microns
+	return log10(lambda);
+}
+
+//retrieve energy in internal units from the data record
+double Kneiske1001EBL::recordToE(double aX, double aZ) const
+{
+	double lambda = pow(10.,aX);//wavelength in microns
+	lambda /= (units.Lunit*1e4);//wavelength in internal units
+	return 2.*M_PI/lambda;
+}
+
+////retrieve concentration E*dn/dE in internal units from the data record
+double Kneiske1001EBL::recordToN(double aX, double aY, double aZ) const
+{//[y] = nW m^-2 sr^-1 in comoving frame
+	double comovingFactor = (1.+aZ);
+	comovingFactor *= (comovingFactor*comovingFactor);
+	double aE = recordToE(aX, aZ);
+	double y = pow(10.,aY);
+	y *= (4.*M_PI*1e-9/(units.MeVinESU*1e-7/units.Eunit)*1e-4*units.Lunit*units.Lunit*units.Tunit);
+	double result = y/aE*comovingFactor;
+	return result;
+}
+
+Kneiske0309EBL::Kneiske0309EBL():
+		TableBackground("Kneiske0309 best fit")
+{
+	const char* files[] = {"0","0.2","0.4","0.6","1","2","3","4",0};
+	Init("Kneiske0309", files, true);
+}
+
+Kneiske0309EBL::~Kneiske0309EBL() {
+}
+
+//convert energy in internal units to table x scale
+double Kneiske0309EBL::EtoRecordX(double aE, double aZ) const
+{
+	double lambda = 2.*M_PI/aE;//wavelength in internal units
+	lambda *= units.Lunit*1e4;//wavelength in microns
+	return log10(lambda);
+}
+
+//retrieve energy in internal units from the data record
+double Kneiske0309EBL::recordToE(double aX, double aZ) const
+{
+	double lambda = pow(10.,aX);//wavelength in microns
+	lambda /= (units.Lunit*1e4);//wavelength in internal units
+	return 2.*M_PI/lambda;
+}
+
+////retrieve concentration E*dn/dE in internal units from the data record
+double Kneiske0309EBL::recordToN(double aX, double aY, double aZ) const
+{//[y] = nW m^-2 sr^-1 in comoving frame
+	double comovingFactor = (1.+aZ);
+	comovingFactor *= (comovingFactor*comovingFactor);
+	double aE = recordToE(aX, aZ);
+	double y = pow(10.,aY);
+	y *= (4.*M_PI*1e-9/(units.MeVinESU*1e-7/units.Eunit)*1e-4*units.Lunit*units.Lunit*units.Tunit);
+	double result = y/aE*comovingFactor;
+	return result;
+}
+
+//////////
+
+Franceschini08EBL::Franceschini08EBL():
+		TableBackground("Franceschini 08")
+{
+	const char* files[] = {"0","0.2","0.4","0.6","0.8","1.0","1.2","1.4","1.6","1.8","2.0", 0};
+	Init("Franceschini08EBL", files, true);
+}
+
+Franceschini08EBL::~Franceschini08EBL() {
+}
+
+//convert energy in internal units to table x scale
+double Franceschini08EBL::EtoRecordX(double aE, double aZ) const
+{
+	return log10(aE/units.eV);
+}
+
+//retrieve energy in internal units from the data record
+double Franceschini08EBL::recordToE(double aX, double aZ) const
+{
+	return pow(10.,aX)*units.eV;
+}
+
+////retrieve concentration E*dn/dE in internal units from the data record
+double Franceschini08EBL::recordToN(double aX, double aY, double aZ) const
+{//[y] = log(E*dn/dE) in cm^-3 in physical frame
+	double y = pow(10.,aY);
+	return y/units.cm3;
+}
+
+////////////
+
+ElmagKneiskeBestFit::ElmagKneiskeBestFit(bool aIsComoving):
+		MatrixBackground("Elmag best fit", "kneiskeElmagBestFit.dat", aIsComoving, true)
+{
+}
+
+ElmagKneiskeMinimal::ElmagKneiskeMinimal(bool aIsComoving):
+		MatrixBackground("Elmag lower-limit", "kneiskeElmagLowerLimit.dat", aIsComoving, true)
+{
+}
+
+///////////////
+Franceschini17EBL::Franceschini17EBL():
+        TableBackground("Franceschini17")
+{
+    const char* files[] = {"0", "0.2", "0.4", "0.6", "0.8", "1", "1.2", "1.4", "1.8", "2", 0};
+    Init("Franceschini17", files, true);
+}
+
+Franceschini17EBL::~Franceschini17EBL() {
+}
+
+//convert energy in internal units to table x scale
+double Franceschini17EBL::EtoRecordX(double aE, double aZ) const
+{
+    return log10(aE/units.eV);
+}
+
+//retrieve energy in internal units from the data record
+double Franceschini17EBL::recordToE(double aX, double aZ) const
+{
+    return pow(10.,aX)*units.eV;
+}
+
+////retrieve concentration E*dn/dE in internal units from the data record
+double Franceschini17EBL::recordToN(double aX, double aY, double aZ) const
+{
+    return pow(10., aY)/units.cm3;
+}
+
+} /* namespace Backgrounds */
+
diff --git a/src/lib/TableBackgrounds.h b/src/lib/TableBackgrounds.h
new file mode 100644
index 0000000..4db8215
--- /dev/null
+++ b/src/lib/TableBackgrounds.h
@@ -0,0 +1,115 @@
+/*
+ * TableBackgrounds.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef TABLEBACKGROUNDS_H_
+#define TABLEBACKGROUNDS_H_
+
+#include "Background.h"
+
+namespace Backgrounds {
+
+using namespace mcray;
+
+class Kneiske1001EBL : public TableBackground{
+public:
+	Kneiske1001EBL();
+	virtual ~Kneiske1001EBL();
+protected:
+	//convert energy in internal units to table x scale
+	double EtoRecordX(double aE, double aZ) const;
+	//retrieve energy in internal units from the data record
+	double recordToE(double aX, double aZ) const;
+	////retrieve concentration E*dn/dE in internal units from the data record
+	double recordToN(double aX, double aY, double aZ) const;
+};
+
+class Kneiske0309EBL : public TableBackground{
+public:
+	Kneiske0309EBL();
+	virtual ~Kneiske0309EBL();
+protected:
+	//convert energy in internal units to table x scale
+	double EtoRecordX(double aE, double aZ) const;
+	//retrieve energy in internal units from the data record
+	double recordToE(double aX, double aZ) const;
+	////retrieve concentration E*dn/dE in internal units from the data record
+	double recordToN(double aX, double aY, double aZ) const;
+};
+
+/// EBL from Table1,2 of http://arxiv.org/pdf/0805.1841v2.pdf
+class Franceschini08EBL : public TableBackground{
+public:
+	Franceschini08EBL();
+	virtual ~Franceschini08EBL();
+protected:
+	//convert energy in internal units to table x scale
+	double EtoRecordX(double aE, double aZ) const;
+	//retrieve energy in internal units from the data record
+	double recordToE(double aX, double aZ) const;
+	////retrieve concentration E*dn/dE in internal units from the data record
+	double recordToN(double aX, double aY, double aZ) const;
+};
+
+class ElmagKneiskeBestFit : public MatrixBackground
+{
+public:
+	/// Kneiske BestFit Background used in Elmag
+	/// There is an error in Elmag 1.02 and earlier, the tables are treated as physical concentrations
+	/// But in fact they are built for comoving frame (see fig 1 of http://lanl.arxiv.org/abs/astro-ph/0309141v1)
+	/// The parameter here is left just for comparison to Elmag 1.02
+	/// In Elmag 2.0 this bug was fixed
+	ElmagKneiskeBestFit(bool aIsComoving = true);
+};
+
+class ElmagKneiskeMinimal : public MatrixBackground
+{
+public:
+	/// Kneiske Lower limit Background used in Elmag
+	/// There is an error in Elmag 1.02 and earlier, the tables are treated as physical concentrations
+	/// But in fact they are built for comoving frame
+	/// The parameter here is left just for comparison to Elmag 1.02
+	/// In Elmag 2.0 this bug was fixed
+	ElmagKneiskeMinimal(bool aIsComoving = true);
+};
+
+class Franceschini17EBL : public TableBackground{
+public:
+    Franceschini17EBL();
+    virtual ~Franceschini17EBL();
+protected:
+    //convert energy in internal units to table x scale
+    double EtoRecordX(double aE, double aZ) const;
+    //retrieve energy in internal units from the data record
+    double recordToE(double aX, double aZ) const;
+    ////retrieve concentration E*dn/dE in internal units from the data record
+    double recordToN(double aX, double aY, double aZ) const;
+};
+
+} /* namespace Backgrounds */
+
+#endif /* TABLEBACKGROUNDS_H_ */
diff --git a/src/lib/TableFunction.cpp b/src/lib/TableFunction.cpp
new file mode 100644
index 0000000..dc266a8
--- /dev/null
+++ b/src/lib/TableFunction.cpp
@@ -0,0 +1,256 @@
+/*
+ * TableFunction.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "PrecisionTests.h"
+#include "TableFunction.h"
+
+namespace Utils {
+
+
+template<typename X> X LogScaleX<X>::ToScale(X aX)
+{
+	ASSERT(aX>=0);
+	return aX>0?log(aX):-1000;
+}
+
+template<typename X> X LogScaleX<X>::FromScale(X aXprime)
+{
+	return exp(aXprime);
+}
+
+template<typename X> IScale<X>* LogScaleX<X>::Clone() const
+{
+	return new LogScaleX();
+}
+
+template class LogScaleX<double>;
+template class LogScaleX<long double>;
+
+template<typename X> void TableFunctionX<X>::Init()
+{
+	ASSERT(m_x.size()==m_y.size() && m_x.size()>1);
+	fXmin = m_x[0];
+	fXmax = m_x[m_x.size()-1];
+}
+
+template<typename X> TableFunctionX<X>::TableFunctionX(const std::vector<X>& _x, const std::vector<X>& _y, X _leftVal, X _rightVal):
+m_x(_x),
+m_y(_y),
+m_leftVal(_leftVal),
+m_rightVal(_rightVal)
+{
+	Init();
+}
+
+template<typename X> TableFunctionX<X>::TableFunctionX(const std::vector<X>& _x, const std::vector<X>& _y, IScale<X>* aXscale, IScale<X>* aYscale, X _leftVal, X _rightVal):
+fCloneX(_x),
+fCloneY(_y),
+m_x(fCloneX),
+m_y(fCloneY),
+m_leftVal(_leftVal),
+m_rightVal(_rightVal),
+fXscale(aXscale),
+fYscale(aYscale)
+{
+	Init();
+	int iMax = _x.size();
+	for(int i=0; i<iMax; i++)
+	{
+		if(aXscale)
+			fCloneX[i] = aXscale->ToScale(_x[i]);
+		if(aYscale)
+			fCloneY[i] = aYscale->ToScale(_y[i]);
+	}
+}
+
+template<typename X> TableFunctionX<X>::TableFunctionX(TableReaderX<X>* aReader, X _leftVal, X _rightVal):
+		m_x(aReader->getColumn(0)),
+		m_y(aReader->getColumn(1)),
+		m_leftVal(_leftVal),
+		m_rightVal(_rightVal),
+		fTable(aReader)
+{
+	Init();
+}
+
+
+template<typename X> TableFunctionX<X>::TableFunctionX(const TableFunctionX<X>& aTableFunction):
+fCloneX(aTableFunction.m_x),
+fCloneY(aTableFunction.m_y),
+m_x(fCloneX),
+m_y(fCloneY),
+m_leftVal(aTableFunction.m_leftVal),
+m_rightVal(aTableFunction.m_rightVal),
+fXscale(aTableFunction.fXscale==0 ? 0 : aTableFunction.fXscale->Clone()),
+fYscale(aTableFunction.fYscale==0 ? 0 : aTableFunction.fYscale->Clone()),
+fXmin(aTableFunction.fXmin),
+fXmax(aTableFunction.fXmax)
+{
+
+}
+
+template<typename X> int TableFunctionX<X>::FindLeftX(const std::vector<X>&aSet, X xValue)
+{
+	int right = aSet.size() - 1;
+	ASSERT(right>=1);
+	if((xValue > aSet[right]) || xValue < aSet[0])
+		return -1;
+	int left = 0;
+
+	for(int i=(left+right)/2;right-left>1;i=(left+right)/2)
+	{
+		if(xValue > aSet[i])
+			left=i;
+		else
+			right=i;
+	}
+	ASSERT((right - left) == 1);
+	return left;
+}
+
+template<typename X> int TableFunctionX<X>::FindLeftX(X xValue) const
+{
+	return FindLeftX(m_x, xValue);
+}
+
+template<typename X> X TableFunctionX<X>::f(X _x) const
+{
+	if(_x<fXmin)
+		return m_leftVal;
+	if(_x>fXmax)
+		return m_rightVal;
+	X scaledX = fXscale ? fXscale->ToScale(_x) : _x;
+	X scaledY = f_scaled(scaledX);
+	return fYscale ? fYscale->FromScale(scaledY) : scaledY;
+}
+
+template<typename X> void TableFunctionX<X>::SetAutoLimits()
+{
+	m_leftVal = fYscale ? fYscale->FromScale(m_y[0]) : m_y[0];
+	m_rightVal = fYscale ? fYscale->FromScale(m_y[m_y.size()-1]) : m_y[m_y.size()-1];
+}
+
+template<typename X> X TableFunctionX<X>::Xmin() const
+{
+	return m_leftVal == 0. ? fXmin : -DBL_MAX;
+}
+
+template<typename X> X TableFunctionX<X>::Xmax() const
+{
+	return m_rightVal == 0. ? fXmax : DBL_MAX;
+}
+
+template<typename X> bool TableFunctionX<X>::InTableRange(X aArg) const
+{
+	return aArg>=fXmin && aArg<=fXmax;
+}
+
+template<typename X> void TableFunctionX<X>::Print(std::ostream& aOut, X aXcoef, X aYcoef)
+{
+	int iMax = m_x.size();
+	aOut << "# " << "leftVal=" << aYcoef*m_leftVal << " ; rightVal=" << aYcoef*m_rightVal << " ; Xmin=" << aXcoef*Xmin() << " ; Xmax=" << aXcoef*Xmax() << "\n";
+	for(int i=0; i<iMax; i++)
+	{
+		X xScaled = m_x[i];
+		X yScaled = m_y[i];
+		X x = fXscale ? fXscale->FromScale(xScaled) : xScaled;
+		X y = fYscale ? fYscale->FromScale(yScaled) : yScaled;
+		aOut << aXcoef*x << "\t" << aYcoef*y << "\t" << xScaled << "\t" << yScaled << "\n";
+	}
+}
+
+template class TableFunctionX<double>;
+template class TableFunctionX<long double>;
+
+template<typename X> X LinearFuncX<X>::f_scaled(X _x) const
+{
+	int i = TableFunctionX<X>::FindLeftX(_x);
+
+	if(i<0)
+		return (_x < this->m_x[0])?this->m_leftVal:this->m_rightVal;
+
+	X x1 = this->m_x[i];
+	X y1 = this->m_y[i];
+	X y2 = this->m_y[i+1];
+	X x2 = this->m_x[i+1];
+
+	return y1+(y2-y1)/(x2-x1)*(_x - x1);
+}
+
+template class LinearFuncX<double>;
+template class LinearFuncX<long double>;
+
+GSLTableFunc::~GSLTableFunc()
+{
+	if(fSpline)
+		gsl_spline_free (fSpline);
+	if(fAcc)
+		gsl_interp_accel_free (fAcc);
+}
+
+double GSLTableFunc::f_scaled(double _x) const
+{
+	return gsl_spline_eval (fSpline, _x, fAcc);
+}
+
+void GSLTableFunc::Init(const gsl_interp_type * aInterpType)
+{
+    fAcc = gsl_interp_accel_alloc ();
+    fSpline = gsl_spline_alloc (aInterpType, m_x.size());
+    try{
+    gsl_spline_init (fSpline, &m_x[0], &m_y[0], m_x.size());
+    }
+    catch(Exception* ex)
+    {
+    	std::cerr << "GSLTableFunc::Init() error\n input data:\n";
+    	Print(std::cerr);
+    	throw ex;
+    }
+}
+
+
+
+#ifdef USE_MPFR
+
+template<> mpfr::mpreal LogScaleX<mpfr::mpreal>::FromScale(mpfr::mpreal aXprime)
+{
+	return mpfr::exp(aXprime);
+}
+
+template<> mpfr::mpreal LogScaleX<mpfr::mpreal>::ToScale(mpfr::mpreal aX)
+{
+	ASSERT(aX>=0);
+	return aX>0?mpfr::log(aX):-1000;
+}
+
+template class TableFunctionX<mpfr::mpreal>;
+template class LinearFuncX<mpfr::mpreal>;
+
+#endif
+
+} /* namespace Utils */
diff --git a/src/lib/TableFunction.h b/src/lib/TableFunction.h
new file mode 100644
index 0000000..f29c7d3
--- /dev/null
+++ b/src/lib/TableFunction.h
@@ -0,0 +1,261 @@
+/*
+ * TableFunction.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef TABLEFUNCTION_H_
+#define TABLEFUNCTION_H_
+
+#include <iostream>
+#include <gsl/gsl_errno.h>
+#include <gsl/gsl_spline.h>
+
+#include "MathUtils.h"
+#include "TableReader.h"
+
+
+namespace Utils {
+
+template<typename X = double >
+class IScale
+{
+public:
+	virtual X ToScale(X aX) = 0;
+	virtual X FromScale(X aXprime) = 0;
+	virtual IScale<X>* Clone() const = 0;
+	virtual ~IScale(){}
+};
+
+template<typename X = double >
+class LogScaleX : public IScale<X>
+{
+public:
+	X ToScale(X aX);
+	X FromScale(X aXprime);
+	IScale<X>* Clone() const;
+};
+
+typedef LogScaleX<double> LogScale;
+
+template<typename X = double >
+class TableFunctionX : public SmartReferencedObj, virtual public FunctionX<X>{
+public:
+	static int FindLeftX(const std::vector<X>&, X xValue);
+	TableFunctionX(const std::vector<X>& _x, const std::vector<X>& _y, X _leftVal=0., X _rightVal=0.);
+
+	//takes ownership of aXscale and aYscale
+	TableFunctionX(const std::vector<X>& _x, const std::vector<X>& _y, IScale<X>* aXscale, IScale<X>* aYscale, X _leftVal=0., X _rightVal=0.);
+
+	//stores smart ref to aReader
+	TableFunctionX(TableReaderX<X>* aReader, X _leftVal=0., X _rightVal=0.);
+
+	bool InTableRange(X aArg) const;
+
+	virtual X f(X _x) const;
+	virtual X f_scaled(X _x) const = 0;
+	virtual X Xmin() const;
+	virtual X Xmax() const;
+
+	void SetAutoLimits();
+	void Print(std::ostream& aOut, X aXcoef=1., X aYcoef=1.);
+private:
+	//common part of constructors
+	void Init();
+protected:
+	int FindLeftX(X xValue) const;
+	//used for cloning
+	TableFunctionX(const TableFunctionX<X>& aTableFunction);
+
+	std::vector<X>			fCloneX;
+	std::vector<X>			fCloneY;
+	const std::vector<X>&	m_x;
+	const std::vector<X>&	m_y;
+	X						m_leftVal;
+	X						m_rightVal;
+	SafePtr<IScale<X> >		fXscale;
+	SafePtr<IScale<X> >		fYscale;
+	SmartPtr<TableReaderX<X> >	fTable;
+	X						fXmin;
+	X						fXmax;
+};
+
+typedef TableFunctionX<double> TableFunction;
+
+template<typename X = double >
+class LinearFuncX  : public TableFunctionX<X>
+{
+public:
+	LinearFuncX(const std::vector<X>& _x,const  std::vector<X>& _y, X _leftVal=0., X _rightVal=0.):
+	  TableFunctionX<X>(_x, _y, _leftVal, _rightVal)  {};
+
+	//stores smart ref to aReader
+	LinearFuncX(TableReaderX<X>* aReader, X _leftVal=0., X _rightVal=0.):
+		TableFunctionX<X>(aReader,_leftVal,_rightVal) {};
+
+	//takes ownership of aXscale and aYscale
+	LinearFuncX(const std::vector<X>& _x,const std::vector<X>& _y, IScale<X>* aXscale, IScale<X>* aYscale, X _leftVal=0., X _rightVal=0.):
+		TableFunctionX<X>(_x, _y, aXscale, aYscale, _leftVal, _rightVal) {}
+
+	X f_scaled(X _x) const;
+	FunctionX<X>* Clone() const { return new LinearFuncX<X>(*this); }
+protected:
+	LinearFuncX(const TableFunctionX<X>& aTableFunction):
+		TableFunctionX<X>(aTableFunction) {};
+};
+
+typedef LinearFuncX<double> LinearFunc;
+
+class GSLTableFunc  : public TableFunction
+{
+public:
+	GSLTableFunc(const Vector& _x, const Vector& _y, const gsl_interp_type * aInterpType=gsl_interp_linear,
+			double _leftVal=0., double _rightVal=0.):
+	  TableFunction(_x, _y, _leftVal, _rightVal),fAcc(0),fSpline(0) {Init(aInterpType);}
+
+	//stores smart ref to aReader
+	GSLTableFunc(TableReader *aReader, const gsl_interp_type * aInterpType=gsl_interp_linear, double _leftVal=0., double _rightVal=0.):
+		TableFunction(aReader,_leftVal,_rightVal),fAcc(0),fSpline(0) {Init(aInterpType);}
+
+	//takes ownership of aXscale and aYscale
+	//_x and _y are values are modified with scale functions given by aXscale and aYscale parameters
+	GSLTableFunc(const Vector& _x, const Vector& _y, IScale<double>* aXscale, IScale<double>* aYscale, const gsl_interp_type * aInterpType=gsl_interp_linear, double _leftVal=0., double _rightVal=0.):
+		TableFunction(_x, _y, aXscale, aYscale, _leftVal, _rightVal) {Init(aInterpType);}
+
+	virtual ~GSLTableFunc();
+
+	double f_scaled(double _x) const;
+	Function* Clone() const { return new GSLTableFunc(*this); }
+protected:
+	GSLTableFunc(const GSLTableFunc& aTableFunction):
+		TableFunction(aTableFunction) {Init(aTableFunction.fSpline->interp->type);};
+private:
+	void Init(const gsl_interp_type * aInterpType);
+    gsl_interp_accel *fAcc;
+    gsl_spline *fSpline;
+    const gsl_interp_type *fInterpType;
+};
+
+template<typename X = double >
+class MatrixFunctionX : public Function2X<X>
+{
+public:
+	MatrixFunctionX(const std::string& aFile)
+	{
+		std::ifstream dataFile(aFile.c_str());
+		if(!dataFile)
+			Exception::Throw("Failed to open " + aFile);
+		std::string header;
+		std::getline(dataFile, header);
+		int logscX,logscY;
+		const char* headerFormat = "# minX=%lg stepX=%lg logscaleX=%d minY=%lg stepY=%lg logscaleY=%d";
+		if(sscanf(header.c_str(),headerFormat,
+				  &xMin,&xStep,&logscX,&yMin,&yStep,&logscY)!=6)
+			Exception::Throw("Invalid header format in " + aFile + "\nExpected format: " + headerFormat);
+		logScaleX = (bool)logscX;
+		logScaleY = (bool)logscY;
+		if((logScaleX && (xMin<=0 || xStep<=1.))||xStep<=0.)
+			Exception::Throw("Invalid X scale in " + aFile);
+		if((logScaleY && (yMin<=0 || yStep<=1.))||yStep<=0.)
+			Exception::Throw("Invalid Y scale in " + aFile);
+		fData = new TableReaderX<X>(dataFile, TableReader::Auto);
+		nCols = fData->numberOfColumns();
+		if(logScaleX) {
+			xMax = xMin*pow(xStep,nCols-1);
+			xStep = log(xStep);//convert multiplier to log step
+		}
+		else{
+			xMax = xMin + xStep*(nCols-1);
+		}
+		nRows = fData->getColumn(0).size();
+		if(logScaleY){
+			yMax = yMin*pow(yStep,nRows-1);
+			yStep = log(yStep);//convert multiplier to log step
+		}
+		else{
+			yMax = yMin + yStep*(nRows-1);
+		}
+	}
+
+	X f(double x, double y) const
+	{
+		double binX, binY;
+
+		if(logScaleX)
+		{//log scale
+			binX = log(x/xMin)/xStep;
+		}
+		else
+		{//linear scale
+			binX = (x-xMin)/xStep;
+		}
+		if(binX<0. || binX>nCols-1)
+			return 0.;
+		if(logScaleY)
+		{//log scale
+			binY = log(y/yMin)/yStep;
+		}
+		else
+		{//linear scale
+			binY = (y-yMin)/yStep;
+		}
+		if(binY<0. || binY>nRows-1)
+			return 0.;
+		int iX = floor(binX);
+		int iY = floor(binY);
+		double weightX = 1.-binX+iX;
+		double weightY = 1.-binY+iY;
+
+		//linear f interpolation //TODO implement logscale f interpolation
+		double f_11 = fData->getColumn(iX)[iY]*weightX*weightY;
+		double f_21 = (weightX<1.)? (fData->getColumn(iX+1)[iY]*(1.-weightX)*weightY) : 0.;
+		double f_12 = (weightY<1.)? (fData->getColumn(iX)[iY+1]*weightX*(1.-weightY)) : 0.;
+		double f_22 = (weightX<1.&&weightY<1.)?(fData->getColumn(iX+1)[binY+1]*(1.-weightX)*(1.-weightY)) : 0.;
+		double result = f_11 + f_21 + f_12 + f_22;
+		return result;
+	}
+
+	virtual X MinArg(int aArgNo) const {return aArgNo==1?xMin:yMin;};
+	virtual X MaxArg(int aArgNo) const {return aArgNo==1?xMax:yMax;}
+
+private:
+	SafePtr<TableReaderX<X> >  fData;
+	X xMin;
+	X yMin;
+	X xMax;
+	X yMax;
+	X xStep;
+	X yStep;
+	int nCols;
+	int nRows;
+	bool logScaleX;
+	bool logScaleY;
+};
+
+typedef MatrixFunctionX<double> MatrixFunction;
+
+} /* namespace Utils */
+
+#endif /* TABLEFUNCTION_H_ */
diff --git a/src/lib/TableReader.cpp b/src/lib/TableReader.cpp
new file mode 100644
index 0000000..147cd51
--- /dev/null
+++ b/src/lib/TableReader.cpp
@@ -0,0 +1,270 @@
+/*
+ * TableReader.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "TableReader.h"
+#include <iterator>
+#include <fstream>
+#include <string.h>
+
+namespace Utils {
+
+	template<typename X> TableReaderX<X>::TableReaderX(std::string aFile, int nColumns) {
+		std::ifstream file(aFile.c_str());
+		if (file.eof())
+			Exception::Throw("Failed to open file " + aFile);
+		try{
+			init(file, nColumns);
+		}
+		catch(Exception* ex){
+			std::string reason = ex->Message();
+			delete ex;
+			Exception::Throw("Failed to parse " + aFile + " : " + reason);
+		}
+	}
+
+	template<typename X> TableReaderX<X>::TableReaderX(std::istream& aInput, int nColumns)
+	{
+		init(aInput, nColumns);
+	}
+
+	template<typename X> void TableReaderX<X>::init(std::istream& aInput, int nColumns)
+	{
+		std::string line;
+		bool firstRow=true;
+		while (std::getline(aInput, line))
+		{
+			std::istringstream iss(line);
+			X val;
+			size_t col=0;
+			for(; iss >> val; col++){
+				if(firstRow)
+					m_columns.push_back(new std::vector<X>());
+				else if(col>=m_columns.size())
+					Exception::Throw("variable number of records in a row");
+				m_columns[col]->push_back(val);
+			}
+			if(col!=m_columns.size())
+				Exception::Throw("variable number of records in a row");
+			if(firstRow && nColumns!=Auto && col!=nColumns){
+				Exception::Throw("unexpected number of records: " + ToString(col) +  " in a row (" + ToString(nColumns) + " expected)");
+			}
+			firstRow=false;
+		}
+
+//		for(int i=0; i<nColumns; i++)
+//			m_columns.push_back(new std::vector<X>());
+//
+//		std::istream_iterator<X> eos;
+//		std::istream_iterator<X> iit (aInput);
+//
+//		int recNo=0;
+//		for(; iit!=eos; recNo++, iit++)
+//			m_columns[recNo%nColumns]->push_back(*iit);
+//
+//		if(recNo%nColumns)
+//			Exception::Throw("unexpected number of rows");
+	}
+
+	template<typename X> TableReaderX<X>::~TableReaderX()
+	{
+		for(int i=m_columns.size()-1; i>=0; i--)
+			delete m_columns[i];
+	}
+
+	template class TableReaderX<double>;
+	template class TableReaderX<long double>;
+
+	CTableWithHeaderReader::CTableWithHeaderReader(const char* theFileName, char theCommentSimbol):
+			m_commentSymbol(theCommentSimbol),
+			m_curHeaderLine(0),
+			m_curDataLine(0)
+	{
+		m_file = fopen(theFileName,"rt");
+	}
+
+	CTableWithHeaderReader::~CTableWithHeaderReader()
+	{
+
+	}
+
+	bool CTableWithHeaderReader::readLine()
+	{
+		for(int i=0;i<TAB_READER_BUF_LENGTH;i++)
+		{
+			int ch = fgetc(m_file);
+			if(ch == EOF)
+			{
+				if(m_buffer[0] == m_commentSymbol)
+					i = 0;
+				m_buffer[i] = '\0';
+				return i;
+			}
+			if(ch == '\n')
+			{
+				if((i==0)||(m_buffer[0] == m_commentSymbol))
+				{
+					i = -1;
+					continue;//skipping empty strings and comments
+				}
+				else
+				{
+					m_buffer[i] = '\0';
+					return true;
+				}
+			}
+			else
+				m_buffer[i] = ch;
+		}
+		Exception::Throw("CDataReader : buffer overflow");
+		return false;
+	}
+
+	bool CTableWithHeaderReader::read()
+	{
+		if(!m_file)
+			return false;
+		//	bool result = false;
+		while(readLine())
+		{
+			m_curHeaderLine++;
+			ECompleteStatus status = readHeaderLine(m_buffer);
+			switch(status)
+			{
+				case notCompletedE: continue;
+				case failedE: return false;
+				case completedE:;
+			}
+			break;
+		}
+		while(readLine())
+		{
+			m_curDataLine++;
+			if(!readDataLine(m_buffer))
+				return false;
+		}
+		if(!testData())
+			return false;
+		processData();
+		fclose(m_file);
+		m_file = NULL;
+		return true;
+	}
+
+/// reads number from standard file header line "data length <number>"
+/// here number must indicate number of data lines below
+	bool CTableWithHeaderReader::readDataLength(int& length, const char* theString)
+	{
+		const char format[] = " data length %d";
+		return (sscanf(theString,format,&length)==1);
+	}
+
+
+/// parameter vector must not be created previously (using CVector::Create() method)
+/// default delimiter string " /t/r/n"
+	bool CTableWithHeaderReader::readVector(std::vector<double>& v, const char* theString, const char* delimiterStr)
+	{
+		bool result = false;
+		char* stringCopy = 0;
+		try{
+			const char defaultDelimStr[] = " \t\n\r";
+			if (delimiterStr==NULL)
+			{
+				delimiterStr = defaultDelimStr;
+			}
+
+			int length = strlen(theString);
+			stringCopy = new char[length + 1];
+			strcpy(stringCopy, theString);
+
+			int i=0;
+			char* curStr = strtok(stringCopy,delimiterStr);
+
+			// calculating number of entries
+			while(curStr)
+			{
+				double val=0;
+				if (sscanf(curStr,"%lg",&val)!=1)
+					Exception::Throw("number format error");
+				v.push_back(val);
+				curStr = strtok(NULL, delimiterStr);
+				i++;
+			}
+			if(i==0)
+			{
+				Exception::Throw("no tokens");
+			}
+
+			result = true;
+		}
+		catch(const char* aError)
+		{
+			//error saved and can be retrieved using LastError() method
+		}
+		delete[] stringCopy;
+		return result;
+	}
+
+	double CTableWithHeaderReader::yValue(double xValue, double *xArray, double *yArray, int xArraySize, double leftValue, double rightValue)
+//find the nearest to xValue x-array element (increasing x-array supposed)
+//like CVector::findX function, and than using 1-st order approximation to find y value
+	{
+		int left = 0;
+		int right = xArraySize-1;
+
+		if(xValue>xArray[right])
+			return rightValue;
+		if(xValue<xArray[left])
+			return leftValue;
+
+		for(int i=(left+right)/2;right-left>1;i=(left+right)/2)
+		{
+			if(xValue>xArray[i])
+				left=i;
+			else
+				right=i;
+		}//finding nearest point
+
+		ASSERT((right - left) == 1);
+
+		double result = yArray[left]+(xValue-xArray[left])*(yArray[right]-yArray[left])/(xArray[right]-xArray[left]);
+		return result;
+	}
+
+	void CTableWithHeaderReader::inverseArray(double *pArray, unsigned int size)
+	{
+		unsigned int maxIndex = size/2;
+		unsigned int i1,i2;
+		for(i1=0,i2=size-1;i1<maxIndex;i1++,i2--)
+		{
+			double mem = pArray[i1];
+			pArray[i1] = pArray[i2];
+			pArray[i2] = mem;
+		}
+	}
+
+} /* namespace Utils */
diff --git a/src/lib/TableReader.h b/src/lib/TableReader.h
new file mode 100644
index 0000000..3735347
--- /dev/null
+++ b/src/lib/TableReader.h
@@ -0,0 +1,105 @@
+/*
+ * TableReader.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#ifndef TABLEREADER_H_
+#define TABLEREADER_H_
+
+#include <istream>
+#include <vector>
+#include "Utils.h"
+
+namespace Utils {
+
+template<typename X = double >
+class TableReaderX : public SmartReferencedObj {
+public:
+	enum{
+		Auto=-1
+	};
+    //use TableReader::Auto for second argument if number of columns is not known in advance
+	TableReaderX(std::string aFile, int nColumns /* =Auto */);
+	TableReaderX(std::istream& aInput, int nColumns /* =Auto */);
+    virtual ~TableReaderX();
+	inline std::vector<X>& getColumn(int i){return *(m_columns[i]);};
+	inline const std::vector<X>& getColumn(int i) const {return *(m_columns[i]);};
+	inline size_t numberOfColumns() const { return m_columns.size(); };
+protected:
+	void init(std::istream& aInput, int nColumns);
+	std::vector<std::vector<X>* > m_columns;
+};
+
+typedef TableReaderX<double> TableReader;
+
+#ifndef TAB_READER_BUF_LENGTH
+#define TAB_READER_BUF_LENGTH 65536
+#endif
+
+	class CTableWithHeaderReader
+	{
+	public:
+		static void inverseArray(double* pArray, unsigned int size);
+		static double yValue(double xValue, double* xArray, double* yArray,int xArraySize, double leftValue=0, double rightValue=0);
+		enum ECompleteStatus
+		{
+			notCompletedE = -1,
+			failedE = 0,
+			completedE = 1
+		};
+		CTableWithHeaderReader(const char* theFileName, char theCommentSimbol = '#');
+		virtual bool read();
+		virtual ~CTableWithHeaderReader();
+
+		/// reads number from standard file header line "data length <number>"
+		/// here number must indicate number of data lines below
+		/// usually called from readHeaderLine
+		/// parameter theString is header string passed to readHeaderLine
+		static bool readDataLength(int& length, const char* theString);
+
+		/// the data read will be appended to vector
+		/// default delimiter string " /t/r/n"
+		static bool readVector(std::vector<double>& aVector, const char* aString, const char* aDelimiterStr=NULL);
+
+	protected:
+		virtual ECompleteStatus readHeaderLine(const char* theString){return completedE;};
+
+		//readDataLine returns false in case of error
+		virtual bool readDataLine(const char* theString) = 0;
+		virtual bool testData(){return true;};
+		virtual void processData(){};
+
+		char m_commentSymbol;
+		int m_curHeaderLine;//starting from 1
+		int m_curDataLine;//starting from 1
+	private:
+		bool readLine();//returns false if EOF was reached
+
+		char m_buffer[TAB_READER_BUF_LENGTH];
+		FILE* m_file;
+	};
+
+} /* namespace Utils */
+#endif /* TABLEREADER_H_ */
diff --git a/src/lib/Test.cpp b/src/lib/Test.cpp
new file mode 100644
index 0000000..f279344
--- /dev/null
+++ b/src/lib/Test.cpp
@@ -0,0 +1,1414 @@
+/*
+ * Test.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Test.h"
+#include <cstdlib>
+#include "ParticleStack.h"
+#include "Utils.h"
+#include "Cosmology.h"
+#include "PropagationEngine.h"
+#include <iostream>
+#include "Output.h"
+#include "TableBackgrounds.h"
+#include "ICS.h"
+#include "GammaPP.h"
+#include "ProtonPP.h"
+#include "PPP.h"
+#include "GZK.h"
+#include "Deflection3D.h"
+#include "NeutronDecay.h"
+#include "SteckerEBL.h"
+#include "PhotoDisintegration.h"
+#include "Stecker16Background.h"
+
+#ifdef ELMAG_TEST
+#include "ElmagTest.h"
+#endif
+
+using namespace std;
+using namespace mcray;
+using namespace Utils;
+using namespace Backgrounds;
+using namespace Interactions;
+
+namespace Test {
+
+void BuildInitialState(ParticleStack& particles, double aZmax)
+{
+	Particle gamma(Photon, aZmax);
+	gamma.Energy = 1e7/units.Eunit;//MeV
+
+	particles.AddPrimary(gamma);
+}
+
+void TestList::EngineTest(double aAlphaThinning)
+{
+	class SimpleSplitting : public RandomInteraction
+	{
+	public:
+		SimpleSplitting(double aRate):fRate(aRate){}
+		RandomInteraction* Clone() const { return new SimpleSplitting(fRate); }
+		virtual ~SimpleSplitting(){};
+		double Rate(const Particle& aParticle) const {return fRate;}
+		bool GetSecondaries(Particle& aParticle, std::vector<Particle>& aSecondaries, Randomizer& aRandomizer) const
+		{
+			Particle product = aParticle;
+			product.Energy = 0.5 * product.Energy;
+			aSecondaries.push_back(product);
+			aSecondaries.push_back(product);
+			return true;
+		}
+	private:
+		double fRate;
+	};
+	double Zmax = 0.01;
+	double Emin = 1.;
+	double Emax = 1000.;
+	int Ninteractions = 100;
+
+	ParticleStack particles;
+	Result result(Emin);
+	string outFile = "engine_alpha_" + ToString(aAlphaThinning);
+	result.AddOutput(new SpectrumOutput(outFile, Emin, pow(10, 0.05)));
+
+	cosmology.Init();
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(aAlphaThinning);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(new SimpleSplitting(Ninteractions/cosmology.z2t(Zmax)));
+	Randomizer r;
+
+	/// build initial state
+	double t1 = cosmology.z2t(Zmax);
+	double t2 = cosmology.z2t(0.);
+	int Nparticles = 10000000;
+	double mult = pow(10, 0.05);
+	int nBins = (int)(log(Emax/Emin)/log(mult) + 0.5);
+	int nParticlesPerBin = Nparticles/nBins;
+	double E=Emin*mult;
+	for(int i=0; i<nBins; i++, E*=mult)
+	{
+		for(int j=0; j<nParticlesPerBin; j++)
+		{
+			double t = t1+r.Rand()*(t2-t1);
+			Particle p(Photon, cosmology.t2z(t));
+			p.Energy = E;
+			p.Weight = 1/E;///dN/dlogE
+			particles.AddPrimary(p);
+		}
+	}
+	pe.Run();
+}
+
+void TestList::FrameworkTest()
+{
+	double Zmax = 2;
+	double Emin = 1./units.Eunit;
+	//double alphaThinning = 0;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	result.AddOutput(new SpectrumOutput("out", Emin, pow(10, 0.05)));
+
+	cosmology.Init();
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-5*cmbTemp, 1e3*cmbTemp));
+	//backgr.AddComponent(new Backgrounds::Kneiske1001EBL());
+	//double stepZ = 0.1;
+	//double logStepK = pow(10,0.05);
+	//double epsRel = 1e-3;
+
+	//BackgroundIntegral backgrI(backgr, stepZ, logStepK, Zmax, epsRel);
+
+	PropagationEngine pe(particles, result, 2011);
+	//EnergyBasedThinning thinning(alphaThinning);
+	//pe.SetThinning(&thinning);
+	pe.AddInteraction(new RedShift());
+	//pe.AddInteraction(new Interactions::ICS(backgrI,Emin));
+	//pe.AddInteraction(new Interactions::IcsCEL(backgrI,Emin));
+	//pe.AddInteraction(new Interactions::GammaPP(backgrI));
+
+	/// build initial state
+	BuildInitialState(particles, Zmax);
+
+	pe.Run();
+}
+
+int TestList::Main(int argc, char** argv)
+{
+	if(argc==0)
+	{
+		FrameworkTest();
+		return 0;
+	}
+	const char* command = argv[0];
+	std::map<std::string,TestProc>::iterator tit = fTests.find(command);
+	if(tit!=fTests.end()){
+		TestProc proc = tit->second;
+		return proc();
+	}
+
+
+
+	if(strcmp(command,"elmag")==0)
+	{
+#ifdef ELMAG_TEST
+		ElmagTests::Main(argc-1, argv+1);
+#else
+		Exception::Throw("Elmag tests are not linked in this version");
+#endif
+	}
+	else if(strcmp(command,"FERMI")==0)
+		FermiTests::Main(argc-1, argv+1);
+	else if(strcmp(command,"cosmology")==0)
+		Cosmology::UnitTest();
+	else if(strcmp(command,"background")==0)
+		BackgroundTest();
+	else if(strcmp(command,"backgroundI")==0)
+		ContinuousBackgroundIntegral::UnitTest();
+	else if(strcmp(command,"GZK")==0)
+		//GZK::UnitTest();
+		GZK::UnitTestEconserv(1e20, 0.1);
+	else if(strcmp(command,"ICS")==0)
+	{
+		//ICS::UnitTest(false, true, true,1./units.Eunit, 1e6/units.Eunit, 1e-6);
+		//ICS::UnitTest(false, true, false,1./units.Eunit, 1e6/units.Eunit, 1e-6);
+		//ICS::UnitTest(false, true, true,100./units.Eunit, 1e6/units.Eunit, 1e-6);
+		//ICS::UnitTest(false, true, false,100./units.Eunit, 1e6/units.Eunit, 1e-6);
+
+		ICS::UnitTest(true, false, false,1./units.Eunit, 1e14/units.Eunit, 1e-6);//print rates only
+		ICS::UnitTest(true, false, true,1./units.Eunit, 1e14/units.Eunit, 1e-6);//print rates only
+
+		//ICS::UnitTest(true, false, false);
+		//ICS::UnitTest(true, false, true);
+
+		//ICS::UnitTestMono(true, 1, true);
+		//ICS::UnitTestMono(false, 1, true);
+		//ICS::UnitTestMono(false, 10, true);
+		//ICS::UnitTestMono(false, 100, true);
+		//ICS::UnitTestMono(false, 1000, true);
+		//ICS::UnitTest();
+	}
+	else if(strcmp(command,"ICSsample")==0)
+	{
+		ICS::UnitTestSampling(1./units.Eunit, 1e8/units.Eunit, 1e-4);
+	}
+	else if(strcmp(command,"ICSdistr")==0)
+	{
+		ICS::UnitTestDistribution(1./units.Eunit, 1e8/units.Eunit, 1e-4);
+	}
+	else if(strcmp(command,"ICScel")==0)
+		IcsCEL::UnitTest();
+	else if(strcmp(command,"PP")==0)
+	{
+		//GammaPP::UnitTest();
+		//GammaPP::UnitTest(true, true, true);
+		//GammaPP::UnitTest(true, true, false);
+		GammaPP::UnitTest(true, false, false, false);//rates only
+		GammaPP::UnitTest(true, false, false, true);//rates only
+	}
+	else if(strcmp(command,"PPsample")==0)
+	{
+		GammaPP::UnitTestSampling(1e8/units.Eunit, 1e-4);
+	}
+	else if(strcmp(command,"PPP")==0)
+	{
+		PPP::UnitTest();
+		//ProtonPP::UnitTest();
+	}
+	else if(strcmp(command,"split")==0)
+	{
+		SplittedBackgroundTest(true);
+		SplittedBackgroundTest(false);
+	}
+	else if(strcmp(command,"thinning")==0)
+	{
+		ThinningTest(1);
+		ThinningTest(0.5);
+		ThinningTest(0);
+	}
+	else if(strcmp(command,"Kachelries")==0)
+	{// KachelriesTest(double aEmax_eV, double aZmax, double aAlphaThinning, bool aSplitted, int aN, bool aNoPP, bool aNoICS, ParticleType aPrimary);
+		if(argc!=12 && argc!=13)//E${E}_z${z}_N${N}_prim${PrimaryPart}_thin${alpha}
+		{
+			Exception::Throw("usage: --test Kachelries Emin/eV E/eV z N primary alpha_thinning NoPP NoICSdiscr NoICScel NoRedshift split [output/dir]\nprimary: 0 - photon; -1 - e-; 1 - e+");
+		}
+		double E,z,alpha,Emin;
+		int N,NoPP,NoICSdiscr,NoICScel,NoRedshift,particle,split;
+		int pos = 1;
+		if(sscanf(argv[pos++],"%lg", &Emin)!=1)
+			Exception::Throw("Failed to read Emin parameter");
+		if(sscanf(argv[pos++],"%lg", &E)!=1)
+			Exception::Throw("Failed to read E parameter");
+		if(sscanf(argv[pos++],"%lg", &z)!=1)
+			Exception::Throw("Failed to read z parameter");
+		if(sscanf(argv[pos++],"%d", &N)!=1)
+			Exception::Throw("Failed to read N parameter");
+		if(sscanf(argv[pos++],"%d", &particle)!=1)
+			Exception::Throw("Failed to read primary parameter");
+		ParticleType pt = ParticleTypeEOF;
+		switch(particle)
+		{
+		case 0:	pt = Photon; break;
+		case 1:	pt = Positron; break;
+		case -1: pt = Electron; break;
+		default:
+			Exception::Throw("Invalid 'primary' parameter");
+			break;
+		}
+		if(sscanf(argv[pos++],"%lg", &alpha)!=1)
+			Exception::Throw("Failed to read alpha_thinning parameter");
+		if(sscanf(argv[pos++],"%d", &NoPP)!=1)
+			Exception::Throw("Failed to read NoPP parameter");
+		if(sscanf(argv[pos++],"%d", &NoICSdiscr)!=1)
+			Exception::Throw("Failed to read NoICSdiscr parameter");
+		if(sscanf(argv[pos++],"%d", &NoICScel)!=1)
+			Exception::Throw("Failed to read NoICScel parameter");
+		if(sscanf(argv[pos++],"%d", &NoRedshift)!=1)
+			Exception::Throw("Failed to read NoRedshift parameter");
+		if(sscanf(argv[pos++],"%d", &split)!=1)
+			Exception::Throw("Failed to read split parameter");
+		const char* outputDir = argc==13 ? argv[pos++] : 0;
+
+		KachelriesTest(Emin, E, z, alpha, split!=0, N, NoPP!=0, NoICSdiscr!=0, NoICScel!=0, NoRedshift!=0, pt, outputDir);
+	}
+	else if(strcmp(command,"mono")==0)
+	{
+		//MonochromaticAndGausianBackgroundTest(true,true,false,false);
+		MonochromaticAndGausianBackgroundTest(true,false,true,false);
+		//MonochromaticAndGausianBackgroundTest(true,false,false,true);
+		//MonochromaticAndGausianBackgroundTest(false,true,false,false);
+		MonochromaticAndGausianBackgroundTest(false,false,true,false);
+		//MonochromaticAndGausianBackgroundTest(false,false,false,true);
+	}
+	else if(strcmp(command,"OpenMP")==0)
+	{
+		MultithreadTest(false);
+		MultithreadTest(true);
+	}
+	else if(strcmp(command,"bi")==0)
+	{
+		BichromaticTest(true);
+	}
+	else if(strcmp(command,"sampling")==0)
+	{
+		SamplingTest();
+	}
+	else if(strcmp(command,"engine")==0)
+	{
+		EngineTest(0.5);
+	}
+	return 0;
+}
+
+void TestList::SamplingTest()
+{
+	ParamlessFunction aDistrib(sin);
+	double aRand = 0.5;
+	double aOutputX;
+	double aOutputIntegral;
+	double xMin = 0;
+	double xMax = M_PI;
+	double aRelError = 1e-4;
+	//SampleDistribution(const Function& aDistrib, double aRand, double& aOutputX, double& aOutputIntegral, double aInitialStep, double xMin, double xMax, double aRelError);
+	MathUtils::SampleDistribution(aDistrib, aRand, aOutputX, aOutputIntegral, xMin, xMax, aRelError);
+
+	aOutputX/=M_PI;
+	aOutputX*=M_PI;
+
+
+	class SamplingTestFunc : public Function
+	{
+		GaussianBackground fTestFunc;
+	public:
+		SamplingTestFunc():
+			fTestFunc(1e10, 1e8, 99, 1)
+		{
+
+		}
+		double f(double aX) const
+		{
+			return fTestFunc.n(aX, 0.)/aX;
+		}
+		virtual double Xmin() const {return fTestFunc.MinE();}
+		virtual double Xmax() const {return fTestFunc.MaxE();}
+	};
+	std::ofstream samplingOut;
+	samplingOut.open("samplingOld",std::ios::out);
+	std::ofstream samplingOutNew;
+	samplingOutNew.open("sampling",std::ios::out);
+	Randomizer r(0);
+	SamplingTestFunc f;
+	std::ofstream funcOut;
+	funcOut.open("comulative.f",std::ios::out);
+	double step = pow(f.Xmax()/f.Xmin(),0.001);
+	double sum = 0;
+	funcOut << f.Xmin() << "\t0\n";
+	MathUtils math;
+	for(double x = f.Xmin(); x<f.Xmax(); x*=step)
+	{
+		sum+=math.Integration_qag(f,x,x*step,1e-300,1e-5,1000);
+		funcOut << x << "\t" << sum << "\n";
+	}
+	funcOut.close();
+
+	int nSteps = 100;
+	xMin=10;
+	xMax=1e20;
+	aRelError = 1e-3;
+	double x;
+	double I;
+	int nTrials = 100000;
+	for(int i=0; i<nTrials; i++)
+	{
+		double rand = r.Rand();
+		math.SampleLogscaleDistribution<double>(f, rand, x, I, nSteps, xMin, xMax, aRelError);
+		samplingOut << x << "\n";
+		MathUtils::SampleLogDistribution(f, rand, x, I, xMin, xMax, aRelError);
+		samplingOutNew << x << "\n";
+
+	}
+	samplingOut.close();
+	samplingOutNew.close();
+	std::cout << "prepare randomly generated comulative distribution file with command:\n"
+			<<"sort -g sampling | awk ' {SUM=SUM+1; printf(\"%g\\t%d\\n\",$1,SUM)}' > comulative.r\n"
+			<<"and compare it to comulative.f using gnuplot command\n\t"
+			<<"pl 'comulative.r' u 1:(" << 1.0/nTrials <<  "*$2) w l, 'comulative.f'" << endl;
+
+}
+
+void TestList::BackgroundTest()
+{
+	double zMax = 7;
+	//cosmology.init(0.73, 71, 10);
+
+	CompoundBackground backgr;
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+	IBackground* plank = new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp, 0, zMax);
+	IBackground* kneiske = new Kneiske0309EBL();
+	double conc = 413*units.Vunit;//413 cm^{-3}
+	double centralE = 6.3e-10/units.Eunit;
+	GaussianBackground backgrGauss(centralE, 0.1*centralE, 1., conc);
+	backgr.AddComponent(plank);
+	backgr.AddComponent(kneiske);
+	ofstream plankout;
+	plankout.open("backgr_plank",ios::out);
+	BackgroundTest(*plank, plankout);
+	ofstream kneiskeout;
+	kneiskeout.open("backgr_kneiskeBF",ios::out);
+	BackgroundTest(*kneiske, kneiskeout);
+	ofstream gaussout;
+	gaussout.open("backgr_gauss",ios::out);
+	BackgroundTest(backgrGauss, gaussout);
+	ofstream sumout;
+	sumout.open("backgr_sum",ios::out);
+	BackgroundTest(backgr, sumout);
+}
+
+void TestList::BackgroundTest(const IBackground& aBackground, std::ostream& aOut)
+{
+	int kIntervals = 100;
+	int zIntervals = aBackground.MaxZ()-aBackground.MinZ();
+	if(zIntervals<1)
+		zIntervals = 2;
+	double zStep = (aBackground.MaxZ()-aBackground.MinZ())/zIntervals;//usually zStep=1;
+	double kStep = pow(aBackground.MaxE()/aBackground.MinE(), 1./kIntervals);
+	double z = aBackground.MinZ();
+	aOut << "# " << aBackground.Name() << " background\n#col1: E/eV\t\tcol2: k dn(k)/dk /cm^{-3}\n";
+	for(int iZ=0; iZ<=zIntervals; iZ++, z+=zStep)
+	{
+		aOut << "#z=" << z << "\n";
+		double k = aBackground.MinE();
+		for(int iK=0; iK<=kIntervals; iK++, k*=kStep)
+			aOut << k*units.Eunit*1e6 << "\t" << aBackground.n(k,z)/units.Vunit << "\n";
+		aOut << "\n\n";
+	}
+}
+
+
+void TestList::SplittedBackgroundTest(bool aSplitted) {
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+	double Zmax = 0.15;
+	double Emin = 1./units.Eunit;
+	double alphaThinning = 1;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	result.AddOutput(new SpectrumOutput(aSplitted?"out_split" : "out_no_split", Emin, pow(10, 0.05)));
+
+	CompoundBackground backgr;
+
+	//double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+	IBackground* b1 = new GaussianBackground(1e-6/units.Eunit, 1e-7/units.Eunit, 1., 1*units.Vunit);
+	IBackground* b2 = new GaussianBackground(6.3e-10/units.Eunit, 1e-11/units.Eunit, 1., 413*units.Vunit);
+	//IBackground* b1 = new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp);
+	//IBackground* b2 = new Kneiske1001EBL();
+	//double n = BackgroundUtils::CalcIntegralDensity(*b1)/units.Vunit;
+
+	backgr.AddComponent(b1);
+	backgr.AddComponent(b2);
+
+
+	double stepZ = Zmax<0.2 ? Zmax/2 : 0.1;
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel);
+	PBackgroundIntegral backgrI1 = new ContinuousBackgroundIntegral(*b1, stepZ, logStepK, Zmax, epsRel);
+	PBackgroundIntegral backgrI2 = new ContinuousBackgroundIntegral(*b1, stepZ, logStepK, Zmax, epsRel);
+
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(new RedShift());
+	if(aSplitted)
+	{
+		pe.AddInteraction(new Interactions::ICS(backgrI1,Emin));
+		pe.AddInteraction(new Interactions::IcsCEL(backgrI1,Emin));
+		pe.AddInteraction(new Interactions::GammaPP(backgrI1));
+		pe.AddInteraction(new Interactions::ICS(backgrI2,Emin));
+		pe.AddInteraction(new Interactions::IcsCEL(backgrI2,Emin));
+		pe.AddInteraction(new Interactions::GammaPP(backgrI2));
+	}
+	else
+	{
+		pe.AddInteraction(new Interactions::ICS(backgrI,Emin));
+		pe.AddInteraction(new Interactions::IcsCEL(backgrI,Emin));
+		pe.AddInteraction(new Interactions::GammaPP(backgrI));
+	}
+
+	int nParticles = 10000;
+	for(int i=0; i<nParticles; i++)
+	{
+		Particle gamma(Photon, Zmax);
+		gamma.Energy = 1e8/units.Eunit;//MeV
+		particles.AddPrimary(gamma);
+	}
+
+	pe.Run();
+}
+
+void TestList::ThinningTest(double aAlpha)
+{
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+	double logStepK = pow(10,0.05);
+	double Zmax = 0.01;
+	double Emin = 1./units.Eunit;
+	double alphaThinning = aAlpha;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	result.AddOutput(new SpectrumOutput("out_alpha" + ToString(aAlpha), Emin, logStepK));
+
+	CompoundBackground backgr;
+
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+	//IBackground* b1 = new GaussianBackground(1e-6/units.Eunit, 1e-7/units.Eunit, 1., 1*units.Vunit);
+	//IBackground* b2 = new GaussianBackground(6.3e-10/units.Eunit, 1e-11/units.Eunit, 1., 413*units.Vunit);
+	IBackground* b1 = new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp);
+	//IBackground* b2 = new Kneiske1001EBL();
+	//double n = BackgroundUtils::CalcIntegralDensity(*b1)/units.Vunit;
+
+	backgr.AddComponent(b1);
+	//backgr.AddComponent(b2);
+
+
+	double stepZ = Zmax<0.2 ? Zmax/2 : 0.1;
+	double epsRel = 1e-3;
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel);
+
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(new RedShift());
+
+	{
+		pe.AddInteraction(new Interactions::ICS(backgrI,Emin));
+		pe.AddInteraction(new Interactions::IcsCEL(backgrI,Emin));
+		pe.AddInteraction(new Interactions::GammaPP(backgrI));
+	}
+
+	int nParticles = 1000;
+	for(int i=0; i<nParticles; i++)
+	{
+		Particle gamma(Photon, Zmax);
+		gamma.Energy = 1e8/units.Eunit;//MeV
+		particles.AddPrimary(gamma);
+	}
+
+	pe.Run();
+}
+
+void TestList::MonochromaticAndGausianBackgroundTest(bool aMonochromatic, bool aICS, bool aICScel, bool aPP)
+{
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+	double logStepK = pow(10,0.05);
+	double Zmax = 0.01;
+	double Emin = 1./units.Eunit;
+	double alphaThinning = 0.5;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	std::string out = aMonochromatic ? "out_mono" : "out_gauss";
+	if(aICS)
+		out = out + "_ICS";
+	if(aICScel)
+			out = out + "_ICScel";
+	if(aPP)
+			out = out + "_PP";
+
+	result.AddOutput(new SpectrumOutput(out , Emin, logStepK));
+
+	double conc = 413*units.Vunit;//413 cm^{-3}
+	double centralE = 6.3e-10/units.Eunit;
+	ConstFunction k(centralE);
+	ConstFunction c(conc);
+
+	GaussianBackground backgr(centralE, 0.1*centralE, 1., conc);
+
+	double stepZ = Zmax<0.2 ? Zmax/2 : 0.1;
+	double epsRel = 1e-3;
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(new RedShift());
+	PBackgroundIntegral backgrM = new MonochromaticBackgroundIntegral(c, k, logStepK, epsRel);
+	PBackgroundIntegral backgrC = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel);
+	BackgroundIntegral* backgrI = aMonochromatic ? backgrM : backgrC;
+
+	if(aICS)
+		pe.AddInteraction(new Interactions::ICS(backgrI,Emin));
+	if(aICScel)
+		pe.AddInteraction(new Interactions::IcsCEL(backgrI,Emin));
+	if(aPP)
+		pe.AddInteraction(new Interactions::GammaPP(backgrI));
+
+	int nParticles = 3000;
+	for(int i=0; i<nParticles; i++)
+	{
+		Particle startParticle(aPP?Photon:Electron, Zmax);
+		startParticle.Energy = 1e9/units.Eunit;//MeV
+		particles.AddPrimary(startParticle);
+	}
+
+	//pe.RunMultithread();
+	pe.Run();
+}
+
+//
+void TestList::BichromaticTest(bool aGauss)
+{
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+	double logStepK = pow(10,0.05);
+	double Zmax = 1.;
+	double Emin = 1./units.Eunit;
+	double alphaThinning = 0.5;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	std::string out = "bichromatic_";
+	out += aGauss ? "gauss" : "mono";
+
+	result.AddOutput(new SpectrumOutput(out , Emin, logStepK));
+
+	double centralE1 = 6.3e-10/units.Eunit;//6.3e-4eV
+	double n1 = 413*units.Vunit;//413cm^-3
+	ConstFunction k1(centralE1);
+	ConstFunction c1(n1);
+	GaussianBackground g1(centralE1, 0.1*centralE1, 1., n1);
+
+	double centralE2 = 1e-6/units.Eunit;//1 eV
+	double n2 = 1.*units.Vunit;//1 cm^-3
+	ConstFunction k2(centralE2);
+	ConstFunction c2(n2);
+	GaussianBackground g2(centralE2, 0.1*centralE2, 1., n2);
+
+	double epsRel = 1e-3;
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	//pe.AddInteraction(new RedShift());
+	PBackgroundIntegral I1,I2;
+	if(aGauss)
+	{
+		I1 = new ContinuousBackgroundIntegral(g1, 0.2, logStepK, Zmax, epsRel);
+		I2 = new ContinuousBackgroundIntegral(g2, 0.2, logStepK, Zmax, epsRel);
+	}
+	else
+	{
+		I1 = new MonochromaticBackgroundIntegral(c1, k1, logStepK, epsRel);
+		I2 = new MonochromaticBackgroundIntegral(c2, k2, logStepK, epsRel);
+	}
+	pe.AddInteraction(new Interactions::ICS(I1,Emin));
+	pe.AddInteraction(new Interactions::IcsCEL(I1,Emin));
+	pe.AddInteraction(new Interactions::GammaPP(I1));
+	pe.AddInteraction(new Interactions::ICS(I2,Emin));
+	pe.AddInteraction(new Interactions::IcsCEL(I2,Emin));
+	pe.AddInteraction(new Interactions::GammaPP(I2));
+
+	int nParticles = 1000;
+	for(int i=0; i<nParticles; i++)
+	{
+		Particle startParticle(Photon, Zmax);
+		startParticle.Energy = 1e9/units.Eunit;//MeV
+		particles.AddPrimary(startParticle);
+	}
+
+	pe.RunMultithread();
+	//pe.Run();
+}
+
+void TestList::MultithreadTest(bool aMultithread)
+{
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+	double logStepK = pow(10,0.05);
+	double Zmax = 0.01;
+	double Emin = 1./units.Eunit;
+	double alphaThinning = 0;//0.5;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	result.AddOutput(new SpectrumOutput(aMultithread ? "out_multithread" : "out_singlethread" , Emin, logStepK));
+
+	double conc = 413*units.Vunit;//413 cm^{-3}
+	double centralE = 6.3e-10/units.Eunit;
+	ConstFunction k(centralE);
+	ConstFunction c(conc);
+
+	GaussianBackground backgr(centralE, 0.1*centralE, 1., conc);
+
+	double stepZ = Zmax<0.2 ? Zmax/2 : 0.1;
+	double epsRel = 1e-3;
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	pe.AddInteraction(new RedShift());
+	PBackgroundIntegral backgrM = new MonochromaticBackgroundIntegral(c, k, logStepK, epsRel);
+	PBackgroundIntegral backgrC = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, Zmax, epsRel);
+
+	pe.AddInteraction(new Interactions::ICS(backgrM,Emin));
+	pe.AddInteraction(new Interactions::IcsCEL(backgrM,Emin));
+	pe.AddInteraction(new Interactions::GammaPP(backgrM));
+	pe.AddInteraction(new Interactions::ICS(backgrC,Emin));
+	pe.AddInteraction(new Interactions::IcsCEL(backgrC,Emin));
+	pe.AddInteraction(new Interactions::GammaPP(backgrC));
+
+	int nParticles = 30;
+	for(int i=0; i<nParticles; i++)
+	{
+		Particle gamma(Photon, Zmax);
+		gamma.Energy = 1e9/units.Eunit;//MeV
+		particles.AddPrimary(gamma);
+	}
+
+	if(aMultithread)
+		pe.RunMultithread();
+	else
+		pe.Run();
+}
+
+//this test is used to compare calculations of this code with Elmag http://uk.arxiv.org/pdf/1106.5508v2
+void TestList::KachelriesTest(double aEmin_eV, double aEmax_eV, double aZmax,
+		double aAlpha, bool aSplitted, int nParticles,
+		bool noPP, bool noICS, bool noICScel, bool aNoRedshift,
+		ParticleType primary, const char* aOutputDir)
+{
+	int kAc = 1;
+	double Emax = aEmax_eV;//eV
+	Emax *= (1e-6/units.Eunit);
+	if(!cosmology.IsInitialized())
+		cosmology.Init();// Elmag 2.0 uses H=70 km/sec/Mpc (I checked this by measuring dt/dz at z=0
+	                                 // in subroutine cascade(icq,e00,weight0,z_in)
+									 // H = z_in/((1.d0-t)*t_0)/3.2407792903e-20
+									 // Omega_vac = 0.7 as it was stated in http://uk.arxiv.org/pdf/1106.5508v2 page 6 (section 2.6. Cosmology)
+	double logStepK = pow(10,0.05/kAc);
+	double Emin = aEmin_eV*1e-6/units.Eunit;
+	double alphaThinning = aAlpha;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+	Result result(Emin);
+	std::string outputDir;
+	if(aOutputDir)
+		outputDir = aOutputDir;
+	else
+	{
+		outputDir = Particle::Name(primary);
+		outputDir += ("_E" + ToString(aEmax_eV) + "_z" + ToString(aZmax) + "_N" + ToString(nParticles));
+		if(noPP)
+			outputDir += "_NoPP";
+		if(noICS)
+			outputDir += "_NoICSdiscr";
+		if(noICScel)
+			outputDir += "_NoICScel";
+		if(aNoRedshift)
+			outputDir += "_NoRedshift";
+		if(aSplitted)
+			outputDir += "_splited";
+	}
+
+	result.AddOutput(new RawOutput(outputDir, true));
+	result.AddOutput(new SpectrumOutput(outputDir + "/spec", Emin, pow(10,0.05)));
+
+	CompoundBackground backgr;
+
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+	//IBackground* b1 = new GaussianBackground(1e-6/units.Eunit, 1e-7/units.Eunit, 1., 1*units.Vunit);
+	//IBackground* b2 = new GaussianBackground(6.3e-10/units.Eunit, 1e-11/units.Eunit, 1., 413*units.Vunit);
+	IBackground* b1 = new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp);
+	IBackground* b2 = new ElmagKneiskeBestFit(false/*using wrong background intentionally to compare to Elmag*/);
+	//double n = BackgroundUtils::CalcIntegralDensity(*b1)/units.Vunit;
+
+	backgr.AddComponent(b1);
+	backgr.AddComponent(b2);
+
+
+	double stepZ = aZmax<0.05 ? aZmax/2 : 0.025;
+	double epsRel = 1e-3;
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, aZmax, epsRel);
+	PBackgroundIntegral backgrI1 = new ContinuousBackgroundIntegral(*b1, stepZ, logStepK, aZmax, epsRel);
+	PBackgroundIntegral backgrI2 = new ContinuousBackgroundIntegral(*b2, stepZ, logStepK, aZmax, epsRel);
+
+	PropagationEngine pe(particles, result, 2011);
+	EnergyBasedThinning thinning(alphaThinning);
+	pe.SetThinning(&thinning);
+	if(!aNoRedshift)
+		pe.AddInteraction(new RedShift());
+	Particle electron(Electron, aZmax);
+	Particle photon(Photon, aZmax);
+	int nSteps = log(Emax/Emin)/log(logStepK)+1.;
+	double mult = pow(Emax/Emin, 1./nSteps);
+	int step;
+	if(aSplitted)
+	{
+		if(!noICS)
+		{
+			std::ofstream rateOut;
+			rateOut.open((outputDir + "/ICS").c_str(),std::ios::out);
+			PRandomInteraction i1,i2;
+			pe.AddInteraction((i1 = new Interactions::ICS(backgrI1,Emin)));
+			pe.AddInteraction((i2 = new Interactions::ICS(backgrI2,Emin)));
+			for(step=0, electron.Energy=Emin; step<=nSteps; electron.Energy*=mult, step++)
+			{
+				double r1 = i1->Rate(electron)*units.Mpc;
+				double r2 = i2->Rate(electron)*units.Mpc;
+				rateOut << electron.Energy * units.Eunit * 1e6 << "\t" << r1+r2 << "\t" << r1 << "\t" << r2 << "\n";
+			}
+			rateOut.close();
+		}
+		if(!noICScel)
+		{
+			std::ofstream rateOut;
+			rateOut.open((outputDir + "/ICScel").c_str(),std::ios::out);
+			PCELInteraction i1,i2;
+			pe.AddInteraction((i1 = new Interactions::IcsCEL(backgrI1,Emin)));
+			pe.AddInteraction((i2 = new Interactions::IcsCEL(backgrI2,Emin)));
+			for(step=0, electron.Energy=Emin; step<=nSteps; electron.Energy*=mult, step++)
+			{
+				double r1 = i1->Rate(electron)*units.Mpc;
+				double r2 = i2->Rate(electron)*units.Mpc;
+				rateOut << electron.Energy * units.Eunit * 1e6 << "\t" << r1+r2 << "\t" << r1 << "\t" << r2 << "\n";
+			}
+			rateOut.close();
+		}
+		if(!noPP)
+		{
+			std::ofstream rateOut;
+			rateOut.open((outputDir + "/PP").c_str(),std::ios::out);
+			PRandomInteraction i1,i2;
+			pe.AddInteraction((i1 = new Interactions::GammaPP(backgrI1)));
+			pe.AddInteraction((i2 = new Interactions::GammaPP(backgrI2)));
+			for(step=0, photon.Energy=Emin; step<=nSteps; photon.Energy*=mult, step++)
+			{
+				double r1 = i1->Rate(photon)*units.Mpc;
+				double r2 = i2->Rate(photon)*units.Mpc;
+				rateOut << photon.Energy * units.Eunit * 1e6 << "\t" << r1+r2 << "\t" << r1 << "\t" << r2 << "\n";
+			}
+			rateOut.close();
+		}
+	}
+	else
+	{
+		if(!noICS)
+		{
+			std::ofstream rateOut;
+			rateOut.open((outputDir + "/ICS").c_str(),std::ios::out);
+			PRandomInteraction i = new Interactions::ICS(backgrI,Emin);
+			pe.AddInteraction(i);
+			for(step=0, electron.Energy=Emin; step<=nSteps; electron.Energy*=mult, step++)
+				rateOut << electron.Energy * units.Eunit * 1e6 << "\t" << i->Rate(electron)*units.Mpc << "\n";
+			rateOut.close();
+		}
+		if(!noICScel)
+		{
+			std::ofstream rateOut;
+			rateOut.open((outputDir + "/ICScel").c_str(),std::ios::out);
+			PCELInteraction i = new Interactions::IcsCEL(backgrI,Emin);
+			pe.AddInteraction(i);
+			for(step=0, electron.Energy=Emin; step<=nSteps; electron.Energy*=mult, step++)
+				rateOut << electron.Energy * units.Eunit * 1e6 << "\t" << i->Rate(electron)*units.Mpc << "\n";
+			rateOut.close();
+		}
+		if(!noPP)
+		{
+			std::ofstream rateOut;
+			rateOut.open((outputDir + "/PP").c_str(),std::ios::out);
+			PRandomInteraction i = new Interactions::GammaPP(backgrI);
+			pe.AddInteraction(i);
+			for(step=0, photon.Energy=Emin; step<=nSteps; photon.Energy*=mult, step++)
+				rateOut << photon.Energy * units.Eunit * 1e6 << "\t" << i->Rate(photon)*units.Mpc << "\n";
+			rateOut.close();
+		}
+	}
+
+	for(int i=0; i<nParticles; i++)
+	{
+		//if(i%100==0)
+		//	std::cerr << i << " of " << nParticles << " ready" << std::endl;
+		Particle gamma(primary, aZmax);
+		gamma.Energy = Emax;//MeV
+		particles.AddPrimary(gamma);
+		//pe.RunMultithread();
+	}
+#ifdef _DEBUG
+	pe.Run();
+#else
+	pe.RunMultithread();
+	//pe.Run();
+#endif
+}
+
+void FermiTests::Main(int argc, char** argv)
+{
+	if(argc==0)
+	{
+		PrintUsage();
+	}
+	const char* command = argv[0];
+	if(argc == 6 && !strcmp(command,"Absorption"))
+	{
+		int aInfiniteB;
+		double aEmax;
+		double aSpecAlpha;
+		int aEBLmodel;
+		double aDistanceMpc;
+		int par=1;
+		if(sscanf(argv[par++], "%d", &aInfiniteB)!=1)
+			Exception::Throw("Failed to read InfiniteB");
+		if(sscanf(argv[par++], "%lg", &aSpecAlpha)!=1)
+			Exception::Throw("Failed to read SpecAlpha");
+		if(sscanf(argv[par++], "%d", &aEBLmodel)!=1)
+			Exception::Throw("Failed to read EBLmodel");
+		if(sscanf(argv[par++], "%lg", &aEmax)!=1)
+			Exception::Throw("Failed to read Emax");
+		if(sscanf(argv[par++], "%lg", &aDistanceMpc)!=1)
+			Exception::Throw("Failed to read DistanceMpc");
+		AbsorptionTest(aInfiniteB!=0, aSpecAlpha, aEBLmodel, aEmax*1e-6/units.Eunit, aDistanceMpc);
+	}
+	else if(argc == 5 && !strcmp(command,"DiffuseToPoint"))
+	{
+		double aSpecAlpha;
+		int aEBLmodel;
+		double aEmax;
+		int par=1;
+		int aEvolutionModel = -1;
+		//if(sscanf(argv[par++], "%d", &aInfiniteB)!=1)
+		//	Exception::Throw("Failed to read InfiniteB");
+		if(sscanf(argv[par++], "%lg", &aSpecAlpha)!=1)
+			Exception::Throw("Failed to read SpecAlpha");
+		if(sscanf(argv[par++], "%d", &aEBLmodel)!=1)
+			Exception::Throw("Failed to read EBLmodel");
+		if(sscanf(argv[par++], "%lg", &aEmax)!=1)
+			Exception::Throw("Failed to read Emax");
+		if(sscanf(argv[par++], "%d", &aEvolutionModel)!=1)
+			Exception::Throw("Failed to read EvolutionModel");
+		DiffuseToPointTest(aSpecAlpha, aEBLmodel, aEmax*1e-6/units.Eunit, aEvolutionModel);
+	}
+	else
+		PrintUsage();
+}
+
+void FermiTests::PrintUsage()
+{
+	cerr << "FERMI test commands syntax:" << std::endl <<
+	"Absorption InfiniteB SpecAlpha EBLmodel Emax/eV DistanceMpc" << std::endl << "\tor" << std::endl <<
+	"DiffuseToPoint SpecAlpha EBLmodel Emax/eV EvolutionModel" << std::endl;
+}
+
+void FermiTests::InitCosmology()
+{
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+}
+
+void FermiTests::InitSpec(int nParticles, ParticleStack& aStack, double aSpecAlpha, double aEmin, double aEmax, double aZ, double aWeight, Result* aInitialOutput)
+{
+	double logStep = pow(aEmax/aEmin, 1./(double)nParticles);
+	Particle gamma(Photon, aZ);
+	double logScaleAlpha = aSpecAlpha - 1.;
+	double E = aEmin*logStep;
+	for(int i=0; i<nParticles; i++, E*=logStep)
+	{
+		gamma.Energy = E;
+		gamma.Weight = aWeight*pow(E/aEmin, -logScaleAlpha);
+		aStack.AddPrimary(gamma);
+		if(aInitialOutput)
+			aInitialOutput->Add(gamma);
+	}
+}
+
+Particle FermiTests::SamplePowerLawSpec(double aSpecAlpha, double aEmin, double aEmax, double aZ, double aWeight, double aRand)
+{
+	double logScaleAlpha = aSpecAlpha - 1.;
+	double E = aEmin * pow(aEmax/aEmin, aRand);//sampling uniform in logscale
+	Particle gamma(Photon, aZ);
+	gamma.Energy = E;
+	gamma.Weight = aWeight*pow(E/aEmin, -logScaleAlpha);//power law spectrum
+	return gamma;
+}
+
+IBackground* FermiTests::CreateEBL(int aEBLmodel)
+{
+	IBackground* b2 = 0;
+	if(aEBLmodel==8)
+		b2 = new Kneiske1001EBL();
+	else if(aEBLmodel==9)
+		b2 = new Kneiske0309EBL();
+	else if(aEBLmodel!=0)
+		Exception::Throw("Unsupported EBLmodel parameter: expected values 0-None, 8-minimal, 9-best fit");
+	return b2;
+}
+
+const double FermiTests::MinE = 10000;//MeV
+
+void FermiTests::AbsorptionTest(bool aInfiniteB, double aSpecAlpha, int aEBLmodel, double aEmax, double aDistanceMpc)
+{
+	int nParticles = 30000;
+	InitCosmology();
+	double aZmax = cosmology.d2z(aDistanceMpc*units.Mpc);
+
+	double EminOutput = MinE/units.Eunit;
+	double EminCalc = 0.99*EminOutput;
+	double alphaThinning = 0.5;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particles;
+
+	std::string modelStr = ToString(aDistanceMpc) + "Mpc_B" + (aInfiniteB ? "1" : "0") +
+			 + "_Emax" + ToString(aEmax/units.eV) + "_Alpha" + ToString(aSpecAlpha);
+	Result resultFin(EminCalc);
+	resultFin.AddOutput(new SpectrumOutput(modelStr + "_final_spec" , EminOutput, pow(10,0.05)));
+	SmartPtr<IntegralSpectrumOutput> finalIntSpec = new IntegralSpectrumOutput(modelStr + "_final_int_spec", 100./units.Eunit);
+	resultFin.AddOutput(finalIntSpec);
+
+	Result resultIni(EminCalc);
+	resultIni.AddOutput(new SpectrumOutput(modelStr + "_ini_spec" , EminOutput, pow(10,0.05)));
+	SmartPtr<IntegralSpectrumOutput> iniIntSpec = new IntegralSpectrumOutput(modelStr + "_ini_int_spec", 100./units.Eunit);
+	resultIni.AddOutput(iniIntSpec);
+
+	PropagationEngine pe(particles, resultFin);
+	EnergyBasedThinning thinning(alphaThinning);
+	InitPropagEngine(pe, aInfiniteB, aEBLmodel, &thinning, aZmax, EminCalc);
+
+	InitSpec(nParticles, particles, aSpecAlpha, EminOutput, aEmax, aZmax, 1., &resultIni);
+	//Randomizer r;
+	//for(; nParticles>0; nParticles--)
+	//{
+	//	Particle p = SamplePowerLawSpec(aSpecAlpha, EminOutput, aEmax, aZmax, 1., r.Rand());
+	//	particles.Add(p);
+	//	resultIni.Add(p);
+	//}
+
+
+	resultIni.Flush();
+
+	//main calculation cycle
+	pe.RunMultithread();
+	//pe.Run();
+
+	//output integral and diff spectra
+	resultFin.Flush();
+
+	//output absorption factor
+	SmartPtr<TableFunction> fIni = iniIntSpec->IntegralSpectrum();
+	SmartPtr<TableFunction> fFin = finalIntSpec->IntegralSpectrum();
+	double step = pow(10,0.05);
+	std::ofstream absOut;
+	absOut.open((modelStr + "_abs").c_str(),std::ios::out);
+
+	//fIni->Print(std::cout);
+
+	double aEmaxeV = aEmax*1e6*units.Eunit;
+	for(double E=fIni->Xmin(); E<=fIni->Xmax(); E*=step)
+	{
+		double initialF = fIni->f(E);
+		ASSERT(initialF>=0);
+		if(initialF==0.)
+			continue;
+		double absorbtion = fFin->f(E)/fIni->f(E);
+		absOut << aEmaxeV <<  "\t" << aEBLmodel <<
+				"\t" << aSpecAlpha << "\t" << (aInfiniteB?1:0) <<
+				"\t" << aDistanceMpc << "\t" << E << "\t" << absorbtion << std::endl;
+	}
+	absOut.close();
+}
+
+void FermiTests::InitPropagEngine(PropagationEngine& pe, bool aInfiniteB, int aEBLmodel, IThinning* aThinning, double aZmax, double EminCalc)
+{
+	bool splitted = false;
+	double logStepK = pow(10,0.05);
+	CompoundBackground backgr;
+
+	double cmbTemp = 2.73/Units::phTemperature_mult/units.Eunit;
+	IBackground* b1 = new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp);
+	IBackground* b2 = CreateEBL(aEBLmodel);
+
+	backgr.AddComponent(b1);//transfers ownership
+	if(b2)
+		backgr.AddComponent(b2);
+
+	double stepZ = aZmax<0.2 ? aZmax/2 : 0.1;
+	double epsRel = 1e-3;
+
+	PBackgroundIntegral backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, aZmax, epsRel);
+	PBackgroundIntegral backgrI1 = new ContinuousBackgroundIntegral(*b1, stepZ, logStepK, aZmax, epsRel);
+	PBackgroundIntegral backgrI2 = b2?new ContinuousBackgroundIntegral(*b2, stepZ, logStepK, aZmax, epsRel):0;
+
+
+	pe.SetThinning(aThinning);
+	pe.AddInteraction(new RedShift());
+
+	if(splitted)
+	{
+		if(!aInfiniteB)
+		{
+			pe.AddInteraction(new Interactions::ICS(backgrI1,EminCalc));
+			pe.AddInteraction(new Interactions::IcsCEL(backgrI1,EminCalc));
+			if(b2)
+			{
+				pe.AddInteraction(new Interactions::ICS(backgrI2,EminCalc));
+				pe.AddInteraction(new Interactions::IcsCEL(backgrI2,EminCalc));
+			}
+		}
+		pe.AddInteraction(new Interactions::GammaPP(backgrI1));
+		if(b2)
+			pe.AddInteraction(new Interactions::GammaPP(backgrI2));
+	}
+	else
+	{
+		if(!aInfiniteB)
+		{
+			pe.AddInteraction(new Interactions::ICS(backgrI,EminCalc));
+			pe.AddInteraction(new Interactions::IcsCEL(backgrI,EminCalc));
+		}
+		pe.AddInteraction(new Interactions::GammaPP(backgrI));
+	}
+}
+
+void FermiTests::DiffuseToPointTest(double aSpecAlpha, int aEBLmodel, double aEmax, int aEvolutionModel)
+{
+	time_t tStart = time(0);
+	int nTotalParticles = 30000;
+	double ZmaxFull = 3;
+	double ZmaxPoint = 0.06;
+
+	InitCosmology();
+	double EminOutput = MinE/units.Eunit;
+	double EminCalc = 0.99*EminOutput;
+	double alphaThinning = 0.5;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	ParticleStack particlesPointB0;
+	ParticleStack particlesPointB1;
+	ParticleStack particlesFull;
+
+	std::string modelStr = //ToString(aInfiniteB ? "B1_" : "B0_") +
+			ToString("Emax") + ToString(aEmax/units.eV) + "_Alpha" + ToString(aSpecAlpha) + "_Evol" + ToString(aEvolutionModel);
+	Result resultFull(EminCalc);
+	resultFull.AddOutput(new SpectrumOutput(modelStr + "_full_spec" , EminOutput, pow(10,0.05)));
+	SmartPtr<IntegralSpectrumOutput> fullIntSpec = new IntegralSpectrumOutput(modelStr + "_full_int_spec", 100./units.Eunit);
+	resultFull.AddOutput(fullIntSpec);
+
+	Result resultPointB0(EminCalc);
+	resultPointB0.AddOutput(new SpectrumOutput(ToString("B0_") + modelStr + "_point_spec" , EminOutput, pow(10,0.05)));
+	SmartPtr<IntegralSpectrumOutput> pointB0IntSpec = new IntegralSpectrumOutput(ToString("B0_") + modelStr + "_point_int_spec", 100./units.Eunit);
+	resultPointB0.AddOutput(pointB0IntSpec);
+
+	Result resultPointB1(EminCalc);
+	resultPointB1.AddOutput(new SpectrumOutput(ToString("B1_") + modelStr + "_point_spec" , EminOutput, pow(10,0.05)));
+	SmartPtr<IntegralSpectrumOutput> pointB1IntSpec = new IntegralSpectrumOutput(ToString("B1_") + modelStr + "_point_int_spec", 100./units.Eunit);
+	resultPointB1.AddOutput(pointB1IntSpec);
+
+	PropagationEngine pePointB0(particlesPointB0, resultPointB0);
+	PropagationEngine pePointB1(particlesPointB1, resultPointB1);
+	PropagationEngine peFull(particlesFull, resultFull);
+	EnergyBasedThinning thinning(alphaThinning);
+	InitPropagEngine(peFull, false, aEBLmodel, &thinning, ZmaxFull, EminCalc);
+	InitPropagEngine(pePointB0, false, aEBLmodel, &thinning, ZmaxFull, EminCalc);
+	InitPropagEngine(pePointB1, true, aEBLmodel, &thinning, ZmaxFull, EminCalc);
+
+	int nEnergyBins=100;
+	int nParticlesClose = nTotalParticles/2;
+	int nParticlesFar = nTotalParticles-nParticlesClose;
+
+	double tEarliest = cosmology.z2t(ZmaxFull);
+	double t0 = cosmology.z2t(0.);
+	double tPoint = cosmology.z2t(ZmaxPoint);
+
+	double weightFar = (tPoint-tEarliest)/(t0-tPoint)*nParticlesClose/nParticlesFar;
+
+	Randomizer r;
+	for(int nSources = nParticlesClose/nEnergyBins; nSources>0; nSources--)
+	{//sampling close sources
+		double t = tPoint + r.Rand()*(t0-tPoint);
+		double z = cosmology.t2z(t);
+		double weight = Evolution::EvolFactor(z, aEvolutionModel);
+		InitSpec(nEnergyBins, particlesPointB0, aSpecAlpha, EminOutput, aEmax, z, weight);
+		InitSpec(nEnergyBins, particlesPointB1, aSpecAlpha, EminOutput, aEmax, z, weight);
+		InitSpec(nEnergyBins, particlesFull, aSpecAlpha, EminOutput, aEmax, z, weight);
+	}
+	for(int nSources = nParticlesFar/nEnergyBins; nSources>0; nSources--)
+	{//sampling far away sources
+		double t = tEarliest + r.Rand()*(tPoint-tEarliest);
+		double z = cosmology.t2z(t);
+		double weight = Evolution::EvolFactor(z, aEvolutionModel)*weightFar;
+		InitSpec(nEnergyBins, particlesFull, aSpecAlpha, EminOutput, aEmax, z, weight);
+	}
+
+	std::cerr << "\n starting B0 calculation on " << time(0) - tStart << " sec" << std::endl;
+
+	//calculate point source spectrum
+	pePointB0.RunMultithread();
+	resultPointB0.Flush();
+
+	std::cerr << "\n starting B1 calculation on " << time(0) - tStart << " sec" << std::endl;
+
+	//calculate point source spectrum
+	pePointB1.RunMultithread();
+	resultPointB1.Flush();
+
+	std::cerr << "\n starting diffuse calculation on " << time(0) - tStart << " sec" << std::endl;
+
+	//calculate full spectrum
+	peFull.RunMultithread();
+	resultFull.Flush();
+
+	std::cerr << "\n starting diffuse to point ratio calculation on " << time(0) - tStart << " sec" << std::endl;
+
+	//output diffuse to point fraction
+	SmartPtr<TableFunction> fPointB0 = pointB0IntSpec->IntegralSpectrum();
+	SmartPtr<TableFunction> fPointB1 = pointB1IntSpec->IntegralSpectrum();
+	SmartPtr<TableFunction> fFull = fullIntSpec->IntegralSpectrum();
+	double step = pow(10,0.05);
+	double aEmaxeV = aEmax*1e6*units.Eunit;
+	TableFunction* intFpoint[2] = {fPointB0, fPointB1};
+	for(int i=0; i<2; i++)
+	{
+		std::ofstream ratioOut;
+		ratioOut.open((ToString("B") + ToString(i) + "_" + modelStr + "_diff_to_point").c_str(),std::ios::out);
+		TableFunction* fPoint = intFpoint[i];
+		for(double E=fPoint->Xmin(); E<=fPoint->Xmax(); E*=step)
+		{
+			double pointF = fPoint->f(E);
+			ASSERT(pointF>=0);
+			if(pointF==0.)
+				continue;
+			double ratio = (fFull->f(E)-pointF)/pointF;
+			ratioOut << E << "\t" << ratio << "\t" << aEmaxeV <<  "\t" << aEBLmodel <<
+					"\t" << aSpecAlpha << "\t" << 0 << std::endl;
+		}
+		ratioOut.close();
+	}
+	std::cerr << "\n finished on " << time(0) - tStart << " sec" << std::endl;
+}
+
+double Evolution::EvolFactor(double z, int aEvolutionModel)
+{
+	switch (aEvolutionModel)
+	{
+	case EZDependenceOff:
+		return 1.;
+	case WaxmanBahcall://if m=0 corresponds to Waxman-Bahcall, hep-ph/9807282; Engel at al astro-ph/0101216 (oSFR in astro-ph/0605327v2)
+		return WaxmanBahcallEvolution(z);
+	case SFR03://if m=0 corresponds to Star Formation Rate from astro-ph/0309141
+		return SfrEvolution(z);
+	case SFR06://if m=0 corresponds to star formation rate from astro-ph/0607087 (lower figure 3)
+		return SfrNewEvolution(z);
+	case Weak://if m=0 corresponds to m=3 to z=1.8 and constant to z=3 going to zero there
+		return weakEvolution(z);
+	case Baseline://if m=0 corresponds to baseline evolution astro-ph-1512479
+		return baselineEvolution(z);
+	case Strong://if m=0 corresponds to fast evolution astro-ph-1512479
+		return strongEvolution(z);
+	case Engel9://if m=0 corresponds to Fig. 9 of http://lanl.arxiv.org/abs/astro-ph/0101216v2
+		return Engel9Evolution(z);
+	case SFR_Yuksel:
+		return SFR_YukselEvolution(z);
+	case GRB_Yuksel:
+		return GRB_YukselEvolution(z);
+	case AGN_Ahlers:
+		return AGN_AhlersEvolution(z);
+	default:
+		Exception::Throw("invalid value of EvolutionModel parameter");
+		break;
+	};
+	return 0.;
+}
+
+FermiTests::IntegralSpectrumOutput::IntegralSpectrumOutput(std::string aFile, double aEmin):
+		fEmin(aEmin)
+{
+	fOut.open(aFile.c_str(),std::ios::out);
+	if(!fOut.is_open())
+		Exception::Throw("Failed to open " + aFile);
+}
+
+FermiTests::IntegralSpectrumOutput::~IntegralSpectrumOutput() {
+	if(fOut.is_open())
+		fOut.close();
+}
+
+void FermiTests::IntegralSpectrumOutput::AddParticle(const Particle& aParticle)
+{
+	if(aParticle.Energy>=fEmin && aParticle.Type == Photon)
+		fParticles.insert(aParticle);
+}
+
+void FermiTests::IntegralSpectrumOutput::Flush(bool aIsFinal)
+{
+    if(!aIsFinal)
+        return;//intermediate output is not supported
+	if(!fOut.is_open())
+	{
+		std::cerr << "FermiTests::IntegralSpectrumOutput::Flush(): ignoring second call";
+		return;
+	}
+	double E=-1;
+	double sum=0.;
+	double mult = units.Eunit*1e6;
+	for(multiset<Particle,More>::iterator it = fParticles.begin(); it != fParticles.end(); it++)
+	{
+		const Particle& p = *it;
+		if(p.Energy != E && sum > 0)
+		{
+			fOut << E*mult << "\t" << sum*mult << "\n";
+			fE.insert(fE.begin(), E*mult);
+			fIntFlux.insert(fIntFlux.begin(), sum*mult);
+		}
+		E = p.Energy;
+		sum += p.Weight;
+	}
+	if(sum>0)
+	{
+		fOut << E*mult << "\t" << sum*mult << "\n";
+		fE.insert(fE.begin(), E*mult);
+		fIntFlux.insert(fIntFlux.begin(), sum*mult);
+	}
+	fOut.close();
+}
+
+TableFunction* FermiTests::IntegralSpectrumOutput::IntegralSpectrum() const
+{
+	ASSERT(fE.size()>0);//End must be called prior to this method call
+	ASSERT(fE.size()>=2);//at least two data points should be present
+	return new LinearFunc(fE, fIntFlux);
+}
+
+void TestList::RegisterTest(std::string aKey, TestProc aFunc) {
+	fTests[aKey] = aFunc;
+}
+
+TestList::TestList() {
+	TestList::RegisterTest("TurbulentMF",TurbulentMF::UnitTest);
+	TestList::RegisterTest("MonochromaticMF",MonochromaticMF::UnitTest);
+	TestList::RegisterTest("Deflection3D",Deflection3D::UnitTest);
+	TestList::RegisterTest("NeutronDecay",NeutronDecay::UnitTestEconserv);
+	TestList::RegisterTest("Stecker05EBL",Stecker2005EBL::UnitTest);
+	TestList::RegisterTest("Stecker16EBL",Stecker16Background::UnitTest);
+	TestList::RegisterTest("nucl_dis",PhotoDisintegration::UnitTest);
+	TestList::RegisterTest("sampler",MathUtils::UnitTest);
+}
+
+
+EConservationTest::EConservationTest(double aMaxZ, double Emin_eV, u_long aRandSeed):
+randomizer(aRandSeed),
+result(Emin_eV*units.eV),
+primary(Electron,0),
+alphaThinning(0)
+{
+	if(!cosmology.IsInitialized())
+		cosmology.Init();
+
+	out = new TotalEnergyOutput();
+	result.AddOutput(out);
+
+	double cmbTemp = 2.73*units.K;
+	backgr.AddComponent(new PlankBackground(cmbTemp, 1e-3*cmbTemp, 1e3*cmbTemp));
+	backgr.AddComponent(new Backgrounds::Kneiske1001EBL());
+
+	double logStepK = pow(10,0.05);
+	double epsRel = 1e-3;
+
+	stepZ = aMaxZ <0.2 ? aMaxZ /2 : 0.1;
+	backgrI = new ContinuousBackgroundIntegral(backgr, stepZ, logStepK, aMaxZ, epsRel);
+	pe = new PropagationEngine(particles, result, randomizer.CreateIndependent());
+}
+
+void EConservationTest::SetPrimary(const Particle& aPrimary, int aCount)
+{
+	ASSERT(nParticles>0);
+	primary = aPrimary;
+	for(int i=0; aCount > i; i++)
+	{
+		particles.AddPrimary(aPrimary);
+	}
+	nParticles = aCount;
+}
+
+void EConservationTest::Run()
+{
+	ASSERT(nParticles>0);//SetPrimary should be called prior to this method
+	EnergyBasedThinning thinning(alphaThinning);
+	pe->SetThinning(&thinning);
+	pe->Run();
+
+	double startE = nParticles* primary.Energy;
+	double Ep = out->TotalE(primary.Type);
+	double Esec = 0;
+	for(int i=0; i<ParticleTypeEOF; i++)
+		if(i!= primary.Type)
+			Esec += out->TotalE((ParticleType)i);
+	double deltaEp = startE-Ep;
+	std::cout << "|deltaE_prim|/E_prim = " << deltaEp/startE << "\t\t(|deltaE_prim|-E_sec)/|deltaE_prim| = " << (deltaEp-Esec)/deltaEp <<  std::endl;
+}
+
+
+} /* namespace Test */
diff --git a/src/lib/Test.h b/src/lib/Test.h
new file mode 100644
index 0000000..bdbe784
--- /dev/null
+++ b/src/lib/Test.h
@@ -0,0 +1,232 @@
+/*
+ * Test.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef TEST_H_
+#define TEST_H_
+
+#include <string>
+#include <map>
+#include "Background.h"
+#include "ParticleStack.h"
+#include "PropagationEngine.h"
+
+namespace Test {
+
+using namespace mcray;
+	typedef int (*TestProc)();
+
+class TestList {
+public:
+
+	TestList();
+
+	int Main(int argc, char** argv);
+	static void FrameworkTest();
+	static void BackgroundTest();
+	static void BackgroundTest(const IBackground& aBackground, std::ostream& aOut);
+	static void SplittedBackgroundTest(bool aSplitted);
+	static void ThinningTest(double aAlpha);
+	static void KachelriesTest(double aEmin_eV, double aEmax_eV, double aZmax,
+			double aAlpha, bool aSplitted, int aNoParticles,
+			bool aNoPP, bool aNoICS, bool aNoICScel, bool aNoRedshift,
+			ParticleType aPrimary, const char* aOutputDir = 0);
+	static void MonochromaticAndGausianBackgroundTest(bool aMonochromatic, bool aICS, bool aICScel, bool aPP);
+	static void MultithreadTest(bool aMultithread);
+	static void BichromaticTest(bool aGauss);
+	static void SamplingTest();
+	static void EngineTest(double aAlphaThinning);
+	void RegisterTest(std::string aKey, TestProc aFunc);
+private:
+	std::map<std::string, TestProc> fTests;
+};
+
+class FermiTests {
+	class IntegralSpectrumOutput : public TSmartReferencedObj<IOutput> {
+	    struct More : public std::binary_function<Particle, Particle, bool>
+	        {
+	          bool
+	          operator()(const Particle& __x, const Particle& __y) const
+	          {
+	        	  return __x.Energy > __y.Energy;
+	          }
+	        };
+	    std::multiset<Particle,More> fParticles;//sort by energy in descending order
+	public:
+	    IntegralSpectrumOutput(std::string aFile, double aEmin);
+		virtual ~IntegralSpectrumOutput();
+		void AddParticle(const Particle& aParticle);
+		void Flush(bool aIsFinal);
+		TableFunction* IntegralSpectrum() const;//may only be called after End()
+	private:
+		std::ofstream	fOut;
+		double fEmin;
+		std::vector<double> fE;
+		std::vector<double> fIntFlux;
+	};
+public:
+	static void Main(int argc, char** argv);
+private:
+	static void InitCosmology();
+	static void PrintUsage();
+	static void AbsorptionTest(bool aInfiniteB, double aSpecAlpha, int aEBLmodel, double aEmax, double aDistanceMpc);
+	static void DiffuseToPointTest(double aSpecAlpha, int aEBLmodel, double aEmax, int aEvolutionModel);
+	static void InitSpec(int aParticlesCount, ParticleStack& aStack, double aSpecAlpha, double aEmin, double aEmax, double aZ, double aWeight, Result* aInitialOutput = 0);
+	static Particle SamplePowerLawSpec(double aSpecAlpha, double aEmin, double aEmax, double aZ, double aWeight, double aRand);
+	static IBackground* CreateEBL(int aEBLmodel);
+	static void InitPropagEngine(PropagationEngine& aEngine, bool aInfiniteB, int aEBLmodel, IThinning* aThinning, double aZmax, double aEminCalc);
+	static const double MinE;
+};
+
+class Evolution
+{
+public:
+	enum TZDependence{
+		EZDependenceOff = 0,//(1+z)^3
+		EZDependencePowerLaw,//(1+z)^(3+m)
+		EZDependencePowerTD,//t^{-4+p}
+
+	//injection spectrum types below are proportional to (1+z)^m:
+
+		WaxmanBahcall,//if m=0 corresponds to Waxman-Bahcall, hep-ph/9807282; Engel at al astro-ph/0101216 (oSFR in astro-ph/0605327v2)
+		SFR03,//if m=0 corresponds to Star Formation Rate from astro-ph/0309141
+		Weak,//if m=0 corresponds to m=3 to z=1.8 and constant to z=3 going to zero there
+		Baseline,//if m=0 corresponds to baseline evolution astro-ph-1512479
+		Strong,//if m=0 corresponds to fast evolution astro-ph-1512479
+		Engel9,//if m=0 corresponds to Fig. 9 of http://lanl.arxiv.org/abs/astro-ph/0101216v2
+		PowerCut,//(1+z)^m from Zmin up to PowerCutZ and const from PowerCutZ to Zmax
+		SFR06,//star formation rate from astro-ph/0607087 (lower figure 3)
+		SFR_Yuksel,//star formation rate from http://lanl.arxiv.org/abs/1103.3574v2  (formula 11)
+		GRB_Yuksel,//GRB rate from http://lanl.arxiv.org/abs/1103.3574v2  (formula 12)
+		AGN_Ahlers,//AGN rate from http://lanl.arxiv.org/abs/1103.3574v2  (formula 13)
+		EZDependenceEOF
+	};
+
+	static double EvolFactor(double aZ, int aEvolutionModel);
+
+	static inline double weakEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+	//m=3 to z=1.8 and constant to z=3 going to zero there
+		return (aZ<1.8)?pow(1.+aZ,3.):((aZ<3.)?21.952:0.);
+	}
+
+	//baseline evolution astro-ph-0512479
+	static inline double baselineEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+	//m=4 to z=1 and then constant to z=6 going to zero there.
+		return (aZ<1.3)?pow(1.+aZ,3.1):((aZ<6)?13.2238005912547:0.);
+	}
+
+	//fast evolution astro-ph-0512479
+	static inline double strongEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+	//m=4 to z=1 and then constant to z=6 going to zero there.
+		return (aZ<1.0)?pow(1.+aZ,4.):((aZ<6)?16.:0.);
+	}
+
+	//AGN
+	//Waxman-Bahcall, hep-ph/9807282; Engel at al astro-ph/0101216 (oSFR in astro-ph/0605327v2)
+	static inline double WaxmanBahcallEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+		return (aZ<1.9)?pow(1.+aZ,3.):(24.389*((aZ<2.7)?1.:exp((2.7-aZ)/2.7)));
+	}
+
+	//Fig. 9 of http://lanl.arxiv.org/abs/astro-ph/0101216v2
+	static inline double Engel9Evolution(double aZ){//corresponds to comoving frame only if m=0!!!
+		return ((aZ<1.9)?pow(1.+aZ,4.):70.7281);
+	}
+
+	//Star Formation Rate from astro-ph/0309141
+	static inline double SfrEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+		return (aZ<1.2)?pow(1.+aZ, 3.5):40.68052976*pow(1.+aZ, -1.2);//40.68052976==(1+1.2)^(3.5+1.2)
+	}
+
+	//star formation rate from astro-ph/0607087 (lower figure 3)
+	static inline double SfrNewEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+		if(aZ<1.)
+			return pow(1.+aZ, 4.65);
+		else if(aZ<3.)
+			return 7.7274906314*pow(1.+aZ, 1.7);
+		else if(aZ<6.)
+			return 7912.950406552335*pow(1.+aZ, -3.3);
+		else if(aZ<8.)
+			return 606880463687.06*pow(1.+aZ, -12.63);
+		else
+			return 0.;
+	}
+
+	//star formation rate from http://lanl.arxiv.org/abs/1103.3574v2  (formula 11)
+	static inline double SFR_YukselEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+		if(aZ<1.)
+			return pow(1.+aZ, 3.4);
+		else if(aZ<4.)
+			return 10.5560632861832*pow((1.+aZ)/2., -0.3);
+		else
+			return 8.01899573803639*pow((1.+aZ)/5., -3.5);
+	}
+
+	//GRB rate from http://lanl.arxiv.org/abs/1103.3574v2  (formula 12)
+	static inline double GRB_YukselEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+		if(aZ<1.)
+			return pow(1.+aZ, 4.8);
+		else if(aZ<4.)
+			return 27.857618025476*pow((1.+aZ)/2., 1.1);
+		else
+			return 76.3269641062938*pow((1.+aZ)/5., -2.1);
+	}
+
+	//AGN rate from http://lanl.arxiv.org/abs/1103.3574v2  (formula 13)
+	static inline double AGN_AhlersEvolution(double aZ){//corresponds to comoving frame only if m=0!!!
+		if(aZ<1.7)
+			return pow(1.+aZ, 5);
+		else if(aZ<2.7)
+			return 143.48907;
+		else
+			return 143.48907*pow(10.,2.7-aZ);
+	}
+};
+
+	class EConservationTest{
+	public:
+		EConservationTest(double aMaxZ, double Emin_eV=1e6, u_long aRandSeed = 0);
+		void SetPrimary(const Particle& aPrimary, int aCount);
+		void Run();
+		Randomizer& GetRandomizer() { return randomizer;}
+		PropagationEngine& Engine(){return *pe;}
+		BackgroundIntegral* Backgr(){ return backgrI;}
+		double alphaThinning;//alpha = 1 conserves number of particles on the average; alpha = 0 disables thinning
+	private:
+		Randomizer randomizer;
+		ParticleStack particles;
+		Result result;
+		SmartPtr<TotalEnergyOutput> out;
+		CompoundBackground backgr;
+		double stepZ;
+		std::vector<PInteraction>     fInteractions;
+		PBackgroundIntegral backgrI;
+		SafePtr<PropagationEngine> pe;
+		Particle primary;
+		int nParticles;
+	};
+} /* namespace Test */
+#endif /* TEST_H_ */
diff --git a/src/lib/Thinning.cpp b/src/lib/Thinning.cpp
new file mode 100644
index 0000000..2a4df82
--- /dev/null
+++ b/src/lib/Thinning.cpp
@@ -0,0 +1,65 @@
+/*
+ * Thinning.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "Thinning.h"
+
+namespace mcray
+{
+
+EnergyBasedThinning::EnergyBasedThinning(double aAlpha, FilterMode aFilterMode) :
+		fAlpha(aAlpha),
+		fEnableFor(ParticleTypeEOF, aFilterMode != BlackList)
+{
+	ASSERT(aAlpha>=0 && aAlpha<=1);
+}
+
+void EnergyBasedThinning::Run(std::vector<Particle>& aParticles, Randomizer& aRandomizer)
+{
+	if(aParticles.size()<=1)
+		return;
+	double totEnergy = 0.;
+	for(std::vector<Particle>::const_iterator it = aParticles.begin(); it != aParticles.end(); it++)
+		if(fEnableFor[it->Type])
+			totEnergy += it->Energy;
+	if(totEnergy == 0.)
+		return;//preserve all particles
+	for(int i=aParticles.size()-1; i>=0; i--)
+	{
+		if(!fEnableFor[aParticles[i].Type])
+			continue;
+		double probability = pow(aParticles[i].Energy/totEnergy, fAlpha);//probability to survive
+		if(aRandomizer.Rand()>probability) {
+			LOG_VERBOSE("thinning:erase\t" + (aParticles.begin() + i)->ToString());
+			aParticles.erase(aParticles.begin() + i);
+		}
+		else
+			aParticles[i].Weight /= probability;
+	}
+}
+
+}//end of namespace mcray
diff --git a/src/lib/Thinning.h b/src/lib/Thinning.h
new file mode 100644
index 0000000..1b87178
--- /dev/null
+++ b/src/lib/Thinning.h
@@ -0,0 +1,63 @@
+/*
+ * Thinning.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef THINNING_H
+#define	THINNING_H
+
+#include <vector>
+#include "Particle.h"
+#include "Randomizer.h"
+
+namespace mcray
+{
+
+class IThinning {
+public:
+	//must be thread-safe
+    virtual void Run(std::vector<Particle>& aParticles, Randomizer& aRandomizer) = 0;
+};
+
+	enum FilterMode{
+		BlackList,
+		WhiteList
+	};
+
+class EnergyBasedThinning : public  IThinning {
+public:
+	EnergyBasedThinning(double aAlpha, FilterMode aFilterMode=WhiteList);
+    void Run(std::vector<Particle>& aParticles, Randomizer& aRandomizer);
+    virtual ~EnergyBasedThinning(){}
+	void EnableFor(ParticleType aType, bool aDoEnable = true) { fEnableFor[aType] = aDoEnable;}
+private:
+    double				fAlpha;
+	std::vector<bool>	fEnableFor;
+};
+
+}
+#endif	/* THINNING_H */
+
diff --git a/src/lib/Units.cpp b/src/lib/Units.cpp
new file mode 100644
index 0000000..3026d79
--- /dev/null
+++ b/src/lib/Units.cpp
@@ -0,0 +1,92 @@
+/*
+ * Units.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "Units.h"
+#include <math.h>
+
+namespace mcray
+{
+const double Units::e = 0.0854245431348626; /* sqrt(alpha) */
+const double Units::plank=6.582122020e-22; /* Plank constant (MeV*s)   */
+const double Units::plank_ESU=1.05457266e-27;/* Plank constant (erg*s)   */
+const double Units::phTemperature_mult=1.16e10;// K/MeV
+const double Units::Mpc_in_cm=3.0856775807e24;     /*  (cm)  */
+#ifdef ELMAG_TEST
+const double Units::lightspeed=3e10; /* (cm/s) */
+#else
+const double Units::lightspeed=2.99792458e10; /* (cm/s) */
+#endif
+const double Units::MeVinESU=1.60217733e-6;  /* 1MeV in Ergs */
+const double Units::YearInSec = 31557600;//number of seconds in 1 year
+
+const Units units;//global units object
+
+Units::Units()
+{
+	Eunit=1.; /* units of energy used (in MeV) */ //TODO: BUG!!! setting Eunit=1e-6 lead to different results: try for ex --test FERMI Absorption 1 2 8 1e15 0.06
+	Eunit3=0; /* Eunit^3 */
+	Lunit=0;   /* length unit in sm. */
+	Tunit=0;   /* time unit in sec.  */
+	Vunit=0;   /* Vunit=Lunit^3   */
+	sigmaUnit=0;  /* sigmaUnit=Lunit^2   */
+	SpecUnit=0;  /* SpecUnit=Eunit*Tunit*Vunit */
+	outEunit=1e-6; /* Output data energy unit in MeV*/
+	Bunit=0;  /* Magnetic field internal unit in Gauss (esu) */
+	barn = 0;/*	1 barn (cross-section unit) in internal units */
+
+	MeV = 1./Eunit;
+	K = MeV/phTemperature_mult;
+	eV = 1e-6*MeV;
+	GeV = 1e3*MeV;
+	TeV = 1e6*MeV;
+	PeV = 1e9*MeV;
+	Tunit=plank/Eunit;      /* time unit in sec.  */
+	Lunit=lightspeed*Tunit; /* length unit in sm. */
+	Eunit3 = Eunit*Eunit*Eunit;
+
+	sigmaUnit=Lunit*Lunit;
+	Vunit=Lunit*Lunit*Lunit;
+	SpecUnit=Eunit*Tunit*Vunit;
+
+	Bunit=MeVinESU*MeVinESU*pow(plank_ESU*lightspeed,-1.5);	// 1MeV^2 in Gauss
+	Bunit*=(Eunit*Eunit);	//B unit in Gauss (esu)
+	Gauss=1./Bunit;
+	barn = 1e-24/sigmaUnit;
+	Mpc = Mpc_in_cm/Lunit;
+	kpc = 1e-3*Mpc;
+	second = 1./Tunit;
+	year = YearInSec * second;
+	cm = 1./Lunit;
+	cm3 = cm*cm*cm;
+	Hz_photon = 2.*M_PI/second;
+	erg = MeV/MeVinESU;
+	J = (1e7*erg);
+	W = (J/second);
+}
+
+} /* namespace mcray */
diff --git a/src/lib/Units.h b/src/lib/Units.h
new file mode 100644
index 0000000..2db4f59
--- /dev/null
+++ b/src/lib/Units.h
@@ -0,0 +1,83 @@
+/*
+ * Units.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef UNITS_H_
+#define UNITS_H_
+
+namespace mcray
+{
+
+class Units
+{
+public:
+	Units();
+	static const double Mpc_in_cm;     /*  (cm)  */
+	static const double lightspeed; /* (cm/s) */
+	static const double e; /* electric charge of electron in system plank=c=1*/
+
+	double Eunit; /* units of energy used (in MeV) */
+	double Eunit3; /* Eunit^3 */
+	double Lunit;   /* length unit in cm. */
+	double Tunit;   /* time unit in sec.  */
+	double Vunit;   /* Vunit=Lunit^3   */
+	double sigmaUnit;  /* sigmaUnit=Lunit^2   */
+	double SpecUnit;  /* SpecUnit=Eunit*Tunit*Vunit */
+	double outEunit; /* Output data energy unit in MeV*/
+	double Bunit;  /* Magnetic field internal unit in Gauss (esu) */
+	static const double MeVinESU;  /* 1MeV in Ergs */
+	static const double YearInSec;//number of seconds in 1 year
+	static const double phTemperature_mult;
+
+//////   conventional units expressed in internal units   ////////////////
+
+	double Mpc;
+	double kpc;
+	double barn;
+	double MeV;
+	double GeV;
+	double eV;
+	double TeV;
+	double PeV;
+	double second;
+	double year;
+	double cm;
+	double cm3;//cubic cm
+	double erg;
+	double K;//Kelvin
+	double Gauss;
+
+	double Hz_photon;// energy of 1 Hz EM wave photon (in internal units)
+	double J; //Joule
+	double W; //Watt
+private:
+	static const double plank; /* Plank constant (MeV*s)   */
+	static const double plank_ESU;/* Plank constant (erg*s)   */
+};
+	 extern const Units units;
+} /* namespace mcray */
+#endif /* UNITS_H_ */
diff --git a/src/lib/Utils.cpp b/src/lib/Utils.cpp
new file mode 100644
index 0000000..3d3f0dd
--- /dev/null
+++ b/src/lib/Utils.cpp
@@ -0,0 +1,32 @@
+/*
+ * Utils.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include "Utils.h"
+
+
+
diff --git a/src/lib/Utils.h b/src/lib/Utils.h
new file mode 100644
index 0000000..3e8651a
--- /dev/null
+++ b/src/lib/Utils.h
@@ -0,0 +1,234 @@
+/*
+ * Utils.h
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#ifndef UTILS_H
+#define	UTILS_H
+
+#include <string>
+#include <sstream>
+#include <vector>
+#include <float.h>
+#include <iostream>
+#include <iomanip>
+#include <omp.h>
+#include <sys/types.h>
+#include "Debug.h"
+#include <algorithm>
+
+namespace Utils {
+
+#define DIR_DELIMITER_STR "/"
+#define TABLES_DIR "tables" DIR_DELIMITER_STR
+
+class Exception
+{
+public:
+	Exception(std::string aErrorMessage):fMessage(aErrorMessage){}
+	std::string Message() const { return fMessage; }
+	inline static void Throw(std::string aErrorMessage) { throw new Exception(aErrorMessage); }
+private:
+	std::string fMessage;
+};
+
+typedef std::vector<double> Vector;
+
+template<class type>
+class SafePtr
+{
+public:
+	SafePtr(type* _pType=0):pType(_pType){};
+	virtual ~SafePtr(){delete pType;};
+	SafePtr& operator=(type* _pType){delete pType; pType=_pType; return (*this);};
+	bool isNull(){return (pType==0);};
+
+	operator type*(){return pType;};
+	operator const type*() const {return pType;};
+
+	//operator type&(){ASSERT(pType);return *pType;};
+	//operator const type&() const {ASSERT(pType);return *pType;};
+
+	type& operator*() {ASSERT(pType);return *pType;};
+	const type& operator*() const {ASSERT(pType);return *pType;};
+
+	inline type* operator->() const
+	{
+		ASSERT(pType);
+		return pType;
+	}
+private:
+	type* pType;
+};
+
+template<class type>
+class AutoDeletePtrArray : public std::vector<type*>
+{
+public:
+	virtual ~AutoDeletePtrArray()
+	{
+		std::remove_if(std::vector<type*>::begin(),std::vector<type*>::end(),deleteAll);
+	};
+	static bool deleteAll( type * aElement ) { delete aElement; return true; }
+
+	inline void setIndexShift(int aShift)
+	{
+		iMin = -aShift;
+	};
+
+	inline type& operator()(int aIndex){
+		return *std::vector<type*>::at(aIndex - iMin);
+	}
+
+	inline const type& operator()(int aIndex) const{
+		return *std::vector<type*>::at(aIndex - iMin);
+	}
+
+	inline type* operator[](int aIndex){
+		return std::vector<type*>::at(aIndex - iMin);
+	}
+
+	inline const type* operator[](int aIndex) const{
+		return std::vector<type*>::at(aIndex - iMin);
+	}
+
+	inline void add(type* aElem){
+		std::vector<type*>::push_back(aElem);
+	}
+private:
+	int iMin;
+};
+
+	class  ISmartReferencedObj
+{
+public:
+	virtual void addRef()=0;
+	virtual void releaseRef()=0;
+};
+
+class SmartReferencedObj : public virtual ISmartReferencedObj{
+public:
+	SmartReferencedObj():iRefCount(0){};
+	void addRef(){
+		iRefCount++;
+	}
+	void releaseRef(){
+		iRefCount--;
+		if (iRefCount<=0) {
+			delete this;
+		}
+	}
+	inline int RefCount() const{return iRefCount;};
+	virtual ~SmartReferencedObj(){};
+private:
+	int iRefCount;
+};
+
+template <class I>
+class TSmartReferencedObj : public I // It is assumed that I is interface which extends ISmartReferencedObj
+{
+public:
+	TSmartReferencedObj():iRefCount(0){};
+	void addRef(){
+		iRefCount++;
+	}
+	void releaseRef(){
+		iRefCount--;
+		if (iRefCount<=0) {
+			delete this;
+		}
+	}
+	inline int RefCount() const{return iRefCount;};
+	virtual ~TSmartReferencedObj(){};
+private:
+	int iRefCount;
+};
+
+template <class T>//class T should have addRef() & releaseRef() methods
+class SmartPtr
+{
+public:
+  SmartPtr(T* pointee = 0) : iPointee(pointee){
+		if (iPointee) iPointee->addRef();
+  };
+
+  SmartPtr(const SmartPtr<T>& other) : iPointee(other.iPointee){
+		if (iPointee) iPointee->addRef();
+  };
+
+  inline SmartPtr& operator=(T* pointee){
+	  if(iPointee==pointee)
+		  return *this;
+	  if (iPointee) iPointee->releaseRef();
+	  iPointee = pointee;
+	  if (pointee) iPointee->addRef();
+	  return *this;
+  }
+
+  inline SmartPtr& operator=(const SmartPtr<T>& other){
+	  if (iPointee) iPointee->releaseRef();
+	  iPointee = other.iPointee;
+	  if (iPointee) iPointee->addRef();
+	  return *this;
+  }
+
+  ~SmartPtr(){
+		if (iPointee) iPointee->releaseRef();
+  }
+
+  inline bool operator==(T* pointee) const{
+	  return iPointee==pointee;
+  }
+
+  inline T& operator*() const
+  {
+	return *iPointee;
+  }
+
+  inline T* operator->() const
+  {
+	return iPointee;
+  }
+  
+  inline operator T*() const
+  {
+	return iPointee;
+  }
+
+  inline T& operator[](int) const{
+	  //! although operator T* is defined, smart pointers should not be used as C-arrays
+	  NOT_IMPLEMENTED;
+	  return *iPointee;
+  }
+
+private:
+  T* iPointee;
+};
+
+}//end of namespace Utils
+
+#endif	/* UTILS_H */
+
diff --git a/src/lib/main.cpp b/src/lib/main.cpp
new file mode 100644
index 0000000..666d48c
--- /dev/null
+++ b/src/lib/main.cpp
@@ -0,0 +1,63 @@
+/*
+ * main.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+#include <cstdlib>
+#include <time.h>
+#include "Test.h"
+
+using namespace Utils;
+
+/*
+ * user_main function performing user defined tasks
+ * the function should be provided by end user
+ */
+extern int user_main(int argc, char** argv);
+
+int main(int argc, char** argv) {
+	int result = 0;
+	time_t startTime = time(0);
+	try
+	{
+		if(argc>=2 && (strcmp(argv[1],"--test"))==0) {
+			Test::TestList testList;
+			result = testList.Main(argc - 2, argv + 2);
+		}
+		else
+			result = user_main(argc, argv);
+	}
+	catch(Exception* ex)
+	{
+		std::cerr << ex->Message() << std::endl;
+		result = 1;
+	}
+	if(!result){
+		std::cerr << "# calculation took " << time(0)-startTime << " sec" << std::endl;
+	}
+	return result;
+}
+
diff --git a/src/lib/sophia2cpp.f b/src/lib/sophia2cpp.f
new file mode 100644
index 0000000..1238450
--- /dev/null
+++ b/src/lib/sophia2cpp.f
@@ -0,0 +1,85 @@
+       subroutine sample_photopion(L0,E0,eps,theta,iSec,energies,nTypes)
+
+       IMPLICIT DOUBLE PRECISION (A-H,O-Z)
+       IMPLICIT INTEGER (I-N)
+       SAVE
+       DIMENSION energies(2000)
+       DIMENSION nTypes(2000)
+       COMMON /S_PLIST/ P(2000,5), LLIST(2000), NP, Ideb
+
+       call eventgen(L0,E0,eps,theta,Imode)
+       iSec=NP
+       do i=1,NP
+        nTypes(i) = abs(LLIST(i))
+        energies(i) = abs(P(i,4))
+       enddo
+
+       RETURN
+       END
+
+       subroutine crossec(L0,eps_prime,sig)
+
+       IMPLICIT DOUBLE PRECISION (A-H,O-Z)
+       IMPLICIT INTEGER (I-N)
+       SAVE
+
+       sig = crossection(eps_prime,3,L0)
+
+       RETURN
+       END
+
+       subroutine sample_photopion_rel(L0,eps_prime,iSec,relEnergies,nTypes)
+
+       IMPLICIT DOUBLE PRECISION (A-H,O-Z)
+       IMPLICIT INTEGER (I-N)
+       SAVE
+       DIMENSION relEnergies(2000)
+       DIMENSION nTypes(2000)
+       COMMON /S_PLIST/ P(2000,5), LLIST(2000), NP, Ideb
+       COMMON /S_MASS1/ AM(49), AM2(49)
+
+c      Using proton rest frame
+       E0=AM(L0)
+c      we assume that lab frame speed is directed along z-axes
+c      than with good accuracy initial photon momentum is directed
+c      against z-axis
+       theta=0
+
+       call eventgen(L0,E0,eps_prime,theta,Imode)
+       iSec=NP
+       do i=1,NP
+        nTypes(i) = abs(LLIST(i))
+c relEnergies(i) - energy of final particle in units of
+c initial nucleon energy in lab frame. Here we use the fact that nucleon
+c rest frame speed in lab frame is directed along z-axis (against photon
+c momentum in rest frame) with good accuracy and assume
+c ultrarelativistic case: beta=1 and so the fraction doesn't depend on
+c the initial nucleon gamma factor.
+c The precise expression would be
+c       relEnergies(i) = abs(P(i,4)+beta*P(i,3))/AM(L0)
+c where beta is speed of initial nucleon in the rest frame
+        relEnergies(i) = abs(P(i,4)+P(i,3))/AM(L0)
+       enddo
+
+       RETURN
+       END
+
+       subroutine get_mass(L0,pm)
+
+       IMPLICIT DOUBLE PRECISION (A-H,O-Z)
+       IMPLICIT INTEGER (I-N)
+       SAVE
+       COMMON /S_MASS1/ AM(49), AM2(49)
+       pm = AM(L0)
+       RETURN
+       END
+
+       subroutine set_random_seed(iseed)
+
+       IMPLICIT DOUBLE PRECISION (A-H,O-Z)
+       IMPLICIT INTEGER (I-N)
+       COMMON/LUDATR/MRLU(6),RRLU(100)
+       SAVE
+       MRLU(1) = iseed
+       RETURN
+       END
\ No newline at end of file
diff --git a/src/lib/z2t.cpp b/src/lib/z2t.cpp
new file mode 100644
index 0000000..2150e85
--- /dev/null
+++ b/src/lib/z2t.cpp
@@ -0,0 +1,55 @@
+/*
+ * z2t.cpp
+ *
+ * Author:
+ *       Oleg Kalashev
+ *
+ * Copyright (c) 2020 Institute for Nuclear Research, RAS
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "Cosmology.h"
+#include "Units.h"
+#include <iostream>
+using namespace mcray;
+
+int main(int argc, char** argv) {
+    //Test::PrecisionTests::SetPrecision(128);
+    int result = 0;
+    try
+    {
+        double z=-1.;
+        if(argc<2 || sscanf(argv[1],"%lg",&z)!=1 || z<0) {
+            std::cerr << "Calculate light travel time (in Mpc) from remote object\n"
+            << "usage: " << argv[0] << "<z>" << std::endl;
+            return 1;
+        }
+        if(!cosmology.IsInitialized())
+            cosmology.Init(z+10.);
+        double t = (cosmology.getAgeOfUniverse()-cosmology.z2t(z));
+        std::cout << t/units.Mpc << std::endl;
+    }
+    catch(Exception* ex)
+    {
+        std::cerr << ex->Message() << std::endl;
+        result = 1;
+    }
+    return result;
+}
\ No newline at end of file
-- 
GitLab