"""

    heattransfer(myARGS)

Extract command-line arguments for computing the radiative heat transfer among a collection
of molecules, then compute the radiative heat transfer appropriately.

!!! note

Any frequencies will be converted to their absolute values. Frequencies are not allowed to
be exactly zero.

!!! note

This computes the integrand ``\\Phi^{(m)}_{n}`` at specified real frequencies, without doing
any integration. This includes neither numerical prefactors outside of the trace, nor the
Planck function.

!!! tip

This function can be embarrassingly parallelized over frequency and wavevector. However, the
integrand at each frequency and wavevector is calculated for every transformation, as this
maximizes the efficiency of the program by virtue of reusing quantities independent of
geometric transformations.

"""
function heattransfer(myARGS)

    mollistfilename = readRequiredFilenameARG(myARGS, "mollistfilename",
                                              "AARMBEMC_heat.jl");
    outfilename = readRequiredFilenameARG(myARGS, "outfilename", "AARMBEMC_heat.jl");

    translistfilename = readOptionalFilenameARG(myARGS, "translistfilename");
    periodicfilename = readOptionalFilenameARG(myARGS, "periodicfilename");

    Genvstr = readGenvstr(myARGS);

    nonretarded, outfilename = readOptionalBoolcondARG(myARGS, "nonretarded", outfilename);

    FT = readOptionalFloatType(myARGS);
    println("FT is ", FT);

    myAllMolData = readMolSystem(mollistfilename, false, FT);
    myTransData = readTransData(myAllMolData, translistfilename);
    myPeriodicData = readPeriodicData(FT, periodicfilename);

    myDeltaKISystemData = readDeltaKISystemData(mollistfilename, myAllMolData);

    freqlist = Array{FT, 1}(undef, 0);
    readfreqlist!(freqlist, myARGS, "AARMBEMC_heat.jl");

    klist = readklist(myARGS, "AARMBEMC_heat.jl", myPeriodicData.numdims, FT);

    freqklist = hcat(vec(repeat(transpose(freqlist), size(klist, 1), 1)),
                     repeat(klist, length(freqlist), 1));

    heattransfer_general(outfilename, myAllMolData, myTransData, myPeriodicData,
                         myDeltaKISystemData, Genvstr, nonretarded, freqklist)

end

"""

    heattransfer_general(outfilename, myAllMolData, myTransData, myPeriodicData,
                         myDeltaKISystemData, Genvstr, nonretarded, freqklist)

Compute the actual heat transfer integrands (for conduction, radiation, and both together)
and print each to appropriate files based on the name specified by `outfilename`.

"""
function heattransfer_general(outfilename::AbstractString,
                              myAllMolData::MolSystem{<:OneMol{FT}},
                              myTransData::TransData{FT},
                              myPeriodicData::PeriodicData{FT},
                              myDeltaKISystemData::DeltaKISystemData{FT},
                              Genvstr::AbstractString, nonretarded::Bool,
                              freqklist::Array{FT, 2}) where FT<:AbstractFloat

    GFSCAGG! = GFVACGG_sca!;
    if (Genvstr == "PEC")
        GFSCAGG! = GFPECGG!;
    end

    totalGenvmat = SharedArray{Complex{FT}}(3*myAllMolData.cumnumatomslist[end],
                                            3*myAllMolData.cumnumatomslist[end]);
    totalGenvmatwithdiag = SharedArray{Complex{FT}}(3*myAllMolData.cumnumatomslist[end],
                                                    3*myAllMolData.cumnumatomslist[end]);
    DeltaKImat = SharedArray{Complex{FT}}(3*myAllMolData.cumnumatomslist[end],
                                          3*myAllMolData.cumnumatomslist[end]);
    totalTeemat = SharedArray{Complex{FT}}(3*myAllMolData.cumnumatomslist[end],
                                           3*myAllMolData.cumnumatomslist[end]);
    totalZIIinvmat = SharedArray{Complex{FT}}(3*myAllMolData.cumnumatomslist[end],
                                              3*myAllMolData.cumnumatomslist[end]);
    tempmat = SharedArray{Complex{FT}}(3*myAllMolData.cumnumatomslist[end],
                                              3*myAllMolData.cumnumatomslist[end]);
    Phimn = zeros(FT, myAllMolData.nummols, myAllMolData.nummols);

    for ll=1:size(freqklist, 1)

        currfreq = abs(freqklist[ll, 1]);

        currfreqG = currfreq;
        if (nonretarded)
            currfreqG = zero(currfreqG);
        end
        currk = vec(freqklist[ll, 2:4]);
        fill!(totalGenvmat, zero(eltype(totalGenvmat)));
        fill!(totalGenvmatwithdiag, zero(eltype(totalGenvmatwithdiag)));
        fill!(totalTeemat, zero(eltype(totalTeemat)));
        fill!(DeltaKImat, zero(eltype(DeltaKImat)));
        fill!(totalZIIinvmat, zero(eltype(totalZIIinvmat)));
        fill!(tempmat, zero(eltype(tempmat)));

        for nn=1:myAllMolData.nummols

            currumolind = myAllMolData.uniquemolinds[nn];

            if (currumolind > -1)

                constructMolAlpha!(myAllMolData.uniqueMolDataArray[currumolind],
                                   myPeriodicData, currfreq, currk);

                assemble1MolGvacinf!(myAllMolData.uniqueMolDataArray[currumolind],
                                     myPeriodicData, currfreqG, currk);

                assemble1MolGvacinfdiag!(myAllMolData.uniqueMolDataArray[currumolind],
                                         currfreqG);

            end

        end

        for tt=1:myTransData.numTrans

            fill!(totalTeemat, zero(eltype(totalTeemat)));
            fill!(DeltaKImat, zero(eltype(DeltaKImat)));
            fill!(totalZIIinvmat, zero(eltype(totalZIIinvmat)));
            fill!(tempmat, zero(eltype(tempmat)));

            transformAtomPos!(myTransData, myAllMolData, tt);

            for nn2=1:myAllMolData.nummols

                startidx2 = 1;
                if (nn2 > 1)
                    startidx2 = 3*myAllMolData.cumnumatomslist[nn2-1] + 1;
                end
                currmolind2 = myAllMolData.uniquemolinds[nn2];
                if (currmolind2 < 0)
                    currmolind2 = myAllMolData.dupemolinds[nn2];
                end

                if (myTransData.changedFromBefore[nn2, tt])
                    assembleGenvDiagBlock!(totalGenvmat, totalGenvmatwithdiag, myAllMolData,
                                           myTransData, nn2, currmolind2, startidx2,
                                           tt, myPeriodicData, GFSCAGG!, currfreqG,
                                           currk);
                end

            end

            for nn2=1:myAllMolData.nummols

                startidx2 = 1;
                if (nn2 > 1)
                    startidx2 = 3*myAllMolData.cumnumatomslist[nn2-1] + 1;
                end

                for nn1=(nn2+1):myAllMolData.nummols

                    startidx1 = 3*myAllMolData.cumnumatomslist[nn1-1] + 1;

                    if (myTransData.changedFromBefore[nn2, tt] ||
                        myTransData.changedFromBefore[nn1, tt])

                        assembleGenvOffDiagBlock!(totalGenvmat, totalGenvmatwithdiag,
                                                  myAllMolData, myTransData, nn1, nn2,
                                                  startidx1, startidx2, myPeriodicData,
                                                  GFSCAGG!, currfreqG, currk);
                    end
                end

            end

            if (myPeriodicData.numdims > 0)

                for nn2=1:myAllMolData.nummols

                    startidx2 = 1;
                    if (nn2 > 1)
                        startidx2 = 3*myAllMolData.cumnumatomslist[nn2-1] + 1;
                    end

                    for nn1=(nn2+1):myAllMolData.nummols

                        startidx1 = 3*myAllMolData.cumnumatomslist[nn1-1] + 1;

                        if (myTransData.changedFromBefore[nn2, tt] ||
                            myTransData.changedFromBefore[nn1, tt])

                            assembleGenvOffDiagBlock!(totalGenvmat, totalGenvmatwithdiag,
                                                      myAllMolData, myTransData, nn2, nn1,
                                                      startidx2, startidx1, myPeriodicData,
                                                      GFSCAGG!, currfreqG, currk);
                        end
                    end

                end
            end

            for cc=1:length(myDeltaKISystemData.DeltaKImolblockdataarray)

                mol1 = myDeltaKISystemData.DeltaKImolblockdataarray[cc].mol1;
                mol2 = myDeltaKISystemData.DeltaKImolblockdataarray[cc].mol2;

                if (myTransData.changedFromBefore[mol2, tt] ||
                    myTransData.changedFromBefore[mol1, tt])

                    assembleDeltaKImolblock!(myDeltaKISystemData.DeltaKImolblockdataarray[cc],
                                             myAllMolData, myTransData, myPeriodicData);

                end

            end

            assembleDeltaKIdiagblocks!(myDeltaKISystemData, myAllMolData);

            assembleDeltaKI!(DeltaKImat, myDeltaKISystemData, myAllMolData,
                             myPeriodicData, currk);

            assembleZIIinvTeeradiation!(totalZIIinvmat, totalTeemat, totalGenvmat,
                                        myAllMolData, myTransData, tt);

            assemblePhimnradiation!(Phimn, myAllMolData, myPeriodicData, totalTeemat,
                                    totalGenvmatwithdiag, totalZIIinvmat, tempmat,
                                    currfreq);

            curroutfilename = string("radiation_", outfilename);

            printPhimn(Phimn, curroutfilename, myAllMolData, myTransData, myPeriodicData,
                       tt, currfreq, currk);

            assembleZIIinvTeeconduction!(totalZIIinvmat, totalTeemat, DeltaKImat,
                                         myAllMolData, myTransData, tt);

            assemblePhimnconduction!(Phimn, myAllMolData, myPeriodicData, totalTeemat,
                                     DeltaKImat, totalZIIinvmat, tempmat, currfreq);

            curroutfilename = string("conduction_", outfilename);

            printPhimn(Phimn, curroutfilename, myAllMolData, myTransData, myPeriodicData,
                       tt, currfreq, currk);

            assembleZIIinvTeeboth!(totalZIIinvmat, totalTeemat, totalGenvmat, DeltaKImat,
                                   myAllMolData, myTransData, tt);

            assemblePhimnboth!(Phimn, myAllMolData, myPeriodicData, totalTeemat,
                               totalGenvmat, DeltaKImat, totalZIIinvmat, tempmat, currfreq);

            curroutfilename = string("both_", outfilename);

            printPhimn(Phimn, curroutfilename, myAllMolData, myTransData, myPeriodicData,
                       tt, currfreq, currk);

            untransformAtomPos!(myTransData, myAllMolData, tt);

        end

    end

end

"""

    printPhimn(Phimn::Array{FT, 2}, outfilename::AbstractString,
               myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
               myTransData::TransData{FT},
               myPeriodicData::PeriodicData{FT},
               tt::Integer, currfreq::Union{FT, Complex{FT}},
               currk::Array{FT, 1}=zeros(FT, 3)) where FT<:AbstractFloat

Print the matrix `Phimn` to the file with name `outfilename`. The elements `Phimn[m, n]`
represent the energy transfer from molecule `m` to molecule `n`, corresponding to the
mathematical quantity ``\\Phi^{(m)}_{n}``. If there is no spatial periodicity,
`Phimn` should be symmetric, and a line of output will be as follows:
`myTransData.transLabels[tt]` `currfreq` `Phimn[1, 1]` `Phimn[1, 2]` ... `Phimn[2, 1]` `Phimn[2, 2]` ...
for example:
`TRANS05 1.2340953873461249e+12 -6.3540963812489034e-01 6.3540963812489034e-01 6.3540963812489034e-01 -6.3540963812489034e-01`
If there is spatial periodicity, the transpose of `Phimn` at wavevector ``\vec{k}`` is the
equivalent of computing `Phimn` at ``-\vec{k}``, and a line of output will be as follows:
`myTransData.transLabels[tt]` `currfreq` `currk[1]` `currk[2]` `currk[3]` `Phimn[1, 1]` `Phimn[1, 2]` ... `Phimn[2, 1]` `Phimn[2, 2]` ...
for example:
`TRANS05 1.2340953873461249e+12 5.0000000000000000e+07 3.0000000000000000e+07 0.0000000000000000e+00 -6.3540963812489034e-01 6.3540963812489034e-01 6.3540963812489034e-01 -6.3540963812489034e-01`

"""
function printPhimn(Phimn::Array{FT, 2}, outfilename::AbstractString,
                    myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                    myTransData::TransData{FT},
                    myPeriodicData::PeriodicData{FT},
                    tt::Integer, currfreq::Union{FT, Complex{FT}},
                    currk::Array{FT, 1}=zeros(FT, 3)) where FT<:AbstractFloat

    outfile = open(outfilename, "a+");
    @printf(outfile, "%s %.16e", myTransData.transLabels[tt], abs(currfreq));
    if (myPeriodicData.numdims == 0)

        for nn1=1:myAllMolData.nummols
            for nn2=1:myAllMolData.nummols
                @printf(outfile, " %.16e", Phimn[nn1, nn2]);;
            end
        end
    else
        @printf(outfile, " %.16e %.16e %.16e", currk[1], currk[2], currk[3]);
        for nn1=1:myAllMolData.nummols
            for nn2=1:myAllMolData.nummols
                @printf(outfile, " %.16e", Phimn[nn1, nn2]);
            end
        end
        @printf(outfile, "\n%s %.16e %.16e %.16e %.16e",
                myTransData.transLabels[tt], abs(currfreq), -1*currk[1],
                -1*currk[2], -1*currk[3]);
        for nn1=1:myAllMolData.nummols
            for nn2=1:myAllMolData.nummols
                @printf(outfile, " %.16e", Phimn[nn2, nn1]);
            end
        end
    end
    @printf(outfile, "\n");
    close(outfile);

