/*****************************************************************************

    TRAVIS - Trajectory Analyzer and Visualizer

    http://www.travis-analyzer.de/

    Copyright (c) 2009-2026 Martin Brehm
                  2012-2024 Martin Thomas
                  2016-2024 Sascha Gehrke
                  2023-2024 Paul Zaby

    Please cite:  J. Chem. Phys. 2020, 152 (16), 164105.         (DOI 10.1063/5.0005078 )
                  J. Chem. Inf. Model. 2011, 51 (8), 2007-2023.  (DOI 10.1021/ci200217w )

    This file was written by Paul Zaby.

    ---------------------------------------------------------------------------

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

*****************************************************************************/


// This must always be the first include directive
#include "config.h"

#include "conduct.h"
#include "conversion.h"
#include "maintools.h"

#include "globalvar.h"

const char *GetRevisionInfo_conduct(unsigned int len)
{
    static char buf[256];
    GET_REVISION_INFO( buf, len );
    return buf;
}

const char *GetSourceVersion_conduct()
{
    static char buf[256];
    GET_SOURCE_VERSION( buf );
    return buf;
}



// Requires modern C++ features
#ifdef TRAVIS_MODERN_CPP



#define BUF_SIZE 4096

static CxObArray g_oaConductObservations;

bool gatherConduct()
{
    CConductObservation *o;

    try { o = new CConductObservation(); } catch(...) { o = NULL; }
    if ( o == NULL ) NewException((double)sizeof(CConductObservation),__FILE__,__LINE__,__PRETTY_FUNCTION__);

    g_oaConductObservations.Add(o);

    return true;
}

bool initializeConduct()
{
    for(int i=0; i<g_oaConductObservations.GetSize(); i++)
        ((CConductObservation*)g_oaConductObservations[i])->initialize();

    return true;
}

bool processConduct(CTimeStep* ts)
{
    for(int i=0; i<g_oaConductObservations.GetSize(); i++)
        ((CConductObservation*)g_oaConductObservations[i])->process(ts);

    return true;
}

bool finalizeConduct()
{
    for(int i=0; i<g_oaConductObservations.GetSize(); i++)
        ((CConductObservation*)g_oaConductObservations[i])->finalize();

    return true;
}

CConductObservation::CConductObservation()
{
    if(!Parse())
    {
        eprintf("CConductObservation::CConductObservation(): Error parsing input file.\n");
        abort();
    }
}

CConductObservation::~CConductObservation()
{
    for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
        for(int j=0; j<(int)m_iaaAtomSet[i].size(); j++)
        {
            if(!m_bGK)
                delete m_oaaCoords[i][j];
            else
                delete m_oaaVelocities[i][j];
        }
}