end

"""

    assembleZIIinvnoDeltaKI!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                             myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                             myTransData::TransData{FT},
                             tt::Integer)

Assemble the matrix `totalZIIinvmat` in-place for the geometrical transformation with index
`tt`, in the absence of internuclear couplings between different molecules.

"""
function assembleZIIinvnoDeltaKI!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                  myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                  myTransData::TransData{FT},
                                  tt::Integer) where FT<:AbstractFloat

    fill!(totalZIIinvmat, zero(eltype(totalZIIinvmat)));
    for nn=1:myAllMolData.nummols

        currumolind = myAllMolData.uniquemolinds[nn];
        if (currumolind < 0)
            currumolind = myAllMolData.dupemolinds[nn];
        end

        rotate3Nmat!(myAllMolData.uniqueMolDataArray[currumolind].KIBloch,
                     myTransData.rotArray[3*nn-2:3*nn, 3*tt-2:3*tt] *
                     myTransData.baselineRot[3*nn-2:3*nn, :]);

        startidx = 1;
        if (nn > 1)
            startidx = 3*myAllMolData.cumnumatomslist[nn-1] + 1;
        end
        endidx = 3*myAllMolData.cumnumatomslist[nn];

        tempview = view(totalZIIinvmat, startidx:endidx, startidx:endidx);
        tempview .= myAllMolData.uniqueMolDataArray[currumolind].KIBloch;
        addtodiag!(tempview,
                   vecNto3N(1 ./ myAllMolData.uniqueMolDataArray[currumolind].Ye0II));
        tempview .= inv(tempview);

        rotate3Nmat!(myAllMolData.uniqueMolDataArray[currumolind].KIBloch,
                     transpose(myTransData.rotArray[3*nn-2:3*nn, 3*tt-2:3*tt] *
                               myTransData.baselineRot[3*nn-2:3*nn, :]));

    end

end

"""

    assembleZIIinvTeeradiation!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                totalTeemat::SharedArray{Complex{FT}, 2},
                                totalGenvmat::SharedArray{Complex{FT}, 2},
                                myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                myTransData::TransData{FT},
                                tt::Integer)

Assemble the matrices `totalZIIinvmat` and `totalTeemat` in-place for the geometrical
transformation with index `tt`, in the absence of internuclear couplings between different
molecules but in the presence of radiative couplings.

"""
function assembleZIIinvTeeradiation!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                     totalTeemat::SharedArray{Complex{FT}, 2},
                                     totalGenvmat::SharedArray{Complex{FT}, 2},
                                     myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                     myTransData::TransData{FT},
                                     tt::Integer) where FT<:AbstractFloat

    fill!(totalZIIinvmat, zero(eltype(totalZIIinvmat)));
    fill!(totalTeemat, zero(eltype(totalTeemat)));

    assembleZIIinvnoDeltaKI!(totalZIIinvmat, myAllMolData, myTransData, tt);

    totalTeemat .= -1 .* totalZIIinvmat;

    for nn=1:myAllMolData.nummols

        currumolind = myAllMolData.uniquemolinds[nn];
        if (currumolind < 0)
            currumolind = myAllMolData.dupemolinds[nn];
        end

        startidx = 1;
        if (nn > 1)
            startidx = 3*myAllMolData.cumnumatomslist[nn-1] + 1;
        end
        endidx = 3*myAllMolData.cumnumatomslist[nn];

        tempview = view(totalTeemat, startidx:endidx, startidx:endidx);
        mulMatDiagleft!(tempview,
                        vecNto3N(myAllMolData.uniqueMolDataArray[currumolind].Ke));
        mulMatDiagright!(tempview,
                        vecNto3N(myAllMolData.uniqueMolDataArray[currumolind].Ke));
        addtodiag!(tempview,
                   vecNto3N(1 ./ myAllMolData.uniqueMolDataArray[currumolind].Ye0ee));

    end

    totalTeemat .= inv(totalTeemat .- totalGenvmat);

end

"""

    assembletempmateI!(tempmat::SharedArray{Complex{FT}, 2},
                       totalTeemat::SharedArray{Complex{FT}, 2},
                       totalZIIinvmat::SharedArray{Complex{FT}, 2},
                       myAllMolData::MolSystem{OneMol_WithPhonons{FT}})

Assemble the matrix `tempmat` in-place, equal to
``(Z_{\\mathrm{ee}} - K_{\\mathrm{e}} Z_{\\mathrm{II}}^{-1} K_{\\mathrm{e}})^{-1} K_{\\mathrm{e}} Z_{\\mathrm{II}}^{-1}``.

"""
function assembletempmateI!(tempmat::SharedArray{Complex{FT}, 2},
                            totalTeemat::SharedArray{Complex{FT}, 2},
                            totalZIIinvmat::SharedArray{Complex{FT}, 2},
                            myAllMolData::MolSystem{OneMol_WithPhonons{FT}}) where FT<:AbstractFloat

    tempmat .= totalZIIinvmat;
    for nn=1:myAllMolData.nummols

        currumolind = myAllMolData.uniquemolinds[nn];
        if (currumolind < 0)
            currumolind = myAllMolData.dupemolinds[nn];
        end

        startidx = 1;
        if (nn > 1)
            startidx = 3*myAllMolData.cumnumatomslist[nn-1] + 1;
        end
        endidx = 3*myAllMolData.cumnumatomslist[nn];

        tempview = view(tempmat, startidx:endidx, 1:3*myAllMolData.cumnumatomslist[end]);
        mulMatDiagleft!(tempview,
                        vecNto3N(myAllMolData.uniqueMolDataArray[currumolind].Ke));

    end
    tempmat .= totalTeemat * tempmat;

end

"""

    assemblePhimnwithBe!(Phimn::Array{FT, 2},
                         myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                         myPeriodicData::PeriodicData{FT},
                         tempmat::SharedArray{Complex{FT}, 2},
                         DeltaZImat::SharedArray{Complex{FT}, 2},
                         currfreq::Union{FT, Complex{FT}})

Assemble the contributions to the matrix `Phimn` in-place, corresponding to the fluctuating
electronic sources (accounting for absorption in both electrons due to radiation and nuclei
due to conduction); contributions are added to the existing values of `Phimn`.

"""
function assemblePhimnwithBe!(Phimn::Array{FT, 2},
                              myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                              myPeriodicData::PeriodicData{FT},
                              tempmat::SharedArray{Complex{FT}, 2},
                              DeltaZImat::SharedArray{Complex{FT}, 2},
                              currfreq::Union{FT, Complex{FT}}) where FT<:AbstractFloat

    currPhi = zero(eltype(Phimn));

    for nn2=1:myAllMolData.nummols

        startidx2 = 1;
        if (nn2 > 1)
            startidx2 = 3*myAllMolData.cumnumatomslist[nn2-1] + 1;
        end
        endidx2 = 3*myAllMolData.cumnumatomslist[nn2];
        DeltaZIview = view(DeltaZImat, startidx2:endidx2,
                           1:3*myAllMolData.cumnumatomslist[end]);

        startmol1 = 1;
        if (myPeriodicData.numdims == 0)
            startmol1 = nn2;
        end

        for nn1=startmol1:myAllMolData.nummols

            startidx1 = 1;
            if (nn1 > 1)
                startidx1 = 3*myAllMolData.cumnumatomslist[nn1-1] + 1;
            end
            endidx1 = 3*myAllMolData.cumnumatomslist[nn1];

            Tview = view(tempmat, 1:3*myAllMolData.cumnumatomslist[end], startidx1:endidx1);

            currmolind1 = myAllMolData.uniquemolinds[nn1];
            if (currmolind1 < 0)
                currmolind1 = myAllMolData.dupemolinds[nn1];
            end

            currPhi = currfreq *
            imag(tr(mulMatDiagleft(adjoint(Tview)[:, startidx2:endidx2] * DeltaZIview *
                                   Tview,
                                   vecNto3N(myAllMolData.uniqueMolDataArray[currmolind1].Be))));

            Phimn[nn1, nn2] += currPhi;

            if (myPeriodicData.numdims == 0 && nn1 != nn2)
                Phimn[nn2, nn1] += currPhi;
            end
        end

    end

end

"""

    assemblePhimnwithBI!(Phimn::Array{FT, 2},
                         myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                         myPeriodicData::PeriodicData{FT},
                         tempmat::SharedArray{Complex{FT}, 2},
                         DeltaZImat::SharedArray{Complex{FT}, 2},
                         currfreq::Union{FT, Complex{FT}})

Assemble the contributions to the matrix `Phimn` in-place, corresponding to the fluctuating
nuclear sources (accounting for absorption in both electrons due to radiation and nuclei
due to conduction); contributions are added to the existing values of `Phimn`.

"""
function assemblePhimnwithBI!(Phimn::Array{FT, 2},
                              myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                              myPeriodicData::PeriodicData{FT},
                              tempmat::SharedArray{Complex{FT}, 2},
                              DeltaZImat::SharedArray{Complex{FT}, 2},
                              currfreq::Union{FT, Complex{FT}}) where FT<:AbstractFloat

    currPhi = zero(eltype(Phimn));

    for nn2=1:myAllMolData.nummols

        startidx2 = 1;
        if (nn2 > 1)
            startidx2 = 3*myAllMolData.cumnumatomslist[nn2-1] + 1;
        end
        endidx2 = 3*myAllMolData.cumnumatomslist[nn2];
        DeltaZIview = view(DeltaZImat, startidx2:endidx2,
                           1:3*myAllMolData.cumnumatomslist[end]);

        startmol1 = 1;
        if (myPeriodicData.numdims == 0)
            startmol1 = nn2;
        end

        for nn1=startmol1:myAllMolData.nummols

            startidx1 = 1;
            if (nn1 > 1)
                startidx1 = 3*myAllMolData.cumnumatomslist[nn1-1] + 1;
            end
            endidx1 = 3*myAllMolData.cumnumatomslist[nn1];

            Tview = view(tempmat, 1:3*myAllMolData.cumnumatomslist[end], startidx1:endidx1);

            currmolind1 = myAllMolData.uniquemolinds[nn1];
            if (currmolind1 < 0)
                currmolind1 = myAllMolData.dupemolinds[nn1];
            end

            currPhi = currfreq *
            imag(tr(mulMatDiagleft(adjoint(Tview)[:, startidx2:endidx2] * DeltaZIview *
                                   Tview,
                                   vecNto3N(myAllMolData.uniqueMolDataArray[currmolind1].BI))));

            Phimn[nn1, nn2] += currPhi;

            if (myPeriodicData.numdims == 0 && nn1 != nn2)
                Phimn[nn2, nn1] += currPhi;
            end
        end

    end

end

"""

    assemblePhimnradiation!(Phimn::Array{FT, 2},
                            myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                            myPeriodicData::PeriodicData{FT},
                            totalTeemat::SharedArray{Complex{FT}, 2},
                            totalGenvmatwithdiag::SharedArray{Complex{FT}, 2},
                            totalZIIinvmat::SharedArray{Complex{FT}, 2},
                            tempmat::SharedArray{Complex{FT}, 2},
                            currfreq::Union{FT, Complex{FT}})

Assemble the matrix `Phimn` in-place for purely radiative couplings.

"""
function assemblePhimnradiation!(Phimn::Array{FT, 2},
                                 myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                 myPeriodicData::PeriodicData{FT},
                                 totalTeemat::SharedArray{Complex{FT}, 2},
                                 totalGenvmatwithdiag::SharedArray{Complex{FT}, 2},
                                 totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                 tempmat::SharedArray{Complex{FT}, 2},
                                 currfreq::Union{FT, Complex{FT}}) where FT<:AbstractFloat

    fill!(tempmat, zero(eltype(tempmat)));

    fill!(Phimn, zero(eltype(Phimn)));

    ## first: radiation into molecule n due to electronic sources in molecule m
    tempmat .= totalTeemat;

    lmul!(-1, totalGenvmatwithdiag);
    assemblePhimnwithBe!(Phimn, myAllMolData, myPeriodicData, tempmat, totalGenvmatwithdiag,
                         currfreq);
    lmul!(-1, totalGenvmatwithdiag);

    ## second: radiation into molecule n due to nuclear sources in molecule m
    assembletempmateI!(tempmat, totalTeemat, totalZIIinvmat, myAllMolData);

    lmul!(-1, totalGenvmatwithdiag);
    assemblePhimnwithBI!(Phimn, myAllMolData, myPeriodicData, tempmat, totalGenvmatwithdiag,
                         currfreq);
    lmul!(-1, totalGenvmatwithdiag);

end

"""

    assembleZIIinvwithDeltaKI!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                               DeltaKImat::SharedArray{Complex{FT}, 2},
                               myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                               myTransData::TransData{FT},
                               tt::Integer)

Assemble the matrix `totalZIIinvmat` in-place for the geometrical transformation with index
`tt`, in the presence of internuclear couplings between different molecules.

"""
function assembleZIIinvwithDeltaKI!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                    DeltaKImat::SharedArray{Complex{FT}, 2},
                                    myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                    myTransData::TransData{FT},
                                    tt::Integer) where FT<:AbstractFloat

    fill!(totalZIIinvmat, zero(eltype(totalZIIinvmat)));
    for nn=1:myAllMolData.nummols

        currumolind = myAllMolData.uniquemolinds[nn];
        if (currumolind < 0)
            currumolind = myAllMolData.dupemolinds[nn];
        end

        rotate3Nmat!(myAllMolData.uniqueMolDataArray[currumolind].KIBloch,
                     myTransData.rotArray[3*nn-2:3*nn, 3*tt-2:3*tt] *
                     myTransData.baselineRot[3*nn-2:3*nn, :]);

        startidx = 1;
        if (nn > 1)
            startidx = 3*myAllMolData.cumnumatomslist[nn-1] + 1;
        end
        endidx = 3*myAllMolData.cumnumatomslist[nn];

        tempview = view(totalZIIinvmat, startidx:endidx, startidx:endidx);
        tempview .= myAllMolData.uniqueMolDataArray[currumolind].KIBloch;
        addtodiag!(tempview,
                   vecNto3N(1 ./ myAllMolData.uniqueMolDataArray[currumolind].Ye0II));

        rotate3Nmat!(myAllMolData.uniqueMolDataArray[currumolind].KIBloch,
                     transpose(myTransData.rotArray[3*nn-2:3*nn, 3*tt-2:3*tt] *
                               myTransData.baselineRot[3*nn-2:3*nn, :]));

    end

    totalZIIinvmat .= inv(totalZIIinvmat .+ DeltaKImat);

end

"""

    assembleZIIinvTeeconduction!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                 totalTeemat::SharedArray{Complex{FT}, 2},
                                 DeltaKImat::SharedArray{Complex{FT}, 2},
                                 myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                 myTransData::TransData{FT},
                                 tt::Integer)

Assemble the matrices `totalZIIinvmat` and `totalTeemat` in-place for the geometrical
transformation with index `tt`, in the presence of internuclear couplings between different
molecules but in the absence of radiative couplings.

"""
function assembleZIIinvTeeconduction!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                      totalTeemat::SharedArray{Complex{FT}, 2},
                                      DeltaKImat::SharedArray{Complex{FT}, 2},
                                      myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                      myTransData::TransData{FT},
                                      tt::Integer) where FT<:AbstractFloat

    fill!(totalZIIinvmat, zero(eltype(totalZIIinvmat)));
    fill!(totalTeemat, zero(eltype(totalTeemat)));

    assembleZIIinvwithDeltaKI!(totalZIIinvmat, DeltaKImat, myAllMolData, myTransData, tt);

    totalTeemat .= -1 .* totalZIIinvmat;

    for nn2=1:myAllMolData.nummols

        currmolind2 = myAllMolData.uniquemolinds[nn2];
        if (currmolind2 < 0)
            currmolind2 = myAllMolData.dupemolinds[nn2];
        end

        startidx2 = 1;
        if (nn2 > 1)
            startidx2 = 3*myAllMolData.cumnumatomslist[nn2-1] + 1;
        end
        endidx2 = 3*myAllMolData.cumnumatomslist[nn2];

        for nn1=1:myAllMolData.nummols

            currmolind1 = myAllMolData.uniquemolinds[nn1];
            if (currmolind1 < 0)
                currmolind1 = myAllMolData.dupemolinds[nn1];
            end

            startidx1 = 1;
            if (nn1 > 1)
                startidx1 = 3*myAllMolData.cumnumatomslist[nn1-1] + 1;
            end
            endidx1 = 3*myAllMolData.cumnumatomslist[nn1];

            tempview = view(totalTeemat, startidx1:endidx1, startidx2:endidx2);
            mulMatDiagleft!(tempview,
                            vecNto3N(myAllMolData.uniqueMolDataArray[currmolind1].Ke));
            mulMatDiagright!(tempview,
                             vecNto3N(myAllMolData.uniqueMolDataArray[currmolind2].Ke));

        end

        tempview = view(totalTeemat, startidx2:endidx2, startidx2:endidx2)
        addtodiag!(tempview,
                   vecNto3N(1 ./ myAllMolData.uniqueMolDataArray[currmolind2].Ye0ee));

    end

    totalTeemat .= inv(totalTeemat);

end

"""

    assembletempmatIe!(tempmat::SharedArray{Complex{FT}, 2},
                       totalTeemat::SharedArray{Complex{FT}, 2},
                       totalZIIinvmat::SharedArray{Complex{FT}, 2},
                       myAllMolData::MolSystem{OneMol_WithPhonons{FT}})

Assemble the matrix `tempmat` in-place, equal to
``Z_{\\mathrm{II}}^{-1} K_{\\mathrm{e}} (Z_{\\mathrm{ee}} - K_{\\mathrm{e}} Z_{\\mathrm{II}}^{-1} K_{\\mathrm{e}})^{-1}``.

"""
function assembletempmatIe!(tempmat::SharedArray{Complex{FT}, 2},
                            totalTeemat::SharedArray{Complex{FT}, 2},
                            totalZIIinvmat::SharedArray{Complex{FT}, 2},
                            myAllMolData::MolSystem{OneMol_WithPhonons{FT}}) where FT<:AbstractFloat

    tempmat .= totalZIIinvmat;
    for nn=1:myAllMolData.nummols

        currumolind = myAllMolData.uniquemolinds[nn];
        if (currumolind < 0)
            currumolind = myAllMolData.dupemolinds[nn];
        end

        startidx = 1;
        if (nn > 1)
            startidx = 3*myAllMolData.cumnumatomslist[nn-1] + 1;
        end
        endidx = 3*myAllMolData.cumnumatomslist[nn];

        tempview = view(tempmat, 1:3*myAllMolData.cumnumatomslist[end], startidx:endidx);
        mulMatDiagright!(tempview,
                         vecNto3N(myAllMolData.uniqueMolDataArray[currumolind].Ke));

    end
    tempmat .= totalTeemat * tempmat;

end

"""

    assembletempmatII!(tempmat::SharedArray{Complex{FT}, 2},
                       totalTeemat::SharedArray{Complex{FT}, 2},
                       totalZIIinvmat::SharedArray{Complex{FT}, 2},
                       myAllMolData::MolSystem{OneMol_WithPhonons{FT}})

Assemble the matrix `tempmat` in-place, equal to
``Z_{\\mathrm{II}}^{-1} + Z_{\\mathrm{II}}^{-1} K_{\\mathrm{e}} (Z_{\\mathrm{ee}} - K_{\\mathrm{e}} Z_{\\mathrm{II}}^{-1} K_{\\mathrm{e}})^{-1} K_{\\mathrm{e}} Z_{\\mathrm{II}}^{-1}``.

"""
function assembletempmatII!(tempmat::SharedArray{Complex{FT}, 2},
                            totalTeemat::SharedArray{Complex{FT}, 2},
                            totalZIIinvmat::SharedArray{Complex{FT}, 2},
                            myAllMolData::MolSystem{OneMol_WithPhonons{FT}}) where FT<:AbstractFloat

    tempmat .= totalTeemat;
    for nn2=1:myAllMolData.nummols

        currmolind2 = myAllMolData.uniquemolinds[nn2];
        if (currmolind2 < 0)
            currmolind2 = myAllMolData.dupemolinds[nn2];
        end

        startidx2 = 1;
        if (nn2 > 1)
            startidx2 = 3*myAllMolData.cumnumatomslist[nn2-1] + 1;
        end
        endidx2 = 3*myAllMolData.cumnumatomslist[nn2];

        for nn1=1:myAllMolData.nummols

            currmolind1 = myAllMolData.uniquemolinds[nn1];
            if (currmolind1 < 0)
                currmolind1 = myAllMolData.dupemolinds[nn1];
            end

            startidx1 = 1;
            if (nn1 > 1)
                startidx1 = 3*myAllMolData.cumnumatomslist[nn1-1] + 1;
            end
            endidx1 = 3*myAllMolData.cumnumatomslist[nn1];

            tempview = view(tempmat, startidx1:endidx1, startidx2:endidx2);
            mulMatDiagleft!(tempview,
                            vecNto3N(myAllMolData.uniqueMolDataArray[currmolind1].Ke));
            mulMatDiagright!(tempview,
                             vecNto3N(myAllMolData.uniqueMolDataArray[currmolind2].Ke));

        end

    end
    tempmat .= totalZIIinvmat .+ (totalZIIinvmat * tempmat * totalZIIinvmat);

end

"""

    assemblePhimnconduction!(Phimn::Array{FT, 2},
                             myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                             myPeriodicData::PeriodicData{FT},
                             totalTeemat::SharedArray{Complex{FT}, 2},
                             DeltaKImat::SharedArray{Complex{FT}, 2},
                             totalZIIinvmat::SharedArray{Complex{FT}, 2},
                             tempmat::SharedArray{Complex{FT}, 2},
                             currfreq::Union{FT, Complex{FT}})

Assemble the matrix `Phimn` in-place for purely conductive couplings.

"""
function assemblePhimnconduction!(Phimn::Array{FT, 2},
                                  myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                  myPeriodicData::PeriodicData{FT},
                                  totalTeemat::SharedArray{Complex{FT}, 2},
                                  DeltaKImat::SharedArray{Complex{FT}, 2},
                                  totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                  tempmat::SharedArray{Complex{FT}, 2},
                                  currfreq::Union{FT, Complex{FT}}) where FT<:AbstractFloat

    fill!(tempmat, zero(eltype(tempmat)));

    fill!(Phimn, zero(eltype(Phimn)));

    ## first: conduction into molecule n due to electronic sources in molecule m
    assembletempmatIe!(tempmat, totalTeemat, totalZIIinvmat, myAllMolData);

    assemblePhimnwithBe!(Phimn, myAllMolData, myPeriodicData, tempmat, DeltaKImat,
                         currfreq);

    ## second: conduction into molecule n due to nuclear sources in molecule m
    assembletempmatII!(tempmat, totalTeemat, totalZIIinvmat, myAllMolData);

    assemblePhimnwithBI!(Phimn, myAllMolData, myPeriodicData, tempmat, DeltaKImat,
                         currfreq);

end

"""

    assembleZIIinvTeeboth!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                           totalTeemat::SharedArray{Complex{FT}, 2},
                           totalGenvmat::SharedArray{Complex{FT}, 2},
                           DeltaKImat::SharedArray{Complex{FT}, 2},
                           myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                           myTransData::TransData{FT},
                           tt::Integer)

Assemble the matrices `totalZIIinvmat` and `totalTeemat` in-place for the geometrical
transformation with index `tt`, in the presence of both radiative and internuclear couplings
between different molecules.

"""
function assembleZIIinvTeeboth!(totalZIIinvmat::SharedArray{Complex{FT}, 2},
                                totalTeemat::SharedArray{Complex{FT}, 2},
                                totalGenvmat::SharedArray{Complex{FT}, 2},
                                DeltaKImat::SharedArray{Complex{FT}, 2},
                                myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                myTransData::TransData{FT},
                                tt::Integer) where FT<:AbstractFloat

    fill!(totalZIIinvmat, zero(eltype(totalZIIinvmat)));
    fill!(totalTeemat, zero(eltype(totalTeemat)));

    assembleZIIinvwithDeltaKI!(totalZIIinvmat, DeltaKImat, myAllMolData, myTransData, tt);

    totalTeemat .= -1 .* totalZIIinvmat;

    for nn2=1:myAllMolData.nummols

        currmolind2 = myAllMolData.uniquemolinds[nn2];
        if (currmolind2 < 0)
            currmolind2 = myAllMolData.dupemolinds[nn2];
        end

        startidx2 = 1;
        if (nn2 > 1)
            startidx2 = 3*myAllMolData.cumnumatomslist[nn2-1] + 1;
        end
        endidx2 = 3*myAllMolData.cumnumatomslist[nn2];

        for nn1=1:myAllMolData.nummols

            currmolind1 = myAllMolData.uniquemolinds[nn1];
            if (currmolind1 < 0)
                currmolind1 = myAllMolData.dupemolinds[nn1];
            end

            startidx1 = 1;
            if (nn1 > 1)
                startidx1 = 3*myAllMolData.cumnumatomslist[nn1-1] + 1;
            end
            endidx1 = 3*myAllMolData.cumnumatomslist[nn1];

            tempview = view(totalTeemat, startidx1:endidx1, startidx2:endidx2);
            mulMatDiagleft!(tempview,
                            vecNto3N(myAllMolData.uniqueMolDataArray[currmolind1].Ke));
            mulMatDiagright!(tempview,
                             vecNto3N(myAllMolData.uniqueMolDataArray[currmolind2].Ke));

        end

        tempview = view(totalTeemat, startidx2:endidx2, startidx2:endidx2)
        addtodiag!(tempview,
                   vecNto3N(1 ./ myAllMolData.uniqueMolDataArray[currmolind2].Ye0ee));

    end

    totalTeemat .= inv(totalTeemat .- totalGenvmat);

end