bool CConductObservation::Parse()
{
    CxString buf;
    double charge;
    CMolecule *m;
    CSingleMolecule *sm;
    CAtomGroup ag;
    std::vector<int> tmp;
    int fftsize;

    mprintf("    >>> Conductivity Observation >>>\n\n");

    // loop over all molecule types
    for(int i=0; i<g_oaMolecules.GetSize(); i++)
    {
        m = (CMolecule*)g_oaMolecules[i];
        // ask if current molecule should be included
        if(!AskYesNo("\n        Include molecule %d (%s) in conductivity calculation? [yes] ",true,i+1,m->m_sName))
            continue;
        // ask for the molecular charge
        charge = AskFloat("        Enter the charge of the molecule: [1.0] ",1.0);
        if(charge == 0.0)
        {
            mprintf("        Charge of the molecule is zero, so it is not needed for the conductivity calculation.\n");
            continue;
        }
        // add molecule index to list of molecules to be included
        m_iaActiveMolecules.push_back(i);
        // ask if molecule consists of only one atom
        if(m->m_iAtomGesNoVirt == 1)
        {
            mprintf("        Molecule %s consists of only one atom, so it will be taken as a whole.\n",m->m_sName);
            ag.Reset();
            ag.m_pMolecule = m;
            ag.AddAtom(0,0,false);
            ag.SortAtoms();
            ag.BuildName();
        }
        else
        {
_as:
            // ask which atom (only one) to take from molecule
            ag.Reset();
            AskString("        Which atom to take to represent molecule %s (e.g. \"C1,O3\")? [#2] ",&buf,"#2",m->m_sName);
            if(!ag.ParseAtoms(m,buf))
                goto _as;
            if(ag.m_iAtomGes != 1)
            {
                mprintf("        Only one atom per molecule allowed, please choose again.\n");
                goto _as;
            }
            /* 
            // Check if a virtual atom was selected and warn about potential velocity issues
            if(m_bGK && ag.m_baAtomType[0] >= m->m_baAtomIndex.GetSize()-1)
            {
                if(m->m_laVirtualAtoms.GetSize() > 0)
                {
                    mprintf(YELLOW,"        Warning: ");
                    mprintf("You have selected a virtual atom for Green-Kubo conductivity calculation.\n");
                    mprintf("        Virtual atom velocities may not be appropriate for this analysis.\n");
                    mprintf("        Consider selecting a real atom instead.\n");
                    if(!AskYesNo("        Continue anyway? [no] ",false))
                        goto _as;
                }
            }
            */
        }
        
        m_sName += "_";
        m_sName += m->m_sName;
        if(m->m_iAtomGesNoVirt > 1)
        {
            m_sName += "_";
            m_sName += ag.m_sName;
        }

        // add all atoms from all the single molecules to atom list
        for(int i=0; i<(int)m->m_laSingleMolIndex.GetSize();i++)
        {
            sm = (CSingleMolecule*)g_oaSingleMolecules[m->m_laSingleMolIndex[i]];

            tmp.push_back(((CxIntArray*)sm->m_oaAtomOffset[ag.m_baAtomType[0]])->GetAt(((CxIntArray*)ag.m_oaAtoms[0])->GetAt(0)));
        }
        if(tmp.size() != 0) // check maybe unnecessary
        {
            m_iaaAtomSet.push_back(tmp); // add atom list of every molecule type
            m_faCharges.push_back(charge);   
            tmp.clear();
        }
    }

    if(m_iaaAtomSet.size() == 0)
    {
        eprintf("Error: Empty atom set is not allowed.\n");
        return false;
    }

    mprintf("\n        Including the following molecules:\n");
    for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
        mprintf("    %8d %-10s\n",(int)m_iaaAtomSet[i].size(),((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName);

    m_fTemperature = AskFloat("\n        Enter the temperature in K: [300.0] ",300.0);

    m_bGK = AskYesNo("\n        Use the Green-Kubo formulation (y) or the Einstein displacement (n)? [no] ",false);
    
    if(m_bGK)
    {
        g_bUseVelocities = true;
        mprintf("\n        Green-Kubo formulation will be used to calculate conductivity via velocity correlation functions (using fast fourier transform).\n");
        if(g_iTrajSteps != -1)
        {
            m_iDepth = (int)(0.75 * g_iTrajSteps);
            m_iDepth = AskUnsignedInteger("\n        Enter the depth of the correlation (in timesteps): [%d] ",m_iDepth,m_iDepth);
        }
        else
            m_iDepth = AskUnsignedInteger("\n        Enter the depth of the correlation (in timesteps): [%d] ",256,256);
        fftsize = CalcFFTSize(m_iDepth, false);
        if(m_iDepth != fftsize)
        {
            mprintf(WHITE, "        The next \"fast\" size for FFT is %d. Using this instead of %d as depth.\n\n",fftsize,m_iDepth);
            m_iDepth = fftsize;
        }
        if (g_bAdvanced2)
        {
            m_bIPS = AskYesNo("\n        Use rule of initial positive sequences for variance estimation? (turning off may decrease stability) [yes] ",true);
        }
        else
        {
            m_bIPS = true;
        }
        if (m_bIPS)
        {
            mprintf("\n        The rule of initial positive sequences will be used for variance estimation.\n");
            mprintf("        This may increase the stability of the uncertainty estimate.\n");
            mprintf("        If you have to turn this off, use the \"advanced mode\".\n\n");
        }
    }
    else
    {
        mprintf("\n        Einstein displacement will be used to calculate the conductivity.\n");

        mprintf("\n        The correlation depth of the collective MSD determines the maximum temporal distance\n");
        mprintf("         between two trajectory frames to be considered, and the maximum correlation time on the\n");
        mprintf("         horizontal axis. It has nothing to do with selecting a certain part of the trajectory.\n");
        mprintf("         The full trajectory is always used to compute the collective MSD, unless otherwise specified further below.\n\n");

        mprintf(YELLOW,"        Note: ");
        mprintf("The correlation depth should not be larger than ~30%% of the total trajectory step count.\n");
    
        m_iDepth = int(g_iTrajSteps * 0.3);
        if (g_iTrajSteps != -1)
		    m_iDepth = AskUnsignedInteger("\n        Enter the depth (in timesteps) of the correlation: [%d] ",m_iDepth,m_iDepth);
	    else
		    m_iDepth = AskUnsignedInteger("\n        Enter the depth (in timesteps) of the correlation: [10000] ",10000);

        // check how many data points to use for collective MSD plot
        m_iStride = m_iDepth / 1000;
        if(m_iStride == 0) m_iStride = 1;
        m_iStride2 = m_iStride;
        if(g_bAdvanced2)
        {
            m_iStride = AskUnsignedInteger("\n        Use every n-th data point for the collective MSD plot: [%d] ",m_iStride,m_iStride);
            m_iStride2 = AskUnsignedInteger("\n        Shift correlation window n time steps at once: [%d] ",m_iStride,m_iStride);
            if(m_iStride == 0) m_iStride = 1;
            if(m_iStride2 == 0) m_iStride2 = 1;
        }
        else
        {
            mprintf("\n        Taking every %d. data point for the collective MSD plot.",m_iStride);
            mprintf("\n        Shifting correlation window %d steps at once.\n",m_iStride2);
            mprintf("        If you have to change these settings, use the \"advanced mode\".\n");
        }
        // check if uncertainty for Conductivity should be calculated
        // m_bUncertainty = AskYesNo("\n        Compute uncertainty for Conductivity (experimental!) (y/n)? [no] ",false);
        if(g_bAdvanced2)
        {
            m_bUncertainty = AskYesNo("\n        Compute uncertainties for collective MSD points and use error-weighted regression (y/n)? [yes] ",true);
            m_bMsdiff = AskYesNo("\n        Use the Msdiff method for finding the linear region of the collective MSD? [yes] ",true);
            if (m_bMsdiff)
                m_bMsdiffLogFit = AskYesNo("\n        Use logarithmic fitting for finding the linear regime (y) or the 2-point-slope method (n)? [no] ",false);
        }
        else
        {
            m_bUncertainty = true;
            m_bMsdiff = true;
            m_bMsdiffLogFit = false;
        }
    }

    // check if conductivity should be decomposed into sigma^+, sigma^- and sigma^+/- (cross)
    m_bDecompConduct = AskYesNo("\n        Use decomposition of the conductivity into contributions from all charged species? [no] ",false);
    /*if(m_bDecompConduct && m_bGK)
    {
        m_bDecompSelfCross = AskYesNo("\n        Further decompose the Green-Kubo conductivity into self and cross terms? [no] ",false);
    }
    else */
    // m_bDecompSelfCross = false;

    // check how to center velocities and coordinates
    m_iCentering = AskRangeInteger("\n        Use mass-fixed (0), number-fixed (1) or solvent-fixed (2) frame of reference? [0] ",0,2,0);
    if (m_iCentering == 0) // mass fixed
    {
        m_bCenterByMass = true;
        m_bCenterByGeometry = false;
        m_bCenterByComponent = false;
    }
    else if (m_iCentering == 1) // number fixed
    {
        m_bCenterByMass = false;
        m_bCenterByGeometry = true;
        m_bCenterByComponent = false;
    }
    else if (m_iCentering == 2) // solvent fixed
    {
        m_bCenterByMass = false;
        m_bCenterByGeometry = false;
        m_bCenterByComponent = true;

        m_iaCenterComponents.clear();
        // get components to be used for centering
        mprintf("\n        The solvent-fixed frame of reference will be used.");
        mprintf("\n        Please specify the molecule types to be considered as solvent:\n");
        for(int i=0; i<g_oaMolecules.GetSize(); i++)
        {
            m = (CMolecule*)g_oaMolecules[i];
            if(AskYesNo("          Consider %d (%s) as (part of the) solvent? [yes] ",true,i+1,m->m_sName))
                m_iaCenterComponents.push_back(i);
        }
        mprintf("\n        The following molecule types will be considered as (part of the) solvent for centering:\n");
        for(int i=0; i<(int)m_iaCenterComponents.size(); i++)
            mprintf("          %-10s\n",((CMolecule*)g_oaMolecules[m_iaCenterComponents[i]])->m_sName);
    }
    else
    {
        eprintf("Error: Invalid centering option.\n");
        abort();
    }
    
    mprintf("\n\n    <<< End of Conductivity Observation <<<\n\n");
    
    return true;
}

void CConductObservation::initialize()
{
    // check if the depth of the correlation is larger than the number of trajectory steps to be processed
    int ti = -1;                                                                                                                                                                                                                                
    if (g_iTrajSteps != -1)                                                                                                                                                                                                                 
    {                                                                                                                                                                                                                                       
        if (g_iMaxStep == -1)                                                                                                                                                                                                           
            ti = (g_iTrajSteps-g_iBeginStep)/g_iStride;                                                                                                                                                                             
                else ti = g_iMaxStep/g_iStride;                                                                                                                                                                                 
    } else if (g_iMaxStep != -1)                                                                                                                                                                                                             
        ti = g_iMaxStep/g_iStride;  
    if(ti >= 0)
    {
        if(m_iDepth > ti)
        {
            eprintf("Error: Depth of the correlation is larger than the number of trajectory steps.\n");
            abort();
        }
        else if(!m_bGK && m_iDepth > 0.5*ti)
        {
            mprintf(YELLOW,"    Warning: ");
            mprintf("Depth of the correlation is larger than 50%% of the total trajectory step count.\n");
            mprintf("    This may lead to noisy results.\n");
        }

        if(!m_bGK && m_iStride2 > ti)
        {
            eprintf("Error: Correlation window shift is larger than the number of trajectory steps.\n");
            abort();
        }
    }
    if(!m_bGK && m_iStride > m_iDepth)
    {
        eprintf("Error: Stride is larger than the depth of the correlation.\n");
        abort();
    }

    // create necessary arrays
    if(m_bGK)
        m_oaaVelocities.resize(m_iaaAtomSet.size());
    else
        m_oaaCoords.resize(m_iaaAtomSet.size());
    for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
    {
        if(!m_bGK)
            m_oaaCoords[i].resize(m_iaaAtomSet[i].size());
        else
            m_oaaVelocities[i].resize(m_iaaAtomSet[i].size());
        for(int j=0; j<(int)m_iaaAtomSet[i].size(); j++)
        {
            if(!m_bGK)
            {
                try { m_oaaCoords[i][j] = new CxDVec3Array(); } catch(...) { m_oaaCoords[i][j] = NULL; }
                if ( m_oaaCoords[i][j] == NULL) NewException((double)sizeof(CxDVec3Array),__FILE__,__LINE__,__PRETTY_FUNCTION__);
            }
            else
            {
                try { m_oaaVelocities[i][j] = new CxDVec3Array(); } catch(...) { m_oaaVelocities[i][j] = NULL; }
                if ( m_oaaVelocities[i][j] == NULL) NewException((double)sizeof(CxDVec3Array),__FILE__,__LINE__,__PRETTY_FUNCTION__);
            }
        }
    }
}

void CConductObservation::process(CTimeStep* ts)
{
    int atomIndex, atomTypeIndex, moleculeTypeIndex, totalMolecules;
    double atomicMass, molMass, totalMass = 0.0;
    // center_vel can be center of mass velocity, center of geometry velocity, or center of component velocity
    CxDVector3 center_vel = CxDVector3(0.0);
    CxDVector3 center_coords = CxDVector3(0.0);
    // loop over all non virtual atoms in the system to calculate center of mass velocities
    //if (g_bLAMMPSVelocity || !m_bCenterByMass) // assuming COM-vel = 0 else
    //{
        if (m_bCenterByMass) // barycentric reference frame (standard), only velocities needed to be centered
        {
            center_vel = CxDVector3(0.0);
            center_coords = CxDVector3(0.0);
            totalMass = 0.0;
            if (m_bGK)
            {
                for (int i=0; i<g_iGesAtomCount; i++)
                {
                    atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                    atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                    if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                        continue;
                    atomicMass = ((CAtom*)g_oaAtoms[atomTypeIndex])->m_pElement->m_fMass; // in amu
                    totalMass += atomicMass;
                    center_vel += ts->m_vaVelocities[i] * atomicMass;
                }
                center_vel /= totalMass; // in m/s
            }
            else // for coordinates
            {
                for (int i=0; i<g_iGesAtomCount; i++)
                {
                    atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                    atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                    if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                        continue;
                    atomicMass = ((CAtom*)g_oaAtoms[atomTypeIndex])->m_pElement->m_fMass; // in amu
                    totalMass += atomicMass;
                    center_coords += ts->m_vaCoords[i] * atomicMass;
                }
                center_coords /= totalMass; // in pm
            }
        }
        else if (m_bCenterByGeometry) // number-fixed reference frame
        {
            CMolecule* mol;
            totalMolecules = g_oaSingleMolecules.GetSize(); // Total number of individual molecules in the system
            center_vel = CxDVector3(0.0);
            center_coords = CxDVector3(0.0);
            moleculeTypeIndex = -1;
            if(m_bGK)
            {
                for (int i=0; i<g_iGesAtomCount; i++)
                {
                    atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                    atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                    if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                        continue;
                    // index of molecule type in global molecule type array
                    moleculeTypeIndex = g_waAtomMolIndex[atomIndex];
                    mol = (CMolecule*)g_oaMolecules[moleculeTypeIndex];
                    molMass = mol->m_fMass; // in amu
                    atomicMass = ((CAtom*)g_oaAtoms[g_waAtomRealElement[atomIndex]])->m_pElement->m_fMass; // in amu
                    center_vel += ts->m_vaVelocities[i] * atomicMass / molMass; // in m/s
                }
                center_vel /= (double)totalMolecules; // in m/s
            }
            else
            {
                for (int i=0; i<g_iGesAtomCount; i++)
                {
                    atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                    atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                    if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                        continue;
                    // index of molecule type in global molecule type array
                    moleculeTypeIndex = g_waAtomMolIndex[atomIndex];
                    mol = (CMolecule*)g_oaMolecules[moleculeTypeIndex];
                    molMass = mol->m_fMass; // in amu
                    atomicMass = ((CAtom*)g_oaAtoms[g_waAtomRealElement[atomIndex]])->m_pElement->m_fMass; // in amu
                    center_coords += ts->m_vaCoords[i] * atomicMass / molMass; // in pm
                }
                center_coords /= (double)totalMolecules; // in pm
            }
        }
        else if (m_bCenterByComponent) // component-fixed / solvent-fixed reference frame
        {
            moleculeTypeIndex = -1;
            center_vel = CxDVector3(0.0);
            center_coords = CxDVector3(0.0);
            totalMass = 0.0;
            if(m_bGK)
            {
                for (int i=0; i<g_iGesAtomCount; i++)
                {
                    atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                    atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                    if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                        continue;
                    // index of molecule type in global molecule type array
                    moleculeTypeIndex = g_waAtomMolIndex[atomIndex]; 
                    // check if atom belongs to one of the selected molecule types
                    for (int m=0; m<(int)m_iaCenterComponents.size(); m++)
                    {
                        if (moleculeTypeIndex == m_iaCenterComponents[m])
                        {
                            atomicMass = ((CAtom*)g_oaAtoms[g_waAtomRealElement[atomIndex]])->m_pElement->m_fMass; // in amu
                            totalMass += atomicMass;
                            center_vel += ts->m_vaVelocities[i] * atomicMass;
                            break;
                        }
                    }
                }
                center_vel /= totalMass; // in m/s
            }
            else
            {
                for (int i=0; i<g_iGesAtomCount; i++)
                {
                    atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                    atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                    if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                        continue;
                    // index of molecule type in global molecule type array
                    moleculeTypeIndex = g_waAtomMolIndex[atomIndex];
                    // check if atom belongs to one of the selected molecule types
                    for (int m=0; m<(int)m_iaCenterComponents.size(); m++)
                    {
                        if (moleculeTypeIndex == m_iaCenterComponents[m])
                        {
                            atomicMass = ((CAtom*)g_oaAtoms[g_waAtomRealElement[atomIndex]])->m_pElement->m_fMass; // in amu
                            totalMass += atomicMass;
                            center_coords += ts->m_vaCoords[i] * atomicMass;
                            break;
                        }
                    }
                }
                center_coords /= totalMass; // in pm
            }
        }
        
        // obtained center velocity or center coordinates, now adjust all atom velocities or coordinates accordingly
        if (m_bGK)
        {
            // subtract center velocity from all atom velocities
            for (int i=0; i<g_iGesAtomCount; i++)
                ts->m_vaVelocities[i] -= center_vel; // in m/s
            // calculate velocities for virtual atoms (centers) referenced to total center velocity
            ts->CalcCenterVelocities();
        }
        else
        {
            // subtract center coordinates from all atom coordinates
            for (int i=0; i<g_iGesAtomCount; i++)
                ts->m_vaCoords[i] -= center_coords; // in pm
            // add virtual atoms to this
            ts->CalcCenters();
        }
    //}      

    // get actual coordinates or velocities
    for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
        for(int j=0; j<(int)m_iaaAtomSet[i].size(); j++)
        {
            if(!m_bGK)
                m_oaaCoords[i][j]->Add(ts->m_vaCoords[m_iaaAtomSet[i][j]]);
            else // velocities in m/s
                m_oaaVelocities[i][j]->Add(ts->m_vaVelocities[m_iaaAtomSet[i][j]]);
        }

    // check if center velocity is actually near zero
    /*
    if (m_bGK)
    {
        if(m_bCenterByMass)
        {
            center_vel = CxDVector3(0.0);
            totalMass = 0.0;
            for (int i=0; i<g_iGesAtomCount; i++)
            {
                atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                    continue;
                atomicMass = ((CAtom*)g_oaAtoms[atomTypeIndex])->m_pElement->m_fMass; // in amu
                totalMass += atomicMass;
                center_vel += ts->m_vaVelocities[i] * atomicMass;
            }
            center_vel /= totalMass; // in m/s
        }
        else if(m_bCenterByGeometry)
        {
            CMolecule* mol;
            totalMolecules = g_oaSingleMolecules.GetSize(); // Total number of individual molecules in the system
            center_vel = CxDVector3(0.0);
            moleculeTypeIndex = -1;
            for (int i=0; i<g_iGesAtomCount; i++)
            {
                atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                        continue;
                // index of molecule type in global molecule type array
                moleculeTypeIndex = g_waAtomMolIndex[atomIndex];
                mol = (CMolecule*)g_oaMolecules[moleculeTypeIndex];
                molMass = mol->m_fMass; // in amu
                atomicMass = ((CAtom*)g_oaAtoms[g_waAtomRealElement[atomIndex]])->m_pElement->m_fMass; // in amu
                center_vel += ts->m_vaVelocities[i] * atomicMass / molMass; // in m/s
            }
            center_vel /= (double)totalMolecules; // in m/s
        }
        else if(m_bCenterByComponent)
        {
            center_vel = CxDVector3(0.0);
            totalMass = 0.0;
            moleculeTypeIndex = -1;
            for (int i=0; i<g_iGesAtomCount; i++)
            {
                atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                    continue;
                // index of molecule type in global molecule type array
                moleculeTypeIndex = g_waAtomMolIndex[atomIndex]; 
                // check if atom belongs to one of the selected molecule types
                for (int m=0; m<(int)m_iaCenterComponents.size(); m++)
                {
                    if (moleculeTypeIndex == m_iaCenterComponents[m])
                    {
                        atomicMass = ((CAtom*)g_oaAtoms[g_waAtomRealElement[atomIndex]])->m_pElement->m_fMass; // in amu
                        totalMass += atomicMass;
                        center_vel += ts->m_vaVelocities[i] * atomicMass;
                        break;
                    }
                }
            }
            center_vel /= totalMass; // in m/s
        }
        // if center velocity is not near zero, print warning that this may affect the accuracy of the analysis
        if (center_vel.GetLength() > 1E-5)
        {
            mprintf(YELLOW,"    Warning: ");
            mprintf("Center velocity is not zero (|v_COM| = %.6f m/s).\n",center_vel.GetLength());
            mprintf("    This may affect the accuracy of the analysis.\n");
        }
    }
    else // check if center of coordinates is actually at center
    {
        if(m_bCenterByMass)
        {
            center_coords = CxDVector3(0.0);
            totalMass = 0.0;
            for (int i=0; i<g_iGesAtomCount; i++)
            {
                atomIndex = i;
                atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                    continue;
                atomicMass = ((CAtom*)g_oaAtoms[atomTypeIndex])->m_pElement->m_fMass; // in amu
                totalMass += atomicMass;
                center_coords += ts->m_vaCoords[i] * atomicMass;
            }
            center_coords /= totalMass; // in pm
        }
        else if(m_bCenterByGeometry)
        {
            CMolecule* mol;
            totalMolecules = g_oaSingleMolecules.GetSize(); // Total number of individual molecules in the system
            center_coords = CxDVector3(0.0);
            moleculeTypeIndex = -1;
            for (int i=0; i<g_iGesAtomCount; i++)
            {
                atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                    continue;
                // index of molecule type in global molecule type array
                moleculeTypeIndex = g_waAtomMolIndex[atomIndex];
                mol = (CMolecule*)g_oaMolecules[moleculeTypeIndex];
                molMass = mol->m_fMass; // in amu
                atomicMass = ((CAtom*)g_oaAtoms[g_waAtomRealElement[atomIndex]])->m_pElement->m_fMass; // in amu
                center_coords += ts->m_vaCoords[i] * atomicMass / molMass; // in pm
            }
            center_coords /= (double)totalMolecules; // in pm
        }
        else if(m_bCenterByComponent)
        {
            center_coords = CxDVector3(0.0);
            totalMass = 0.0;
            moleculeTypeIndex = -1;
            for (int i=0; i<g_iGesAtomCount; i++)
            {
                atomIndex = i; // index of atom in global atom array (all atoms in simulation)
                atomTypeIndex = g_baAtomIndex[atomIndex]; // index of atom-type in global atom-type array
                if (((CAtom*)g_oaAtoms[atomTypeIndex])->m_bExclude) // excluded atom (like drudes), skip
                    continue;
                // index of molecule type in global molecule type array
                moleculeTypeIndex = g_waAtomMolIndex[atomIndex];
                // check if atom belongs to one of the selected molecule types
                for (int m=0; m<(int)m_iaCenterComponents.size(); m++)
                {
                    if (moleculeTypeIndex == m_iaCenterComponents[m])
                    {
                        atomicMass = ((CAtom*)g_oaAtoms[g_waAtomRealElement[atomIndex]])->m_pElement->m_fMass; // in amu
                        totalMass += atomicMass;
                        center_coords += ts->m_vaCoords[i] * atomicMass;
                        break;
                    }
                }
            }
            center_coords /= totalMass; // in pm
        }

        // if not centered, print warning that this may affect the accuracy of the analysis
        if (center_coords.GetLength() > 1E-5)
        {
            mprintf(YELLOW,"    Warning: ");
            mprintf("Center of coordinates is not at the origin (|r_center| = %.6f pm).\n",center_coords.GetLength());
            mprintf("    This may affect the accuracy of the analysis.\n");
        }
    }
    */
}

void CConductObservation::finalize()
{
    m_fVol = g_fBoxX * g_fBoxY * g_fBoxZ; // boxvolume in pm^3
    double conversion = CONST_ELEMENTARY_CHARGE * CONST_ELEMENTARY_CHARGE / (6 * CONST_BOLTZMANN * m_fTemperature * m_fVol * 1E-24);
    double chargeMult, step;

    if(!m_bGK)
    {
        std::vector<double> total_cond(m_iDepth/m_iStride + 1,0.0);
        std::tuple<double, double, double, double, double> mbr2;
        double msd, cross, max_dist_sqr, current_dist_sqr, tmp;
        CxDVector3 V1, V2, V3, V4;
        step = (double)(m_iDepth/m_iStride + 1) / 100.0;
        max_dist_sqr = g_fMinPeriodic * g_fMinPeriodic / 4.0;
        double x_conversion = 1E-3 * g_fTimestepLength * m_iStride * g_iStride;
        int num_points;
        std::pair<int, int> range;

        if(!m_bDecompConduct)
        {
            mprintf(WHITE, "      [");
            if(!m_bUncertainty)
            {   
                //mprintf("\nm_iDepth = %d, m_iStride = %d, m_iStride2 = %d, m_oaaCoords[0][0]->GetSize() = %d\n",m_iDepth,m_iStride,m_iStride2,m_oaaCoords[0][0]->GetSize());
                // use einstein relation to calculate conductivity
                for(int d=0; d<m_iDepth+1; d+=m_iStride)
                { // loop over depths
                    if (fmod(d/m_iStride, step) < 1.0)
                        mprintf(WHITE, "#");
                    num_points = (m_oaaCoords[0][0]->GetSize()-d-1)/m_iStride2 + 1;
                    for(int t=0; t<m_oaaCoords[0][0]->GetSize(); t+=m_iStride2)
                    {   // loop over (non-skipped) timesteps
                        // break (and continue with next molecule) if current timestep is too large for depth
                        if(t+d >= m_oaaCoords[0][0]->GetSize())
                            break;
                        V2 = CxDVector3(0.0);
                        for(size_t m=0; m<m_iaaAtomSet.size(); m++)
                        { // loop over molecules
                            for(size_t i=0; i<m_iaaAtomSet[m].size(); i++)
                            { // loop over single molecules
                                V1 = (m_oaaCoords[m][i]->GetAt(t+d) - m_oaaCoords[m][i]->GetAt(t))*m_faCharges[m];
                                //if(d == m_iStride && t < 10 && m+i == 0) mprintf("\nm_oaaCoords[%ld][%ld]->GetAt(%d).GetLength() = %f\n",m,i,t,(m_oaaCoords[m][i]->GetAt(t)).GetLength());
                                if(d == m_iStride && V1.GetLengthSqr() > max_dist_sqr)
                                    eprintf("\nDiscontinuity in step %d: %s[%d] atom moved %.2f pm. Can't compute Conductivity from wrapped trajectory - please use unwrapped trajectory.",t,((CMolecule*)g_oaMolecules[(int)m])->m_sName,(int)i+1,V1.GetLength());
                                V2 += V1;
                            }
                        }
                        total_cond[d/m_iStride] += V2.GetLengthSqr();
                    }
                    total_cond[d/m_iStride] /= (double)num_points;
                    total_cond[d/m_iStride] *= conversion;
                }
                mprintf(WHITE, "]\n");
                // perform linear regression on collective MSD
                if (m_bMsdiff)
                    range = FindLinearRegion(total_cond);
                else
                    range = std::make_pair( (int)total_cond.size()/2, (int)total_cond.size()-1 );
                mprintf(WHITE,"\n    Performing linear regression on interval %.2f - %.2f ps...\n\n",range.first*x_conversion,range.second*x_conversion);
                mbr2 = LinReg(total_cond, range.first, range.second, false); // 0: slope a, 1: a-uncertainty, 2: y-intercept b, 3: b-uncertainty, 4: R^2
                mprintf("      Collective MSD(t) = %.6f + %.6f * t   (units: [col. MSD] = ps*S/m, [t] = ps).\n",std::get<2>(mbr2),std::get<0>(mbr2));
                mprintf("      R^2 = %.6f (correlation coefficient)\n",std::get<4>(mbr2));
                mprintf("        Conductivity = %.6f +/- %.6f S/m\n",std::get<0>(mbr2),std::get<1>(mbr2));
                mprintf("        y-intercept  = %.6f +/- %.6f ps*S/m\n\n",std::get<2>(mbr2),std::get<3>(mbr2));
                mprintf("      Warning: the uncertainties shown above are just fit errors from ordinary least squares\n");
                mprintf("               and do not include the uncertainty of the data itself.\n\n");
                //mprintf("      For the fitting procedures, the second half of each plot was used.\n");
                //mprintf("      Please check the plot for the most linear part if R^2 is too small (less than 0.95).\n\n");
                DumpPlots(total_cond);
            }
            else
            {
                std::vector<double> total_cond_sq(m_iDepth/m_iStride + 1,0.0);
                std::vector<double> variance(m_iDepth/m_iStride + 1,0.0);
                std::tuple<double, double, double, double, double> mberr;
                //double x, delta, mean, M2;
                //int n;
                // use einstein relation to calculate conductivity
                for(int d=0; d<m_iDepth+1; d+=m_iStride)
                { // loop over depths
                    //n = 0;
                    //mean = 0.0;
                    //M2 = 0.0;
                    if (fmod(d/m_iStride, step) < 1.0)
                        mprintf(WHITE, "#");
                    num_points = (m_oaaCoords[0][0]->GetSize()-d-1)/m_iStride2 + 1;
                    for(int t=0; t<m_oaaCoords[0][0]->GetSize(); t+=m_iStride2)
                    {   // loop over all (non-skipped) timesteps
                        // break (and continue with next molecule) if current timestep is too large for depth
                        if(t+d >= m_oaaCoords[0][0]->GetSize())
                            break;
                        V2 = CxDVector3(0.0);
                        for(size_t m=0; m<m_iaaAtomSet.size(); m++)
                        { // loop over molecules
                            for(size_t i=0; i<m_iaaAtomSet[m].size(); i++)
                            { // loop over single molecules
                                V1 = (m_oaaCoords[m][i]->GetAt(t+d) - m_oaaCoords[m][i]->GetAt(t))*m_faCharges[m];
                                if(d == m_iStride && V1.GetLengthSqr() > max_dist_sqr)
                                    eprintf("\nDiscontinuity in step %d: %s[%d] atom moved %.2f pm. Can't compute Conductivity from wrapped trajectory - please use unwrapped trajectory.",t,((CMolecule*)g_oaMolecules[(int)m])->m_sName,(int)i+1,V1.GetLength());
                                V2 += V1;
                            }
                        }
                        //x = V2.GetLengthSqr() * conversion; // conductivity value for current point
                        //n++;
                        //delta = x - mean;
                        //mean += delta / (double)n;
                        //M2 += delta * (x - mean);
                        total_cond[d/m_iStride] += V2.GetLengthSqr();
                        total_cond_sq[d/m_iStride] += V2.GetLengthSqr() * V2.GetLengthSqr();
                    }
                    //total_cond[d/m_iStride] = mean;
                    total_cond[d/m_iStride] /= (double)num_points;
                    total_cond[d/m_iStride] *= conversion;
                    total_cond_sq[d/m_iStride] /= (double)num_points;
                    total_cond_sq[d/m_iStride] *= conversion*conversion;
                    // calculate variance of the mean = Variance of the sample / number of points (unbiased estimator with n-1 in denominator)
                    //variance[d/m_iStride] = (M2 / (double)(n-1)) / n;
                    variance[d/m_iStride] = (total_cond_sq[d/m_iStride] - total_cond[d/m_iStride]*total_cond[d/m_iStride]) / (double)(num_points-1);
                }
                mprintf(WHITE, "]\n");
                // perform linear regression on collective MSD
                if (m_bMsdiff)
                    range = FindLinearRegion(total_cond);
                else
                    range = std::make_pair( (int)total_cond.size()/2, (int)total_cond.size()-1 );
                mprintf(WHITE,"\n    Performing linear regression on interval %.2f - %.2f ps...\n\n",range.first*x_conversion,range.second*x_conversion);
                //mbr2 = LinReg(total_cond, range.first, range.second, false); // 0: slope a, 1: a-uncertainty, 2: y-intercept b, 3: b-uncertainty, 4: R^2
                mberr = LinReg(total_cond,range.first, range.second, true, variance); // 0: slope a, 1: a-uncertainty, 2: y-intercept b, 3: b-uncertainty
                mprintf("      Collective MSD(t) = %.6f + %.6f * t   (units: [col. MSD] = ps*S/m, [t] = ps).\n",std::get<2>(mberr),std::get<0>(mberr));
                mprintf("      R^2 = %.6f (correlation coefficient)\n",std::get<4>(mberr));
                mprintf("        Conductivity = %.6f +/- %.6f S/m\n",std::get<0>(mberr),std::get<1>(mberr));
                mprintf("        y-intercept  = %.6f +/- %.6f ps*S/m\n\n",std::get<2>(mberr),std::get<3>(mberr));

                //mprintf("      Non-weighted regression yields:\n");
                //mprintf("      R^2 = %.6f (correlation coefficient)\n",std::get<4>(mbr2));
                //mprintf("        Conductivity = %.6f +/- %.6f S/m\n",std::get<0>(mbr2),std::get<1>(mbr2));
                //mprintf("        y-intercept  = %.6f +/- %.6f ps*S/m\n\n",std::get<2>(mbr2),std::get<3>(mbr2));
                
                //mprintf("      Warning: Uncertainties for the non-weighted regression are only fit errors and do not include the uncertainty of the data itself.\n\n");
                //mprintf("      For the fitting procedures, the second half of each plot was used.\n");
                //mprintf("      Please check the plot for the most linear part if R^2 is too small (less than 0.95).\n\n");
                DumpPlots(total_cond,variance);
            }
        }
        else
        {
            int decompsize = (int)(2*m_iaActiveMolecules.size()+((m_iaActiveMolecules.size()-1)*m_iaActiveMolecules.size())/2);
            std::vector<std::vector<double> > cond(decompsize,std::vector<double>(m_iDepth/m_iStride + 1,0.0));
            double vector_sub;
            int index;
            //double transp_sum;
            //double transp_sum_id;
            std::vector<double> separate_terms(decompsize,0.0);
            std::vector<double> separate_term_errors(decompsize,0.0);
            //std::vector<double> transp_num(m_iaActiveMolecules.size(),0.0);
            //std::vector<double> transp_num_id(m_iaActiveMolecules.size(),0.0);
            std::pair<std::vector<double>, std::vector<double>> transp_num_id, transp_num;
            transp_num_id.first.resize(m_iaActiveMolecules.size(),0.0);
            transp_num_id.second.resize(m_iaActiveMolecules.size(),0.0);
            transp_num.first.resize(m_iaActiveMolecules.size(),0.0);
            transp_num.second.resize(m_iaActiveMolecules.size(),0.0);
            mprintf(WHITE, "      [");
            if(!m_bUncertainty)
            {
                // use einstein relation to calculate conductivity
                for(int d=0; d<m_iDepth+1; d+=m_iStride)
                { // loop over depths
                    if (fmod(d/m_iStride, step) < 1.0)
                        mprintf(WHITE, "#");
                    num_points = (m_oaaCoords[0][0]->GetSize()-d-1)/m_iStride2 + 1;
                    for(int t=0; t<m_oaaCoords[0][0]->GetSize(); t+=m_iStride2)
                    { // loop over all (non-skipped) timesteps
                        // break (and continue with next molecule) if current timestep is too large for depth
                        if(t+d >= m_oaaCoords[0][0]->GetSize())
                            break;
                        for(size_t m=0; m<m_iaaAtomSet.size(); m++)
                        { // loop over molecules
                            for(size_t n=m; n<m_iaaAtomSet.size(); n++)
                            { // loop over molecules again
                                chargeMult = m_faCharges[m] * m_faCharges[n];
                                if(m==n) // same molecule
                                {
                                    msd = 0.0;
                                    V2 = CxDVector3(0.0);
                                    for(size_t i=0; i<m_iaaAtomSet[m].size(); i++)
                                    { // loop over all single molecules
                                        V1 = m_oaaCoords[m][i]->GetAt(t+d) - m_oaaCoords[m][i]->GetAt(t);
                                        if(d == m_iStride && V1.GetLengthSqr() > max_dist_sqr)
                                            eprintf("\nDiscontinuity in step %d: %s[%d] atom moved %.2f pm. Can't compute Conductivity from wrapped trajectory - please use unwrapped trajectory.",t,((CMolecule*)g_oaMolecules[(int)m])->m_sName,(int)i+1,V1.GetLength());
                                        msd += V1.GetLengthSqr();
                                        V2 += V1;
                                    }
                                    cross = V2.GetLengthSqr() - msd; // all cross terms
                                    cond[m][d/m_iStride] += chargeMult * msd; // all self terms
                                    cond[m+m_iaaAtomSet.size()][d/m_iStride] += chargeMult * cross; // all cross terms
                                    continue;
                                }
                                // different molecules
                                V2 = CxDVector3(0.0);
                                V3 = CxDVector3(0.0);
                                for(size_t i=0; i<m_iaaAtomSet[m].size(); i++)
                                { // loop over all molcules of type m
                                    V1 = m_oaaCoords[m][i]->GetAt(t+d) - m_oaaCoords[m][i]->GetAt(t);
                                    V2 += V1; // all terms from molecule m added
                                }
                                for(size_t i=0; i<m_iaaAtomSet[n].size(); i++)
                                { // loop over all molecules of type n
                                    V1 = m_oaaCoords[n][i]->GetAt(t+d) - m_oaaCoords[n][i]->GetAt(t);
                                    V3 += V1; // all terms from molecule n added
                                }
                                V4 = V3 + V2; // all cross terms added
                                // V4^2 - V3^2 - V2^2 gives all the cross terms two times
                                cond[m+2*m_iaaAtomSet.size() + n - (m+1)*(m+2)/2][d/m_iStride] += chargeMult * (V4.GetLengthSqr() - V3.GetLengthSqr() - V2.GetLengthSqr());
                            }
                        }
                    }
                    for(int j=0; j<(int)cond.size(); j++)
                    { // loop over contributions
                        // divide by number of timesteps that were used to average
                        cond[j][d/m_iStride] /= (double)num_points;
                        // multiply by conversion factor to obtain conductivity in S/m (maybe change that again)
                        cond[j][d/m_iStride] *= conversion;
                        // add up all contributions
                        total_cond[d/m_iStride] += cond[j][d/m_iStride];
                    }
                }
                mprintf(WHITE, "]\n");
                // perform linear regression on collective MSD
                if (m_bMsdiff)
                    range = FindLinearRegion(total_cond);
                else
                    range = std::make_pair( (int)total_cond.size()/2, (int)total_cond.size()-1 );
                mprintf(WHITE,"\n    Performing linear regression on interval %.2f - %.2f ps...\n\n",range.first*x_conversion,range.second*x_conversion);
                mbr2 = LinReg(total_cond, range.first, range.second, false); // 0: slope a, 1: a-uncertainty, 2: y-intercept b, 3: b-uncertainty, 4: R^2
                mprintf("      Collective MSD(t) = %.6f + %.6f * t   (units: [col. MSD] = ps*S/m, [t] = ps).\n",std::get<2>(mbr2),std::get<0>(mbr2));
                mprintf("      R^2 = %.6f (correlation coefficient)\n",std::get<4>(mbr2));
                mprintf("        Conductivity = %.6f +/- %.6f S/m\n",std::get<0>(mbr2),std::get<1>(mbr2));
                mprintf("        y-intercept  = %.6f +/- %.6f ps*S/m\n\n",std::get<2>(mbr2),std::get<3>(mbr2));
                // calculate slope of every contribution and the resulting R
                mprintf("      Contributions to the conductivity:\n");
                // self diffusion
                //if (g_oaMolecules.GetSize() > 2)
                //    transp_sum = 0.0;
                //transp_sum_id = 0.0;
                for(int i = 0; i < (int)m_iaActiveMolecules.size(); i++)
                {
                    mbr2 = LinReg(cond[i], range.first, range.second, false);
                    separate_terms[i] = std::get<0>(mbr2);
                    separate_term_errors[i] = std::get<1>(mbr2);
                    /*if (g_oaMolecules.GetSize() > 2)
                    {
                        transp_num[i] += separate_terms[i];
                        transp_sum += separate_terms[i];
                    }
                    */
                    //transp_num_id[i] = separate_terms[i];
                    //transp_sum_id += separate_terms[i];
                    mprintf("        %s: %.6f +/- %.6f S/m,   R^2 = %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,separate_terms[i],separate_term_errors[i],std::get<4>(mbr2));
                }
                // cross diffusion of same molecule type
                for(int i = 0; i < (int)m_iaActiveMolecules.size(); i++)
                {              
                    index = i+(int)m_iaActiveMolecules.size();      
                    mbr2 = LinReg(cond[index], range.first, range.second, false);
                    separate_terms[index] = std::get<0>(mbr2);
                    separate_term_errors[index] = std::get<1>(mbr2);
                    /*if (g_oaMolecules.GetSize() > 2)
                    {
                        transp_num[i] += separate_terms[index];
                        transp_sum += separate_terms[index];
                    }
                    */
                    mprintf("        %s/%s: %.6f +/- %.6f S/m,   R^2 = %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,separate_terms[index],separate_term_errors[index],std::get<4>(mbr2));
                }
                // cross diffusion of different molecule types
                for(int j=0; j < (int)m_iaActiveMolecules.size(); j++)
                    for(int k=j+1; k < (int)m_iaActiveMolecules.size(); k++)
                    {
                        index = (j+2)*(int)m_iaActiveMolecules.size() + k - (j+1)*(j+2)/2;
                        mbr2 = LinReg(cond[index], range.first, range.second, false);
                        separate_terms[index] = std::get<0>(mbr2);
                        separate_term_errors[index] = std::get<1>(mbr2);
                        /*if (g_oaMolecules.GetSize() > 2)
                        {
                            transp_num[j] += separate_terms[index]/2.0;
                            transp_num[k] += separate_terms[index]/2.0;
                            transp_sum += separate_terms[index];
                        }
                        */
                        mprintf("        %s/%s: %.6f +/- %.6f S/m,   R^2 = %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName,((CMolecule*)g_oaMolecules[m_iaActiveMolecules[k]])->m_sName,separate_terms[index],separate_term_errors[index],std::get<4>(mbr2));
                    }
                /*
                for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                {
                    if (g_oaMolecules.GetSize() > 2)
                    {
                        transp_num[i] /= transp_sum;
                    }
                    //transp_num_id[i] /= transp_sum_id;
                }
                */
                if (g_oaMolecules.GetSize() > 2)
                    transp_num = CalcTransportNumbers(separate_terms, separate_term_errors, true);
                transp_num_id = CalcTransportNumbers(separate_terms, separate_term_errors);
                mprintf("      Warning: the uncertainties shown above are just fit errors from ordinary least squares\n");
                mprintf("               and do not include the uncertainty of the data itself.\n\n");
                //mprintf("      For the fitting procedures, the second half of each plot was used.\n");
                //mprintf("      Please check the plots for the most linear parts if R^2 is too small (less than 0.95).\n\n");
                if (g_oaMolecules.GetSize() > 2)
                {
                    mprintf("\n      Transference numbers based on the weighted regression:\n");
                    for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                        mprintf("        %s: %.6f +/- %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,transp_num.first[i],transp_num.second[i]);
                    mprintf("\n      Ideal transference numbers based on the weighted regression (only self diffusion):\n");
                }
                else
                {
                    mprintf("\n      Ideal transference numbers based on the weighted regression (only self diffusion):\n");
                    mprintf("        (since this is a binary system, true transference numbers don't have any meaning)\n");
                }
                for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                    mprintf("        %s: %.6f +/- %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,transp_num_id.first[i],transp_num_id.second[i]);
                mprintf("\n");
                DumpPlots(cond,total_cond);
            }
            else
            {
                std::vector<std::vector<double> > cond_sq(decompsize,std::vector<double>(m_iDepth/m_iStride + 1,0.0));
                std::vector<double> variance(m_iDepth/m_iStride + 1,0.0);
                std::vector<std::vector<double> > variance_mat(decompsize,std::vector<double>(m_iDepth/m_iStride + 1,0.0));
                std::vector<double> total_cond_sq(m_iDepth/m_iStride + 1,0.0);
                std::tuple<double, double, double, double, double> mberr;
                // use einstein relation to calculate conductivity
                for(int d=0; d<m_iDepth+1; d+=m_iStride)
                { // loop over depths
                    if (fmod(d/m_iStride, step) < 1.0)
                        mprintf(WHITE, "#");
                    num_points = (m_oaaCoords[0][0]->GetSize()-d-1)/m_iStride2 + 1;
                    for(int t=0; t<m_oaaCoords[0][0]->GetSize(); t+=m_iStride2)
                    { // loop over timesteps (non-skipped)
                        if(t+d >= m_oaaCoords[0][0]->GetSize()) // break if current timestep is too large for depth
                            break;
                        tmp = 0.0;
                        for(size_t m=0; m<m_iaaAtomSet.size(); m++)
                        { // loop over molecules
                            for(size_t n=m; n<m_iaaAtomSet.size(); n++)
                            { // loop over molecules again
                                chargeMult = m_faCharges[m] * m_faCharges[n];
                                if(m == n) // same molecule
                                {
                                    msd = 0.0;
                                    V2 = CxDVector3(0.0);
                                    for(size_t i=0; i<m_iaaAtomSet[m].size(); i++)
                                    { // loop over all single molecules
                                        // maybe calculate distances directly in process() and store them in new array (still has to be implemented)
                                        V1 = m_oaaCoords[m][i]->GetAt(t+d) - m_oaaCoords[m][i]->GetAt(t);
                                        current_dist_sqr = V1.GetLengthSqr();
                                        if(d == m_iStride && current_dist_sqr > max_dist_sqr)
                                            eprintf("\nDiscontinuity in step %d: %s[%d] atom moved %.2f pm. Can't compute Conductivity from wrapped trajectory - please use unwrapped trajectory.",t,((CMolecule*)g_oaMolecules[(int)m])->m_sName,(int)i+1,V1.GetLength());
                                        msd += current_dist_sqr;
                                        V2 += V1;
                                    }
                                    index = (int)(m+m_iaaAtomSet.size());
                                    cross = V2.GetLengthSqr() - msd; // all cross terms
                                    cond[m][d/m_iStride] += chargeMult * msd; // all MSD terms
                                    tmp += chargeMult * msd;
                                    cond_sq[m][d/m_iStride] += chargeMult * chargeMult * msd * msd; // all MSD^2 terms
                                    cond[index][d/m_iStride] += chargeMult * cross; // all cross terms
                                    tmp += chargeMult * cross;
                                    cond_sq[index][d/m_iStride] += chargeMult * chargeMult * cross * cross; // all cross^2 terms
                                    continue;
                                }
                                // different molecules
                                V2 = CxDVector3(0.0);
                                V3 = CxDVector3(0.0);
                                for(size_t i=0; i<m_iaaAtomSet[m].size(); i++)
                                { // loop over all molcules of type m
                                    V1 = m_oaaCoords[m][i]->GetAt(t+d) - m_oaaCoords[m][i]->GetAt(t);
                                    V2 += V1; // all terms from molecule m added
                                }
                                for(size_t i=0; i<m_iaaAtomSet[n].size(); i++)
                                { // loop over all molecules of type n
                                    V1 = m_oaaCoords[n][i]->GetAt(t+d) - m_oaaCoords[n][i]->GetAt(t);
                                    V3 += V1; // all terms from molecule n added
                                }
                                index = (int)((m+2)*m_iaaAtomSet.size() + n - (m+1)*(m+2)/2);
                                V4 = V3 + V2; // all cross terms added
                                // V4^2 - V3^2 - V2^2 gives all the cross terms two times 
                                vector_sub = V4.GetLengthSqr() - V3.GetLengthSqr() - V2.GetLengthSqr();
                                cond[index][d/m_iStride] += chargeMult * vector_sub;
                                tmp += chargeMult * vector_sub;
                                cond_sq[index][d/m_iStride] += chargeMult * chargeMult * vector_sub * vector_sub;
                            }
                        }
                        // sum up contributions to obtain total conductivity
                        total_cond[d/m_iStride] += tmp;
                        total_cond_sq[d/m_iStride] += tmp * tmp;
                    }
                    for(int j=0; j<(int)cond.size(); j++)
                    { // loop over contributions
                        // divide by number of timesteps that were used to average
                        cond[j][d/m_iStride] /= (double)num_points;
                        cond_sq[j][d/m_iStride] /= (double)num_points;

                        // multiply by conversion factor to obtain conductivity in S/m (maybe change that again)
                        cond[j][d/m_iStride] *= conversion;
                        cond_sq[j][d/m_iStride] *= conversion*conversion;
                        variance_mat[j][d/m_iStride] = (cond_sq[j][d/m_iStride] - cond[j][d/m_iStride]*cond[j][d/m_iStride]) / (double)(num_points - 1);
                    }
                    total_cond[d/m_iStride] /= (double)num_points;
                    total_cond_sq[d/m_iStride] /= (double)num_points;
                    total_cond[d/m_iStride] *= conversion;
                    total_cond_sq[d/m_iStride] *= conversion*conversion;
                    variance[d/m_iStride] = (total_cond_sq[d/m_iStride] - total_cond[d/m_iStride]*total_cond[d/m_iStride]) / (double)(num_points - 1);
                }
                mprintf(WHITE, "]\n");
                // perform linear regression on collective MSD
                if (m_bMsdiff)
                    range = FindLinearRegion(total_cond);
                else
                    range = std::make_pair( (int)total_cond.size()/2, (int)total_cond.size()-1 );
                mprintf(WHITE,"\n    Performing linear regression on interval %.2f - %.2f ps...\n\n",range.first*x_conversion,range.second*x_conversion);
                //mbr2 = LinReg(total_cond, range.first, range.second, false); // 0: slope a, 1: a-uncertainty, 2: y-intercept b, 3: b-uncertainty, 4: R^2
                mberr = LinReg(total_cond, range.first, range.second, true, variance); // 0: slope a, 1: a-uncertainty, 2: y-intercept b, 3: b-uncertainty
                mprintf("      Collective MSD(t) = %.6f + %.6f * t   (units: [col. MSD] = ps*S/m, [t] = ps).\n",std::get<2>(mberr),std::get<0>(mberr));
                mprintf("      R^2 = %.6f (correlation coefficient)\n",std::get<4>(mberr));
                mprintf("        Conductivity = %.6f +/- %.6f S/m\n",std::get<0>(mberr),std::get<1>(mberr));
                mprintf("        y-intercept  = %.6f +/- %.6f ps*S/m\n\n",std::get<2>(mberr),std::get<3>(mberr));
                //mprintf("      Non-weighted regression yields:\n");
                //mprintf("        Conductivity = %.6f +/- %.6f S/m\n",std::get<0>(mbr2),std::get<1>(mbr2));
                //mprintf("        y-intercept  = %.6f +/- %.6f ps*S/m\n\n",std::get<2>(mbr2),std::get<3>(mbr2));
                //mprintf("      Warning: Uncertainties for the non-weighted regression are only fit errors and do not include the uncertainty of the data itself.\n\n");

                mprintf("      Contributions to the conductivity:\n");
                // self diffusion
                //if (g_oaMolecules.GetSize() > 2)
                //    transp_sum = 0.0;
                //transp_sum_id = 0.0;
                for(int i = 0; i < (int)m_iaActiveMolecules.size(); i++)
                {                 
                    mberr = LinReg(cond[i], range.first, range.second, true, variance_mat[i]);
                    separate_terms[i] = std::get<0>(mberr);
                    separate_term_errors[i] = std::get<1>(mberr);
                    /*if (g_oaMolecules.GetSize() > 2)
                    {
                        transp_num[i] += separate_terms[i];
                        transp_sum += separate_terms[i];
                    }
                    */
                    //transp_num_id[i] = std::get<0>(mberr);
                    //transp_sum_id += std::get<0>(mberr);
                    mprintf("        %s: %.6f +/- %.6f S/m,   R^2 = %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,separate_terms[i],separate_term_errors[i],std::get<4>(mberr));
                }
                // cross diffusion of same molecule type
                for(int i = 0; i < (int)m_iaActiveMolecules.size(); i++)
                {                    
                    index = i + (int)m_iaActiveMolecules.size();
                    mberr = LinReg(cond[index], range.first, range.second, true, variance_mat[index]);
                    separate_terms[index] = std::get<0>(mberr);
                    separate_term_errors[index] = std::get<1>(mberr);
                    /*if (g_oaMolecules.GetSize() > 2)
                    {
                        transp_num[i] += separate_terms[index];
                        transp_sum += separate_terms[index];
                    }
                    */
                    mprintf("        %s/%s: %.6f +/- %.6f S/m,   R^2 = %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,separate_terms[index],separate_term_errors[index],std::get<4>(mberr));
                }
                // cross diffusion of different molecule types
                for(int j=0; j < (int)m_iaActiveMolecules.size(); j++)
                    for(int k=j+1; k < (int)m_iaActiveMolecules.size(); k++)
                    {
                        index = (j+2)*(int)m_iaActiveMolecules.size() + k - (j+1)*(j+2)/2;
                        mberr = LinReg(cond[index], range.first, range.second, true, variance_mat[index]);
                        separate_terms[index] = std::get<0>(mberr);
                        separate_term_errors[index] = std::get<1>(mberr);
                        /*if (g_oaMolecules.GetSize() > 2)
                        {
                            transp_num[j] += separate_terms[index]/2.0;
                            transp_num[k] += separate_terms[index]/2.0;
                            transp_sum += separate_terms[index];
                        }
                        */
                        mprintf("        %s/%s: %.6f +/- %.6f S/m,   R^2 = %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName,((CMolecule*)g_oaMolecules[m_iaActiveMolecules[k]])->m_sName,separate_terms[index],separate_term_errors[index],std::get<4>(mberr));
                    }
                /*
                for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                {
                    if (g_oaMolecules.GetSize() > 2)
                    {
                        transp_num[i] /= transp_sum;
                    }
                    //transp_num_id[i] /= transp_sum_id;
                }
                */
                if (g_oaMolecules.GetSize() > 2)
                    transp_num = CalcTransportNumbers(separate_terms, separate_term_errors, true);
                transp_num_id = CalcTransportNumbers(separate_terms, separate_term_errors);
                //mprintf("\n      For the fitting procedures, the second half of each plot was used.\n");
                //mprintf("      This is not necessarily correct, so please check the plot for the most linear part if R^2 is too small (less than 0.99).\n\n");
                if (g_oaMolecules.GetSize() > 2)
                {
                    mprintf("\n      Transference numbers based on the weighted regression:\n");
                    for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                        mprintf("        %s: %.6f +/- %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,transp_num.first[i],transp_num.second[i]);
                    mprintf("\n      Ideal transference numbers based on the weighted regression (only self diffusion):\n");
                }
                else
                {
                    mprintf("\n      Ideal transference numbers based on the weighted regression (only self diffusion):\n");
                    mprintf("        (since this is a binary system, true transference numbers don't have any meaning)\n");
                }
                for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                    mprintf("        %s: %.6f +/- %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,transp_num_id.first[i],transp_num_id.second[i]);
                mprintf("\n");
                DumpPlots(cond,total_cond,variance_mat,variance);
            }
        }
    }
    else
    {
        std::vector<double> total_current(m_iDepth,0.0);
        CxDoubleArray *ccfInp, *ccfOut;
        CCrossCorrelation* cc;
        FILE* ccfFile;
        char filename[BUF_SIZE];
        double dt = g_fTimestepLength * (double)g_iStride;
        
        int start_plateau = m_iDepth * 2 / 3; // start of plateau region for integration (in fs)
        int n_plateau = m_iDepth - start_plateau; // number of points in plateau region
        int incr_size = n_plateau - 1;
        std::vector<double> weights(incr_size, 0.0), c_weights(incr_size, 0.0); // weights for autocovariance terms in variance estimation
        // bool m_bIPS = false; // use initial positive sequence rule for error estimation
        bool compute_variance = true;

        if(m_oaaVelocities[0][0]->GetSize() < m_iDepth)
        {
            eprintf("\nError: Correlation depth is %d, but only %d timesteps evaluated.\n",m_iDepth,m_oaaVelocities[0][0]->GetSize());
            eprintf("       Reduce depth or increase trajectory length.\n\n");
            abort();
        }

        // setup cc object
        try { cc = new CCrossCorrelation(); } catch(...) { cc = NULL; }
        if(cc == NULL) NewException((double)sizeof(CCrossCorrelation), __FILE__, __LINE__, __PRETTY_FUNCTION__);

        cc->Init(m_oaaVelocities[0][0]->GetSize(),m_iDepth,true); // FFT is used by default

        // setup acf input and output array
        try { ccfInp = new CxDoubleArray(); } catch(...) { ccfInp = NULL; }
        if(ccfInp == NULL) NewException((double)sizeof(CxDoubleArray), __FILE__, __LINE__, __PRETTY_FUNCTION__);
        ccfInp->SetSize(m_oaaVelocities[0][0]->GetSize());

        try { ccfOut = new CxDoubleArray(); } catch(...) { ccfOut = NULL; }
        if (ccfOut == NULL) NewException((double)sizeof(CxDoubleArray),__FILE__,__LINE__,__PRETTY_FUNCTION__);
        ccfOut->SetSize(m_iDepth);

        if (n_plateau < 2)
        {
            mprintf("\n      Not enough data points in plateau region for error estimation of integral value.\n");
            compute_variance = false;
        }
        if(compute_variance)
        {
            // compute weights for autocovariance terms in variance estimation (Bartlett window)
            for(int i = 0; i < incr_size; i++)
                weights[i] = 1.0 - (double)(i + 1) / (double)n_plateau;
            // compute cumulative weights for variance estimation (from X = x1 + sum wi yi, where X is the average over all xi and yi = xi - xi-1 are increments)
            for (int h = 0; h < incr_size; h++)
                for (int i = 0; i < incr_size - h; i++)
                    c_weights[h] += weights[i] * weights[i + h];
        }
        // no decomposition into contributions of the different ions
        if(!m_bDecompConduct)
        {
            double integral = 0.0, integral_avg_tot = 0.0, integral_stderr_tot = 0.0, integral_var_tot, increment_avg_tot = 0.0;
            int H_star = incr_size - 1;
            CxDVec3Array* sys_current;
            std::vector<double> plateau_values(n_plateau, 0.0); // values in plateau region for error estimation
            std::vector<double> increments(incr_size, 0.0), gamma(incr_size, 0.0); // increments and their autocovariance for error estimation
            // initialize system charge current
            try { sys_current = new CxDVec3Array(); } catch(...) { sys_current = NULL; }
            if(sys_current == NULL) NewException((double)sizeof(CxDVec3Array), __FILE__, __LINE__, __PRETTY_FUNCTION__);

            // calculate system charge current (velocities in pm/fs)
            for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
            {
                sys_current->Add(CxDVector3(0.0));
                for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
                {
                    for(int i=0; i<(int)m_iaaAtomSet[m].size(); i++)
                    {
                        // current in [z * m / s]
                        sys_current->GetAt(t) += ((CxDVec3Array*)m_oaaVelocities[m][i])->GetAt(t) * m_faCharges[m];
                    }
                }
            }
            // correlate system charge current with itself
            for(int dim=0; dim<3; dim++)
            {
                for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
                {
                    ccfInp->GetAt(t) = sys_current->GetAt(t)[dim];
                }
                cc->CrossCorrelate(ccfInp,ccfInp,ccfOut);
                for(int t=0; t<m_iDepth; t++)
                {
                    // now in [z^2 * m^2 / s^2]
                    total_current[t] += ccfOut->GetAt(t);
                }
            }

            // free memory
            delete sys_current;
            delete cc;
            delete ccfInp;
            delete ccfOut;

            // print charge currents to file (with integrals)
            #ifdef TARGET_LINUX
                snprintf(filename, BUF_SIZE, "charge_current%s.csv", m_sName.c_str());
            #else
                sprintf(filename, "charge_current%s.csv", m_sName.c_str());
            #endif
            mprintf("    Saving charge currents as \"%s\"...\n", filename);
            ccfFile = OpenFileWrite(filename, false);
            fprintf(ccfFile, "#Correlation Depth / fs;  CCF(total) / S m^-1 fs^-1;  Integral / S m^-1\n");
            for(int i=0; i<m_iDepth; i++)
            {
                if (i == 0)
                {
                    fprintf(ccfFile, "0.00; %.8G; 0.00\n", total_current[i]*conversion*2*1E-3);
                    continue;
                }
                // integral unit [z^2 * fm^2 / fs]. current unit [z^2 * fm^2 / fs^2]
                integral += 0.5 * (total_current[i-1] + total_current[i]) * dt; // integral in [S m^-1]
                if (i >= start_plateau)
                    plateau_values[i-start_plateau] = integral * conversion * 2.0 * 1E-3; // convert to S/m and store for error estimation
                // conversion units [C^2 / z^2] * [fs^2 / kg / fm^2] / [pm^3]
                // integral * conversion: [C^2 * fs / kg / pm^3] -> [C^2 * s / kg / m^3] * 1E-15 * 1E+36 = [S/m] * 1E+21
                // current * conversion: [C^2 / kg / pm^3] -> [C^2 / kg / m^3] * 1E+36 = [S/m/s] * 1E+36 = [S/m/fs] * 1E+21
                fprintf(ccfFile, "%.2f; %.8G; %.8G\n", i*dt, total_current[i]*conversion*2*1E-3, integral * conversion * 2 * 1E-3);
            }
            fclose(ccfFile);
            // estimate error of tail average using autocovariance of increments
            for(int i=0; i<n_plateau; i++)
            {
                if (compute_variance && i < incr_size)
                {
                    increments[i] = plateau_values[i+1] - plateau_values[i];
                    increment_avg_tot += increments[i];
                }
                integral_avg_tot += plateau_values[i];
            }
            integral_avg_tot /= (double)n_plateau;
            if (compute_variance)
            {
                increment_avg_tot /= (double)incr_size;
                // calculate autocovariance of increments
                for(int k=0; k<incr_size; k++)
                {
                    for(int i = 0; i < incr_size - k; i++)
                        gamma[k] += (increments[i] - increment_avg_tot) * (increments[i+k] - increment_avg_tot);
                    gamma[k] /= (double)incr_size; // slightly biased estimator for autocovariance, but should be fine for large n and is more stable
                }
                // IPS rule for truncation of autocovariance sum
                if (m_bIPS)
                {
                    double ips_sum = gamma[0];
                    for (int h = 1; h < incr_size; h++)
                    {
                        ips_sum += 2.0 * gamma[h];
                        if (ips_sum < 0.0)
                        {
                            H_star = h - 1;
                            break;
                        }
                    }
                }
                // now compute variance of integral average using autocovariance of increments
                integral_var_tot = gamma[0] * c_weights[0];
                for(int h = 1; h < incr_size; h++)
                {
                    if (h > H_star) // truncate autocovariance sum at H_star. Can only happen if m_bIPS if True, otherwise H_star = incr_size - 1
                        break;
                    integral_var_tot += 2.0 * c_weights[h] * gamma[h];
                }
                integral_stderr_tot = sqrt(integral_var_tot);
                mprintf("      Conductivity = %.6f +/- %.6f S/m\n", integral_avg_tot, integral_stderr_tot);
            }
            else
                mprintf("      Conductivity = %.6f S/m\n", integral_avg_tot);
        }
        else
        {
            std::vector<std::vector<double> > charge_current(3,std::vector<double>(m_iDepth,0.0));
            /*
            if(!m_bDecompSelfCross)
            {
                // might be useful to differentiate between 2 different molecules and more than 2 different molecules
                int decompsize = m_iaActiveMolecules.size()+((m_iaActiveMolecules.size()-1)*m_iaActiveMolecules.size())/2;
                std::vector<CxDoubleArray*> ccfs(decompsize);
                std::vector<CxDVec3Array*> init_currents(decompsize+1);
                std::vector<double> integrals(decompsize+4,0.0);

                // setup ccfs arrays
                for(int i=0; i<(int)ccfs.size(); i++)
                {
                    try { ccfs[i] = new CxDoubleArray(); } catch(...) { ccfs[i] = NULL; }
                    if(ccfs[i] == NULL) NewException((double)sizeof(CxDoubleArray), __FILE__, __LINE__, __PRETTY_FUNCTION__);

                    ccfs[i]->SetSize(m_iDepth);
                    for(int j = 0; j < m_iDepth; j++)
                        ccfs[i]->GetAt(j) = 0.0;
                }
                // initialize charge currents
                for(int i=0; i<(int)init_currents.size(); i++)
                {
                    try { init_currents[i] = new CxDVec3Array(); } catch(...) { init_currents[i] = NULL; }
                    if(init_currents[i] == NULL) NewException((double)sizeof(CxDVec3Array), __FILE__, __LINE__, __PRETTY_FUNCTION__);
                }
                // calculate initial charge current for each molecule type
                for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
                {
                    for(int k=0; k<(int)init_currents.size(); k++)
                        init_currents[k]->Add(CxDVector3(0.0));

                    for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
                        for(int i=0; i<(int)m_iaaAtomSet[m].size(); i++)
                            init_currents[m]->GetAt(t) += ((CxDVec3Array*)m_oaaVelocities[m][i])->GetAt(t) * m_faCharges[m];
                }
                // calculate system charge current and charge current of molecule type pairs
                for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
                {
                    // system charge current
                    for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
                        init_currents[init_currents.size()-1]->GetAt(t) += init_currents[m]->GetAt(t);
                    // binary charge currents
                    for(int n=m+1; n<(int)m_iaaAtomSet.size(); n++)
                    {
                        int index = (m+1)*m_iaActiveMolecules.size() + n - (m+1)*(m+2)/2;
                        // add init_currents[m] and init_currents[n] to init_currents[index]
                        for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
                            init_currents[index]->GetAt(t) = init_currents[m]->GetAt(t) + init_currents[n]->GetAt(t);
                    }
                }

                // correlate charge currents with themselves
                for(int dim=0; dim<3; dim++)
                {
                    for(int k=0; k<(int)init_currents.size(); k++)
                    {
                        for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
                        {
                            ccfInp->GetAt(t) = init_currents[k]->GetAt(t)[dim];
                        }
                        cc->CrossCorrelate(ccfInp,ccfInp,ccfOut);
                        for(int t=0; t<m_iDepth; t++)
                        {
                            if(k != decompsize)
                                ccfs[k]->GetAt(t) += ccfOut->GetAt(t);
                            else
                            {
                                total_current[t] += ccfOut->GetAt(t);
                            }
                        }
                    }
                }

                // loop over all molecule ccfs and add them to cation and anion charge currents
                for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
                {
                    for(int t=0; t<m_iDepth; t++)
                    {
                        if(m_faCharges[m] > 0) // cation
                            charge_current[0][t] += ccfs[m]->GetAt(t);
                        else // anion
                            charge_current[1][t] += ccfs[m]->GetAt(t);
                    }
                    for(int n=m+1; n<(int)m_iaaAtomSet.size(); n++)
                    {
                        chargeMult = m_faCharges[m]*m_faCharges[n];
                        int index = (m+1)*m_iaActiveMolecules.size() + n - (m+1)*(m+2)/2;
                        for(int t=0; t<m_iDepth; t++)
                        {
                            ccfs[index]->GetAt(t) -= ccfs[m]->GetAt(t) + ccfs[n]->GetAt(t);
                            if(chargeMult > 0) // same charge
                            {
                                if(m_faCharges[m] > 0) // cation
                                    charge_current[0][t] += ccfs[index]->GetAt(t);
                                else // anion
                                    charge_current[1][t] += ccfs[index]->GetAt(t);
                            }
                            else // cross
                                charge_current[2][t] += ccfs[index]->GetAt(t);
                        }
                    }
                }

                // free memory
                delete cc;
                delete ccfInp;
                delete ccfOut;

                // deallocate memory in init currents
                for(int i=0; i<(int)init_currents.size(); i++)
                    delete init_currents[i];

                // print ccfs and charge_currents to file (with integrals)
                #ifdef TARGET_LINUX
                    snprintf(filename, BUF_SIZE, "ccf_decomp%s.csv", m_sName.c_str());
                #else
                    sprintf(filename, "ccf_decomp%s.csv", m_sName.c_str());
                #endif
                mprintf("    Saving correlation functions as \"%s\"...\n", filename);
                ccfFile = OpenFileWrite(filename, false);
                fprintf(ccfFile, "#Correlation Depth / fs");
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    fprintf(ccfFile, ";  CCF(%s) / S m^-1 fs^-1;  Integral / S m^-1", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName);
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    for(int j=i+1; j<(int)m_iaaAtomSet.size(); j++)
                        fprintf(ccfFile, ";  CCF(%s/%s) / S m^-1 fs^-1;  Integral / S m^-1", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName);
                fprintf(ccfFile, "\n");
                for(int i=0; i<m_iDepth; i++)
                {
                    fprintf(ccfFile, "%.2f", i*g_fTimestepLength*(double)g_iStride);
                    for(int j=0; j<(int)ccfs.size(); j++)
                    {
                        // [z^2 * fm^2 / fs]
                        integrals[j] += ccfs[j]->GetAt(i) * g_fTimestepLength * (double)g_iStride;
                        fprintf(ccfFile, "; %.8G; %.8G", ccfs[j]->GetAt(i)*conversion*1E-3*2.0, integrals[j]*conversion*1E-3*2.0);
                    }
                    fprintf(ccfFile, "\n");
                }
                fclose(ccfFile);

                // print charge currents to file (with integrals)
                #ifdef TARGET_LINUX
                    snprintf(filename, BUF_SIZE, "charge_current_decomp%s.csv", m_sName.c_str());
                #else
                    sprintf(filename, "charge_current_decomp%s.csv", m_sName.c_str());
                #endif
                mprintf("    Saving charge currents as \"%s\"...\n", filename);
                ccfFile = OpenFileWrite(filename, false);
                fprintf(ccfFile, "#Correlation Depth / fs;  CCF(cat) / S m^-1 fs^-1;  Integral(cat) / S m^-1;  CCF(an) / S m^-1 fs^-1;  Integral(an) / S m^-1;  CCF(cross) / S m^-1 fs^-1;  Integral(cross) / S m^-1;  CCF(total) / S m^-1 fs^-1;  Integral(total) / S m^-1\n");
                for(int t=0; t<m_iDepth; t++)
                {
                    // integral unit [z^2 * fm^2 / fs]. current unit [z^2 * fm^2 / fs^2]
                    fprintf(ccfFile, "%.2f; ", t*g_fTimestepLength*(double)g_iStride);
                    for(int i=0; i<3; i++)
                    {
                        fprintf(ccfFile, "%.8G; ", charge_current[i][t]*conversion*1E-3*2.0);
                        integrals[decompsize+i] += charge_current[i][t] * g_fTimestepLength * (double)g_iStride;
                        fprintf(ccfFile, "%.8G; ", integrals[decompsize+i]*conversion*1E-3*2.0);
                    }
                    integrals[decompsize+3] += total_current[t] * g_fTimestepLength * (double)g_iStride;
                    fprintf(ccfFile, "%.8G; %.8G\n", total_current[t]*conversion*1E-3*2.0, integrals[decompsize+3]*conversion*1E-3*2.0);
                }
                fclose(ccfFile);
                // print contributions to the conductivities to console (from integrals)
                mprintf("    Contributions to the conductivities:\n");
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    mprintf("        %s: %.8G\n", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, integrals[i]*conversion*1E-3*2.0);
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    for(int j=i+1; j<(int)m_iaaAtomSet.size(); j++)
                        mprintf("        %s/%s: %.8G\n", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName, integrals[(i+1)*m_iaaAtomSet.size()+j-(i+1)*(i+2)/2]*conversion*1E-3*2.0);
                mprintf("\n        cations: %.8G\n", integrals[decompsize+0]*conversion*1E-3*2.0);
                mprintf("        anions: %.8G\n", integrals[decompsize+1]*conversion*1E-3*2.0);
                mprintf("        cross: %.8G\n", integrals[decompsize+2]*conversion*1E-3*2.0);
                mprintf("        total: %.8G\n\n", integrals[decompsize+3]*conversion*1E-3*2.0);
            } 
            else
            {
            }
            */
            // new version
            int decompsize = (int)(m_iaActiveMolecules.size()+((m_iaActiveMolecules.size()-1)*m_iaActiveMolecules.size())/2);
            std::vector<CxDoubleArray*> ccfs(decompsize+m_iaActiveMolecules.size());
            std::vector<double> integrals(decompsize+m_iaActiveMolecules.size()+4,0.0), integral_avgs(decompsize+m_iaActiveMolecules.size()+4,0.0), integral_stderrs(decompsize+m_iaActiveMolecules.size()+4,0.0);
            std::vector<double> integral_vars(decompsize+m_iaActiveMolecules.size()+4,0.0), increment_avgs(decompsize+m_iaActiveMolecules.size()+4,0.0);
            std::vector<int> h_stars(decompsize+m_iaActiveMolecules.size()+4,incr_size - 1); // truncation point for autocovariance sum in variance estimation using IPS rule
            std::vector<std::vector<double> > plateau_values(decompsize+m_iaActiveMolecules.size()+4,std::vector<double>(n_plateau,0.0)); // values in plateau region for error estimation
            // increments and their autocovariance for error estimation
            std::vector<std::vector<double> > increments(decompsize+m_iaActiveMolecules.size()+4,std::vector<double>(incr_size,0.0)), gamma(decompsize+m_iaActiveMolecules.size()+4,std::vector<double>(incr_size,0.0)); 
            // arrays for correlations
            std::vector<CxDVec3Array*> init_currents(decompsize+1);
            CxDoubleArray *ccfInp2, *ccfOut2;
            // arrays for transport numbers
            //std::vector<double> transp_num(m_iaActiveMolecules.size(),0.0);
            //std::vector<double> transp_num_id(m_iaActiveMolecules.size(),0.0);
            std::pair<std::vector<double>, std::vector<double> > transp_num_id, transp_num;
            transp_num_id.first.resize(m_iaActiveMolecules.size(), 0.0);
            transp_num_id.second.resize(m_iaActiveMolecules.size(), 0.0);
            transp_num.first.resize(m_iaActiveMolecules.size(), 0.0);
            transp_num.second.resize(m_iaActiveMolecules.size(), 0.0);
            // double transp_sum;
            // double transp_sum_id;

            // initialize charge currents
            for(int i=0; i<(int)init_currents.size(); i++)
            {
                try { init_currents[i] = new CxDVec3Array(); } catch(...) { init_currents[i] = NULL; }
                if(init_currents[i] == NULL) NewException((double)sizeof(CxDVec3Array), __FILE__, __LINE__, __PRETTY_FUNCTION__);
            }

            // setup ccfs arrays
            for(int i=0; i<(int)ccfs.size(); i++)
            {
                try { ccfs[i] = new CxDoubleArray(); } catch(...) { ccfs[i] = NULL; }
                if(ccfs[i] == NULL) NewException((double)sizeof(CxDoubleArray), __FILE__, __LINE__, __PRETTY_FUNCTION__);

                ccfs[i]->SetSize(m_iDepth);
                for(int j = 0; j < m_iDepth; j++)
                    ccfs[i]->GetAt(j) = 0.0;
            }

            try { ccfInp2 = new CxDoubleArray(); } catch(...) { ccfInp2 = NULL; }
            if(ccfInp2 == NULL) NewException((double)sizeof(CxDoubleArray), __FILE__, __LINE__, __PRETTY_FUNCTION__);
            ccfInp2->SetSize(m_oaaVelocities[0][0]->GetSize());

            try { ccfOut2 = new CxDoubleArray(); } catch(...) { ccfOut2 = NULL; }
            if (ccfOut2 == NULL) NewException((double)sizeof(CxDoubleArray),__FILE__,__LINE__,__PRETTY_FUNCTION__);
            ccfOut2->SetSize(m_iDepth);

            // calculate initial charge current for each molecule type
            for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
            {
                for(int k=0; k<(int)init_currents.size(); k++)
                    init_currents[k]->Add(CxDVector3(0.0));

                for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
                    for(int i=0; i<(int)m_iaaAtomSet[m].size(); i++)
                        init_currents[m]->GetAt(t) += ((CxDVec3Array*)m_oaaVelocities[m][i])->GetAt(t) * m_faCharges[m];
            }

            // calculate system charge current and charge current of molecule type pairs
            for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
            {
                // system charge current
                for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
                    init_currents[init_currents.size()-1]->GetAt(t) += init_currents[m]->GetAt(t);
                // binary charge currents
                for(int n=m+1; n<(int)m_iaaAtomSet.size(); n++)
                {
                    int index = (m+1)*(int)m_iaActiveMolecules.size() + n - (m+1)*(m+2)/2;
                    // add init_currents[m] and init_currents[n] to init_currents[index]
                    for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
                        init_currents[index]->GetAt(t) = init_currents[m]->GetAt(t) + init_currents[n]->GetAt(t);
                }
            }

            // first calculate ccfs[#mols:] from the initial currents by correlation them with themselves
            for(int dim=0; dim<3; dim++)
            {
                for(int k=0; k<(int)init_currents.size(); k++)
                {
                    for(int t=0; t<m_oaaVelocities[0][0]->GetSize(); t++)
                    {
                        ccfInp->GetAt(t) = init_currents[k]->GetAt(t)[dim];
                    }
                    cc->CrossCorrelate(ccfInp,ccfInp,ccfOut);
                    for(int t=0; t<m_iDepth; t++)
                    {
                        // for last initial current add output to total_current
                        if(k != decompsize)
                            ccfs[k+m_iaActiveMolecules.size()]->GetAt(t) += ccfOut->GetAt(t);
                        else
                        {
                            total_current[t] += ccfOut->GetAt(t);
                        }
                    }
                }
            }

            // deallocate memory in init currents
            for(int i=0; i<(int)init_currents.size(); i++)
                delete init_currents[i];

            // from the ccfs[2*#mols:] subtract the corresponding ccfs[#mols:2*#mols-1] to get the binary ccfs
            for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
            {
                for(int n=m+1; n<(int)m_iaaAtomSet.size(); n++)
                {
                    int index = (m+1)*(int)m_iaActiveMolecules.size() + n - (m+1)*(m+2)/2;
                    for(int t=0; t<m_iDepth; t++)
                    {
                        ccfs[index+m_iaaAtomSet.size()]->GetAt(t) -= ccfs[m+m_iaaAtomSet.size()]->GetAt(t) + ccfs[n+m_iaaAtomSet.size()]->GetAt(t);
                    }
                }
            }

            // now calculate the ccfs for the single molecules with themselves (self terms) as ccfs[:#mols] (bottleneck)
            mprintf("    Starting calculation of correlation functions for single molecules...\n");
            mprintf(WHITE, "      [");
            for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
            {
                step = (double)m_iaaAtomSet[m].size() / 50.0;
                for(int i=0; i<(int)m_iaaAtomSet[m].size(); i++)
                {
                    if(fmod(i,step) < 1.0)
                        mprintf(WHITE, "#");
                    for(int dim=0; dim<3; dim++)
                    {
                        for(int t=0; t<m_oaaVelocities[m][i]->GetSize(); t++)
                        {
                            ccfInp->GetAt(t) = ((CxDVec3Array*)m_oaaVelocities[m][i])->GetAt(t)[dim];
                        }
                        cc->CrossCorrelate(ccfInp,ccfInp,ccfOut);
                        for(int t=0; t<m_iDepth; t++)
                        {
                            ccfs[m]->GetAt(t) += ccfOut->GetAt(t) * m_faCharges[m] * m_faCharges[m];
                        }
                    }
                }
            }
            mprintf(WHITE, "]\n");

            // now subtract ccfs[:#mols-1] from ccfs[#mols:2*#mols-1] to remove self terms from ccfs
            for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
            {
                for(int t=0; t<m_iDepth; t++)
                {
                    ccfs[m+m_iaaAtomSet.size()]->GetAt(t) -= ccfs[m]->GetAt(t);
                }
            }

            // calculate charge currents of cations, anions and cross terms (maybe not really needed)
            for(int m=0; m<(int)m_iaaAtomSet.size(); m++)
            {
                for(int t=0; t<m_iDepth; t++)
                {
                    if(m_faCharges[m] > 0) // cation
                        charge_current[0][t] += ccfs[m]->GetAt(t) + ccfs[m+m_iaaAtomSet.size()]->GetAt(t);
                    else // anion
                        charge_current[1][t] += ccfs[m]->GetAt(t) + ccfs[m+m_iaaAtomSet.size()]->GetAt(t);
                }
                for(int n=m+1; n<(int)m_iaaAtomSet.size(); n++)
                {
                    chargeMult = m_faCharges[m]*m_faCharges[n];
                    int index = (m+1)*(int)m_iaActiveMolecules.size() + n - (m+1)*(m+2)/2;
                    for(int t=0; t<m_iDepth; t++)
                    {
                        if(chargeMult > 0.0) // same charge
                        {
                            if(m_faCharges[m] > 0) // cation
                                charge_current[0][t] += ccfs[index+m_iaaAtomSet.size()]->GetAt(t);
                            else // anion
                                charge_current[1][t] += ccfs[index+m_iaaAtomSet.size()]->GetAt(t);
                        }
                        else // cross
                            charge_current[2][t] += ccfs[index+m_iaaAtomSet.size()]->GetAt(t);
                    }
                }
            }

            // print ccfs and charge_currents to file (with integrals)
            #ifdef TARGET_LINUX
                snprintf(filename, BUF_SIZE, "ccf_decomp_self_cross%s.csv", m_sName.c_str());
            #else
                sprintf(filename, "ccf_decomp_self_cross%s.csv", m_sName.c_str());
            #endif
            mprintf("      Saving correlation functions as \"%s\"...\n", filename);
            ccfFile = OpenFileWrite(filename, false);
            fprintf(ccfFile, "#Correlation Depth / fs");
            for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                fprintf(ccfFile, ";  CCF(%s_self) / S m^-1 fs^-1;  Integral / S m^-1", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName);
            for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                fprintf(ccfFile, ";  CCF(%s_cross) / S m^-1 fs^-1;  Integral / S m^-1", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName);
            for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                for(int j=i+1; j<(int)m_iaaAtomSet.size(); j++)
                    fprintf(ccfFile, ";  CCF(%s/%s) / S m^-1 fs^-1;  Integral / S m^-1", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName);
            fprintf(ccfFile, "\n");
            for(int i=0; i<m_iDepth; i++)
            {
                if (i == 0)
                {
                    fprintf(ccfFile, "0.00");
                    for(int j=0; j<(int)ccfs.size(); j++)
                        fprintf(ccfFile, "; %.8G; 0.00", ccfs[j]->GetAt(i)*conversion*1E-3*2.0);
                    fprintf(ccfFile, "\n");
                    continue;
                }
                fprintf(ccfFile, "%.2f", i*dt);
                for(int j=0; j<(int)ccfs.size(); j++)
                {
                    // [z^2 * fm^2 / fs]
                    integrals[j] += 0.5 * (ccfs[j]->GetAt(i-1) + ccfs[j]->GetAt(i));
                    if(i >= start_plateau)
                        plateau_values[j][i-start_plateau] = integrals[j] * conversion * 1E-3 * 2.0; // convert to S/m and store for error estimation
                    fprintf(ccfFile, "; %.8G; %.8G", ccfs[j]->GetAt(i)*conversion*1E-3*2.0, integrals[j]*conversion*1E-3*2.0);
                }
                fprintf(ccfFile, "\n");
            }
            fclose(ccfFile);
            // deallocate memory in ccfs
            for(int i=0; i<(int)ccfs.size(); i++)
                delete ccfs[i];
            // estimate error of tail average using autocovariance of increments
            for(int j=0; j<(int)ccfs.size(); j++)
            {
                for(int i=0; i<n_plateau; i++)
                {
                    if (compute_variance && i < incr_size)
                    {
                        increments[j][i] = plateau_values[j][i+1] - plateau_values[j][i];
                        increment_avgs[j] += increments[j][i];
                    }
                    integral_avgs[j] += plateau_values[j][i];
                }
                integral_avgs[j] /= (double)n_plateau;
                if (compute_variance)
                {
                    increment_avgs[j] /= (double)incr_size;
                    // compute autocovariances of increments
                    for(int k = 0; k < incr_size; k++)
                    {
                        for(int i = 0; i < incr_size - k; i++)
                            gamma[j][k] += (increments[j][i] - increment_avgs[j]) * (increments[j][i+k] - increment_avgs[j]);
                        gamma[j][k] /= (double)incr_size; // slightly biased estimator for autocovariance, but should be fine for large n and is more stable
                    }
                    // IPS rule for truncation of autocovariance sum
                    if (m_bIPS)
                    {
                        double ips_sum = gamma[j][0];
                        for (int h = 1; h < incr_size; h++)
                        {
                            ips_sum += 2.0 * gamma[j][h];
                            if (ips_sum < 0.0)
                            {
                                h_stars[j] = h - 1;
                                break;
                            }
                        }
                    }
                    // now compute variance of integral average using autocovariance of increments
                    integral_vars[j] = gamma[j][0] * c_weights[0];
                    for(int h = 1; h < incr_size; h++)
                    {
                        if (h > h_stars[j]) // truncate autocovariance sum at h_star. Can only happen if m_bIPS if True, otherwise h_star = incr_size - 1
                            break;
                        integral_vars[j] += 2.0 * c_weights[h] * gamma[j][h];
                    }
                    integral_stderrs[j] = sqrt(integral_vars[j]);
                }
            }
            // print charge currents to file (with integrals). (Maybe not really needed)
            #ifdef TARGET_LINUX
                snprintf(filename, BUF_SIZE, "charge_current_decomp_self_cross%s.csv", m_sName.c_str());
            #else
                sprintf(filename, "charge_current_decomp_self_cross%s.csv", m_sName.c_str());
            #endif
            mprintf("      Saving charge currents as \"%s\"...\n", filename);
            ccfFile = OpenFileWrite(filename, false);
            fprintf(ccfFile, "#Correlation Depth / fs;  CCF(cat) / S m^-1 fs^-1;  Integral / S m^-1;  CCF(an) / S m^-1 fs^-1;  Integral / S m^-1;  CCF(cross) / S m^-1 fs^-1;  Integral / S m^-1;  CCF(total) / S m^-1 fs^-1;  Integral / S m^-1\n");
            for(int i=0; i<m_iDepth; i++)
            {
                if (i == 0)
                {
                    fprintf(ccfFile, "0.00; ");
                    for(int j=0; j<3; j++)
                        fprintf(ccfFile, "%.8G; 0.00; ", charge_current[j][i]*conversion*1E-3*2.0);
                    fprintf(ccfFile, "%.8G; 0.00\n", total_current[i]*conversion*1E-3*2.0);
                    continue;
                }
                for(int j=0; j<4; j++)
                {
                    // integral unit [z^2 * fm^2 / fs]. current unit [z^2 * fm^2 / fs^2]
                    if (j < 3)
                        integrals[ccfs.size()+j] += 0.5 * (charge_current[j][i-1] + charge_current[j][i]) * dt;
                    else
                        integrals[ccfs.size()+j] += 0.5 * (total_current[i-1] + total_current[i]) * dt;
                    if(i >= start_plateau)
                        plateau_values[ccfs.size()+j][i-start_plateau] = integrals[ccfs.size()+j] * conversion * 1E-3 * 2.0; // convert to S/m and store for error estimation
                }
                fprintf(ccfFile, "%.2f; %.8G; %.8G; %.8G; %.8G; %.8G; %.8G; %.8G; %.8G\n", i*dt, charge_current[0][i]*conversion*1E-3*2.0, integrals[ccfs.size()]*conversion*1E-3*2.0, charge_current[1][i]*conversion*1E-3*2.0, integrals[ccfs.size()+1]*conversion*1E-3*2.0, charge_current[2][i]*conversion*1E-3*2.0, integrals[ccfs.size()+2]*conversion*1E-3*2.0, total_current[i]*conversion*1E-3*2.0, integrals[ccfs.size()+3]*conversion*1E-3*2.0);
            }
            // estimate error of tail average for charge currents using autocovariance of increments
            for(int j=0; j<4; j++)
            {
                for (int i=0; i<n_plateau; i++)
                {
                    if (compute_variance && i < incr_size)
                    {
                        increments[ccfs.size()+j][i] = plateau_values[ccfs.size()+j][i+1] - plateau_values[ccfs.size()+j][i];
                        increment_avgs[ccfs.size()+j] += increments[ccfs.size()+j][i];
                    }
                    integral_avgs[ccfs.size()+j] += plateau_values[ccfs.size()+j][i];
                }
                integral_avgs[ccfs.size()+j] /= (double)n_plateau;
                if (compute_variance)
                {
                    increment_avgs[ccfs.size()+j] /= (double)incr_size;
                    // compute autocovariances of increments
                    for(int k = 0; k < incr_size; k++)
                    {
                        for(int i = 0; i < incr_size - k; i++)
                            gamma[ccfs.size()+j][k] += (increments[ccfs.size()+j][i] - increment_avgs[ccfs.size()+j]) * (increments[ccfs.size()+j][i+k] - increment_avgs[ccfs.size()+j]);
                        gamma[ccfs.size()+j][k] /= (double)incr_size; // slightly biased estimator for autocovariance, but should be fine for large n and is more stable
                    }
                    // IPS rule for truncation of autocovariance sum
                    if (m_bIPS)
                    {
                        double ips_sum = gamma[ccfs.size()+j][0];
                        for (int h = 1; h < incr_size; h++)
                        {
                            ips_sum += 2.0 * gamma[ccfs.size()+j][h];
                            if (ips_sum < 0.0)
                            {
                                h_stars[ccfs.size()+j] = h - 1;
                                break;
                            }
                        }
                    }
                    // now compute variance of integral average using autocovariance of increments
                    integral_vars[ccfs.size()+j] = gamma[ccfs.size()+j][0] * c_weights[0];
                    for(int h = 1; h < incr_size; h++)
                    {
                        if (h > h_stars[ccfs.size()+j]) // truncate autocovariance sum at h_star. Can only happen if m_bIPS if True, otherwise h_star = incr_size - 1
                            break;
                        integral_vars[ccfs.size()+j] += 2.0 * c_weights[h] * gamma[ccfs.size()+j][h];
                    }
                    integral_stderrs[ccfs.size()+j] = sqrt(integral_vars[ccfs.size()+j]);
                }
            }
            fclose(ccfFile);
            if (compute_variance)
            {
                mprintf("      Conductivity = %.6f +/- %.6f S/m\n", integral_avgs[ccfs.size()+3], integral_stderrs[ccfs.size()+3]);

                // print contributions to the conductivity to console (from integrals)
                mprintf("\n      Contributions to the conductivity:\n");
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    mprintf("        %s: %.6f +/- %.6f S/m\n", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, integral_avgs[i], integral_stderrs[i]);
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    mprintf("        %s/%s: %.6f +/- %.6f S/m\n", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, integral_avgs[m_iaaAtomSet.size()+i], integral_stderrs[m_iaaAtomSet.size()+i]);
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    for(int j=i+1; j<(int)m_iaaAtomSet.size(); j++)
                        mprintf("        %s/%s: %.6f +/- %.6f S/m\n", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName, integral_avgs[(i+2)*m_iaaAtomSet.size()+j-(i+1)*(i+2)/2], integral_stderrs[(i+2)*m_iaaAtomSet.size()+j-(i+1)*(i+2)/2]);
                // [A^2 * s^2 * fs / kg * pm^2 * m] -> [S/m]
                // multiply by (10^-15/10^-24) = 10^9 -> [A^2 * s^3 / kg * m^3] = [S/m]
                // mprintf("\n        cations: %.8G +/- %.8G S/m\n", integral_avgs[ccfs.size()], integral_stderrs[ccfs.size()]);
                // mprintf("        anions: %.8G +/- %.8G S/m\n", integral_avgs[ccfs.size()+1], integral_stderrs[ccfs.size()+1]);
                // mprintf("        mixed: %.8G +/- %.8G S/m\n\n", integral_avgs[ccfs.size()+2], integral_stderrs[ccfs.size()+2]);
                // transport numbers
                //if (g_oaMolecules.GetSize() > 2)
                //    transp_sum = 0.0;
                //transp_sum_id = 0.0;
                /*
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                {
                    //transp_num_id[i] = integral_avgs[i];
                    //transp_sum_id += transp_num_id[i];
                    if (g_oaMolecules.GetSize() > 2)
                    {
                        transp_num[i] += integral_avgs[i] + integral_avgs[m_iaaAtomSet.size()+i];
                        transp_sum += transp_num[i];
                        for (int j=i+1; j<(int)m_iaaAtomSet.size(); j++)
                        {
                            transp_num[i] += integral_avgs[(i+2)*m_iaaAtomSet.size()+j-(i+1)*(i+2)/2] / 2.0; // split cross term equally between the two species
                            transp_num[j] += integral_avgs[(i+2)*m_iaaAtomSet.size()+j-(i+1)*(i+2)/2] / 2.0;
                            transp_sum += integral_avgs[(i+2)*m_iaaAtomSet.size()+j-(i+1)*(i+2)/2];
                        }
                    }
                }
                */
                /*
                for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                {
                    if (g_oaMolecules.GetSize() > 2)
                    {
                        transp_num[i] /= transp_sum;
                    }
                    //transp_num_id[i] /= transp_sum_id;
                }
                */
                if (g_oaMolecules.GetSize() > 2)
                    transp_num = CalcTransportNumbers(integral_avgs, integral_stderrs, true);
                transp_num_id = CalcTransportNumbers(integral_avgs, integral_stderrs);
                if (g_oaMolecules.GetSize() > 2)
                {
                    mprintf("\n      Transference numbers based on the weighted regression:\n");
                    for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                    {
                        mprintf("        %s: %.6f +/- %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,transp_num.first[i],transp_num.second[i]);
                    }
                    mprintf("\n      Ideal transference numbers based on the weighted regression (only self diffusion):\n");
                }
                else
                {
                    mprintf("\n      Ideal transference numbers based on the weighted regression (only self diffusion):\n");
                    mprintf("        (since this is a binary system, true transference numbers don't have any meaning)\n");
                }
                for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                {
                    mprintf("        %s: %.6f +/- %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,transp_num_id.first[i],transp_num_id.second[i]);
                }
            }
            else
            {
                mprintf("      Conductivity = %.6f S/m\n", integral_avgs[ccfs.size()+3]);

                // print contributions to the conductivity to console (from integrals)
                mprintf("\n      Contributions to the conductivity:\n");
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    mprintf("        %s: %.6f S/m\n", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, integral_avgs[i]);
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    mprintf("        %s/%s: %.6f S/m\n", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, integral_avgs[m_iaaAtomSet.size()+i]);
                for(int i=0; i<(int)m_iaaAtomSet.size(); i++)
                    for(int j=i+1; j<(int)m_iaaAtomSet.size(); j++)
                        mprintf("        %s/%s: %.6f S/m\n", ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName, ((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName, integral_avgs[(i+2)*m_iaaAtomSet.size()+j-(i+1)*(i+2)/2]);
            
                // get transport numbers
                if (g_oaMolecules.GetSize() > 2)
                    transp_num = CalcTransportNumbers(integral_avgs, {}, true);
                transp_num_id = CalcTransportNumbers(integral_avgs);
                if (g_oaMolecules.GetSize() > 2)
                {
                    mprintf("\n      Transference numbers based on the weighted regression:\n");
                    for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                    {
                        mprintf("        %s: %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,transp_num.first[i]);
                    }
                    mprintf("\n      Ideal transference numbers based on the weighted regression (only self diffusion):\n");
                }
                else
                {
                    mprintf("\n      Ideal transference numbers based on the weighted regression (only self diffusion):\n");
                    mprintf("        (since this is a binary system, true transference numbers don't have any meaning)\n");
                }
                for (int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                {
                    mprintf("        %s: %.6f\n",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,transp_num_id.first[i]);
                }
            }
        }
    }
}

std::pair<int, int> CConductObservation::FindLinearRegion(const std::vector<double>& y, double tol, int nslice, double incr)
{
    struct Candidate {
        int t1, t2;
        int npoints;
        double slope_dev;
    };
    int start = 0, end = 0;
    int step;
    bool lin_reg = true;
    int ndata = (int)y.size();
    double slice_size = ndata / nslice;
    double yval_start, yval_end, xval_end, xval_start, slope;
    double r2=1.0;
    std::vector<Candidate> candidates;
    // bool m_bMsdiffLogFit = true; // if true, perform linear regression on log-log values instead of just calculating slope from start and end points. This is more robust to noise but also more computationally expensive. Also check R^2 of the fit and reject linear regions with low R^2 to avoid selecting noisy regions.
    for(int i=0; i<2*nslice-1; i++)
    {
        lin_reg = true;
        start = ndata - int(((i + 2) / 2.0) * slice_size) + 1;
        end = ndata - int((i / 2.0) * slice_size) - 1;
        while(lin_reg && start < end)
        {
            yval_start = y[start] - y[start-1];
            xval_start = 1.0;
            yval_end = y[end] - y[start-1];
            xval_end = end - start + 1;
            if (yval_start <= 0.0 || yval_end <= 0.0)
                break; // if either start or end point is non-positive, reject linear region to avoid issues with log of non-positive number
            if (!m_bMsdiffLogFit)
                slope = (log(yval_end) - log(yval_start)) / (log(xval_end) - log(xval_start));
            else
            {
                // calculate log y and log x for the region and perform linear regression to get slope
                // shift x and y values to beginning (i.e. yval_start = y[start] - y[start-1], xval_start = 1.0) to avoid numerical issues with log(0) and log of large numbers
                // for regression use LinReg function with weighted = false and var = vector of ones (i.e. ordinary least squares)
                std::vector<double> log_y(end - start + 1), log_x(end - start + 1);
                for(int j = start; j <= end; j++)
                {
                    // check if y[j] - y[start-1] is positive to avoid log of non-positive number. If not, reject linear region
                    if (y[j] - y[start-1] <= 0.0)
                    {
                        lin_reg = false;
                        break;
                    }
                    log_y[j - start] = log(y[j] - y[start-1]);
                    log_x[j - start] = log(j - start + 1);
                }
                if(!lin_reg)
                    break;
                std::tuple<double, double, double, double, double> reg_result = LinReg(log_x, log_y, false);
                slope = std::get<0>(reg_result);
                r2 = std::get<4>(reg_result);
                /*
                x1 = y1 = xy = x2 = yaxb2 = var_y_n = 0.0;
                // calc sums over x, y, xy and x^2
                for(int i=0; i<log_x.size(); i++)
                {
                    x1 += log_x[i];
                    y1 += log_y[i];
                    xy += log_x[i] * log_y[i];
                    x2 += log_x[i] * log_x[i];
                }
                // convert sums to SI units
                slope = (log_x.size()*xy - x1*y1)/(log_x.size()*x2 - x1*x1);
                b = (x2*y1 - x1*xy)/(log_x.size()*x2 - x1*x1);
                // calculate uncertainty (of fit) and R2 value
                y_avg = y1 / (double)log_x.size();
                for(int i=0; i<log_x.size(); i++)
                {
                    yaxb2 += (log_y[i] - (slope * log_x[i]) - b)*(log_y[i] - (slope * log_x[i]) - b);
                    var_y_n += (log_y[i] - y_avg)*(log_y[i] - y_avg);
                }
                //s = sqrt(yaxb2/(double)(n-2));
                //dm = s*sqrt((double)n/((double)n*x2 - x1*x1));
                //db = s*sqrt(x2/(n*x2 - x1*x1));
                r2 = 1 - yaxb2/var_y_n;
                //std::tuple<double, double, double, double, double> reg_result = LinReg(log_y, 0, log_y.size() - 1, false);
                //slope = std::get<0>(reg_result);
                //r2 = std::get<4>(reg_result);
                mprintf("      Candidate linear region: start = %d, end = %d, slope = %.4f, R^2 = %.4f\n", start, end, slope, r2);
                */
            }
            if (fabs(slope - 1.0) > tol || (m_bMsdiffLogFit && r2 < 0.95)) // if slope deviates too much from 1 or if log-log fit is used and R^2 is too low, reject linear region
                lin_reg = false;
            else
            {
                candidates.push_back({start, end, end - start + 1, fabs(slope - 1.0)});
                step = (int)(ndata * incr);
                if (step == 0)
                    step = 1;
                start -= step;
                if (start < 1)
                    lin_reg = false;
            }
        }
    }
    if (candidates.empty())
        return {-1, -1};
    
    // sort candidates by number of points (descending) and slope deviation (ascending)
    std::sort(candidates.begin(), candidates.end(), [](const Candidate& a, const Candidate& b) {
        if (a.npoints != b.npoints)
            return a.npoints > b.npoints; // more points is better
        return a.slope_dev < b.slope_dev; // smaller slope deviation is better
    });
    return {candidates[0].t1, candidates[0].t2};
}

std::tuple<double, double, double, double> CConductObservation::ErrWeightLinReg(const std::vector<double>& y, const std::vector<double>& var, bool find_linear_region)
{
    // standard weighted regression
    double m, b, dm, db, xe2, ye2, xye2, x2e2, e2, d;
    double x_conversion = 1E-3 * g_fTimestepLength * m_iStride * g_iStride;
    double epsilon = 1E-12; // to avoid division by zero
    int start, end;
    xe2 = ye2 = xye2 = x2e2 = e2 = 0.0;
    if (find_linear_region)
    {
        std::pair<int, int> range = FindLinearRegion(y);
        start = range.first;
        end = range.second + 1;
    }
    else
    {
        start = (int)((y.size() % 2 == 0) ? y.size()/2 : y.size()/2+1);
        end = (int)y.size();
    }
    // mprintf(WHITE,"\n      Performing weighted linear regression on interval %.2f - %.2f ps...\n\n",start*x_conversion,(end-1)*x_conversion);
    // calculate sums over x/var, y/var, xy/var, x^2/var and 1/var
    for(int i=start; i<end; i++)
    {
        xe2 += i / (var[i] + epsilon);
        ye2 += y[i] / (var[i] + epsilon);
        xye2 += i * y[i] / (var[i] + epsilon);
        x2e2 += i * i / (var[i] + epsilon);
        e2 += 1.0 / (var[i] + epsilon);
    }
    // convert sums to SI units
    xe2 *= x_conversion;
    xye2 *= x_conversion;
    x2e2 *= x_conversion * x_conversion;
    // calculate slope and intercept with errors
    m = (xe2 * ye2 - xye2 * e2)/(xe2*xe2 - x2e2*e2);
    b = (xye2 - m*x2e2)/xe2;
    d = (x2e2 * e2 - xe2 * xe2);
    dm = sqrt(e2/d);
    db = sqrt(x2e2/d);

    return std::make_tuple(m, dm, b, db);
}

std::tuple<double, double, double, double, double> CConductObservation::LinReg(const std::vector<double>& y, int start, int end, bool weighted, const std::vector<double>& var)
{    
    double x_conversion = 1E-3 * g_fTimestepLength * m_iStride * g_iStride;
    /*
    if (find_linear_region)
    {
        std::pair<int, int> range = FindLinearRegion(y);
        start = range.first;
        end = range.second + 1;
    }
    else
    {
        start = (y.size() % 2 == 0) ? y.size()/2 : y.size()/2+1;
        end = y.size();
    }
    */
    // check that start and end are valid
    if (start < 0 || end >= (int)y.size() || start >= end)
        return std::make_tuple(0.0, 0.0, 0.0, 0.0, 0.0);

    std::vector<double> x(end - start + 1);
    std::vector<double> y_region(end - start + 1);
    std::vector<double> var_region;
    if (weighted)
        var_region.resize(end - start + 1);
    for(int i=start; i<=end; i++)
    {
        x[i - start] = (double)i * x_conversion;
        y_region[i - start] = y[i];
        if (weighted)
            var_region[i - start] = var[i];
    }
    return LinReg(x, y_region, weighted, var_region);
}

std::tuple<double, double, double, double, double> CConductObservation::LinReg(const std::vector<double>& x, const std::vector<double>& y, bool weighted, const std::vector<double>& var)
{
    double x1 = 0.0, y1 = 0.0, x2 = 0.0, xy = 0.0, yaxb2 = 0.0, var_y_n = 0.0, s;
    double xe2 = 0.0, ye2 = 0.0, xye2 = 0.0, x2e2 = 0.0, e2 = 0.0;
    double epsilon = 1E-12; // to avoid division by zero
    double m, b, d, dm=0.0, db=0.0, r2, y_avg;
    int n = (int)x.size();

    if (x.size() != y.size() || x.empty())
        return std::make_tuple(0.0, 0.0, 0.0, 0.0, 0.0);

    if (!weighted)
    {
        // calc sums over x, y, xy and x^2
        for(int i=0; i<n; i++)
        {
            x1 += x[i];
            y1 += y[i];
            xy += x[i] * y[i];
            x2 += x[i] * x[i];
        }
        m = (n*xy - x1*y1)/(n*x2 - x1*x1);
        b = (x2*y1 - x1*xy)/(n*x2 - x1*x1);
        // calculate uncertainty (of fit) and R2 value
        y_avg = y1 / (double)n;
        for(int i=0; i<n; i++)
        {
            yaxb2 += (y[i] - (m * x[i]) - b)*(y[i] - (m * x[i]) - b);
            var_y_n += (y[i] - y_avg)*(y[i] - y_avg);
        }
        if (n > 2)
        {
            s = sqrt(yaxb2/(double)(n-2));
            dm = s*sqrt((double)n/((double)n*x2 - x1*x1));
            db = s*sqrt(x2/(n*x2 - x1*x1));
        }
        r2 = (var_y_n == 0.0) ? 1.0 : 1 - yaxb2/var_y_n; // if var_y_n is zero (i.e. all y values are the same), set R^2 to 1.0 to avoid division by zero
    }
    else
    {
        for(int i=0; i<n; i++)
        {
            y1 += y[i];
            xe2 += x[i] / (var[i] + epsilon);
            ye2 += y[i] / (var[i] + epsilon);
            xye2 += x[i] * y[i] / (var[i] + epsilon);
            x2e2 += x[i] * x[i] / (var[i] + epsilon);
            e2 += 1.0 / (var[i] + epsilon);
        }
        // calculate slope and intercept with errors
        m = (xe2 * ye2 - xye2 * e2)/(xe2*xe2 - x2e2*e2);
        b = (xye2 - m*x2e2)/xe2;
        d = (x2e2 * e2 - xe2 * xe2);
        dm = sqrt(e2/d);
        db = sqrt(x2e2/d);
        // calculate R2 value
        //y_avg = y1 / (double)n;
        y_avg = ye2 / e2; // weighted average of y values
        for(int i=0; i<n; i++)
        {
            double e_i = 1.0 / (var[i] + epsilon);
            double y_pred = m * x[i] + b;
            yaxb2 += (y[i] - y_pred)*(y[i] - y_pred) * e_i;
            var_y_n += (y[i] - y_avg)*(y[i] - y_avg) * e_i;
        }
        r2 = 1 - yaxb2/var_y_n;
    }
    return std::make_tuple(m, dm, b, db, r2);
}

std::pair<std::vector<double>, std::vector<double>> CConductObservation::CalcTransportNumbers(const std::vector<double>& term_values, const std::vector<double>& term_errors, bool real_transference)
{
    // If real = false, use only self terms. If real = true, use all terms (self, cross and mixed) for transference number calculation.
    // number of components = m_iaActiveMolecules.size()
    std::vector<double> enumerators(m_iaActiveMolecules.size(), 0.0);
    std::vector<double> enumerator_errors(m_iaActiveMolecules.size(), 0.0);
    std::vector<double> transp_num(m_iaActiveMolecules.size(), 0.0);
    std::vector<double> variances(m_iaActiveMolecules.size(), 0.0);
    std::vector<double> transp_err(m_iaActiveMolecules.size(), 0.0);
    double total = 0.0, total2 = 0.0, jac = 0.0;
    bool has_errors = true;
    if (term_errors.empty())
        has_errors = false;
    // compute enumerators for transference numbers and their errors (if available)
    if (!real_transference)
    {
        for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
        {
            enumerators[i] = term_values[i];
            total += enumerators[i];
            if (has_errors)
                enumerator_errors[i] = term_errors[i];
        }
        total2 = total * total;
        // get transference numbers
        for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
            transp_num[i] = enumerators[i] / total;
        if (has_errors)
        {
            // calculate variances using jacobian matrix
            for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
            {
                for(int j=0; j<(int)m_iaActiveMolecules.size(); j++)
                {
                    if (i == j)
                    {
                        jac = (total - enumerators[i]) / total2;
                        variances[i] += jac * jac * enumerator_errors[j] * enumerator_errors[j];
                    }
                    else
                    {
                        jac = -enumerators[i] / total2;
                        variances[i] += jac * jac * enumerator_errors[j] * enumerator_errors[j];
                    }
                }
            }
            // calculate transference number errors
            for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                transp_err[i] = sqrt(std::max(variances[i], 0.0));
        }
        else
        {
            for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
                transp_err[i] = 0.0;
        }

        return std::make_pair(transp_num, transp_err);
    }

    // real transference numbers
    int pair_index;
    // compute single-species contributions (self + cross terms)
    for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
    {
        enumerators[i] = term_values[i] + term_values[m_iaActiveMolecules.size()+i]; // self + cross term for each species
        total += enumerators[i];
        if (has_errors)
            enumerator_errors[i] = sqrt(term_errors[i] * term_errors[i] + term_errors[m_iaActiveMolecules.size()+i] * term_errors[m_iaActiveMolecules.size()+i]); // error propagation for sum of self and cross terms
    }
    // compute pair contributions (mixed terms)
    for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
        for(int j=i+1; j<(int)m_iaActiveMolecules.size(); j++)
        {
            pair_index = (i+2)*(int)m_iaActiveMolecules.size() + j - (i+1)*(i+2)/2;
            enumerators[i] += term_values[pair_index] / 2.0; // split mixed term equally between the two species
            enumerators[j] += term_values[pair_index] / 2.0;
            total += term_values[pair_index];
        }
    total2 = total * total;
    // get transference numbers
    for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
        transp_num[i] = enumerators[i] / total;
    if (has_errors)
    {
        // calculate variances using jacobian matrix
        for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
        {
            for(int j=0; j<(int)m_iaActiveMolecules.size(); j++)
            {
                if (i == j)
                {
                    jac = (total - enumerators[i]) / total2;
                    variances[i] += jac * jac * enumerator_errors[j] * enumerator_errors[j];
                }
                else
                {
                    jac = -enumerators[i] / total2;
                    variances[i] += jac * jac * enumerator_errors[j] * enumerator_errors[j];
                    jac = (0.5 * total - enumerators[i]) / total2; // contribution of mixed term to variance of species i
                    if (i < j)
                        pair_index = (i+2)*(int)m_iaActiveMolecules.size() + j - (i+1)*(i+2)/2;
                    else
                        pair_index = (j+2)*(int)m_iaActiveMolecules.size() + i - (j+1)*(j+2)/2;
                    variances[i] += jac * jac * term_errors[pair_index] * term_errors[pair_index];
                }
            }
        }
        // calculate transference number errors
        for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
            transp_err[i] = sqrt(std::max(variances[i], 0.0));
    }
    else
    {
        for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
            transp_err[i] = 0.0;
    }
    return std::make_pair(transp_num, transp_err);
}

std::pair<double,double> CConductObservation::LinRegOld(const std::vector<double>& y)
{
    double x_avg = 0.0;
    double y_avg = 0.0;
    double sxy = 0.0;
    double sxx = 0.0;
    double m, n, r2;
    double enumer = 0.0;
    double denom = 0.0;
    int tmp = (int)((y.size() % 2 == 0) ? y.size()/2 : y.size()/2+1);
    double x_conversion = 1E-3 * g_fTimestepLength * m_iStride * g_iStride;
    // calculate averages
    for(size_t i=tmp; i<y.size(); i++)
    {
        x_avg += i;
        y_avg += y[i];
        sxy += i * y[i];
        sxx += i * i;
    }
    x_avg = x_avg * x_conversion * 2 / y.size();
    y_avg = y_avg * 2 / y.size();
    // calculate sxy, sxx as well as slope and intercept
    sxy = (sxy * x_conversion) - (y.size() * x_avg * y_avg / 2);
    sxx = (sxx * x_conversion * x_conversion) - (y.size() * x_avg * x_avg / 2);
    m = sxy / sxx;
    n = y_avg - m * x_avg;
    // calculate R^2
    for(size_t i=tmp; i<y.size(); i++)
    {
        enumer += (y[i] - (m * i * x_conversion + n)) * (y[i] - (m * i * x_conversion + n));
        denom += (y[i] - y_avg) * (y[i] - y_avg);
    }
    r2 = 1 - enumer / denom;
    return std::make_pair(m, r2);
}

std::pair<double,double> CConductObservation::MinMaxEntry(const std::vector<double>& x)
{
    double min = x[0];
    double max = x[0];
    for(size_t i=0; i<x.size(); i++)
    {
        if(x[i] < min)
            min = x[i];
        if(x[i] > max)
            max = x[i];
    }
    return std::make_pair(min,max);
}

std::pair<double,double> CConductObservation::MinMaxEntry(const std::vector<std::vector<double> >& x, const std::vector<double>& y)
{
    double min = y[0];
    double max = y[0];
    // first find min and max in x
    for(size_t i=0; i<x.size(); i++)
        for(size_t j=0; j<x[i].size(); j++)
        {
            if(x[i][j] < min)
                min = x[i][j];
            if(x[i][j] > max)
                max = x[i][j];
        }
    // then find min and max in y
    for(size_t i=0; i<y.size(); i++)
    {
        if(y[i] < min)
            min = y[i];
        if(y[i] > max)
            max = y[i];
    }
    return std::make_pair(min,max);
}

void CConductObservation::DumpPlots(const std::vector<double>& total_cond, const std::vector<double>& var)
{
    // dump agr files for grace
    CxString buf;
    CGrace *g;
    std::pair<double,double> minmax;
    double x_conversion = 1E-3 * g_fTimestepLength * m_iStride * g_iStride;

    try { g = new CGrace(); } catch(...) { g = NULL; }
    if ( g == NULL ) NewException((double)sizeof(CGrace),__FILE__,__LINE__,__PRETTY_FUNCTION__);

    g->SetLabelX("Depth [ps]");
    g->SetLabelY("Collective MSD [ps*S/m]");
    g->CurrentGraph()->m_bLegend = true;    

    // set x and y range of grace plot
    minmax = MinMaxEntry(total_cond);
    g->SetRangeX(0.0,m_iDepth*g_iStride*g_fTimestepLength*1E-3);
    g->SetRangeY(minmax.first*1.1,minmax.second*1.1);
    g->MakeTicks();

    // add total conductivity to grace plot
    g->AddDataset();
    //g->SetSetLineColorLong(cond.size(),GraceColor(cond.size(),0.0));
    g->SetDatasetName(0,"Total");
    //g->AddXYTupel(0.0,0.0);
    for(int i=0; i<(int)total_cond.size(); i++)
        g->AddXYTupel(i*x_conversion,total_cond[i]);

    // name the file
    buf.sprintf("conduct%s.agr",m_sName.c_str());
    g->WriteAgr(buf,false);
    *strrchr(buf.GetWritePointer(),'.') = 0;
    buf.strcat(".csv");
    WriteCSV(buf,total_cond,var);
}

void CConductObservation::DumpPlots(const std::vector<std::vector<double> >& cond, const std::vector<double>& total_cond, const std::vector<std::vector<double> >& var_mat, const std::vector<double>& var)
{
    // dump agr files for grace
    CxString buf;
    CGrace *g;
    std::pair<double,double> minmax;
    int index;
    double x_conversion = 1E-3 * g_fTimestepLength * m_iStride * g_iStride;

    try { g = new CGrace(); } catch(...) { g = NULL; }
    if ( g == NULL ) NewException((double)sizeof(CGrace),__FILE__,__LINE__,__PRETTY_FUNCTION__);

    g->SetLabelX("Depth [ps]");
    g->SetLabelY("Collective MSD [ps*S/m]");
    g->CurrentGraph()->m_bLegend = true;    

    // set x and y range of grace plot
    minmax = MinMaxEntry(cond,total_cond);
    g->SetRangeX(0.0,m_iDepth*g_iStride*g_fTimestepLength*1E-3);
    g->SetRangeY(minmax.first*1.1,minmax.second*1.1);
    g->MakeTicks();

    // print values to grace plot

    //g->SetSetLineColorLong(i,GraceColor(i,0.0));
    for(int i=0; i<(int)m_iaActiveMolecules.size(); i++)
    {
        g->AddDataset();
        buf.sprintf("%s",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName);
        g->SetDatasetName(i,buf);
        for(int j=0; j<(int)cond[0].size(); j++)
            g->AddXYTupel(j*x_conversion,cond[i][j]);
    }
    for(int i=0; i<(int)m_iaActiveMolecules.size();i++)
    {
        index = i + (int)m_iaActiveMolecules.size();
        g->AddDataset();
        buf.sprintf("%s/%s",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName,((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName);
        g->SetDatasetName(index,buf);
        for(int j=0; j<(int)cond[0].size(); j++)
            g->AddXYTupel(j*x_conversion,cond[index][j]);
    }
    for(int j=0; j < (int)m_iaActiveMolecules.size(); j++)
        for(int k=j+1; k < (int)m_iaActiveMolecules.size(); k++)
        {
            index = (j+2)*(int)m_iaActiveMolecules.size() + k - (j+1)*(j+2)/2;
            g->AddDataset();
            buf.sprintf("%s/%s",((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName,((CMolecule*)g_oaMolecules[m_iaActiveMolecules[k]])->m_sName);
            g->SetDatasetName(index,buf);
            for(int l=0; l<(int)cond[0].size(); l++)
                g->AddXYTupel(l*x_conversion,cond[index][l]);
        }

    // add total conductivity to grace plot
    g->AddDataset();
    //g->SetSetLineColorLong(cond.size(),GraceColor(cond.size(),0.0));
    g->SetDatasetName((int)cond.size(),"Total");
    for(int i=0; i<(int)total_cond.size(); i++)
        g->AddXYTupel(i*x_conversion,total_cond[i]);

    // name the file
    buf.sprintf("conduct%s.agr",m_sName.c_str());
    g->WriteAgr(buf,false);
    *strrchr(buf.GetWritePointer(),'.') = 0;
    buf.strcat(".csv");
    WriteCSV(buf,cond,total_cond,var_mat,var);
    /* -> does not allow for comment line in csv file 
    g->WriteCSV(buf);
    */
}

void CConductObservation::WriteCSV(const char *name, const std::vector<double>& total_cond, const std::vector<double>& var)
{
    FILE *a;
    std::string header;
    double x_conversion = 1E-3 * g_fTimestepLength * m_iStride * g_iStride;

    mprintf("\n      Writing results to \"%s\"...\n",name);

    a = OpenFileWrite(name,true);

    header = "# tau [ps];  total [ps*S/m]";
    if(m_bUncertainty)
        header += ";  variance [ps^2*S^2/m^2]";

    mfprintf(a,"%s\n",header.c_str()); 
    // print line with tau = 0
    /*mfprintf(a,"%f;  %f",0.0,0.0);
    if(m_bUncertainty)
        mfprintf(a,";  %f",0.0);
    mfprintf(a,"\n");*/
    // print the rest of the data
    for(int i=0; i<(int)total_cond.size(); i++)
    {
        mfprintf(a,"%f;  %f",i*x_conversion,total_cond[i]);
        if(m_bUncertainty)
            mfprintf(a,";  %f",var[i]);
        mfprintf(a,"\n");    
    }
    fclose(a);
}

void CConductObservation::WriteCSV(const char *name, const std::vector<std::vector<double> >& cond, const std::vector<double>& total_cond, const std::vector<std::vector<double> >& var_mat, const std::vector<double>& var)
{
    FILE *a;
    std::string header;
    double x_conversion = 1E-3 * g_fTimestepLength * m_iStride * g_iStride;

    mprintf("\n      Writing results to \"%s\"...\n",name);

    a = OpenFileWrite(name,true);

    header = "# tau [ps];";
    for(size_t i=0; i<m_iaaAtomSet.size(); i++)
    {
        header += "  " + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName) + " [ps*S/m];";
        if(m_bUncertainty)
            header += "  " + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName) + "-var [ps^2*S^2/m^2];";
    }
    for(size_t i=0; i<m_iaaAtomSet.size(); i++)
    {
        header += "  " + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName) + "/" + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName) + " [ps*S/m];";
        if (m_bUncertainty)
            header += "  " + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName) + "/" + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[i]])->m_sName) + "-var [ps^2*S^2/m^2];";
    }
    for(int j=0; j < (int)m_iaActiveMolecules.size(); j++)
        for(int k=j+1; k < (int)m_iaActiveMolecules.size(); k++)
        {
            header += "  " + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName) + "/" + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[k]])->m_sName) + " [ps*S/m];";
            if(m_bUncertainty)
                header += "  " + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[j]])->m_sName) + "/" + std::string(((CMolecule*)g_oaMolecules[m_iaActiveMolecules[k]])->m_sName) + "-var [ps^2*S^2/m^2];";
        }
    header += "  total [ps*S/m]";
    if(m_bUncertainty)
        header += ";  total-var [ps^2*S^2/m^2]";
    mfprintf(a,"%s\n",header.c_str());
    // print line with tau = 0
    /*mfprintf(a,"%f;",0.0);
    for(size_t i=0; i<cond.size(); i++)
    {
        mfprintf(a,"  %f;",0.0);
        if(m_bUncertainty)
            mfprintf(a,"  %f;",0.0);
    }
    mfprintf(a,"  %f",0.0);
    if(m_bUncertainty)
        mfprintf(a,";  %f\n",0.0);
    else
        mfprintf(a,"\n");*/
    // print the rest of the data
    for(int i=0; i<(int)cond[0].size(); i++)
    {
        mfprintf(a,"%f;",i*x_conversion);
        for(size_t j=0; j<cond.size(); j++)
        {
            mfprintf(a,"  %f;",cond[j][i]);
            if(m_bUncertainty)
                mfprintf(a,"  %f;",var_mat[j][i]);
        }
        mfprintf(a,"  %f",total_cond[i]);
        if(m_bUncertainty)
            mfprintf(a,";  %f\n",var[i]);
        else
            mfprintf(a,"\n");
    }

    fclose(a);
}



#endif