"""

    assemblePhimnboth!(Phimn::Array{FT, 2},
                       myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                       myPeriodicData::PeriodicData{FT},
                       totalTeemat::SharedArray{Complex{FT}, 2},
                       totalGenvmatwithdiag::SharedArray{Complex{FT}, 2},
                       DeltaKImat::SharedArray{Complex{FT}, 2},
                       totalZIIinvmat::SharedArray{Complex{FT}, 2},
                       tempmat::SharedArray{Complex{FT}, 2},
                       currfreq::Union{FT, Complex{FT}})

Assemble the matrix `Phimn` in-place for both radiative and conductive couplings.

"""
function assemblePhimnboth!(Phimn::Array{FT, 2},
                            myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                            myPeriodicData::PeriodicData{FT},
                            totalTeemat::SharedArray{Complex{FT}, 2},
                            totalGenvmatwithdiag::SharedArray{Complex{FT}, 2},
                            DeltaKImat::SharedArray{Complex{FT}, 2},
                            totalZIIinvmat::SharedArray{Complex{FT}, 2},
                            tempmat::SharedArray{Complex{FT}, 2},
                            currfreq::Union{FT, Complex{FT}}) where FT<:AbstractFloat

    fill!(tempmat, zero(eltype(tempmat)));

    fill!(Phimn, zero(eltype(Phimn)));

    ## first: radiation into molecule n due to electronic sources in molecule m
    tempmat .= totalTeemat;

    lmul!(-1, totalGenvmatwithdiag);
    assemblePhimnwithBe!(Phimn, myAllMolData, myPeriodicData, tempmat, totalGenvmatwithdiag,
                         currfreq);
    lmul!(-1, totalGenvmatwithdiag);

    ## second: radiation into molecule n due to nuclear sources in molecule m
    assembletempmateI!(tempmat, totalTeemat, totalZIIinvmat, myAllMolData);

    lmul!(-1, totalGenvmatwithdiag);
    assemblePhimnwithBI!(Phimn, myAllMolData, myPeriodicData, tempmat, totalGenvmatwithdiag,
                         currfreq);
    lmul!(-1, totalGenvmatwithdiag);

    ## third: conduction into molecule n due to electronic sources in molecule m
    assembletempmatIe!(tempmat, totalTeemat, totalZIIinvmat, myAllMolData);

    assemblePhimnwithBe!(Phimn, myAllMolData, myPeriodicData, tempmat, DeltaKImat,
                         currfreq);

    ## fourth: conduction into molecule n due to nuclear sources in molecule m
    assembletempmatII!(tempmat, totalTeemat, totalZIIinvmat, myAllMolData);

    assemblePhimnwithBI!(Phimn, myAllMolData, myPeriodicData, tempmat, DeltaKImat,
                         currfreq);

end

"""

    assembleGenvDiagBlock!(totalGenvmat::SharedArray{Complex{FT}, 2},
                           totalGenvmatwithdiag::SharedArray{Complex{FT}, 2},
                           myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                           myTransData::TransData{FT}, nn2::Integer,
                           currmolind2::Integer, startidx2::Integer,
                           tt::Integer, myPeriodicData::PeriodicData{FT},
                           GFSCAGG!::Function,
                           currfreqG::Union{FT, Complex{FT}},
                           currk::Array{FT, 1}=zeros(FT, 3)) where FT<:AbstractFloat

Assemble the molecular diagonal block of the scattering Green's function interaction matrix
of molecule `nn2` (whose unique molecular index `currmolind2` is precomputed and passed as
an argument) at frequency `currfreqG` and wavevector `currk` using `GFSCAGG!`, then add the
vacuum contribution of molecule `nn2`, performing any required matrix rotations
corresponding to transformation `tt` for molecule `nn2`. Stamp that into
`totalGenvmat[(startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]), (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2])]`,
and added to coincident contributions into
`totalGenvmatwithdiag[(startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]), (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2])]`.
Finally, undo any rotation corresponding to transformation `tt` in molecule `nn2` for reuse
with future transformations. This assumes the presence of phonons.

"""
function assembleGenvDiagBlock!(totalGenvmat::SharedArray{Complex{FT}, 2},
                                totalGenvmatwithdiag::SharedArray{Complex{FT}, 2},
                                myAllMolData::MolSystem{OneMol_WithPhonons{FT}},
                                myTransData::TransData{FT}, nn2::Integer,
                                currmolind2::Integer, startidx2::Integer,
                                tt::Integer, myPeriodicData::PeriodicData{FT},
                                GFSCAGG!::Function,
                                currfreqG::Union{FT, Complex{FT}},
                                currk::Array{FT, 1}=zeros(FT, 3)) where FT<:AbstractFloat

    tempview = view(totalGenvmatwithdiag, (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]),
                    (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]));

    fill!(tempview, zero(eltype(tempview)));

    assembleGscaMolBlock!(tempview, myAllMolData, myTransData, nn2, nn2, 1, 1,
                          myPeriodicData, GFSCAGG!, currfreqG, currk);

    rotate3Nmat!(myAllMolData.uniqueMolDataArray[currmolind2].GvacGGinf,
                 myTransData.rotArray[3*nn2-2:3*nn2, 3*tt-2:3*tt] *
                 myTransData.baselineRot[3*nn2-2:3*nn2, :]);

    tempview .+= myAllMolData.uniqueMolDataArray[currmolind2].GvacGGinf[:, :];

    totalGenvmat[(startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]),
                 (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2])] .=
    tempview;

    addtodiag!(tempview,
               vecNto3N(myAllMolData.uniqueMolDataArray[currmolind2].GvacGGinfdiag));

    rotate3Nmat!(myAllMolData.uniqueMolDataArray[currmolind2].GvacGGinf,
                 transpose(myTransData.rotArray[3*nn2-2:3*nn2, 3*tt-2:3*tt] *
                           myTransData.baselineRot[3*nn2-2:3*nn2, :]));

end

"""

    assembleGenvOffDiagBlock!(totalGenvmat::SharedArray{Complex{FT}, 2},
                              totalGenvmatwithdiag::SharedArray{Complex{FT}, 2},
                              myAllMolData::MolSystem{<:OneMol{FT}},
                              myTransData::TransData{FT},
                              nn1::Integer, nn2::Integer,
                              startidx1::Integer,
                              startidx2::Integer,
                              myPeriodicData::PeriodicData{FT},
                              GFSCAGG!::Function,
                              currfreqG::Union{FT, Complex{FT}},
                              currk::Array{FT, 1}=zeros(FT, 3)) where FT<:AbstractFloat

Assemble the molecular off-diagonal block of the scattering Green's function interaction
matrix of molecules `nn2` as the source and `nn1` as the field (whose respective unique
molecular indices `currmolind1` and `currmolind2` are precomputed and passed as arguments)
at frequency `currfreqG` and wavevector `currk` using `GFSCAGG!`, multiply on
the left by the electronic polarizabilities, add the vacuum contribution, and stamp that
into
`totalGenvmat[(startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]), (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2])]`
and
`totalGenvmatwithdiag[(startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]), (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2])]`.
If there is no spatial periodicity, do the same for the (unconjugated) transpose, swapping
`nn2` and `nn1` without performing further Green's function computations.

"""
function assembleGenvOffDiagBlock!(totalGenvmat::SharedArray{Complex{FT}, 2},
                                   totalGenvmatwithdiag::SharedArray{Complex{FT}, 2},
                                   myAllMolData::MolSystem{<:OneMol{FT}},
                                   myTransData::TransData{FT},
                                   nn1::Integer, nn2::Integer,
                                   startidx1::Integer,
                                   startidx2::Integer,
                                   myPeriodicData::PeriodicData{FT},
                                   GFSCAGG!::Function,
                                   currfreqG::Union{FT, Complex{FT}},
                                   currk::Array{FT, 1}=zeros(FT, 3)) where FT<:AbstractFloat

    tempview = view(totalGenvmatwithdiag,
                    (startidx1-1).+(1:3*myAllMolData.numatomslist[nn1]),
                    (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]));
    fill!(tempview, zero(eltype(tempview)));

    assembleGvacMolBlock!(tempview, myAllMolData, myTransData, nn1, nn2, 1, 1,
                          myPeriodicData, currfreqG, currk);
    assembleGscaMolBlock!(tempview, myAllMolData, myTransData, nn1, nn2, 1, 1,
                          myPeriodicData, GFSCAGG!, currfreqG, currk);

    totalGenvmat[(startidx1-1).+(1:3*myAllMolData.numatomslist[nn1]),
                         (startidx2-1).+(1:3*myAllMolData.numatomslist[nn2])] .=
    tempview;

    if (myPeriodicData.numdims == 0)
        totalGenvmat[(startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]),
                     (startidx1-1).+(1:3*myAllMolData.numatomslist[nn1])] .=
        transpose(deepcopy(tempview));
        totalGenvmatwithdiag[(startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]),
                             (startidx1-1).+(1:3*myAllMolData.numatomslist[nn1])] .=
        totalGenvmat[(startidx2-1).+(1:3*myAllMolData.numatomslist[nn2]),
                     (startidx1-1).+(1:3*myAllMolData.numatomslist[nn1])];
   end

end


module LiiBRA

using UnitSystems, Parameters, LinearAlgebra, FFTW
using TSVD, Roots, Statistics, Interpolations
export C_e, Flux, C_se, Phi_s, Phi_e, Phi_se, DRA
export flatten_, R, F, Sim_Model, D_Linear, Construct, tuple_len, interp
export Realise, HPPC, fh!, mag!

include("Functions/Transfer/C_e.jl")
include("Functions/Transfer/C_se.jl")
include("Functions/Transfer/Flux.jl")
include("Functions/Transfer/Phi_s.jl")
include("Functions/Transfer/Phi_e.jl")
include("Functions/Transfer/Phi_se.jl")
include("Methods/DRA.jl")
include("Functions/Sim_Model.jl")

const F,R = faraday(Metric), universal(SI2019) #Faraday Constant / Universal Gas Constant

#---------- Generate Model -----------------#
function Realise(Cell, SList::Array, T::Float64)
    A = B = C = D = tuple()
    for i in SList
        #Arrhenius
        Cell.Const.T = T
        Arr_Factor = (1/Cell.Const.T_ref-1/Cell.Const.T)/R

        #Set Cell Constants
        Cell.Const.SOC = i
        Cell.Const.κ = Cell.Const.κf(Cell.Const.ce0)*exp(Cell.Const.Ea_κ*Arr_Factor)
        Cell.RA.Nfft = Cell.RA.Nfft!(Cell.RA.Fs, Cell.RA.Tlen)
        Cell.RA.f = Cell.RA.f!(Cell.RA.Nfft)
        Cell.RA.s = Cell.RA.s!(Cell.RA.Fs,Cell.RA.Nfft,Cell.RA.f)
        Cell.Neg.β = Cell.Neg.β!(Cell.RA.s)
        Cell.Pos.β = Cell.Pos.β!(Cell.RA.s)

        #Realisation
        Aϕ, Bϕ, Cϕ, Dϕ = DRA(Cell)

        #Flatten output into Tuples
        A = flatten_(A,Aϕ)
        B = flatten_(B,Bϕ)
        C = flatten_(C,Cϕ)
        D = flatten_(D,Dϕ)
    end
return A, B, C, D
end

#---------- Simulate Model -----------------#
function HPPC(Cell,SList::Array,SOC::Float64,λ::Float64,ϕ::Float64,A::Tuple,B::Tuple,C::Tuple,D::Tuple)

    #Set Experiment
    i = Int64(1/Cell.RA.SamplingT) #Sampling Frequency
    Input = [ones(1)*0.; ones(10*i)*λ; ones(40*i)*0.; ones(10*i)*ϕ; ones(40*i)*0.] #1C HPPC Experiment Current Profile
    Tk = ones(size(Input))*Cell.Const.T #Cell Temperature
    t = 0:(1.0/i):((length(Input)-1)/i)

    #Simulate Model
    return Sim_Model(Cell,Input,"Current",Tk,SList,SOC,A,B,C,D,t)
end

#---------- Hankel Formation & SVD -----------------#
function fh!(H,Hlen1,Hlen2,puls,M,Puls_L)
    for lp1 in 1:length(Hlen2), lp2 in 1:length(Hlen1)
            H[Puls_L*(lp2-1)+1:Puls_L*lp2,lp1] .= @view puls[:,Hlen2[lp1]+Hlen1[lp2]]
    end

    #Init = abs.(convert(Vector{float(eltype(H))}, randn(size(H,1))))
    Init = convert(Vector{float(eltype(H))}, ones(size(H,1)))
    U,S,V = tsvd(H, M; initvec=Init, maxiter=1e4)
    println("---")

    for lp1 in 1:length(Hlen2), lp2 in 1:length(Hlen1)
           H[Puls_L*(lp2-1)+1:Puls_L*lp2,lp1] .= @view puls[:,Hlen2[lp1]+Hlen1[lp2]+1]
    end
    return U,S,V'
end

"""
    D_Linear(Cell,ν_neg,ν_pos,σ_eff_Neg, κ_eff_Neg, σ_eff_Pos, κ_eff_Pos, κ_eff_Sep)

    Function to linearise D array from input cell type and conductivites.

"""
function D_Linear(Cell, ν_neg, ν_pos, σ_eff_Neg, κ_eff_Neg, σ_eff_Pos, κ_eff_Pos, κ_eff_Sep)
    D = Array{Float64}(undef,0,1)
    Dt = Array{Float64}(undef,0,1)
    D_ = Array{Float64}(undef,size(Cell.Transfer.Locs[2],1),1)
    q = Int64(1)
    tfs = Cell.Transfer.tfs
    Elec = Cell.Transfer.Elec
    Locs = Cell.Transfer.Locs
    for i in 1:size(tfs,1)
        if tfs[i] == C_e
            Dt =  zeros(length(Locs[i]))
        elseif tfs[i] == Phi_e
            Dt = Array{Float64}(undef,0,1)
            for pt in Locs[i]
                if pt <= Cell.Neg.L+eps()
                D_[q]  =  @. (Cell.Neg.L*(σ_eff_Neg/κ_eff_Neg)*(1-cosh(ν_neg*pt/Cell.Neg.L)) - pt*ν_neg*sinh(ν_neg) + Cell.Neg.L*(cosh(ν_neg)-cosh(ν_neg*(Cell.Neg.L-pt)/Cell.Neg.L)))/(Cell.Const.CC_A*(κ_eff_Neg+σ_eff_Neg)*sinh(ν_neg)*ν_neg) #Lee. Eqn. 4.22 @ ∞
                elseif pt <= Cell.Neg.L + Cell.Sep.L + eps()
                D_[q] = @. (Cell.Neg.L - pt)/(Cell.Const.CC_A*κ_eff_Sep) + (Cell.Neg.L*((1-σ_eff_Neg/κ_eff_Neg)*tanh(ν_neg/2)-ν_neg))/(Cell.Const.CC_A*(κ_eff_Neg+σ_eff_Neg)*ν_neg) #Lee. Eqn. 4.23 @ ∞
                else
                D_[q] = @. -Cell.Sep.L/(Cell.Const.CC_A*κ_eff_Sep) + Cell.Neg.L*((1-σ_eff_Neg/κ_eff_Neg)*tanh(ν_neg/2)-ν_neg)/(Cell.Const.CC_A*(σ_eff_Neg+ κ_eff_Neg)*ν_neg) + (Cell.Pos.L*(-σ_eff_Pos*cosh(ν_pos) + σ_eff_Pos*cosh((Cell.Const.Ltot-pt)*ν_pos/Cell.Pos.L) +  κ_eff_Pos*(cosh((pt-Cell.Neg.L-Cell.Sep.L)*ν_pos/Cell.Pos.L)-1)) - (pt-Cell.Neg.L-Cell.Sep.L)*κ_eff_Pos*sinh(ν_pos)*ν_pos)/(Cell.Const.CC_A*κ_eff_Pos*(κ_eff_Pos+σ_eff_Pos)*ν_pos*sinh(ν_pos))
                end
                Dt = [Dt; D_[q]]
                q = q+1
            end

        elseif tfs[i] == C_se
            Dt = zeros(length(Locs[i]))
        elseif Elec[i] == "Pos"
            σ_eff = σ_eff_Pos
            κ_eff = κ_eff_Pos
            if tfs[i] == Phi_se
                Dt = @. -1*Cell.Pos.L/(Cell.Const.CC_A*ν_pos*sinh(ν_pos))*((1/κ_eff)*cosh(ν_pos*Locs[i])+(1/σ_eff)*cosh(ν_pos*(Locs[i]-1))) # Contribution to D as G->∞
            elseif tfs[i] == Flux
                Dt = @. -1*ν_pos*(σ_eff*cosh(ν_pos*Locs[i])+κ_eff*cosh(ν_pos*(Locs[i]-1)))/(Cell.Pos.as*F*Cell.Pos.L*Cell.Const.CC_A*(κ_eff+σ_eff)*sinh(ν_pos))
            elseif tfs[i] == Phi_s
                Dt = @. -1*(-Cell.Pos.L*(κ_eff*(cosh(ν_pos)-cosh(Locs[i]-1)*ν_pos))-Cell.Pos.L*(σ_eff*(1-cosh(Locs[i]*ν_pos)+Locs[i]*ν_pos*sinh(ν_pos))))/(Cell.Const.CC_A*σ_eff*(κ_eff+σ_eff)*ν_pos*sinh(ν_pos)) # Contribution to D as G->∞
            end
        elseif Elec[i] == "Neg"
            σ_eff = σ_eff_Neg
            κ_eff = κ_eff_Neg
            if tfs[i] == Phi_se
                Dt = @. Cell.Neg.L/(Cell.Const.CC_A*ν_neg*sinh(ν_neg))*((1/κ_eff)*cosh(ν_neg*Locs[i])+(1/σ_eff)*cosh(ν_neg*(Locs[i]-1))) # Contribution to D as G->∞
            elseif tfs[i] == Flux
                Dt = @. ν_neg*(σ_eff*cosh(ν_neg*Locs[i])+κ_eff*cosh(ν_neg*(Locs[i]-1)))/(Cell.Neg.as*F*Cell.Neg.L*Cell.Const.CC_A*(κ_eff+σ_eff)*sinh(ν_neg))
            elseif tfs[i] == Phi_s
                Dt = @. (-Cell.Neg.L*(κ_eff*(cosh(ν_neg)-cosh(Locs[i]-1)*ν_neg))-Cell.Neg.L*(σ_eff*(1-cosh(Locs[i]*ν_neg)+Locs[i]*ν_neg*sinh(ν_neg))))/(Cell.Const.CC_A*σ_eff*(κ_eff+σ_eff)*ν_neg*sinh(ν_neg)) # Contribution to D as G->∞
            end
        end
        D = [D; Dt]
    end
    return D
end

"""

    flatten_(a::Tuple, b...)

Flattens input Tuple "a" and inserts "b"

"""
function flatten_ end
flatten_() = ()
flatten_(a::Tuple) = Tuple(a)
flatten_(a) = (a,)
flatten_(a::Tuple, b...) = tuple(a..., flatten_(b...)...)
flatten_(a, b...) = tuple(a, flatten_(b...)...)
flatten_tuple(x::Tuple) = flatten_(x...)

"""
    Construct(::String)

    Function to create Construct dictionary.

    Currently supports:

    1. Doyle '94 parameterisation
    2. Chen 2020 parameterisation

"""
function Construct(CellType::String)
    if CellType == "Doyle 94"
        CellType = string(CellType,".jl")
        include(joinpath(dirname(pathof(LiiBRA)), "Data/Doyle_94", CellType))
    elseif CellType == "LG M50"
        CellType = string("LG_M50.jl")
        include(joinpath(dirname(pathof(LiiBRA)), "Data/Chen_2020", CellType))
    end
    return Cell
end

"""
    tuple_len(::NTuple)

    Function to return Tuple length.

"""
tuple_len(::NTuple{N, Any}) where {N} = N #Tuple Size

"""
    interp(MTup::Tuple,SList::Array,SOC)

    Function to interpolate Tuple indices

"""
function interp(MTup::Tuple,SList::Array,SOC)
    T1 = 0
    T2 = 0
    for i in 1:length(SList)-1
        if SList[i] > SOC >= SList[i+1]
            T2 = i
            T1 = i+1
        end
    end
    return M =  @. MTup[T1]+(MTup[T2]-MTup[T1])*(SOC-SList[T1])/(SList[T2]-SList[T1])
end

function mag!(γ::AbstractArray)
    ψ = Array{Float64}(undef,size(γ))
    for j in 1:size(γ,2), i in 1:size(γ,1)
        if real(γ[i,j]) < 0
            ψ[i,j] = -abs(γ[i,j])
        else
            ψ[i,j] = abs(γ[i,j])
        end
    end
    return ψ
end

function mag!(γ::AbstractVector)
    ψ = Vector{Float64}(undef,length(γ))
    for i in 1:length(γ)
        if real(γ[i]) < 0
            ψ[i] = -abs(γ[i])
        else
            ψ[i] = abs(γ[i])
        end
    end
    return ψ
end

end # module


# add all packages here
using DataFrames
using Gadfly
using Vega # for word cloud
using TextAnalysis #, DimensionalityReduction, Clustering
using Plotly
using Query

#include("clean-data.jl")

    function fetchTopNTopic(df, min_count = 100 )
        # define min count
        # filter out
        df = df |> @query(i, begin
                    @where i.count>min_count
                    @orderby descending(i.count)
                    @select {i.term, i.count}
                  end) |> DataFrame

         plot = @time Gadfly.plot(df,y="count", x="term", Geom.bar, color="term")
        # save plot
        #draw(PNG("plot/diagnosis_top_n.png", 3inch, 3inch), plot)
        # return
         return(df,plot)
     end

     function fetchNgramTopic(full_text_array, min_count = 100, ngram =3 )
         # define min count
         # filter out
         full_text_sd, full_text_tx= cleanText( join(full_text_array,"\n"))

        # create ngrams
        full_text_ng=ngrams(full_text_sd, ngram)
        # convert to dataframe
        full_text_ng_df = DataFrame(term=collect(keys(full_text_ng)), count=collect(values(full_text_ng)))

        # add new column size
        full_text_ng_df = full_text_ng_df |> @query(i, begin
                            @orderby descending(i.count)
                            @select {i.term, i.count, size=length(split(get(i.term)))}
                          end) |> DataFrame

        # filter out < min_count
        full_text_ng_df = full_text_ng_df |> @query(i, begin
                            @where i.count>min_count && i.size==ngram
                            @select {i.term, i.count,i.size }
                          end) |> DataFrame

        # plot graph
        plot = @time Gadfly.plot(full_text_ng_df,y="count", x="term",
                Geom.bar(orientation= :vertical), color="term",
                #Guide.xlabel(nothing),
                #Guide.ylabel(nothing),
                Theme(key_position = :none))
        # save plot
         #draw(PNG("plot/diagnosis_top_n.png", 3inch, 3inch), plot)
         # return
          return(full_text_ng_df,plot)
      end

    # this function generates trends for an array of genes, cells, concepts, therapy
  function generateTrends(df_full_text, terms=[], startYear = 2008, endYear =2018 )

                  # Create df
          trend_df = DataFrame(
                          term=String[],
                          count=Int64[],
                          year=Int64[])

            for yr in startYear:endYear
                # filter by year
                filtered_df = df_full_text |> @query(i, begin
                                        @where i.year==yr
                                        @select {i.fullText}
                                      end) |> DataFrame

                # clean text and convert to sd
                sd_array=[]
                for row in eachrow(filtered_df)
                    sd,tx=cleanText(row[:fullText])
                    push!(sd_array,sd)
                end

                # convert to corpus
                crp = Corpus(sd_array)
                #standardize
                standardize!(crp, StringDocument)
                #update crp
                update_lexicon!(crp)
                update_inverse_index!(crp)
                lexicon(crp)

                for term in terms
                    # query corpus
                   count=sum(crp[term])
                    # create df and push it to trend_df
                     temp_df = DataFrame(term=term, count=count, year=yr)
                     append!(trend_df, temp_df)

                end

            end

           return(trend_df)
  end


# Automatically generated using Clang.jl wrap_c, version 0.0.0

#using Compat

const cublasCreate = cublasCreate_v2
const cublasDestroy = cublasDestroy_v2
const cublasGetVersion = cublasGetVersion_v2
const cublasSetStream = cublasSetStream_v2
const cublasGetStream = cublasGetStream_v2
const cublasGetPointerMode = cublasGetPointerMode_v2
const cublasSetPointerMode = cublasSetPointerMode_v2
const cublasSnrm2 = cublasSnrm2_v2
const cublasDnrm2 = cublasDnrm2_v2
const cublasScnrm2 = cublasScnrm2_v2
const cublasDznrm2 = cublasDznrm2_v2
const cublasSdot = cublasSdot_v2
const cublasDdot = cublasDdot_v2
const cublasCdotu = cublasCdotu_v2
const cublasCdotc = cublasCdotc_v2
const cublasZdotu = cublasZdotu_v2
const cublasZdotc = cublasZdotc_v2
const cublasSscal = cublasSscal_v2
const cublasDscal = cublasDscal_v2
const cublasCscal = cublasCscal_v2
const cublasCsscal = cublasCsscal_v2
const cublasZscal = cublasZscal_v2
const cublasZdscal = cublasZdscal_v2
const cublasSaxpy = cublasSaxpy_v2
const cublasDaxpy = cublasDaxpy_v2
const cublasCaxpy = cublasCaxpy_v2
const cublasZaxpy = cublasZaxpy_v2
const cublasScopy = cublasScopy_v2
const cublasDcopy = cublasDcopy_v2
const cublasCcopy = cublasCcopy_v2
const cublasZcopy = cublasZcopy_v2
const cublasSswap = cublasSswap_v2
const cublasDswap = cublasDswap_v2
const cublasCswap = cublasCswap_v2
const cublasZswap = cublasZswap_v2
const cublasIsamax = cublasIsamax_v2
const cublasIdamax = cublasIdamax_v2
const cublasIcamax = cublasIcamax_v2
const cublasIzamax = cublasIzamax_v2
const cublasIsamin = cublasIsamin_v2
const cublasIdamin = cublasIdamin_v2
const cublasIcamin = cublasIcamin_v2
const cublasIzamin = cublasIzamin_v2
const cublasSasum = cublasSasum_v2
const cublasDasum = cublasDasum_v2
const cublasScasum = cublasScasum_v2
const cublasDzasum = cublasDzasum_v2
const cublasSrot = cublasSrot_v2
const cublasDrot = cublasDrot_v2
const cublasCrot = cublasCrot_v2
const cublasCsrot = cublasCsrot_v2
const cublasZrot = cublasZrot_v2
const cublasZdrot = cublasZdrot_v2
const cublasSrotg = cublasSrotg_v2
const cublasDrotg = cublasDrotg_v2
const cublasCrotg = cublasCrotg_v2
const cublasZrotg = cublasZrotg_v2
const cublasSrotm = cublasSrotm_v2
const cublasDrotm = cublasDrotm_v2
const cublasSrotmg = cublasSrotmg_v2
const cublasDrotmg = cublasDrotmg_v2
const cublasSgemv = cublasSgemv_v2
const cublasDgemv = cublasDgemv_v2
const cublasCgemv = cublasCgemv_v2
const cublasZgemv = cublasZgemv_v2
const cublasSgbmv = cublasSgbmv_v2
const cublasDgbmv = cublasDgbmv_v2
const cublasCgbmv = cublasCgbmv_v2
const cublasZgbmv = cublasZgbmv_v2
const cublasStrmv = cublasStrmv_v2
const cublasDtrmv = cublasDtrmv_v2
const cublasCtrmv = cublasCtrmv_v2
const cublasZtrmv = cublasZtrmv_v2
const cublasStbmv = cublasStbmv_v2
const cublasDtbmv = cublasDtbmv_v2
const cublasCtbmv = cublasCtbmv_v2
const cublasZtbmv = cublasZtbmv_v2
const cublasStpmv = cublasStpmv_v2
const cublasDtpmv = cublasDtpmv_v2
const cublasCtpmv = cublasCtpmv_v2
const cublasZtpmv = cublasZtpmv_v2
const cublasStrsv = cublasStrsv_v2
const cublasDtrsv = cublasDtrsv_v2
const cublasCtrsv = cublasCtrsv_v2
const cublasZtrsv = cublasZtrsv_v2
const cublasStpsv = cublasStpsv_v2
const cublasDtpsv = cublasDtpsv_v2
const cublasCtpsv = cublasCtpsv_v2
const cublasZtpsv = cublasZtpsv_v2
const cublasStbsv = cublasStbsv_v2
const cublasDtbsv = cublasDtbsv_v2
const cublasCtbsv = cublasCtbsv_v2
const cublasZtbsv = cublasZtbsv_v2
const cublasSsymv = cublasSsymv_v2
const cublasDsymv = cublasDsymv_v2
const cublasCsymv = cublasCsymv_v2
const cublasZsymv = cublasZsymv_v2
const cublasChemv = cublasChemv_v2
const cublasZhemv = cublasZhemv_v2
const cublasSsbmv = cublasSsbmv_v2
const cublasDsbmv = cublasDsbmv_v2
const cublasChbmv = cublasChbmv_v2
const cublasZhbmv = cublasZhbmv_v2
const cublasSspmv = cublasSspmv_v2
const cublasDspmv = cublasDspmv_v2
const cublasChpmv = cublasChpmv_v2
const cublasZhpmv = cublasZhpmv_v2
const cublasSger = cublasSger_v2
const cublasDger = cublasDger_v2
const cublasCgeru = cublasCgeru_v2
const cublasCgerc = cublasCgerc_v2
const cublasZgeru = cublasZgeru_v2
const cublasZgerc = cublasZgerc_v2
const cublasSsyr = cublasSsyr_v2
const cublasDsyr = cublasDsyr_v2
const cublasCsyr = cublasCsyr_v2
const cublasZsyr = cublasZsyr_v2
const cublasCher = cublasCher_v2
const cublasZher = cublasZher_v2
const cublasSspr = cublasSspr_v2
const cublasDspr = cublasDspr_v2
const cublasChpr = cublasChpr_v2
const cublasZhpr = cublasZhpr_v2
const cublasSsyr2 = cublasSsyr2_v2
const cublasDsyr2 = cublasDsyr2_v2
const cublasCsyr2 = cublasCsyr2_v2
const cublasZsyr2 = cublasZsyr2_v2
const cublasCher2 = cublasCher2_v2
const cublasZher2 = cublasZher2_v2
const cublasSspr2 = cublasSspr2_v2
const cublasDspr2 = cublasDspr2_v2
const cublasChpr2 = cublasChpr2_v2
const cublasZhpr2 = cublasZhpr2_v2
const cublasSgemm = cublasSgemm_v2
const cublasDgemm = cublasDgemm_v2
const cublasCgemm = cublasCgemm_v2
const cublasZgemm = cublasZgemm_v2
const cublasSsyrk = cublasSsyrk_v2
const cublasDsyrk = cublasDsyrk_v2
const cublasCsyrk = cublasCsyrk_v2
const cublasZsyrk = cublasZsyrk_v2
const cublasCherk = cublasCherk_v2
const cublasZherk = cublasZherk_v2
const cublasSsyr2k = cublasSsyr2k_v2
const cublasDsyr2k = cublasDsyr2k_v2
const cublasCsyr2k = cublasCsyr2k_v2
const cublasZsyr2k = cublasZsyr2k_v2
const cublasCher2k = cublasCher2k_v2
const cublasZher2k = cublasZher2k_v2
const cublasSsymm = cublasSsymm_v2
const cublasDsymm = cublasDsymm_v2
const cublasCsymm = cublasCsymm_v2
const cublasZsymm = cublasZsymm_v2
const cublasChemm = cublasChemm_v2
const cublasZhemm = cublasZhemm_v2
const cublasStrsm = cublasStrsm_v2
const cublasDtrsm = cublasDtrsm_v2
const cublasCtrsm = cublasCtrsm_v2
const cublasZtrsm = cublasZtrsm_v2
const cublasStrmm = cublasStrmm_v2
const cublasDtrmm = cublasDtrmm_v2
const cublasCtrmm = cublasCtrmm_v2
const cublasZtrmm = cublasZtrmm_v2

# begin enum ANONYMOUS_1
typealias ANONYMOUS_1 UInt32
const CUBLAS_STATUS_SUCCESS = (UInt32)(0)
const CUBLAS_STATUS_NOT_INITIALIZED = (UInt32)(1)
const CUBLAS_STATUS_ALLOC_FAILED = (UInt32)(3)
const CUBLAS_STATUS_INVALID_VALUE = (UInt32)(7)
const CUBLAS_STATUS_ARCH_MISMATCH = (UInt32)(8)
const CUBLAS_STATUS_MAPPING_ERROR = (UInt32)(11)
const CUBLAS_STATUS_EXECUTION_FAILED = (UInt32)(13)
const CUBLAS_STATUS_INTERNAL_ERROR = (UInt32)(14)
const CUBLAS_STATUS_NOT_SUPPORTED = (UInt32)(15)
const CUBLAS_STATUS_LICENSE_ERROR = (UInt32)(16)
# end enum ANONYMOUS_1

# begin enum cublasStatus_t
typealias cublasStatus_t UInt32
const CUBLAS_STATUS_SUCCESS = (UInt32)(0)
const CUBLAS_STATUS_NOT_INITIALIZED = (UInt32)(1)
const CUBLAS_STATUS_ALLOC_FAILED = (UInt32)(3)
const CUBLAS_STATUS_INVALID_VALUE = (UInt32)(7)
const CUBLAS_STATUS_ARCH_MISMATCH = (UInt32)(8)
const CUBLAS_STATUS_MAPPING_ERROR = (UInt32)(11)
const CUBLAS_STATUS_EXECUTION_FAILED = (UInt32)(13)
const CUBLAS_STATUS_INTERNAL_ERROR = (UInt32)(14)
const CUBLAS_STATUS_NOT_SUPPORTED = (UInt32)(15)
const CUBLAS_STATUS_LICENSE_ERROR = (UInt32)(16)
# end enum cublasStatus_t

# begin enum ANONYMOUS_2
typealias ANONYMOUS_2 UInt32
const CUBLAS_FILL_MODE_LOWER = (UInt32)(0)
const CUBLAS_FILL_MODE_UPPER = (UInt32)(1)
# end enum ANONYMOUS_2

# begin enum cublasFillMode_t
typealias cublasFillMode_t UInt32
const CUBLAS_FILL_MODE_LOWER = (UInt32)(0)
const CUBLAS_FILL_MODE_UPPER = (UInt32)(1)
# end enum cublasFillMode_t

# begin enum ANONYMOUS_3
typealias ANONYMOUS_3 UInt32
const CUBLAS_DIAG_NON_UNIT = (UInt32)(0)
const CUBLAS_DIAG_UNIT = (UInt32)(1)
# end enum ANONYMOUS_3

# begin enum cublasDiagType_t
typealias cublasDiagType_t UInt32
const CUBLAS_DIAG_NON_UNIT = (UInt32)(0)
const CUBLAS_DIAG_UNIT = (UInt32)(1)
# end enum cublasDiagType_t

# begin enum ANONYMOUS_4
typealias ANONYMOUS_4 UInt32
const CUBLAS_SIDE_LEFT = (UInt32)(0)
const CUBLAS_SIDE_RIGHT = (UInt32)(1)
# end enum ANONYMOUS_4

# begin enum cublasSideMode_t
typealias cublasSideMode_t UInt32
const CUBLAS_SIDE_LEFT = (UInt32)(0)
const CUBLAS_SIDE_RIGHT = (UInt32)(1)
# end enum cublasSideMode_t

# begin enum ANONYMOUS_5
typealias ANONYMOUS_5 UInt32
const CUBLAS_OP_N = (UInt32)(0)
const CUBLAS_OP_T = (UInt32)(1)
const CUBLAS_OP_C = (UInt32)(2)
# end enum ANONYMOUS_5

# begin enum cublasOperation_t
typealias cublasOperation_t UInt32
const CUBLAS_OP_N = (UInt32)(0)
const CUBLAS_OP_T = (UInt32)(1)
const CUBLAS_OP_C = (UInt32)(2)
# end enum cublasOperation_t

# begin enum ANONYMOUS_6
typealias ANONYMOUS_6 UInt32
const CUBLAS_POINTER_MODE_HOST = (UInt32)(0)
const CUBLAS_POINTER_MODE_DEVICE = (UInt32)(1)
# end enum ANONYMOUS_6

# begin enum cublasPointerMode_t
typealias cublasPointerMode_t UInt32
const CUBLAS_POINTER_MODE_HOST = (UInt32)(0)
const CUBLAS_POINTER_MODE_DEVICE = (UInt32)(1)
# end enum cublasPointerMode_t

# begin enum ANONYMOUS_7
typealias ANONYMOUS_7 UInt32
const CUBLAS_ATOMICS_NOT_ALLOWED = (UInt32)(0)
const CUBLAS_ATOMICS_ALLOWED = (UInt32)(1)
# end enum ANONYMOUS_7

# begin enum cublasAtomicsMode_t
typealias cublasAtomicsMode_t UInt32
const CUBLAS_ATOMICS_NOT_ALLOWED = (UInt32)(0)
const CUBLAS_ATOMICS_ALLOWED = (UInt32)(1)
# end enum cublasAtomicsMode_t

# begin enum ANONYMOUS_8
typealias ANONYMOUS_8 UInt32
const CUBLAS_DATA_FLOAT = (UInt32)(0)
const CUBLAS_DATA_DOUBLE = (UInt32)(1)
const CUBLAS_DATA_HALF = (UInt32)(2)
const CUBLAS_DATA_INT8 = (UInt32)(3)
# end enum ANONYMOUS_8

# begin enum cublasDataType_t
typealias cublasDataType_t UInt32
const CUBLAS_DATA_FLOAT = (UInt32)(0)
const CUBLAS_DATA_DOUBLE = (UInt32)(1)
const CUBLAS_DATA_HALF = (UInt32)(2)
const CUBLAS_DATA_INT8 = (UInt32)(3)
# end enum cublasDataType_t

typealias cublasContext Void
typealias cublasHandle_t Ptr{cublasContext}


module kernels
  using CUDAnative, CuArrays
  export sync, dispatch
  macro sync(type, expr)
    ex = quote
      if $type == CuArray
        CuArrays.@sync begin
        $expr
        end
      end
      if $type == Array
        $expr
      end
    end
    return esc(ex)
  end

  macro dispatch(type, threads, blocks, expr)
    cuda = Meta.parse("kernels.cuda_$expr")
    cpu = Meta.parse("kernels.cpu_$expr")
    ex = nothing
    ex = quote
      if $type == CuArray
          @cuda $threads $blocks $cuda
      end
      if $type == Array
          $cpu
      end
    end
    return esc(ex)
  end
  function cuda_term1(viewToR, cuVm, colptrVm, nzvalVm, colptrYftR, nzvalYftR,
                              cuVa, colptrVa, nzvalVa, colptrYftI, nzvalYftI,
                              sizeFromLines)
      index = (blockIdx().x - 1) * blockDim().x + threadIdx().x
      stride = blockDim().x * gridDim().x
      for b in index:stride:size(viewToR,1)
        for (i,c) in enumerate(colptrVm[b]:colptrVm[b+1]-1)
          @inbounds viewToR[b] += cuVm[b] * nzvalVm[c] *
                                  (  nzvalYftR[c] * CUDAnative.cos(cuVa[b] - nzvalVa[c])
                                   + nzvalYftI[c] * CUDAnative.sin(cuVa[b] - nzvalVa[c])
                                  )
        end
      end
      return nothing
  end

  function cuda_term2(viewFromR, cuVm, colptrVm, nzvalVm, colptrYtfR, nzvalYtfR,
                                  cuVa, colptrVa, nzvalVa, colptrYtfI, nzvalYtfI,
                                  sizeToLines)
      index = (blockIdx().x - 1) * blockDim().x + threadIdx().x
      stride = blockDim().x * gridDim().x
      for b in index:stride:size(viewFromR,1)
        for (i,c) in enumerate(colptrVm[b]:colptrVm[b+1]-1)
          @inbounds viewFromR[b] += cuVm[b] * nzvalVm[c] *
                                    (  nzvalYtfR[c] * CUDAnative.cos(cuVa[b] - nzvalVa[c])
                                     + nzvalYtfI[c] * CUDAnative.sin(cuVa[b] - nzvalVa[c])
                                    )
        end
      end
      return nothing
  end

  function cuda_term3(viewToI, cuVm, colptrVm, nzvalVm, colptrYftI, nzvalYftI,
                              cuVa, colptrVa, nzvalVa, colptrYftR, nzvalYftR,
                              sizeFromLines)
      index = (blockIdx().x - 1) * blockDim().x + threadIdx().x
      stride = blockDim().x * gridDim().x
      for b in index:stride:size(viewToI,1)
        for (i,c) in enumerate(colptrVm[b]:colptrVm[b+1]-1)
          @inbounds viewToI[b] += cuVm[b] * nzvalVm[c] *
                                  (  ( -nzvalYftI[c]) * CUDAnative.cos(cuVa[b] - nzvalVa[c])
                                      + nzvalYftR[c] * CUDAnative.sin(cuVa[b] - nzvalVa[c])
                                  )
        end
      end
      return nothing
  end

  function cuda_term4(viewFromI, cuVm, colptrVm, nzvalVm, colptrYtfI, nzvalYtfI,
                                  cuVa, colptrVa, nzvalVa, colptrYtfR, nzvalYtfR,
                                  sizeToLines)
      index = (blockIdx().x - 1) * blockDim().x + threadIdx().x
      stride = blockDim().x * gridDim().x
      for b in index:stride:size(viewFromI,1)
        for (i,c) in enumerate(colptrVm[b]:colptrVm[b+1]-1)
          @inbounds viewFromI[b] += cuVm[b] * nzvalVm[c] *
                                    (  ( -nzvalYtfI[c]) * CUDAnative.cos(cuVa[b] - nzvalVa[c])
                                        + nzvalYtfR[c] * CUDAnative.sin(cuVa[b] - nzvalVa[c])
                                    )
        end
      end
      return nothing
  end
  function cuda_sumPg(sumPg, colptr, nzval)
      index = (blockIdx().x - 1) * blockDim().x + threadIdx().x
      stride = blockDim().x * gridDim().x
      for b in index:stride:size(sumPg,1)
        for c in colptr[b]:colptr[b+1]-1
            @inbounds sumPg[b] += nzval[c]
        end
      end
      return nothing
  end
  function cuda_sumQg(sumQg, colptr, nzval)
      index = (blockIdx().x - 1) * blockDim().x + threadIdx().x
      stride = blockDim().x * gridDim().x
      for b in index:stride:size(sumQg,1)
        for c in colptr[b]:colptr[b+1]-1
            @inbounds sumQg[b] += nzval[c]
        end
      end
      return nothing
  end
  function cpu_term1(viewToR, cuVm, colptrVm, nzvalVm, colptrYftR, nzvalYftR,
                              cuVa, colptrVa, nzvalVa, colptrYftI, nzvalYftI,
                              sizeFromLines)
      Threads.@threads for b in 1:size(viewToR,1)
        for (i,c) in enumerate(colptrVm[b]:colptrVm[b+1]-1)
          viewToR[b] += cuVm[b] * nzvalVm[c] *
                                  (  nzvalYftR[c] * cos(cuVa[b] - nzvalVa[c])
                                   + nzvalYftI[c] * sin(cuVa[b] - nzvalVa[c])
                                  )
        end
      end
      return nothing
  end

  function cpu_term2(viewFromR, cuVm, colptrVm, nzvalVm, colptrYtfR, nzvalYtfR,
                                  cuVa, colptrVa, nzvalVa, colptrYtfI, nzvalYtfI,
                                  sizeToLines)
      for b in 1:size(viewFromR,1)
        for (i,c) in enumerate(colptrVm[b]:colptrVm[b+1]-1)
          viewFromR[b] += cuVm[b] * nzvalVm[c] *
                                    (  nzvalYtfR[c] * cos(cuVa[b] - nzvalVa[c])
                                     + nzvalYtfI[c] * sin(cuVa[b] - nzvalVa[c])
                                    )
        end
      end
      return nothing
  end

  function cpu_term3(viewToI, cuVm, colptrVm, nzvalVm, colptrYftI, nzvalYftI,
                              cuVa, colptrVa, nzvalVa, colptrYftR, nzvalYftR,
                              sizeFromLines)
      for b in 1:size(viewToI,1)
        for (i,c) in enumerate(colptrVm[b]:colptrVm[b+1]-1)
          viewToI[b] += cuVm[b] * nzvalVm[c] *
                                  (  ( -nzvalYftI[c]) * cos(cuVa[b] - nzvalVa[c])
                                      + nzvalYftR[c] * sin(cuVa[b] - nzvalVa[c])
                                  )
        end
      end
      return nothing
  end

  function cpu_term4(viewFromI, cuVm, colptrVm, nzvalVm, colptrYtfI, nzvalYtfI,
                                  cuVa, colptrVa, nzvalVa, colptrYtfR, nzvalYtfR,
                                  sizeToLines)
      for b in 1:size(viewFromI,1)
        for (i,c) in enumerate(colptrVm[b]:colptrVm[b+1]-1)
          viewFromI[b] += cuVm[b] * nzvalVm[c] *
                                    (  ( -nzvalYtfI[c]) * cos(cuVa[b] - nzvalVa[c])
                                        + nzvalYtfR[c] * sin(cuVa[b] - nzvalVa[c])
                                    )
        end
      end
      return nothing
  end
  function cpu_sumPg(sumPg, colptr, nzval)
      for b in 1:size(sumPg,1)
        for c in colptr[b]:colptr[b+1]-1
            @inbounds sumPg[b] += nzval[c]
        end
      end
      return nothing
  end
  function cpu_sumQg(sumQg, colptr, nzval)
      for b in 1:size(sumQg,1)
        for c in colptr[b]:colptr[b+1]-1
            sumQg[b] += nzval[c]
        end
      end
      return nothing
  end
end


# Dog <-> World
function move( dog::Dog, world::World, dt, motion::Motion )
    new_position, new_velocity = step( motion, dog.position, dog.velocity, dt )
    new_position = pbc( new_position, world )
    return Dog( dog; position = new_position, velocity = new_velocity )
end # function

function move_dogs!( world::World, dt )
    dogs = world.dogs
    dogs.member .= move.( dogs.member, Ref(world), dt, Ref(dogs.motion) )
    return nothing
end # function


# We use getters so the model never accesses struct fields directly,
# except in methods for specific leaf types where declaring all the gettier
# methods would be overkill.
#
# A benefit of this is parameters can be easily moved between shared and specific
# parameter structs.

# So we don't have to depend on all of Lazy.jl
macro forward(ex, fs)
  @capture(ex, T_.field_) || error("Syntax: @forward T.x f, g, h")
  T = esc(T)
  fs = isexpr(fs, :tuple) ? map(esc, fs.args) : [esc(fs)]
  :($([:($f(x::$T, args...) = (Base.@_inline_meta; $f(x.$field, args...)))
       for f in fs]...);
    nothing)
end

@inline dead(o::AbstractOrganism) = o.dead[]
@inline set_dead!(o::AbstractOrganism, val) = o.dead[] = val
@inline environment(o::AbstractOrganism) = o.environment

@inline vars(o::AbstractOrgan) = o.vars
@inline flux(o::AbstractOrgan) = o.J
@inline flux1(o::AbstractOrgan) = o.J1

@inline tstep(v) = v.t[1]
@inline set_tstep!(v, val) = v.t[1] = val

const varfuncs = [:shape, :rate, :temp, :θE, :tempcorrection, :height]
for (v, sv) in zip(varfuncs, Symbol.(:set_, varfuncs, :!))
    @eval @inline ($v)(vars) = vars.$v[tstep(vars)]
    @eval @inline ($sv)(vars, val) = vars.$v[tstep(vars)] = val
end

@inline depth(v) = height(v)

rate_formula(p) = p.rate_formula
assimilation_pars(p) = p.assimilation_pars
shape_pars(p) = p.shape_pars
allometry_pars(p) = p.allometry_pars
maturity_pars(p) = p.maturity_pars
trans_pars(p) = p.trans_pars
production_pars(p) = p.production_pars
rejection_pars(p) = p.rejection_pars
germination_pars(p) = p.germination_pars
turnover_pars(p) = p.turnover_pars
feedback_pars(p) = p.feedback_pars
tempcorr_pars(p) = p.tempcorr_pars
core_pars(p) = p.core_pars
su_pars(p) = p.su_pars
catabolism_pars(p) = p.catabolism_pars
maintenance_pars(p) = p.maintenance_pars

@forward AbstractOrgan.vars θE, temp, set_temp!, tempcorrection, set_tempcorrection!,
         height, set_height!, rate, set_rate!, shape, set_shape!, tstep, set_tstep!

@forward AbstractOrgan.params rate_formula, assimilation_pars, shape_pars, allometry_pars, maturity_pars,
                              trans_pars, production_pars, rejection_pars, germination_pars, turnover_pars

@forward AbstractOrgan.shared maintenance_pars, feedback_pars, su_pars, tempcorr_pars, catabolism_pars, core_pars,
                              y_V_E, y_E_EC, y_E_EN, n_N_P, n_N_V, n_N_E, n_N_EC, n_N_EN, w_V, w_C, w_N, w_E

n_N_P(p) = production_pars(p).n_N_P
y_V_E(p) = core_pars(p).y_V_E
y_E_EC(p) = core_pars(p).y_E_EC
y_E_EN(p) = core_pars(p).y_E_EN
n_N_V(p) = core_pars(p).n_N_V
n_N_E(p) = core_pars(p).n_N_E
n_N_EC(p) = core_pars(p).n_N_EC
n_N_EN(p) = core_pars(p).n_N_EN
w_V(p) = core_pars(p).w_V
w_C(p) = core_pars(p).w_C
w_N(p) = core_pars(p).w_N
w_E(p) = core_pars(p).w_E

j_E_mai(o::AbstractOrgan) = j_E_mai(maintenance_pars(o))

κtra(o::AbstractOrgan) = κtra(trans_pars(o))
κtra(o::Nothing) = 0.0

κmat(o::AbstractOrgan) = κmat(maturity_pars(o))
κmat(::Nothing) = 0.0

κsoma(o::AbstractOrgan) = oneunit(κtra(o)) - κtra(o) - κmat(o)

mass(o, u) = u.V * w_V(o)


"""
    mbimport(cmd0::String="", arg1=nothing, arg2=nothing, arg3=nothing; kwargs...)

Produces a gray-shaded (or colored) map by plotting rectangles centered on each grid node and assigning them a gray-shade (or color) based on the z-value.

Parameters
----------

- **A** | **footprint** :: [Type => Str | Tuple]	``Arg = factor/mode/depth``

    Determines how the along-track dimension of the beam or pixel footprints is calculated.
- $(GMT.opt_J)
- $(GMT.opt_C)
- **D** | **scaling** :: [Type => Str | Tuple]	``Arg = mode/scale/min/max``

    Sets scaling of beam amplitude or sidescan pixel values which can be applied before plotting.
- **E** | **dpi** :: [Type => Int]

    Sets the resolution of the projected image that will be created.
- **G** | **bit_color** :: [Type => Str | Tuple]	``Arg = magnitude/azimuth or magnitude/median``

    Sets the parameters controlng simulated illumination of bathymetry.
- **S** | **speed** :: [Type => Number]

    Sets the minimum speed in km/hr (5.5 kts ~ 10 km/hr) allowed in the input data.
- **T** | **timegap** :: [Type => number]

    Sets the maximum time gap in minutes between adjacent pings before being considered a gap.
- **Z** | **type_plot** :: [Type => Str | Number]

    Sets the style of the plot.
- $(GMT.opt_R)
- $(GMT.opt_U)
- $(GMT.opt_V)
- $(GMT.opt_X)
- $(GMT.opt_Y)
- $(GMT.opt_n)
- $(GMT.opt_t)
"""
function mbimport(cmd0::String=""; kwargs...)

	length(kwargs) == 0 && occursin(" -", cmd0) && return monolitic("mbimport", cmd0)

	d = init_module(false, kwargs...)[1]		# Also checks if the user wants ONLY the HELP mode

	cmd, opt_R = parse_R(d, "")
	cmd, opt_J = parse_J(d, cmd, " -JX12cd/0", true)
	cmd, = parse_common_opts(d, cmd, [:UVXY :params :n :t], true)

	cmd  = parse_these_opts(cmd, d, [[:A :footprint], [:D :scaling], [:F :format], [:E :dpi], [:G :shade],
	                                 [:S :speed], [:T :timegap], [:b :star_time], [:e :end_time]])
	cmd = add_opt(d, cmd, 'Z', [:Z :type_plot], (bat="_1", shaded_bat="_2", shaded_amp="_3", amp="_4", sscan="_5"))

	cmd = add_opt(d, cmd, "%", [:layout :mem_layout], nothing)
	cmd, arg1, = add_opt_cpt(d, cmd, CPTaliases, 'C', 0, nothing)
	N_args = (arg1 === nothing) ? 0 : 1
	cmd, arg1, arg2, = add_opt_cpt(d, cmd, [:N :color_amp], 'N', N_args, arg1, nothing)

	cmd = "mbimport -I" * cmd0 * cmd				# In any case we need this
	finish_PS_module(d, cmd, "", true, false, false, arg1, arg2)
end


"""
    TrackingResult

This type stores a vector of `blobs` that were active when tracking terminated, a vector `dead` of blobs that died during tracking and a vector of all measurements obtained during tracking. This type supports `allblobs, trace`
"""
TrackingResult

Base.@kwdef mutable struct TrackingResult
    blobs::Vector{Blob} = Blob[]
    dead::Vector{Blob} = Blob[]
    coordinates::Vector{Trace} = Trace[]
end

Base.isempty(result::TrackingResult) = isempty(result.blobs) && isempty(result.dead)

"""
    allblobs(tr)

Get all blobs in a tracking result
"""
allblobs(tr::TrackingResult) = [tr.dead; tr.blobs]

"""
    trace(tr::TrackingResult; minlife=0)

Get all traces in a tracking result. Optionally filter based on minimum lifetime of a blob (the blob must have seen this many measurements).
"""
function trace(tr::TrackingResult; minlife=0)
    blobs = allblobs(tr)
    filter!(b->lifetime(b)>=minlife, blobs)
    trace.(blobs)
end

function tracem(tr::TrackingResult; minlife=0)
    blobs = allblobs(tr)
    filter!(b->lifetime(b)>=minlife, blobs)
    tracem.(blobs)
end

log_measurement(result::TrackingResult, m::Measurement) = push!(result.coordinates, m.coordinates)
log_measurement(result::TrackingResult, ms::Vector{Measurement}) = push!(result.coordinates, ms[1].coordinates)
Distributions.location(result::TrackingResult) = result.coordinates


# Define a linear model, cross validate it and add some synthetic data.
#

abstract type ExampleModel end


using NeRCA
using Test

const ONLINEFILE = joinpath(@__DIR__, "data", "km3net_online.root")

hits = [Hit(1, 8, 100, 20, false),
        Hit(2, 9, 101, 21, true),
        Hit(3, 8, 112, 22, true),
        Hit(4, 8, 114, 23, false),
        Hit(5, 8, 134, 24, true),
        Hit(6, 8, 156, 25, false),
        Hit(7, 8, 133, 26, true),
        Hit(8, 8, 145, 26, false),
        Hit(9, 10, 900, 26, false),
        Hit(1, 11, 1000, 26, false),
        Hit(1, 11, 1001, 26, false),
        Hit(1, 11, 1002, 26, false)]

# triggered()
@testset "triggered()" begin
    thits = triggered(hits)
    @test 4 == length(thits)
    @test 9 == thits[1].dom_id
end

# nfoldhits()
@testset "nfoldhits()" begin
    twofoldhits = nfoldhits(hits, 10, 2)
    @test 4 == length(twofoldhits)
    threefoldhits = nfoldhits(hits, 15, 3)
    @test 3 == length(threefoldhits)
end

# domhits()
@testset "domhits()" begin
    dhits = domhits(hits)
    @test 7 == length(dhits[8])
    @test 20 == dhits[8][1].tot
    @test 1 == dhits[8][6].trigger_mask
end

# multiplicities
sorted_hits = sort(hits, by=h->h.t)
sort!(sorted_hits, by=h->h.dom_id)
mtps, mtp_ids = NeRCA.count_multiplicities(sorted_hits, 10)
@test (1, 1) == (mtps[1], mtp_ids[1])
@test (2, 2) == (mtps[2], mtp_ids[2])
@test (2, 2) == (mtps[3], mtp_ids[3])
@test (2, 3) == (mtps[4], mtp_ids[4])
@test (2, 3) == (mtps[5], mtp_ids[5])
@test (1, 4) == (mtps[6], mtp_ids[6])
@test (1, 5) == (mtps[7], mtp_ids[7])
@test (1, 6) == (mtps[8], mtp_ids[8])
@test (1, 7) == (mtps[9], mtp_ids[9])
@test (3, 8) == (mtps[10], mtp_ids[10])
@test (3, 8) == (mtps[11], mtp_ids[11])
@test (3, 8) == (mtps[12], mtp_ids[12])

# combine
event_shits = NeRCA.read_snapshot_hits(NeRCA.OnlineFile(ONLINEFILE))
event_thits = NeRCA.read_triggered_hits(NeRCA.OnlineFile(ONLINEFILE))
for (shits, thits) in zip(event_shits, event_thits)
    local hits = NeRCA.combine(shits, thits)
    @test length(hits) == length(shits)
    @test length(NeRCA.triggered(hits)) == length(thits)
end


@testset "Constructors" begin
    @testset "Vector" begin
        @testset "Dense Vector" begin
            test_frule(GBVector, rand(-10.0:0.05:10.0, 10))
            test_rrule(GBVector, rand(-10.0:0.05:10.0, 10))
        end
        @testset "Sparse Vector" begin
            test_frule(GBVector, [1,3,5] ⊢ NoTangent(), rand(-10.0:0.05:10.0, 3))
            test_rrule(GBVector, [1,4,5] ⊢ NoTangent(), rand(-10.0:0.05:10.0, 3))
        end
    end
    @testset "Matrix" begin
        @testset "Dense Matrix and Vector" begin
            M = rand(-10.0:0.05:10.0, 10, 20)
            v = rand(-10.0:0.05:10.0, 10)
            test_frule(GBMatrix, M)
            test_rrule(GBMatrix, M)
            test_frule(GBMatrix, v)
            test_rrule(GBMatrix, v)
        end
    end
end


const metre = 1e2
const meter = metre
const cm = metre / 1e2
const mm = metre / 1e3
const um = metre / 1e6
const nm = metre / 1e9

const second = 1e3
const ms = second / 1e3
const Hz = 1 / second

const voltage = 1e3
const mV = voltage / 1e3

const ampere = 1e6
const mA = ampere / 1e3
const uA = ampere / 1e6
const nA = ampere / 1e9

const farad=1e6
const uF = farad/1e6
const ufarad = uF

const siemens = 1e3
const mS = siemens / 1e3
const msiemens = mS
const nS = siemens / 1e9
const nsiemens = nS


"""
	$(SIGNATURES)

Initialize an empty, preallocated `WorkHistories` object.
"""
function WorkHistories(F1 :: DataType, nSim :: Integer)
    return WorkHistories(
        zeros(UInt8, nSim),
        zeros(TimeInt, nSim),
        zeros(F1, nSim), zeros(F1, nSim),
        zeros(F1, nSim), zeros(F1, nSim),
        zeros(F1, nSim), zeros(F1, nSim)
    )
end

"""
	$(SIGNATURES)

Create `WorkHistories` with work start endowments filled in.
"""
function WorkHistories(iSchoolV, workStartAgeV,
    hWorkStartV :: Vector{F1}, kWorkStartV :: Vector{F1}) where F1 <: AbstractFloat

    nSim = length(iSchoolV);
    return WorkHistories(
        UInt8.(iSchoolV), TimeInt.(workStartAgeV), hWorkStartV, kWorkStartV,
        zeros(F1, nSim), zeros(F1, nSim),
        zeros(F1, nSim), zeros(F1, nSim)
    )
end

Base.show(io :: IO, wh :: WorkHistories{F1}) where F1 =
    print(io, "WorkHistories of length $(length(wh)).")

Base.length(wh :: WorkHistories) = length(wh.workStartAgeV);
school_index(wh :: WorkHistories) = wh.iSchoolV;
work_start_ages(wh :: WorkHistories) = wh.workStartAgeV;
h_work_start(wh :: WorkHistories{F1}) where F1 = wh.hWorkStartV;
k_work_start(wh :: WorkHistories{F1}) where F1 = wh.kWorkStartV;
n_school(wh :: WorkHistories) = maximum(school_index(wh));

function validate_wh(wh :: WorkHistories{F1}) where F1
    isValid = true;
    if any_less(wh.workStartAgeV, 1)  ||  any_greater(wh.workStartAgeV, 9)
        isValid = false;
        @warn "Invalid work start ages"
    end
    if any_at_most(wh.earnWorkStartV, zero(F1))
        isValid = false;
        @warn "Earnings at work start should be positive"
    end
    if any_at_most(wh.ltEarnV, zero(F1))
        isValid = false;
        @warn "Lifetime earnings should be positive"
    end
    if any_at_most(wh.cWorkStartV, zero(F1))
        isValid = false
        @warn "Negative consumption at work start"
    end
    if any_at_most(wh.muWealthV, zero(F1))
        isValid = false;
        @warn "Negative marginal utility of wealth"
    end
    return isValid
end

"""
	$(SIGNATURES)

Simulate work histories. Input are `WorkHistories` that are initialized with workers' initial conditions: work start age, schooling, initial h and k.
"""
function simulate_workers!(wh :: WorkHistories{F1},
    workerV :: Vector{Worker{F1}}) where F1

    for iSchool = 1 : n_school(wh)
        sIdxV = findall(s -> s == iSchool,  school_index(wh));
        if !isempty(sIdxV)
            simulate_one_worker!(wh, workerV[iSchool], sIdxV);
        end
    end

    @assert validate_wh(wh)
    return nothing
end

"""
	$(SIGNATURES)

Simulate workers for one school level. Workers in rows `sIdxV`.
"""
function simulate_one_worker!(wh :: WorkHistories{F1}, wk :: Worker{F1},
    sIdxV :: AbstractVector{I1}) where {F1 <: AbstractFloat, I1 <: Integer}

    startAgeV = unique(wh.workStartAgeV[sIdxV]);
    for workStartAge in startAgeV
        idxV = findall(a -> a == workStartAge, work_start_ages(wh)[sIdxV]);
        simulate_one_work_start!(wh, wk, workStartAge, sIdxV[idxV]);
    end
    return nothing
end

# Work start age must be common to all
function simulate_one_work_start!(wh :: WorkHistories{F1}, wk :: Worker{F1},
    workStartAge :: Integer, idxV :: AbstractVector{I1}) where {F1 <: AbstractFloat, I1 <: Integer}

    # Earnings at work start
    # Earnings profile, up to `h` factor
    earnWorkStart = earn_profile(wk, workStartAge, 1.0; experV = 1);
    wh.earnWorkStartV[idxV] .= h_work_start(wh)[idxV] .* earnWorkStart;

    wh.ltEarnV[idxV] .= lifetime_earnings(wk, workStartAge,
        h_work_start(wh)[idxV]);
    # Bound below in case we have bad parameter values during testing
    ltIncomeV = max.(0.1, wh.ltEarnV[idxV] .+ k_work_start(wh)[idxV]);
    # Consumption at work start
    wh.cWorkStartV[idxV] .= cons_age1(wk, workStartAge, ltIncomeV);
    # Marginal utility of wealth as of work start
    wh.muWealthV[idxV] .= mu_wealth(wk, workStartAge, ltIncomeV);
    return nothing
end

function make_test_work_histories(nSchool :: Integer, nSim :: Integer)
    rng = MersenneTwister(43);
    iSchoolV = rand(rng, UInt8.(1 : nSchool), nSim);
    workStartAgeV = rand(rng, TimeInt.(1:5), nSim);
    hWorkStartV = 1.0 .+ 2.0 .* rand(rng, nSim);
    kWorkStartV = rand(rng, nSim) .- 0.1;
    earnWorkStartV = 9.0 .+ 3.0 .* rand(rng, nSim);
    ltEarnV = 10.5 .* earnWorkStartV;
    muWealthV = 5.0 ./ earnWorkStartV;
    cWorkStartV = 0.7 .* earnWorkStartV;

    wh = WorkHistories(iSchoolV, workStartAgeV, hWorkStartV, kWorkStartV,
        earnWorkStartV, ltEarnV, muWealthV, cWorkStartV);
    @assert validate_wh(wh)  "Invalid Test work history"
    return wh
end

# ----------------


export LaneFollowingDriver

type LaneFollowingDriver <: DriverModel{LaneFollowingAccel, IntegratedContinuous}
    C::IntegratedContinuous
    mlon::LongitudinalDriverModel
end

get_name(model::LaneFollowingDriver) = @sprintf("%s", get_name(model.mlon))
action_context(model::LaneFollowingDriver) = model.C
function observe!(model::LaneFollowingDriver, scene::Scene, roadway::Roadway, egoid::Int)
    observe!(model.mlon, scene, roadway, egoid)
    model
end
Base.rand(model::LaneFollowingDriver) = LaneFollowingAccel(rand(model.mlon))
Distributions.pdf(model::LaneFollowingDriver, a::LaneFollowingAccel) = pdf(model.mlon, a.a)
Distributions.logpdf(model::LaneFollowingDriver, a::LaneFollowingAccel) = logpdf(model.mlon, a.a)


export MILLISECONDS_IN_SECOND, NANOSECONDS_IN_SECOND, NANOSECONDS_IN_MILLISECOND
export setTimeout

const MILLISECONDS_IN_SECOND     = 1_000.0::Float64
const NANOSECONDS_IN_SECOND      = 1_000_000_000.0::Float64
const NANOSECONDS_IN_MILLISECOND = 1_000_000.0::Float64

"""
    setTimeout(f::Function, timeout::Int)

Creates a `Task` which will asynchornously invoke fucntion `f` after specified `timeout` time in milliseconds.

# Arguments
- `f`::Function, function to be invoked asynchronously
- `timeout`::Int, timeout in milliseconds

# Examples

```
using Rocket
using Dates

println("Before: ", Dates.format(now(), "MM:SS"))
setTimeout(1000) do
    println("Inside: ", Dates.format(now(), "MM:SS"))
end
println("Right after: ", Dates.format(now(), "MM:SS"))
;

# Logs
# Before: 20:59
# Right after: 20:59
# Inside: 21:00
```
"""
function setTimeout(f::Function, timeout::Int)
    @async begin
        sleep(timeout / MILLISECONDS_IN_SECOND)
        f()
    end
end

"""
    combined_type(sources)

Returns a Tuple el-type of observable el-types in `sources` argument in the same order
"""
combined_type(sources) = Tuple{ map(source -> subscribable_extract_type(source), sources)... }

"""
    union_type(sources)

Returns a Union el-type of observable el-types in `sources` argument
"""
union_type(sources) = Union{ map(source -> subscribable_extract_type(source), sources)... }