l_min(SHModes::SphericalHarmonicModes.SHModeRange) = minimum(l_range(SHModes))
l_max(SHModes::SphericalHarmonicModes.SHModeRange) = maximum(l_range(SHModes))

m_min(SHModes::SphericalHarmonicModes.SHModeRange) = minimum(m_range(SHModes))
m_max(SHModes::SphericalHarmonicModes.SHModeRange) = maximum(m_range(SHModes))

########################################################################################
# Utility function
########################################################################################

function getkernelmodes(; kwargs...)
	s_min = get(kwargs, :s_min, 0) :: Int
	s_max = get(kwargs, :s_max, s_min) :: Int
	t_min = get(kwargs, :t_min, 0) :: Int
	t_max = get(kwargs, :t_max, t_min) :: Int

	m = get(kwargs, :SHModes, LM(s_min:s_max, t_min:t_max))
	LM{UnitRange{Int}, UnitRange{Int}}(m)
end

function unpackGfnparams(p_Gsrc, r_src, p_Gobs1, r_obs1, p_Gobs2, r_obs2)
	p_Gsrc = read_all_parameters(p_Gsrc, r_src = r_src)
	p_Gobs1 = read_all_parameters(p_Gobs1, r_src = r_obs1)
	p_Gobs2 = read_all_parameters(p_Gobs2, r_src = r_obs2)

	Gfn_path_src, NGfn_files_src = p_Gsrc.path, p_Gsrc.num_procs
	Gfn_path_obs1, NGfn_files_obs1 =  p_Gobs1.path, p_Gobs1.num_procs
	Gfn_path_obs2, NGfn_files_obs2 = p_Gobs2.path, p_Gobs2.num_procs

	(; Gfn_path_src, NGfn_files_src,
		Gfn_path_obs1, NGfn_files_obs1,
		Gfn_path_obs2, NGfn_files_obs2,
		p_Gsrc, p_Gobs1, p_Gobs2)
end

VSHType(::los_radial) = SH()
VSHType(::los_earth) = GSH()

function los_projected_spheroidal_biposh_flippoints(xobs1::SphericalPoint, xobs2::SphericalPoint, los, SHModes, jₒjₛ_allmodes)
	l1, l2 = line_of_sight_covariant.((xobs1, xobs2), los)
	_Y12, _Y21 = biposh_flippoints(VSHType(los), Point2D(xobs1)..., Point2D(xobs2)..., SHModes, jₒjₛ_allmodes)
	Y12 = biposh_spheroidal(los_projected_biposh!(los, _Y12, l1, l2))
	Y21 = biposh_spheroidal(los_projected_biposh!(los, _Y21, l2, l1))
	return Y12, Y21
end

function trim_m(Y::SHArray{<:Any,1, <:Vector, <:Tuple{LM}}, SHModes)
	Yp = parent(Y)
	modes = only(SphericalHarmonicArrays.modes(Y))
	modes_new = LM(l_range(modes), intersect(m_range(SHModes), m_range(modes)))
	inds = [ind for (ind,(l,m)) in enumerate(modes) if m in m_range(SHModes, l)]
	Yp = Yp[inds]
	SHArray(Yp, modes_new)
end
function trim_m(Y, SHModes)
	SHArray([trim_m(Y[ind], SHModes) for ind in eachindex(Y)],
		SphericalHarmonicArrays.modes(Y))
end

function rotate_biposh!(Y1′2′_j1j2, Y12_j1j2, M, D)
	modes = only(SphericalHarmonicArrays.modes(Y1′2′_j1j2))
	modes_allm = only(SphericalHarmonicArrays.modes(Y12_j1j2))
	for l in l_range(modes)
		Dl = D[l]
		for m in m_range(modes, l)
			Y1′2′_j1j2[(l,m)] = M * sum(Dl[m′,m] * Y12_j1j2[(l,m′)] for m′ in m_range(modes_allm, l))
		end
	end
	return Y1′2′_j1j2
end
function rotate_biposh(Y12, M, D, SHModes)
	Y1′2′ = trim_m(Y12, SHModes)
	for ind in eachindex(Y12)
		Y12_j1j2 = Y12[ind]
		Y1′2′_j1j2 = Y1′2′[ind]
		rotate_biposh!(Y1′2′_j1j2, Y12_j1j2, M, D)
	end
	return Y1′2′
end

_basisrotationmatrix(::los_earth, M1, M2) = Diagonal(LinearAlgebra.kron(M1, M2))
_basisrotationmatrix(::los_radial, M1, M2) = 1

function los_projected_spheroidal_biposh_flippoints((xobs1, xobs1′)::NTuple{2,SphericalPoint},
	(xobs2, xobs2′)::NTuple{2,SphericalPoint}, los, SHModes, jₒjₛ_allmodes)

	SHModes_allm = LM(l_range(SHModes))

	_Y12_allm, _Y21_allm = biposh_flippoints(VSHType(los), Point2D(xobs1)..., Point2D(xobs2)..., SHModes_allm, jₒjₛ_allmodes)
	_Y12 = trim_m(_Y12_allm, SHModes)
	_Y21 = trim_m(_Y21_allm, SHModes)
	Y12 = los_projected_biposh_spheroidal(_Y12, xobs1, xobs2, los)
	Y21 = los_projected_biposh_spheroidal(_Y21, xobs2, xobs1, los)
	R, M11′, M22′ = rotation_points_12(HelicityCovariant(), xobs1, xobs2, xobs1′, xobs2′)
	M11′22′ = _basisrotationmatrix(los, M11′, M22′)
	M22′11′ = _basisrotationmatrix(los, M22′, M11′)
	Rinv = inv(R)
	α, β, γ = Rinv.theta1, Rinv.theta2, Rinv.theta3
	lmax = maximum(l_range(SHModes))
	D = OffsetArray([OffsetArray(wignerD(j, α, β, γ), -j:j, -j:j) for j in 0:lmax], 0:lmax)
	_Y1′2′ = rotate_biposh(_Y12_allm, M11′22′, D, SHModes)
	_Y2′1′ = rotate_biposh(_Y21_allm, M22′11′, D, SHModes)
	Y1′2′ = los_projected_biposh_spheroidal(_Y1′2′, xobs1′, xobs2′, los)
	Y2′1′ = los_projected_biposh_spheroidal(_Y2′1′, xobs2′, xobs1′, los)
	return Y12, Y21, Y1′2′, Y2′1′
end

########################################################################################
# Validation for uniform rotation
########################################################################################

function populatekernelvalidation!(::Flow, ::los_radial, K::AbstractVector{<:Real},
	(j, ω), ∂ϕ₂Pⱼ_cosχ, conjhω, H¹₁jj_r₁r₂, H¹₁jj_r₂r₁, dω)

	pre = 2*√(3/π) * dω/2π * ω^3 * Powspec(ω) * ∂ϕ₂Pⱼ_cosχ
	for r_ind in UnitRange(axes(K, 1))
		K[r_ind] += pre * imag( conjhω * ( H¹₁jj_r₁r₂[r_ind] + conj(H¹₁jj_r₂r₁[r_ind]) ) )
	end
end

function populatekernelvalidation!(::Flow, ::los_earth, K::AbstractVector{<:Real},
	(j, ω), Y12j, conjhω, H¹₁jj_r₁r₂, H¹₁jj_r₂r₁, dω)

	pre = 2 * dω/2π * ω^3 * Powspec(ω) * √((j*(j+1))/π)

	for r_ind in UnitRange(axes(K, 1))
		for α₂ in 0:1, α₁ in 0:1
			Pʲʲ₁₀_α₁α₂ = pre * imag(Y12j[α₁, α₂])
			iszero(Pʲʲ₁₀_α₁α₂) && continue

			K[r_ind] += Pʲʲ₁₀_α₁α₂ * imag(conjhω *
				( H¹₁jj_r₁r₂[α₁, α₂, r_ind] + conj(H¹₁jj_r₂r₁[α₂, α₁, r_ind] ) ) )
		end
	end
end

function populatekernelvalidation!(::Flow, ::los_radial, K::AbstractMatrix{<:Real},
	(j, ω), ∂ϕ₂Pⱼ_cosχ::AbstractVector{<:Real},
	conjhω::AbstractVector{ComplexF64}, H¹₁jj_r₁r₂, H¹₁jj_r₂r₁, dω)

	pre = 2*√(3/4π) * dω/2π * ω^3 * Powspec(ω)
	for n2ind in axes(K, 2)
		pre2 = pre * ∂ϕ₂Pⱼ_cosχ[n2ind]
		conjhω_n2 = conjhω[n2ind]
		for r_ind in axes(K, 1)
			K[r_ind, n2ind] +=  pre2 * 2imag( conjhω_n2 * ( H¹₁jj_r₁r₂[r_ind] + conj(H¹₁jj_r₂r₁[r_ind]) ) )
		end
	end
end

function populatekernelvalidation!(::Flow, ::los_earth, K::AbstractMatrix{<:Real},
	(j, ω), Y12j::AbstractVector, conjhω, H¹₁jj_r₁r₂, H¹₁jj_r₂r₁, dω)

	pre = 2 * dω/2π * ω^3 * Powspec(ω) * √((j*(j+1))/π)

	for n2ind in axes(K, 2)
		Y12j_n₂ = Y12j[n2ind]
		conjhω_n₂ = conjhω[n2ind]

		for r_ind in UnitRange(axes(K, 1))
			for α₂ in 0:1, α₁ in 0:1
				Pʲʲ₁₀_α₁α₂ = pre * imag(Y12j_n₂[α₁, α₂])
				iszero(Pʲʲ₁₀_α₁α₂) && continue

				K[r_ind, n2ind] += Pʲʲ₁₀_α₁α₂ * imag(conjhω_n₂ *
					(H¹₁jj_r₁r₂[α₁, α₂, r_ind] + conj(H¹₁jj_r₂r₁[α₂, α₁, r_ind] ) ) )
			end
		end
	end
end

function kernel_ℑu⁺₁₀_partial(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	xobs1::SphericalPoint, xobs2::SphericalPoint, los::los_direction, hω,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = nothing,
	r_src = r_src_default)

	@unpack p_Gsrc, p_Gobs1, p_Gobs2, Gfn_path_src, NGfn_files_src,
	Gfn_path_obs1, NGfn_files_obs1, Gfn_path_obs2, NGfn_files_obs2 =
		unpackGfnparams(p_Gsrc, r_src, p_Gobs1, radius(xobs1), p_Gobs2, radius(xobs2))

	@unpack ℓ_arr, ω_arr, Nν_Gfn, dω = p_Gsrc

	r₁_ind, r₂_ind = radial_grid_index.((xobs1, xobs2))

	Gfn_fits_files_src, Gfn_fits_files_obs1, Gfn_fits_files_obs2 =
			Gfn_fits_files.((Gfn_path_src, Gfn_path_obs1, Gfn_path_obs2),
				((ℓ_arr, 1:Nν_Gfn),), (ℓ_ωind_iter_on_proc,),
				(NGfn_files_src, NGfn_files_obs1, NGfn_files_obs2))

	K = zeros(nr)

	arrs = allocatearrays(Flow(), los, r₁_ind == r₂_ind)
	@unpack Gsrc, Gobs1, Gobs2, G¹₁jj_r₁, G¹₁jj_r₂, H¹₁jj_r₁r₂, H¹₁jj_r₂r₁ = arrs

	Y12 = los_projected_biposh_spheroidal(computeY₁₀, xobs1, xobs2, los, ℓ_ωind_iter_on_proc)

	for (j, ω_ind) in ℓ_ωind_iter_on_proc
		ω = ω_arr[ω_ind]
		conjhω = conj(hω[ω_ind])

		# Green function about the source radius
		read_Gfn_file_at_index!(Gsrc, Gfn_fits_files_src,
			(ℓ_arr, 1:Nν_Gfn), (j, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 1)

		Gγr_r₁_rsrc = αrcomp(Gsrc, r₁_ind, los)
		Gγr_r₂_rsrc = αrcomp(Gsrc, r₂_ind, los)

		# Green function about receiver location
		read_Gfn_file_at_index!(Gobs1, Gfn_fits_files_obs1,
			(ℓ_arr, 1:Nν_Gfn), (j, ω_ind), NGfn_files_obs1, 1:2, srcindFITS(los), :, 1)

		radial_fn_uniform_rotation_firstborn!(G¹₁jj_r₁, Gsrc, Gobs1, j, los)
		Hjₒjₛω!(H¹₁jj_r₁r₂, G¹₁jj_r₁, Gγr_r₂_rsrc)

		if r₁_ind != r₂_ind
			# Green function about receiver location
			read_Gfn_file_at_index!(Gobs2, Gfn_fits_files_obs2,
			(ℓ_arr, 1:Nν_Gfn), (j, ω_ind), NGfn_files_obs2, 1:2, srcindFITS(los), :, 1)

			radial_fn_uniform_rotation_firstborn!(G¹₁jj_r₂, Gsrc, Gobs2, j, los)
			Hjₒjₛω!(H¹₁jj_r₂r₁, G¹₁jj_r₂, Gγr_r₁_rsrc)
		end

		populatekernelvalidation!(Flow(), los, K, (j, ω),
			Y12[j], conjhω, H¹₁jj_r₁r₂, H¹₁jj_r₂r₁, dω)
	end

	foreach(closeGfnfits, (Gfn_fits_files_src, Gfn_fits_files_obs1, Gfn_fits_files_obs2))
	return @. (ρ/r)*K
end

function kernel_ℑu⁺₁₀_partial(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	nobs1::Point2D, nobs2_arr::Vector{<:Point2D}, los::los_direction, hω,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = p_Gobs1,
	r_src = r_src_default)

	hω = permutedims(hω) #  convert to (n2, ω)

	r_obs_ind = radial_grid_index(nobs1)

	@unpack p_Gsrc, p_Gobs1, Gfn_path_src, NGfn_files_src =
		unpackGfnparams(p_Gsrc, r_src, p_Gobs1, r_obs_default, p_Gobs2, r_obs_default)

	Gfn_path_obs, NGfn_files_obs = p_Gobs1.path, p_Gobs1.num_procs

	@unpack ℓ_arr, ω_arr, Nν_Gfn, dω = p_Gsrc

	Gfn_fits_files_src, Gfn_fits_files_obs =
			Gfn_fits_files.((Gfn_path_src, Gfn_path_obs),
				((ℓ_arr, 1:Nν_Gfn),), (ℓ_ωind_iter_on_proc,),
				(NGfn_files_src, NGfn_files_obs))

	K = zeros(nr, length(nobs2_arr))

	arrs = allocatearrays(Flow(), los, true)
	@unpack Gsrc, H¹₁jj_r₁r₂, G¹₁jj_r₁, Gobs1 = arrs

	Y12 = los_projected_biposh_spheroidal(computeY₁₀, nobs1, nobs2_arr, los, ℓ_ωind_iter_on_proc)

	for (ℓ, ω_ind) in ℓ_ωind_iter_on_proc
		ω = ω_arr[ω_ind]
		conjhω = conj(@view hω[:, ω_ind])

		# Green function about the source radius
		read_Gfn_file_at_index!(Gsrc, Gfn_fits_files_src,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 1)

		G_r₂_rsrc = αrcomp(Gsrc, r_obs_ind, los)

		# Green function about receiver location
		read_Gfn_file_at_index!(Gobs1, Gfn_fits_files_obs,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_obs, 1:2, srcindFITS(los), :, 1)

		radial_fn_uniform_rotation_firstborn!(G¹₁jj_r₁, Gsrc, Gobs1, ℓ, los)
		Hjₒjₛω!(H¹₁jj_r₁r₂, G¹₁jj_r₁, G_r₂_rsrc)

		populatekernelvalidation!(Flow(), los, K, (ℓ, ω),
			view(Y12, :, ℓ), conjhω, H¹₁jj_r₁r₂, H¹₁jj_r₁r₂, dω)
	end

	map(closeGfnfits, (Gfn_fits_files_src, Gfn_fits_files_obs))
	return @. (ρ/r)*K
end

# Traveltimes

kernelfilenameℑu⁺₁₀(::TravelTimes, ::los_radial) = "K_δτ_ℑu⁺₁₀.fits"
kernelfilenameℑu⁺₁₀(::TravelTimes, ::los_earth) = "K_δτ_ℑu⁺₁₀_los.fits"
kernelfilenameℑu⁺₁₀(::Amplitudes, ::los_radial) = "K_A_ℑu⁺₁₀.fits"
kernelfilenameℑu⁺₁₀(::Amplitudes, ::los_earth) = "K_A_ℑu⁺₁₀_los.fits"

function kernel_ℑu⁺₁₀(comm, m::SeismicMeasurement, xobs1, xobs2, los::los_direction = los_radial(); kwargs...)
	r_src, = read_rsrc_robs_c_scale(kwargs)
	p_Gsrc, p_Gobs1, p_Gobs2 = read_parameters_for_points(xobs1, xobs2; kwargs...)
	@unpack ν_arr, ℓ_arr = p_Gsrc
	iters = ℓ_and_ν_range(ℓ_arr, ν_arr; kwargs...)

	# Compute the appropriate window function depending on the parameter of interest
	hω_arr = get(kwargs, :hω) do
		h = hω(comm, m, xobs1, xobs2, los; kwargs..., print_timings = false)
		_broadcast(h, 0, comm)
	end

	# Use the window function to compute the kernel
	K = pmapsum(comm, kernel_ℑu⁺₁₀_partial, iters,
		xobs1, xobs2, los, hω_arr, p_Gsrc, p_Gobs1, p_Gobs2, r_src)

	K === nothing && return nothing

	if get(kwargs, :save, true)
		filename = joinpath(SCRATCH_kerneldir[], kernelfilenameℑu⁺₁₀(m, los))
		FITS(filename,"w") do f
			write(f, reinterpret_as_float(K))
		end
	end

	return K
end

########################################################################################
# Kernels for flow velocity
########################################################################################

@inline function mulprefactor!(K::AbstractArray{<:Real}, vindskip = 0)
	@turbo @. K *= ρ
	@assert axes(K,1) == eachindex(r) "first axis of K does not match that of r"
	for trail_ind in CartesianIndices(UnitRange.(axes(K)[3:end])), vind in UnitRange(axes(K, 2))
		vind == vindskip && continue # radial component
		for r_ind in eachindex(r)
			K[r_ind, vind, trail_ind] /= r[r_ind]
		end
	end
end
@inline function mulprefactor!(K::StructArray{<:Complex}, vindskip = 0)
	mulprefactor!(K.re, vindskip)
	mulprefactor!(K.im, vindskip)
end

function populatekernel!(::Flow, ::los_radial, K::AbstractArray{<:Complex,3},
	SHModes, Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₂n₁,
	(l, m), (jₛ, jₒ, ω),
	twoimagconjhωHγℓjₒjₛ_r₁r₂,
	twoimagconjhωconjHγℓjₒjₛ_r₂r₁,
	pre⁰, pre⁺, phase)

	mode_ind = modeindex(firstshmodes(Yjₒjₛ_lm_n₁n₂), (l, m))

	Pʲₒʲₛₗₘ_₀₀_n₁n₂ = Yjₒjₛ_lm_n₁n₂[mode_ind]
	Pʲₒʲₛₗₘ_₀₀_n₂n₁ = Yjₒjₛ_lm_n₂n₁[mode_ind]

	iszero(Pʲₒʲₛₗₘ_₀₀_n₁n₂) && iszero(Pʲₒʲₛₗₘ_₀₀_n₂n₁) && return

	anyzeromom = l == 0 || jₛ == 0
	evenmom = iseven(jₒ + jₛ + l)

	zeroindT, plusindT = axes(twoimagconjhωconjHγℓjₒjₛ_r₂r₁, 2)
	minusindK, zeroindK, plusindK = axes(K, 1)
	lm_ind_K = modeindex(SHModes, (l, m))

	if evenmom
		@turbo for r_ind in eachindex(r)
			TP₁₂⁰ = pre⁰ * Pʲₒʲₛₗₘ_₀₀_n₁n₂ * twoimagconjhωHγℓjₒjₛ_r₁r₂[r_ind, zeroindT]
			TP₂₁⁰ = pre⁰ * Pʲₒʲₛₗₘ_₀₀_n₂n₁ * twoimagconjhωconjHγℓjₒjₛ_r₂r₁[r_ind, zeroindT]

			K[zeroindK, r_ind, lm_ind_K] += TP₂₁⁰ - TP₁₂⁰
		end
	end
	anyzeromom && return K
	@turbo for r_ind in eachindex(r)
		TP₁₂¹ = pre⁺ * Pʲₒʲₛₗₘ_₀₀_n₁n₂ * twoimagconjhωHγℓjₒjₛ_r₁r₂[r_ind, plusindT]
		TP₂₁¹ = pre⁺ * Pʲₒʲₛₗₘ_₀₀_n₂n₁ * twoimagconjhωconjHγℓjₒjₛ_r₂r₁[r_ind, plusindT]

		K[plusindK, r_ind, lm_ind_K] += TP₂₁¹ - TP₁₂¹
		K[minusindK, r_ind, lm_ind_K] += phase * (TP₂₁¹ - TP₁₂¹)
	end

	return K
end
function populatekernel!(::Flow, ::los_radial, K::AbstractArray{<:Complex,4},
	SHModes, (Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₁′n₂′)::Tuple{Any, Any},
	(Yjₒjₛ_lm_n₂n₁, Yjₒjₛ_lm_n₂′n₁′)::Tuple{Any, Any},
	(l, m), (jₛ, jₒ, ω),
	twoimagconjhωHγℓjₒjₛ_r₁r₂,
	twoimagconjhωconjHγℓjₒjₛ_r₂r₁,
	pre⁰, pre⁺, phase)

	mode_ind = modeindex(firstshmodes(Yjₒjₛ_lm_n₁n₂), (l, m))

	Pʲₒʲₛₗₘ_₀₀_n₁n₂ = Yjₒjₛ_lm_n₁n₂[mode_ind]
	Pʲₒʲₛₗₘ_₀₀_n₂n₁ = Yjₒjₛ_lm_n₂n₁[mode_ind]
	Pʲₒʲₛₗₘ_₀₀_n₁′n₂′ = Yjₒjₛ_lm_n₁′n₂′[mode_ind]
	Pʲₒʲₛₗₘ_₀₀_n₂′n₁′ = Yjₒjₛ_lm_n₂′n₁′[mode_ind]

	iszero(Pʲₒʲₛₗₘ_₀₀_n₁n₂) && iszero(Pʲₒʲₛₗₘ_₀₀_n₂n₁) &&
	iszero(Pʲₒʲₛₗₘ_₀₀_n₁′n₂′) && iszero(Pʲₒʲₛₗₘ_₀₀_n₂′n₁′) && return

	anyzeromom = l == 0 || jₛ == 0
	evenmom = iseven(jₒ + jₛ + l)

	zeroindT, plusindT = axes(twoimagconjhωconjHγℓjₒjₛ_r₂r₁, 3)
	minusindK, zeroindK, plusindK = axes(K, 2)
	lm_ind_K = modeindex(SHModes, (l, m))

	if evenmom
		@turbo for r_ind in eachindex(r)
			TP₁₂⁰ = pre⁰ * Pʲₒʲₛₗₘ_₀₀_n₁n₂ * twoimagconjhωHγℓjₒjₛ_r₁r₂[1, r_ind, zeroindT]
			TP₂₁⁰ = pre⁰ * Pʲₒʲₛₗₘ_₀₀_n₂n₁ * twoimagconjhωconjHγℓjₒjₛ_r₂r₁[1, r_ind, zeroindT]
			TP₁′₂′⁰ = pre⁰ * Pʲₒʲₛₗₘ_₀₀_n₁′n₂′ * twoimagconjhωHγℓjₒjₛ_r₁r₂[2, r_ind, zeroindT]
			TP₂′₁′⁰ = pre⁰ * Pʲₒʲₛₗₘ_₀₀_n₂′n₁′ * twoimagconjhωconjHγℓjₒjₛ_r₂r₁[2, r_ind, zeroindT]

			K[1, zeroindK, r_ind, lm_ind_K] += TP₂₁⁰ - TP₁₂⁰
			K[2, zeroindK, r_ind, lm_ind_K] += TP₂′₁′⁰ - TP₁′₂′⁰
		end
	end
	anyzeromom && return K
	@turbo for r_ind in eachindex(r)
		TP₁₂¹ = pre⁺ * Pʲₒʲₛₗₘ_₀₀_n₁n₂ * twoimagconjhωHγℓjₒjₛ_r₁r₂[1, r_ind, plusindT]
		TP₂₁¹ = pre⁺ * Pʲₒʲₛₗₘ_₀₀_n₂n₁ * twoimagconjhωconjHγℓjₒjₛ_r₂r₁[1, r_ind, plusindT]
		TP₁′₂′¹ = pre⁺ * Pʲₒʲₛₗₘ_₀₀_n₁′n₂′ * twoimagconjhωHγℓjₒjₛ_r₁r₂[2, r_ind, plusindT]
		TP₂′₁′¹ = pre⁺ * Pʲₒʲₛₗₘ_₀₀_n₂′n₁′ * twoimagconjhωconjHγℓjₒjₛ_r₂r₁[2, r_ind, plusindT]

		K[1, plusindK, r_ind, lm_ind_K] += TP₂₁¹ - TP₁₂¹
		K[2, plusindK, r_ind, lm_ind_K] += TP₂′₁′¹ - TP₁′₂′¹
		K[1, minusindK, r_ind, lm_ind_K] += phase * (TP₂₁¹ - TP₁₂¹)
		K[2, minusindK, r_ind, lm_ind_K] += phase * (TP₂′₁′¹ - TP₁′₂′¹)
	end

	return K
end
function populatekernel!(::Flow, ::los_earth, K::AbstractArray{<:Complex,3},
	SHModes, Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₂n₁,
	(l, m), (jₛ, jₒ, ω),
	twoimagconjhωHγℓjₒjₛ_r₁r₂,
	twoimagconjhωconjHγℓjₒjₛ_r₂r₁,
	pre⁰, pre⁺, phase)

	mode_ind = modeindex(firstshmodes(Yjₒjₛ_lm_n₁n₂), (l, m))
	lm_ind_K = modeindex(SHModes, (l, m))

	_Yjₒjₛ_lm_n₁n₂ = parent(Yjₒjₛ_lm_n₁n₂[mode_ind])
	_Yjₒjₛ_lm_n₂n₁ = parent(Yjₒjₛ_lm_n₂n₁[mode_ind])

	minusindK, zeroindK, plusindK = axes(K, 1)
	zeroindT, plusindT = axes(twoimagconjhωconjHγℓjₒjₛ_r₂r₁, 2)

	anyzeromom = l == 0 || jₛ == 0
	evenmom = iseven(jₒ + jₛ + l)
	if evenmom
		@turbo for r_ind in eachindex(r)
			TP₁₂⁰ = pre⁰ * sum(_Yjₒjₛ_lm_n₁n₂ .* twoimagconjhωHγℓjₒjₛ_r₁r₂[r_ind, zeroindT])
			TP₂₁⁰ = pre⁰ * sum(_Yjₒjₛ_lm_n₂n₁ .* twoimagconjhωconjHγℓjₒjₛ_r₂r₁[r_ind, zeroindT])

			K[zeroindK, r_ind, lm_ind_K] += TP₂₁⁰ - TP₁₂⁰
		end
	end
	anyzeromom && return K
	@turbo for r_ind in eachindex(r)
		TP₁₂¹ = pre⁺ * sum(_Yjₒjₛ_lm_n₁n₂ .* twoimagconjhωHγℓjₒjₛ_r₁r₂[r_ind, plusindT])
		TP₂₁¹ = pre⁺ * sum(_Yjₒjₛ_lm_n₂n₁ .* twoimagconjhωconjHγℓjₒjₛ_r₂r₁[r_ind, plusindT])

		K[plusindK, r_ind, lm_ind_K] += TP₂₁¹ - TP₁₂¹
		K[minusindK, r_ind, lm_ind_K] += phase * (TP₂₁¹ - TP₁₂¹)
	end

	return K
end
function populatekernel!(::Flow, ::los_earth, K::AbstractArray{<:Complex,4},
	SHModes,
	(Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₁′n₂′)::NTuple{2,Any},
	(Yjₒjₛ_lm_n₂n₁, Yjₒjₛ_lm_n₂′n₁′)::NTuple{2,Any},
	(l, m), (jₛ, jₒ, ω),
	twoimagconjhωHγℓjₒjₛ_r₁r₂,
	twoimagconjhωconjHγℓjₒjₛ_r₂r₁,
	pre⁰, pre⁺, phase)

	mode_ind = modeindex(firstshmodes(Yjₒjₛ_lm_n₁n₂), (l, m))
	lm_ind_K = modeindex(SHModes, (l, m))

	_Yjₒjₛ_lm_n₁n₂ = parent(Yjₒjₛ_lm_n₁n₂[mode_ind])
	_Yjₒjₛ_lm_n₂n₁ = parent(Yjₒjₛ_lm_n₂n₁[mode_ind])
	_Yjₒjₛ_lm_n₁′n₂′ = parent(Yjₒjₛ_lm_n₁′n₂′[mode_ind])
	_Yjₒjₛ_lm_n₂′n₁′ = parent(Yjₒjₛ_lm_n₂′n₁′[mode_ind])

	minusindK, zeroindK, plusindK = axes(K, 2)
	zeroindT, plusindT = axes(twoimagconjhωconjHγℓjₒjₛ_r₂r₁, 3)

	anyzeromom = l == 0 || jₛ == 0
	evenmom = iseven(jₒ + jₛ + l)
	if evenmom
		@turbo for r_ind in eachindex(r)
			TP₁₂⁰ = pre⁰ * sum(_Yjₒjₛ_lm_n₁n₂ .* twoimagconjhωHγℓjₒjₛ_r₁r₂[1, r_ind, zeroindT])
			TP₂₁⁰ = pre⁰ * sum(_Yjₒjₛ_lm_n₂n₁ .* twoimagconjhωconjHγℓjₒjₛ_r₂r₁[1, r_ind, zeroindT])
			TP₁′₂′⁰ = pre⁰ * sum(_Yjₒjₛ_lm_n₁′n₂′ .* twoimagconjhωHγℓjₒjₛ_r₁r₂[2, r_ind, zeroindT])
			TP₂′₁′⁰ = pre⁰ * sum(_Yjₒjₛ_lm_n₂′n₁′ .* twoimagconjhωconjHγℓjₒjₛ_r₂r₁[2, r_ind, zeroindT])

			K[1, zeroindK, r_ind, lm_ind_K] += TP₂₁⁰ - TP₁₂⁰
			K[2, zeroindK, r_ind, lm_ind_K] += TP₂′₁′⁰ - TP₁′₂′⁰
		end
	end
	anyzeromom && return K
	@turbo for r_ind in eachindex(r)
		TP₁₂¹ = pre⁺ * sum(_Yjₒjₛ_lm_n₁n₂ .* twoimagconjhωHγℓjₒjₛ_r₁r₂[1, r_ind, plusindT])
		TP₂₁¹ = pre⁺ * sum(_Yjₒjₛ_lm_n₂n₁ .* twoimagconjhωconjHγℓjₒjₛ_r₂r₁[1, r_ind, plusindT])
		TP₁′₂′¹ = pre⁺ * sum(_Yjₒjₛ_lm_n₁′n₂′ .* twoimagconjhωHγℓjₒjₛ_r₁r₂[2, r_ind, plusindT])
		TP₂′₁′¹ = pre⁺ * sum(_Yjₒjₛ_lm_n₂′n₁′ .* twoimagconjhωconjHγℓjₒjₛ_r₂r₁[2, r_ind, plusindT])

		K[1, plusindK, r_ind, lm_ind_K] += TP₂₁¹ - TP₁₂¹
		K[2, plusindK, r_ind, lm_ind_K] += TP₂′₁′¹ - TP₁′₂′¹
		K[1, minusindK, r_ind, lm_ind_K] += phase * (TP₂₁¹ - TP₁₂¹)
		K[2, minusindK, r_ind, lm_ind_K] += phase * (TP₂′₁′¹ - TP₁′₂′¹)
	end

	return K
end

function populatekernelrθϕl0!(::Flow, ::los_radial, K::AbstractArray{<:Real, 3},
	SHModes, Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₂n₁,
	(l,_), (jₛ, jₒ, ω), twoimagconjhωconjHγℓjₒjₛ_r₂r₁, twoimagconjhωHγℓjₒjₛ_r₁r₂,
	pre⁰, pre⁺, phase)

	lm_ind_K = modeindex(SHModes, (l, 0))
	lm_ind = modeindex(firstshmodes(Yjₒjₛ_lm_n₁n₂), (l, 0))
	Pʲₒʲₛₗₘ_₀₀_n₁n₂ = Yjₒjₛ_lm_n₁n₂[lm_ind]
	Pʲₒʲₛₗₘ_₀₀_n₂n₁ = Yjₒjₛ_lm_n₂n₁[lm_ind]

	rindK, θindK, ϕindK = axes(K, 1)
	zeroindT, plusindT = axes(twoimagconjhωconjHγℓjₒjₛ_r₂r₁, 2)

	anyzeromom = l == 0 || jₛ == 0
	evenmom = iseven(jₒ + jₛ + l)

	if evenmom
		@turbo for r_ind in eachindex(r)
			TP₁₂⁰ = pre⁰ * Pʲₒʲₛₗₘ_₀₀_n₁n₂ * twoimagconjhωHγℓjₒjₛ_r₁r₂[r_ind, zeroindT]
			TP₂₁⁰ = pre⁰ * Pʲₒʲₛₗₘ_₀₀_n₂n₁ * twoimagconjhωconjHγℓjₒjₛ_r₂r₁[r_ind, zeroindT]

			K[rindK, r_ind, lm_ind_K] += real(TP₂₁⁰) - real(TP₁₂⁰)
		end
	end

	# l = 0 only has an r component
	anyzeromom && return K
	@turbo for r_ind in eachindex(r)
		TP₁₂¹ = pre⁺ * Pʲₒʲₛₗₘ_₀₀_n₁n₂ * twoimagconjhωHγℓjₒjₛ_r₁r₂[r_ind, plusindT]
		TP₂₁¹ = pre⁺ * Pʲₒʲₛₗₘ_₀₀_n₂n₁ * twoimagconjhωconjHγℓjₒjₛ_r₂r₁[r_ind, plusindT]

		tempre = real(TP₂₁¹) - real(TP₁₂¹)
		tempim = imag(TP₂₁¹) - imag(TP₁₂¹)

		K[θindK, r_ind, lm_ind_K] += evenmom * 2tempre
		K[ϕindK, r_ind, lm_ind_K] += !evenmom * (-2tempim)
	end

	return K
end
function populatekernelrθϕl0!(::Flow, ::los_earth, K::AbstractArray{<:Real, 3},
	SHModes, Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₂n₁,
	(l,_), (jₛ, jₒ, ω), twoimagconjhωconjHγℓjₒjₛ_r₂r₁, twoimagconjhωHγℓjₒjₛ_r₁r₂,
	pre⁰, pre⁺, phase)

	lm_ind_K = modeindex(SHModes, (l, 0))
	lm_ind = modeindex(firstshmodes(Yjₒjₛ_lm_n₁n₂), (l, 0))

	_Yjₒjₛ_lm_n₁n₂ = parent(Yjₒjₛ_lm_n₁n₂[lm_ind])
	_Yjₒjₛ_lm_n₂n₁ = parent(Yjₒjₛ_lm_n₂n₁[lm_ind])

	rindK, θindK, ϕindK = axes(K, 1)
	zeroindT, plusindT = axes(twoimagconjhωconjHγℓjₒjₛ_r₂r₁, 2)

	anyzeromom = l == 0 || jₛ == 0
	evenmom = iseven(jₒ + jₛ + l)

	if evenmom
		@turbo for r_ind in eachindex(r)
			TP₁₂⁰ = pre⁰ * sum(_Yjₒjₛ_lm_n₁n₂ .* twoimagconjhωHγℓjₒjₛ_r₁r₂[r_ind, zeroindT])
			TP₂₁⁰ = pre⁰ * sum(_Yjₒjₛ_lm_n₂n₁ .* twoimagconjhωconjHγℓjₒjₛ_r₂r₁[r_ind, zeroindT])

			K[rindK, r_ind, lm_ind_K] += real(TP₂₁⁰) - real(TP₁₂⁰)
		end
	end
	anyzeromom && return K
	@turbo for r_ind in eachindex(r)
		TP₁₂¹ = pre⁺ * sum(_Yjₒjₛ_lm_n₁n₂ .* twoimagconjhωHγℓjₒjₛ_r₁r₂[r_ind, plusindT])
		TP₂₁¹ = pre⁺ * sum(_Yjₒjₛ_lm_n₂n₁ .* twoimagconjhωconjHγℓjₒjₛ_r₂r₁[r_ind, plusindT])

		tempre = real(TP₂₁¹) - real(TP₁₂¹)
		tempim = imag(TP₂₁¹) - imag(TP₁₂¹)

		K[θindK, r_ind, lm_ind_K] += evenmom * 2tempre
		K[ϕindK, r_ind, lm_ind_K] += !evenmom * (-2tempim)
	end

	return K
end

reinterpretSMatrix(A::AbstractArray, ::los_earth) = dropdims(reinterpret(SMatrix{2,2,eltype(A),4}, reshape(A, 4, size(A)[3:end]...)), dims = 1)
reinterpretSMatrix(A::AbstractArray, ::los_radial) = A

function kernel_uₛₜ_partial(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	xobs1::SphericalPoint, xobs2::SphericalPoint, los::los_direction, SHModes, hω_arr,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = nothing,
	r_src = r_src_default)

	@unpack p_Gsrc, p_Gobs1, p_Gobs2, Gfn_path_src, NGfn_files_src, Gfn_path_obs1, NGfn_files_obs1,
	Gfn_path_obs2, NGfn_files_obs2 = unpackGfnparams(p_Gsrc, r_src, p_Gobs1, radius(xobs1), p_Gobs2, radius(xobs2));

	@unpack ℓ_arr, ω_arr, Nν_Gfn, dω = p_Gsrc

	s_range = l_range(SHModes)
	s_max = maximum(s_range)

	r₁_ind, r₂_ind = radial_grid_index.((xobs1, xobs2))
	obs_at_same_height = r₁_ind == r₂_ind

	ℓ_range_proc = _extremaelementrange(ℓ_ωind_iter_on_proc, dims = 1)

	# Get a list of all modes that will be accessed.
	# This can be used to open the fits files before the loops begin.
	# This will cut down on FITS IO costs
	Gfn_fits_files_src = Gfn_fits_files(Gfn_path_src,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, NGfn_files_src)

	# Gℓ′ω(r, robs) files
	Gfn_fits_files_obs1 = Gfn_fits_files(Gfn_path_obs1,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, s_max, NGfn_files_obs1)
	# Gℓ′ω(r, robs) files
	Gfn_fits_files_obs2 = Gfn_fits_files(Gfn_path_obs2,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, s_max, NGfn_files_obs2)

	Kre = zeros(3, nr, length(SHModes)); # first axis is for vector indices (-1,0,1 in the helicity basis)
	K = StructArray{ComplexF64}((Kre, zero(Kre))); # Kγₗₘ(r, x₁, x₂)

	ind⁰ = 2 # index of the vector component 0

	arrs = allocatearrays(Flow(), los, obs_at_same_height);
	@unpack Gsrc, drGsrc, Gparts_r₁, Gparts_r₂,
	Gγℓjₒjₛ_r₁, Gγℓjₒjₛ_r₂, Hγℓjₒjₛ_r₁r₂, Hγℓjₒjₛ_r₂r₁ = arrs;
	@unpack Gparts_r₁_2, Gparts_r₂_2 = arrs;
	@unpack twoimagconjhωconjHγℓjₒjₛ_r₂r₁, twoimagconjhωHγℓjₒjₛ_r₁r₂ = arrs;
	twoimagconjhωconjHγℓjₒjₛ_r₂r₁P, twoimagconjhωHγℓjₒjₛ_r₁r₂P = map(parent, (twoimagconjhωconjHγℓjₒjₛ_r₂r₁, twoimagconjhωHγℓjₒjₛ_r₁r₂));

	HT = HybridArray{Tuple{sizeindG(los)...,sizeindG(los)...,StaticArrays.Dynamic(),2}}
	H₂₁ = _structarrayparent(Hγℓjₒjₛ_r₂r₁, HT)
	H₁₂ = _structarrayparent(Hγℓjₒjₛ_r₁r₂, HT)

	twoimagconjhωconjHγℓjₒjₛ_r₂r₁S = reinterpretSMatrix(twoimagconjhωconjHγℓjₒjₛ_r₂r₁P, los);
	twoimagconjhωHγℓjₒjₛ_r₁r₂S = reinterpretSMatrix(twoimagconjhωHγℓjₒjₛ_r₁r₂P, los);

	jₒjₛ_allmodes = L2L1Triangle(ℓ_range_proc, s_max, ℓ_arr)
	jₒrange = l2_range(jₒjₛ_allmodes)
	@unpack Gobs1_cache, Gobs2_cache = arrs
	for jₒ in jₒrange
		Gobs1_cache[jₒ] = zeros(ComplexF64, axes(Gsrc))
		if !obs_at_same_height
			Gobs2_cache[jₒ] = zeros(ComplexF64, axes(Gsrc))
		end
	end

	@timeit localtimer "biposh" begin
		Y12, Y21 = los_projected_spheroidal_biposh_flippoints(xobs1, xobs2, los, SHModes, jₒjₛ_allmodes)
	end

	C = zeros(0:1, 0:s_max, l2_range(jₒjₛ_allmodes), ℓ_range_proc)

	for jₛ in axes(C, 4), jₒ in axes(C, 3), ℓ in axes(C, 2)

		C[0, ℓ, jₒ, jₛ] = if iseven(jₒ+jₛ+ℓ)
							clebschgordan(Float64, jₒ, 0, jₛ, 0, ℓ, 0)
						else
							zero(Float64)
						end

		C[1, ℓ, jₒ, jₛ] = if ℓ > 0 && jₛ > 0
							clebschgordan(Float64, jₒ, 0, jₛ, 1, ℓ, 1)
						else
							zero(Float64)
						end
	end

	# for some reason the types of these variables were not being inferred
	C⁰::Float64, C⁺::Float64 = 0, 0
	phase::Int = 1
	pre⁰::Float64, pre⁺::Float64 = 0, 0

	ω_ind_prev = -1 # something unrealistic to start off with

	# Loop over the Greenfn files
	for (jₛ, ω_ind) in ℓ_ωind_iter_on_proc
		ω = ω_arr[ω_ind]
		h_ω = hω_arr[ω_ind]
		dωω³Pω = dω/2π * ω^3 * Powspec(ω)
		Ωjₛ0 = Ω(jₛ, 0)

		@timeit localtimer "FITS" begin
		# Green function about the source radius
		read_Gfn_file_at_index!(Gsrc, Gfn_fits_files_src,
		(ℓ_arr, 1:Nν_Gfn), (jₛ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 1)

		# Derivative of Green function about the source radius
		read_Gfn_file_at_index!(drGsrc, Gfn_fits_files_src,
		(ℓ_arr, 1:Nν_Gfn), (jₛ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 2)
		end # timer

		Gα₂r_r₁_rₛ = αrcomp(Gsrc, r₁_ind, los)
		Gα₂r_r₂_rₛ = αrcomp(Gsrc, r₂_ind, los)

		for jₒ in l2_range(jₒjₛ_allmodes, jₛ)

			jₒjₛ_ind = modeindex(jₒjₛ_allmodes, (jₒ, jₛ))

			Yjₒjₛ_lm_n₁n₂ = Y12[jₒjₛ_ind]
			Yjₒjₛ_lm_n₂n₁ = Y21[jₒjₛ_ind]

			(Yjₒjₛ_lm_n₁n₂ === nothing || Yjₒjₛ_lm_n₂n₁ === nothing) && continue

			# Change shmodes to ML to avoid recomputing the radial term for same l
			modesML = intersect(ML(firstshmodes(Yjₒjₛ_lm_n₁n₂)), ML(SHModes))
			modesML === nothing && continue

			Gobs1 = Gobs1_cache[jₒ]
			@timeit localtimer "FITS" begin
			# Green functions based at the observation point
			if ω_ind != ω_ind_prev || jₒ ∉ l2_range(jₒjₛ_allmodes, jₛ-1)
				read_Gfn_file_at_index!(Gobs1, Gfn_fits_files_obs1,
				(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs1, 1:2, srcindFITS(los), :, 1)
			end
			end # timer

			@timeit localtimer "radial term 1" begin
			Gⱼₒⱼₛω_u⁰⁺_firstborn!(Gparts_r₁, Gparts_r₁_2, Gsrc, drGsrc, jₛ, Gobs1, jₒ, los)
			end # timer

			if !obs_at_same_height
				Gobs2 = Gobs2_cache[jₒ]
				@timeit localtimer "FITS" begin
				if ω_ind != ω_ind_prev || jₒ ∉ l2_range(jₒjₛ_allmodes, jₛ-1)
					read_Gfn_file_at_index!(Gobs2, Gfn_fits_files_obs2,
					(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs2, 1:2, srcindFITS(los), :, 1)
				end
				end # timer

				@timeit localtimer "radial term 1" begin
				Gⱼₒⱼₛω_u⁰⁺_firstborn!(Gparts_r₂, Gparts_r₂_2, Gsrc, drGsrc, jₛ, Gobs2, jₒ, los)
				end #timer
			end

			l_prev = -1 # something unrealistic to start off

			for (l, m) in modesML
				# Check if triangle condition is satisfied
				# The outer loop is over all possible jₛ and jₒ for a given l_max
				# Not all combinations would contribute towards a specific l
				δ(jₛ, jₒ, l) || continue

				if l != l_prev
					@timeit localtimer "radial term 2" begin
						@timeit localtimer "H" begin
							Hγₗⱼₒⱼₛω_α₁α₂_firstborn!(Hγℓjₒjₛ_r₁r₂, Gparts_r₁_2, Gγℓjₒjₛ_r₁, Gα₂r_r₂_rₛ, jₒ, jₛ, l)

							if !obs_at_same_height
								Hγₗⱼₒⱼₛω_α₁α₂_firstborn!(Hγℓjₒjₛ_r₂r₁, Gparts_r₂_2, Gγℓjₒjₛ_r₂, Gα₂r_r₁_rₛ, jₒ, jₛ, l)
							end
						end

						@timeit localtimer "T" begin
							@turbo for I in CartesianIndices(H₂₁)
								twoimagconjhωconjHγℓjₒjₛ_r₂r₁P[I] = -2imag(h_ω * H₂₁[I])
								twoimagconjhωHγℓjₒjₛ_r₁r₂P[I] = 2imag(conj(h_ω) * H₁₂[I])
							end
						end

						C⁰ = C[0, l, jₒ, jₛ]
						C⁺ = C[1, l, jₒ, jₛ]
						phase = (-1)^(jₒ + jₛ + l)
						coeffj = √((2jₒ+1)*(2jₛ+1)/π/(2l+1))

						pre⁰ = dωω³Pω * coeffj * C⁰
						pre⁺ = dωω³Pω * coeffj * Ωjₛ0 * C⁺
						l_prev = l
					end
				end

				@timeit localtimer "kernel" begin
				populatekernel!(Flow(), los, K,
					SHModes,
					Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₂n₁,
					(l, m), (jₛ, jₒ, ω),
					twoimagconjhωHγℓjₒjₛ_r₁r₂S,
					twoimagconjhωconjHγℓjₒjₛ_r₂r₁S,
					pre⁰, pre⁺, phase)
				end #timer
			end
		end

		ω_ind_prev = ω_ind
	end

	foreach(closeGfnfits, (Gfn_fits_files_src, Gfn_fits_files_obs1, Gfn_fits_files_obs2))

	_K = permutedims(K, [2,1,3])
	mulprefactor!(_K, ind⁰)
	AK = Array(no_offset_view(_K))

	return AK
end

function kernel_uₛₜ_partial(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	(xobs1, xobs1′)::NTuple{2,SphericalPoint},
	(xobs2, xobs2′)::NTuple{2,SphericalPoint}, los::los_direction, SHModes, hω_arr,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = nothing,
	r_src = r_src_default)

	@unpack p_Gsrc, p_Gobs1, p_Gobs2, Gfn_path_src, NGfn_files_src, Gfn_path_obs1, NGfn_files_obs1,
	Gfn_path_obs2, NGfn_files_obs2 = unpackGfnparams(p_Gsrc, r_src, p_Gobs1, radius(xobs1), p_Gobs2, radius(xobs2));

	@unpack ℓ_arr, ω_arr, Nν_Gfn, dω = p_Gsrc

	s_range = l_range(SHModes)
	s_max = maximum(s_range)

	r₁_ind, r₂_ind = radial_grid_index.((xobs1, xobs2))
	obs_at_same_height = r₁_ind == r₂_ind

	ℓ_range_proc = _extremaelementrange(ℓ_ωind_iter_on_proc, dims = 1)

	# Get a list of all modes that will be accessed.
	# This can be used to open the fits files before the loops begin.
	# This will cut down on FITS IO costs
	Gfn_fits_files_src = Gfn_fits_files(Gfn_path_src,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, NGfn_files_src)

	# Gℓ′ω(r, robs) files
	Gfn_fits_files_obs1 = Gfn_fits_files(Gfn_path_obs1,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, s_max, NGfn_files_obs1)
	# Gℓ′ω(r, robs) files
	Gfn_fits_files_obs2 = Gfn_fits_files(Gfn_path_obs2,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, s_max, NGfn_files_obs2)

	Kre = zeros(2, 3, nr, length(SHModes)); # first axis is for vector indices (-1,0,1 in the helicity basis)
	K = StructArray{ComplexF64}((Kre, zero(Kre))); # Kγₗₘ(r, x₁, x₂)

	ind⁰ = 2 # index of the vector component 0

	arrs = allocatearrays(Flow(), los, obs_at_same_height, Val(2));
	@unpack Gsrc, drGsrc, Gparts_r₁, Gparts_r₂,
	Gγℓjₒjₛ_r₁, Gγℓjₒjₛ_r₂, Hγℓjₒjₛ_r₁r₂, Hγℓjₒjₛ_r₂r₁ = arrs;
	@unpack Gparts_r₁_2, Gparts_r₂_2 = arrs;
	@unpack twoimagconjhωconjHγℓjₒjₛ_r₂r₁, twoimagconjhωHγℓjₒjₛ_r₁r₂ = arrs;
	twoimagconjhωconjHγℓjₒjₛ_r₂r₁P, twoimagconjhωHγℓjₒjₛ_r₁r₂P = map(parent, (twoimagconjhωconjHγℓjₒjₛ_r₂r₁, twoimagconjhωHγℓjₒjₛ_r₁r₂));

	HT = HybridArray{Tuple{sizeindG(los)...,sizeindG(los)...,StaticArrays.Dynamic(),2}}
	H₂₁ = _structarrayparent(Hγℓjₒjₛ_r₂r₁, HT)
	H₁₂ = _structarrayparent(Hγℓjₒjₛ_r₁r₂, HT)

	twoimagconjhωconjHγℓjₒjₛ_r₂r₁S = reinterpretSMatrix(twoimagconjhωconjHγℓjₒjₛ_r₂r₁P, los);
	twoimagconjhωHγℓjₒjₛ_r₁r₂S = reinterpretSMatrix(twoimagconjhωHγℓjₒjₛ_r₁r₂P, los);

	inds_lead_T = CartesianIndices((sizeindG(los)..., sizeindG(los)...))
	inds_trail_T = CartesianIndices(axes(H₂₁)[ndims(inds_lead_T) .+ (1:2)])

	jₒjₛ_allmodes = L2L1Triangle(ℓ_range_proc, s_max, ℓ_arr)
	jₒrange = l2_range(jₒjₛ_allmodes)
	@unpack Gobs1_cache, Gobs2_cache = arrs
	for jₒ in jₒrange
		Gobs1_cache[jₒ] = zeros(ComplexF64, axes(Gsrc))
		if !obs_at_same_height
			Gobs2_cache[jₒ] = zeros(ComplexF64, axes(Gsrc))
		end
	end

	@timeit localtimer "biposh" begin
		Y12, Y21, Y1′2′, Y2′1′ =
			los_projected_spheroidal_biposh_flippoints((xobs1, xobs1′),
			(xobs2, xobs2′), los, SHModes, jₒjₛ_allmodes)
	end

	C = zeros(0:1, 0:s_max, l2_range(jₒjₛ_allmodes), ℓ_range_proc)

	for jₛ in axes(C, 4), jₒ in axes(C, 3), ℓ in axes(C, 2)

		C[0, ℓ, jₒ, jₛ] = if iseven(jₒ+jₛ+ℓ)
							clebschgordan(Float64, jₒ, 0, jₛ, 0, ℓ, 0)
						else
							zero(Float64)
						end

		C[1, ℓ, jₒ, jₛ] = if ℓ > 0 && jₛ > 0
							clebschgordan(Float64, jₒ, 0, jₛ, 1, ℓ, 1)
						else
							zero(Float64)
						end
	end

	# for some reason the types of these variables were not being inferred
	C⁰::Float64, C⁺::Float64 = 0, 0
	phase::Int = 1
	pre⁰::Float64, pre⁺::Float64 = 0, 0

	ω_ind_prev = -1 # something unrealistic to start off with

	# Loop over the Greenfn files
	for (jₛ, ω_ind) in ℓ_ωind_iter_on_proc
		ω = ω_arr[ω_ind]
		h_ω_1 = hω_arr[ω_ind, 1]
		h_ω_2 = hω_arr[ω_ind, 2]
		h_ω = SVector{2}(h_ω_1, h_ω_2)
		dωω³Pω = dω/2π * ω^3 * Powspec(ω)
		Ωjₛ0 = Ω(jₛ, 0)

		@timeit localtimer "FITS" begin
		# Green function about the source radius
		read_Gfn_file_at_index!(Gsrc, Gfn_fits_files_src,
		(ℓ_arr, 1:Nν_Gfn), (jₛ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 1)

		# Derivative of Green function about the source radius
		read_Gfn_file_at_index!(drGsrc, Gfn_fits_files_src,
		(ℓ_arr, 1:Nν_Gfn), (jₛ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 2)
		end # timer

		Gα₂r_r₁_rₛ = αrcomp(Gsrc, r₁_ind, los)
		Gα₂r_r₂_rₛ = αrcomp(Gsrc, r₂_ind, los)

		for jₒ in l2_range(jₒjₛ_allmodes, jₛ)

			jₒjₛ_ind = modeindex(jₒjₛ_allmodes, (jₒ, jₛ))

			Yjₒjₛ_lm_n₁n₂ = Y12[jₒjₛ_ind]
			Yjₒjₛ_lm_n₂n₁ = Y21[jₒjₛ_ind]
			Yjₒjₛ_lm_n₁′n₂′ = Y1′2′[jₒjₛ_ind]
			Yjₒjₛ_lm_n₂′n₁′ = Y2′1′[jₒjₛ_ind]

			(Yjₒjₛ_lm_n₁n₂ === nothing || Yjₒjₛ_lm_n₂n₁ === nothing) && continue
			(Yjₒjₛ_lm_n₁′n₂′ === nothing || Yjₒjₛ_lm_n₂′n₁′ === nothing) && continue

			# Change shmodes to ML to avoid recomputing the radial term for same l
			modesML = intersect(ML(firstshmodes(Yjₒjₛ_lm_n₁n₂)), ML(SHModes))
			modesML === nothing && continue

			Gobs1 = Gobs1_cache[jₒ]
			@timeit localtimer "FITS" begin
			# Green functions based at the observation point
			if ω_ind != ω_ind_prev || jₒ ∉ l2_range(jₒjₛ_allmodes, jₛ-1)
				read_Gfn_file_at_index!(Gobs1, Gfn_fits_files_obs1,
				(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs1, 1:2, srcindFITS(los), :, 1)
			end
			end # timer

			@timeit localtimer "radial term 1" begin
			Gⱼₒⱼₛω_u⁰⁺_firstborn!(Gparts_r₁, Gparts_r₁_2, Gsrc, drGsrc, jₛ, Gobs1, jₒ, los)
			end # timer

			if !obs_at_same_height
				Gobs2 = Gobs2_cache[jₒ]
				@timeit localtimer "FITS" begin
				if ω_ind != ω_ind_prev || jₒ ∉ l2_range(jₒjₛ_allmodes, jₛ-1)
					read_Gfn_file_at_index!(Gobs2, Gfn_fits_files_obs2,
					(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs2, 1:2, srcindFITS(los), :, 1)
				end
				end # timer

				@timeit localtimer "radial term 1" begin
				Gⱼₒⱼₛω_u⁰⁺_firstborn!(Gparts_r₂, Gparts_r₂_2, Gsrc, drGsrc, jₛ, Gobs2, jₒ, los)
				end #timer
			end

			l_prev = -1 # something unrealistic to start off

			for (l, m) in modesML
				# Check if triangle condition is satisfied
				# The outer loop is over all possible jₛ and jₒ for a given l_max
				# Not all combinations would contribute towards a specific l
				δ(jₛ, jₒ, l) || continue

				if l != l_prev
					@timeit localtimer "radial term 2" begin
						@timeit localtimer "H" begin
							Hγₗⱼₒⱼₛω_α₁α₂_firstborn!(Hγℓjₒjₛ_r₁r₂, Gparts_r₁_2, Gγℓjₒjₛ_r₁, Gα₂r_r₂_rₛ, jₒ, jₛ, l)

							if !obs_at_same_height
								Hγₗⱼₒⱼₛω_α₁α₂_firstborn!(Hγℓjₒjₛ_r₂r₁, Gparts_r₂_2, Gγℓjₒjₛ_r₂, Gα₂r_r₁_rₛ, jₒ, jₛ, l)
							end
						end

						@timeit localtimer "T" begin
							for Itrail in inds_trail_T, nind in eachindex(h_ω), Ilead in inds_lead_T
								twoimagconjhωconjHγℓjₒjₛ_r₂r₁P[Ilead, nind, Itrail] = -2imag(h_ω[nind] * H₂₁[Ilead, Itrail])
								twoimagconjhωHγℓjₒjₛ_r₁r₂P[Ilead, nind, Itrail] = 2imag(conj(h_ω[nind]) * H₁₂[Ilead, Itrail])
							end
						end

						C⁰ = C[0, l, jₒ, jₛ]
						C⁺ = C[1, l, jₒ, jₛ]
						phase = (-1)^(jₒ + jₛ + l)
						coeffj = √((2jₒ+1)*(2jₛ+1)/π/(2l+1))

						pre⁰ = dωω³Pω * coeffj * C⁰
						pre⁺ = dωω³Pω * coeffj * Ωjₛ0 * C⁺
						l_prev = l
					end
				end

				@timeit localtimer "kernel" begin
				populatekernel!(Flow(), los, K,
					SHModes,
					(Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₁′n₂′),
					(Yjₒjₛ_lm_n₂n₁, Yjₒjₛ_lm_n₂′n₁′),
					(l, m), (jₛ, jₒ, ω),
					twoimagconjhωHγℓjₒjₛ_r₁r₂S,
					twoimagconjhωconjHγℓjₒjₛ_r₂r₁S,
					pre⁰, pre⁺, phase)
				end #timer
			end
		end

		ω_ind_prev = ω_ind
	end

	foreach(closeGfnfits, (Gfn_fits_files_src, Gfn_fits_files_obs1, Gfn_fits_files_obs2))

	_K = permutedims(K, [3,2,4,1])
	mulprefactor!(_K, ind⁰)
	AK = Array(no_offset_view(_K))

	return AK
end

# Compute Kₛₜ first and then compute Kₛ₀_rθϕ from that
function kernel_uₛ₀_rθϕ_partial(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	xobs1::SphericalPoint, xobs2::SphericalPoint, los::los_direction, SHModes, hω_arr,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = nothing,
	r_src = r_src_default)

	K = kernel_uₛₜ_partial(localtimer, ℓ_ωind_iter_on_proc,
		xobs1, xobs2, los, SHModes, hω_arr,
		p_Gsrc, p_Gobs1, p_Gobs2, r_src)

	K_rθϕ = zeros(size(K, 1), 3, size(K, 3))

	ind⁰, ind⁺ = UnitRange(axes(K,2))[2:3]

	@turbo for st_ind in UnitRange(axes(K, 3))
		for r_ind in UnitRange(axes(K, 1))
			# r-component
			K_rθϕ[r_ind, 1, st_ind] = real(K[r_ind, ind⁰, st_ind])
		end
		for r_ind in UnitRange(axes(K, 1))
			# θ-component
			K_rθϕ[r_ind, 2, st_ind] = 2real(K[r_ind, ind⁺, st_ind])
		end
		for r_ind in UnitRange(axes(K, 1))
			# ϕ-component
			K_rθϕ[r_ind, 3, st_ind] = -2imag(K[r_ind, ind⁺, st_ind])
		end
	end

	K_rθϕ
end

# Compute Kₛ₀_rθϕ directly
function kernel_uₛ₀_rθϕ_partial_2(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	xobs1::SphericalPoint, xobs2::SphericalPoint, los::los_direction, SHModes, hω_arr,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = nothing,
	r_src = r_src_default)

	@unpack p_Gsrc, p_Gobs1, p_Gobs2, Gfn_path_src, NGfn_files_src, Gfn_path_obs1, NGfn_files_obs1,
	Gfn_path_obs2, NGfn_files_obs2 = unpackGfnparams(p_Gsrc, r_src, p_Gobs1, radius(xobs1), p_Gobs2, radius(xobs2))

	@unpack ℓ_arr, ω_arr, Nν_Gfn, dω = p_Gsrc

	s_max = l_max(SHModes)

	r₁_ind, r₂_ind = radial_grid_index.((xobs1, xobs2))
	obs_at_same_height = r₁_ind == r₂_ind

	ℓ_range_proc = _extremaelementrange(ℓ_ωind_iter_on_proc, dims = 1)

	# Get a list of all modes that will be accessed.
	# This can be used to open the fits files before the loops begin.
	# This will cut down on FITS IO costs
	Gfn_fits_files_src = Gfn_fits_files(Gfn_path_src,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, NGfn_files_src)

	# Gℓ′ω(r, robs) files
	Gfn_fits_files_obs1 = Gfn_fits_files(Gfn_path_obs1,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, s_max, NGfn_files_obs1)
	# Gℓ′ω(r, robs) files
	Gfn_fits_files_obs2 = Gfn_fits_files(Gfn_path_obs2,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, s_max, NGfn_files_obs2)

	K = zeros(3, nr, length(SHModes)) # Kγₗₘ(r, x₁, x₂)
	indr = 1

	arrs = allocatearrays(Flow(), los, obs_at_same_height)
	@unpack Gsrc, drGsrc, Gobs1, Gobs2, Gparts_r₁, Gparts_r₂, Gparts_r₁_2, Gparts_r₂_2,
	Gγℓjₒjₛ_r₁, Gγℓjₒjₛ_r₂, Hγℓjₒjₛ_r₁r₂, Hγℓjₒjₛ_r₂r₁ = arrs
	@unpack twoimagconjhωconjHγℓjₒjₛ_r₂r₁, twoimagconjhωHγℓjₒjₛ_r₁r₂ = arrs
	twoimagconjhωconjHγℓjₒjₛ_r₂r₁P, twoimagconjhωHγℓjₒjₛ_r₁r₂P = map(parent, (twoimagconjhωconjHγℓjₒjₛ_r₂r₁, twoimagconjhωHγℓjₒjₛ_r₁r₂))

	HT = HybridArray{Tuple{sizeindG(los)...,sizeindG(los)...,StaticArrays.Dynamic(),2}}
	H₂₁R = HT(parent(Hγℓjₒjₛ_r₂r₁.re))
	H₂₁I = HT(parent(Hγℓjₒjₛ_r₂r₁.im))
	H₁₂R = HT(parent(Hγℓjₒjₛ_r₁r₂.re))
	H₁₂I = HT(parent(Hγℓjₒjₛ_r₁r₂.im))

	twoimagconjhωconjHγℓjₒjₛ_r₂r₁S = reinterpretSMatrix(twoimagconjhωconjHγℓjₒjₛ_r₂r₁P, los)
	twoimagconjhωHγℓjₒjₛ_r₁r₂S = reinterpretSMatrix(twoimagconjhωHγℓjₒjₛ_r₁r₂P, los)

	jₒjₛ_allmodes = L2L1Triangle(ℓ_range_proc, s_max, ℓ_arr)

	@timeit localtimer "biposh" begin
		Y12, Y21 = los_projected_spheroidal_biposh_flippoints(xobs1, xobs2, los, SHModes, jₒjₛ_allmodes)
	end

	C = zeros(0:1, 0:s_max, l2_range(jₒjₛ_allmodes), ℓ_range_proc)

	@timeit localtimer "CG" begin
	for jₛ in axes(C, 4), jₒ in axes(C, 3), ℓ in axes(C, 2)

		C[0, ℓ, jₒ, jₛ] = if iseven(jₒ+jₛ+ℓ)
							clebschgordan(Float64, jₒ, 0, jₛ, 0, ℓ, 0)
						else
							zero(Float64)
						end

		C[1, ℓ, jₒ, jₛ] = if ℓ > 0 && jₛ > 0
							clebschgordan(Float64, jₒ, 0, jₛ, 1, ℓ, 1)
						else
							zero(Float64)
						end
	end
	end # timer

	C⁰, C⁺ = zero(Float64), zero(Float64)
	phase = 1
	pre⁰, pre⁺ = zero(Float64), zero(Float64)

	# Loop over the Greenfn files
	for (jₛ, ω_ind) in ℓ_ωind_iter_on_proc
		ω = ω_arr[ω_ind]
		h_ω = hω_arr[ω_ind]
		dωω³Pω = dω/2π * ω^3 * Powspec(ω)
		Ωjₛ0 = Ω(jₛ, 0)

		@timeit localtimer "FITS" begin
		# Green function about the source radius
		read_Gfn_file_at_index!(Gsrc, Gfn_fits_files_src,
		(ℓ_arr, 1:Nν_Gfn), (jₛ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 1)

		# Derivative of Green function about the source radius
		read_Gfn_file_at_index!(drGsrc, Gfn_fits_files_src,
		(ℓ_arr, 1:Nν_Gfn), (jₛ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 2)
		end # timer

		Gα₂r_r₁_rₛ = αrcomp(Gsrc, r₁_ind, los)
		Gα₂r_r₂_rₛ = αrcomp(Gsrc, r₂_ind, los)

		for jₒ in l2_range(jₒjₛ_allmodes, jₛ)

			Yjₒjₛ_lm_n₁n₂ = Y12[(jₒ, jₛ)]
			Yjₒjₛ_lm_n₂n₁ = Y21[(jₒ, jₛ)]

			(Yjₒjₛ_lm_n₁n₂ === nothing || Yjₒjₛ_lm_n₂n₁ === nothing) && continue

			@timeit localtimer "FITS" begin
			# Green functions based at the observation point
			read_Gfn_file_at_index!(Gobs1, Gfn_fits_files_obs1,
			(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs1, 1:2, srcindFITS(los), :, 1)
			end # timer

			@timeit localtimer "radial term 1" begin
			Gⱼₒⱼₛω_u⁰⁺_firstborn!(Gparts_r₁, Gparts_r₁_2, Gsrc, drGsrc, jₛ, Gobs1, jₒ, los)
			end # timer

			if !obs_at_same_height
				@timeit localtimer "FITS" begin
					read_Gfn_file_at_index!(Gobs2, Gfn_fits_files_obs2,
					(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs2, 1:2, srcindFITS(los), :, 1)
				end # timer

				@timeit localtimer "radial term 1" begin
				Gⱼₒⱼₛω_u⁰⁺_firstborn!(Gparts_r₂, Gparts_r₂_2, Gsrc, drGsrc, jₛ, Gobs2, jₒ, los)
				end #timer
			end

			for (l, m) in firstshmodes(Yjₒjₛ_lm_n₁n₂)
				# Check if triangle condition is satisfied
				# The outer loop is over all possible jₛ and jₒ for a given s_max
				# Not all combinations would contribute towards a specific l
				δ(jₛ, jₒ, l) || continue

				@timeit localtimer "radial term 2" begin
					@timeit localtimer "H" begin
						Hγₗⱼₒⱼₛω_α₁α₂_firstborn!(Hγℓjₒjₛ_r₁r₂, Gparts_r₁_2, Gγℓjₒjₛ_r₁, Gα₂r_r₂_rₛ, jₒ, jₛ, l)

						if !obs_at_same_height
							Hγₗⱼₒⱼₛω_α₁α₂_firstborn!(Hγℓjₒjₛ_r₂r₁, Gparts_r₂_2, Gγℓjₒjₛ_r₂, Gα₂r_r₁_rₛ, jₒ, jₛ, l)
						end
					end
					# @. twoimagconjhωconjHγℓjₒjₛ_r₂r₁ = -2imag(hω * Hγℓjₒjₛ_r₂r₁)
					# @. twoimagconjhωHγℓjₒjₛ_r₁r₂ = 2imag(conjhω * Hγℓjₒjₛ_r₁r₂)
					@timeit localtimer "T" begin
						@turbo for I in CartesianIndices(H₂₁R)
							twoimagconjhωconjHγℓjₒjₛ_r₂r₁P[I] = -2(real(h_ω) * H₂₁I[I] + imag(h_ω) * H₂₁R[I])
							twoimagconjhωHγℓjₒjₛ_r₁r₂P[I] = 2(real(h_ω) * H₁₂I[I] - imag(h_ω) * H₁₂R[I])
						end
					end
				end #timer

				C⁰ = C[0, l, jₒ, jₛ]
				C⁺ = C[1, l, jₒ, jₛ]
				phase = (-1)^(jₒ + jₛ + l)
				coeffj = √((2jₒ+1)*(2jₛ+1)/π/(2l+1))

				pre⁰ = dωω³Pω * coeffj * C⁰
				pre⁺ = dωω³Pω * coeffj * Ωjₛ0 * C⁺

				@timeit localtimer "kernel" begin
				populatekernelrθϕl0!(Flow(), los, K, SHModes, Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₂n₁,
				(l, m), (jₛ, jₒ, ω), twoimagconjhωconjHγℓjₒjₛ_r₂r₁S, twoimagconjhωHγℓjₒjₛ_r₁r₂S,
				pre⁰, pre⁺, phase)
				end #timer
			end
		end
	end

	foreach(closeGfnfits, (Gfn_fits_files_src, Gfn_fits_files_obs1, Gfn_fits_files_obs2))
	_K = Base.PermutedDimsArray(K, (2,1,3))
	mulprefactor!(_K, indr)

	return Array(_K)
end

function generatefitsheader(xobs1::SphericalPoint, xobs2::SphericalPoint, SHModes, j_range, ν_ind_range)
	FITSHeader(["r1","th1","phi1","r2","th2","phi2",
		"l_min","m_min","l_max","m_max",
		"j_min","j_max","nui_min","nui_max"],
		Any[float(radius(xobs1)), float(xobs1.θ), float(xobs1.ϕ),
		float(radius(xobs2)), float(xobs2.θ), float(xobs2.ϕ),
		l_min(SHModes), m_min(SHModes), l_max(SHModes), m_max(SHModes),
		minimum(j_range), maximum(j_range),
		minimum(ν_ind_range), maximum(ν_ind_range)],
		["Radius of the first observation point",
		"Colatitude of the first observation point",
		"Azimuth of the first observation point",
		"Radius of the second observation point",
		"Colatitude of the second observation point",
		"Azimuth of the second observation point",
		"Minimum angular degree of the flow",
		"Minimum azimuthal order of the flow",
		"Maximum angular degree of the flow",
		"Maximum azimuthal order of the flow",
		"Minimum wave mode","Maximum wave mode",
		"Minimum wave frequency index","Maximum wave frequency index"])
end
function generatefitsheader((xobs1, xobs1′)::NTuple{2,SphericalPoint},
		(xobs2, xobs2′)::NTuple{2,SphericalPoint}, SHModes, j_range, ν_ind_range)

	f = generatefitsheader(xobs1, xobs2, SHModes, j_range, ν_ind_range)

	for (k, v, c) in zip(["r1p","th1p","phi1p","r2p","th2p","phi2p"],
		Any[radius(xobs1′), Point2D(xobs1′)..., radius(xobs2′), Point2D(xobs2′)...],
		["Radius of the first observation point rotated",
		"Colatitude of the first observation point rotated",
		"Azimuth of the first observation point rotated",
		"Radius of the second observation point rotated",
		"Colatitude of the second observation point rotated",
		"Azimuth of the second observation point rotated"])

		f[k] = v
		set_comment!(f, k, c)
	end

	return f
end

function modetag(j_range, SHModes)
   "jmax$(maximum(j_range))_lmax$(l_max(SHModes))_mmax$(m_max(SHModes))"
end
function kernelfilenameuₛₜ(m::SeismicMeasurement, los::los_direction, j_range, SHModes, tag="")
	mtag = m isa TravelTimes ? "δτ" : "A"
	lostag = los isa los_radial ? "" : "_los"
	"Kst_$(mtag)_u_$(modetag(j_range, SHModes))$(lostag)"*string(tag)*".fits"
end

function kernel_uₛₜ(comm, m::SeismicMeasurement, xobs1, xobs2,
	los::los_direction = los_radial(); kwargs...)

	SHModes = getkernelmodes(; kwargs...)

	r_src, = read_rsrc_robs_c_scale(kwargs)
	p_Gsrc, p_Gobs1, p_Gobs2 = read_parameters_for_points(xobs1, xobs2; kwargs...)
	@unpack ν_arr, ℓ_arr = p_Gsrc
	iters = ℓ_and_ν_range(ℓ_arr,ν_arr; kwargs...)
	j_range, ν_ind_range = iters

	hω_arr = get(kwargs, :hω) do
		h = hω(comm, m, xobs1, xobs2, los; kwargs..., print_timings = false)
		_broadcast(h, 0, comm)
	end

	K_δτ_uₛₜ = pmapsum(comm, kernel_uₛₜ_partial, iters,
		xobs1, xobs2, los, SHModes, hω_arr, p_Gsrc, p_Gobs1, p_Gobs2, r_src)

	K_δτ_uₛₜ === nothing  && return nothing
	K = OffsetArray(K_δτ_uₛₜ, :, -1:1, ..)

	if get(kwargs, :save, true)

		filepath = joinpath(SCRATCH_kerneldir[], kernelfilenameuₛₜ(m, los, j_range, SHModes))

		header = generatefitsheader(xobs1, xobs2, SHModes, j_range, ν_ind_range)

		FITS(filepath,"w") do f
			FITSIO.write(f, reinterpret_as_float(K_δτ_uₛₜ), header = header)
		end
	end

	SHArray(K, (axes(K)[1:2]..., SHModes, axes(K)[4:end]...))
end

kernelfilenameuₛ₀rθϕ(::TravelTimes, ::los_radial) = "Kl0_δτ_u_rθϕ.fits"
kernelfilenameuₛ₀rθϕ(::TravelTimes, ::los_earth) = "Kl0_δτ_u_rθϕ_los.fits"
kernelfilenameuₛ₀rθϕ(::Amplitudes, ::los_radial) = "Kl0_A_u_rθϕ.fits"
kernelfilenameuₛ₀rθϕ(::Amplitudes, ::los_earth) = "Kl0_A_u_rθϕ_los.fits"

# Compute Kₛₜ first and then compute Kₛ₀_rθϕ from that
function kernel_uₛ₀_rθϕ(comm, m::SeismicMeasurement, xobs1, xobs2, los::los_direction = los_radial(); kwargs...)
	_SHModes = getkernelmodes(; kwargs...)
	SHModes = LM(l_range(_SHModes), 0)

	r_src, = read_rsrc_robs_c_scale(kwargs)
	p_Gsrc, p_Gobs1, p_Gobs2 = read_parameters_for_points(xobs1, xobs2; kwargs...)
	@unpack ν_arr, ℓ_arr = p_Gsrc
	iters = ℓ_and_ν_range(ℓ_arr, ν_arr; kwargs...)
	j_range, ν_ind_range = iters

	hω_arr = get(kwargs, :hω) do
		h = hω(comm, m, xobs1, xobs2, los; kwargs..., print_timings = false)
		_broadcast(h, 0, comm)
	end

	K_δτ_uₛ₀ = pmapsum(comm, kernel_uₛ₀_rθϕ_partial, iters,
		xobs1, xobs2, los, SHModes, hω_arr, p_Gsrc, p_Gobs1, p_Gobs2, r_src)

	K_δτ_uₛ₀ === nothing  && return nothing

	if get(kwargs, :save, true)
		filename = joinpath(SCRATCH_kerneldir[], kernelfilenameuₛ₀rθϕ(m, los))

		header = generatefitsheader(xobs1, xobs2, SHModes, j_range, ν_ind_range)

		FITS(filename,"w") do f
			FITSIO.write(f, reinterpret_as_float(K_δτ_uₛ₀), header = header)
		end
	end

	SHArray(K_δτ_uₛ₀, (axes(K_δτ_uₛ₀)[1:2]..., SHModes))
end

# Compute Kₛ₀_rθϕ directly
function kernel_uₛ₀_rθϕ_2(comm, m::SeismicMeasurement, xobs1, xobs2, los::los_direction = los_radial(); kwargs...)
	_SHModes = getkernelmodes(; kwargs...)
	SHModes = LM(l_range(_SHModes), 0)

	r_src, = read_rsrc_robs_c_scale(kwargs)
	p_Gsrc, p_Gobs1, p_Gobs2 = read_parameters_for_points(xobs1, xobs2; kwargs...)
	@unpack ν_arr, ℓ_arr = p_Gsrc
	iters = ℓ_and_ν_range(ℓ_arr,ν_arr; kwargs...)
	j_range, ν_ind_range = iters

	hω_arr = get(kwargs, :hω) do
		h = hω(comm, m, xobs1, xobs2, los; kwargs..., print_timings = false)
		_broadcast(h, 0, comm)
	end

	K_δτ_uₛ₀ = pmapsum(comm, kernel_uₛ₀_rθϕ_partial_2, iters,
		xobs1, xobs2, los, SHModes, hω_arr, p_Gsrc, p_Gobs1, p_Gobs2, r_src)

	K_δτ_uₛ₀ === nothing  && return nothing
	if get(kwargs, :save, true)
		filename = joinpath(SCRATCH_kerneldir[], kernelfilenameuₛ₀rθϕ(m, los))

		header = generatefitsheader(xobs1, xobs2, SHModes, j_range, ν_ind_range)

		FITS(filename,"w") do f
			FITSIO.write(f, reinterpret_as_float(K_δτ_uₛ₀), header = header)
		end
	end

	SHArray(K_δτ_uₛ₀, (axes(K_δτ_uₛ₀)[1:2]..., SHModes))
end

function kernel_ψϕₛ₀(comm, m::SeismicMeasurement, xobs1, xobs2, los::los_direction = los_radial(); kwargs...)
	Kv = kernel_uₛ₀_rθϕ(comm, m, xobs1, xobs2, los; kwargs..., save = false)
	Kv === nothing && return nothing
	kernel_ψϕₛ₀(Kv; kwargs...)
end

function kernel_ψϕₛ₀(Kv::AbstractArray; kwargs...)
	mode_range = firstshmodes(Kv);
	Kψϕ = zeros(axes(Kv, 1), mode_range);
	kernel_ψϕₛ₀!(Kψϕ, Kv; save = get(kwargs, :save, true))

	if get(kwargs, :save, true)
		filename = joinpath(SCRATCH_kerneldir[], "Kpsi_imag.fits")
		FITS(filename,"w") do f
			write(f, reinterpret_as_float(Kψϕ))
		end
	end

	return Kψϕ
end

function kernel_ψϕₛ₀!(Kψϕ::SHArray, Kv::SHArray; kwargs...)
	mode_range = firstshmodes(Kv)

	for l in l_range(mode_range)
		@views Kψϕ[:, (l, 0)] .= -(ddr*(Kv[:, 2, (l, 0)]./ρ) .+
			@. (Kv[:, 2, (l, 0)] - 2Ω(l, 0)*Kv[:, 1, (l, 0)])/(ρ*r) )
	end

	return Kψϕ
end

#################################################################################################################
# Validation for isotropic sound speed perturbation
#################################################################################################################

function populatekernelvalidation!(::SoundSpeed, ::los_radial, K::AbstractVector,
	(ℓ, ω), Y12ℓ, conjhω, H¹₁jjω_r₁r₂, H¹₁jj_r₂r₁, dω)

	pre = 1/√(4π) * dω/2π * ω^2 * Powspec(ω) * Y12ℓ
	@. K += pre * 2real(conjhω * ( H¹₁jjω_r₁r₂ + conj(H¹₁jj_r₂r₁) ) )
end

function populatekernelvalidation!(::SoundSpeed, ::los_radial, K::AbstractMatrix,
	(ℓ, ω), Y12ℓ, conjhω, H¹₁jjω_r₁r₂::AbstractVector{<:Complex},
	H¹₁jj_r₂r₁::AbstractVector{<:Complex}, dω)

	pre = 1/√(4π) * dω/2π * ω^2 * Powspec(ω)

	for n2ind in UnitRange(axes(K, 2))
		temp_ωℓ_n2 = pre * Y12ℓ[n2ind]
		conjhω_n2 = conjhω[n2ind]
		for r_ind in UnitRange(axes(K, 1))
			K[r_ind, n2ind] += temp_ωℓ_n2 *
				2real(conjhω_n2 * ( H¹₁jjω_r₁r₂[r_ind] + conj(H¹₁jj_r₂r₁[r_ind]) ) )
		end
	end
end

function populatekernelvalidation!(::SoundSpeed, ::los_earth, K::AbstractVector,
	(j, ω), Y12j, conjhω, Hjj_r₁r₂, Hjj_r₂r₁, dω)

	pre = dω/2π * ω^2 * Powspec(ω) * √(1/π)

	for r_ind in UnitRange(axes(K, 1))
		for α₂ in 0:1, α₁ in 0:1

			llY = pre * real(Y12j[α₁, α₂])
			iszero(llY) && continue

			K[r_ind] += real(conjhω *
				( Hjj_r₁r₂[α₁, α₂, r_ind] + conj(Hjj_r₂r₁[α₂, α₁, r_ind]) ) ) * llY
		end
	end
end

function populatekernelvalidation!(::SoundSpeed, ::los_earth, K::AbstractMatrix,
	(j, ω), Y12j, conjhω, Hjjω_r₁r₂, Hjj_r₂r₁, dω)

	pre = dω/2π * ω^2 * Powspec(ω) * √(1/π)

	for n2ind in axes(K, 2)
		Yjn₂ = Y12j[n2ind]
		conjhω_n2 = conjhω[n2ind]

		for r_ind in UnitRange(axes(K, 1))
			for α₂ in 0:1, α₁ in 0:1

				llY = pre * real(Yjn₂[α₁, α₂])
				iszero(llY) && continue

				K[r_ind, n2ind] += real(conjhω_n2 *
					( Hjjω_r₁r₂[α₁, α₂, r_ind] + conj(Hjj_r₂r₁[α₂, α₁, r_ind]) ) ) * llY
			end
		end
	end
end

function kernel_δc₀₀_partial(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	xobs1::SphericalPoint, xobs2::SphericalPoint, los::los_direction, hω,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = nothing,
	r_src = r_src_default)

	@unpack p_Gsrc, p_Gobs1, p_Gobs2, Gfn_path_src, NGfn_files_src,
	Gfn_path_obs1, NGfn_files_obs1, Gfn_path_obs2, NGfn_files_obs2 =
		unpackGfnparams(p_Gsrc, r_src, p_Gobs1, radius(xobs1), p_Gobs2, radius(xobs2))

	@unpack ℓ_arr, ω_arr, Nν_Gfn, dω = p_Gsrc

	r₁_ind, r₂_ind = radial_grid_index.((xobs1, xobs2))

	Gfn_fits_files_src, Gfn_fits_files_obs1, Gfn_fits_files_obs2 =
		Gfn_fits_files.((Gfn_path_src, Gfn_path_obs1, Gfn_path_obs2),
			((ℓ_arr, 1:Nν_Gfn),), (ℓ_ωind_iter_on_proc,),
			(NGfn_files_src, NGfn_files_obs1, NGfn_files_obs2))

	K = zeros(nr)

	arrs = allocatearrays(SoundSpeed(), los, r₁_ind == r₂_ind);
	@unpack Gsrc, drGsrc, divGsrc, Gobs1, drGobs1, Gobs2, divGobs = arrs;
	fjj_r₁_rsrc, fjj_r₂_rsrc = arrs.fjₒjₛ_r₁_rsrc, arrs.fjₒjₛ_r₂_rsrc;
	H¹₁jj_r₁r₂, H¹₁jj_r₂r₁ = arrs.Hjₒjₛω_r₁r₂, arrs.Hjₒjₛω_r₂r₁;

	Y12 = los_projected_biposh_spheroidal(computeY₀₀, xobs1, xobs2, los, ℓ_ωind_iter_on_proc)

	for (ℓ, ω_ind) in ℓ_ωind_iter_on_proc
		ω = ω_arr[ω_ind]
		conjhω = conj(hω[ω_ind])

		# Green function about the source radius
		read_Gfn_file_at_index!(Gsrc, Gfn_fits_files_src,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 1)

		# Derivative of Green function about the source radius
		read_Gfn_file_at_index!(drGsrc, Gfn_fits_files_src,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_src, 1:1, srcindFITS(los), :, 2)

		Gγr_r₁_rsrc = αrcomp(Gsrc, r₁_ind, los)
		Gγr_r₂_rsrc = αrcomp(Gsrc, r₂_ind, los)

		# Green function about receiver location
		read_Gfn_file_at_index!(Gobs1, Gfn_fits_files_obs1,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_obs1, 1:2, srcindFITS(los), :, 1)

		# Derivative of Green function about receiver location
		read_Gfn_file_at_index!(drGobs1, Gfn_fits_files_obs1,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_obs1, 1:1, srcindFITS(los), :, 2)

		radial_fn_isotropic_δc_firstborn!(fjj_r₁_rsrc,
			Gsrc, drGsrc, divGsrc, Gobs1, drGobs1, divGobs, ℓ)

		Hjₒjₛω!(H¹₁jj_r₁r₂, fjj_r₁_rsrc, Gγr_r₂_rsrc)

		if r₁_ind != r₂_ind
			# Green function about receiver location
			read_Gfn_file_at_index!(Gobs2, Gfn_fits_files_obs2,
				(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_obs2, 1:2, srcindFITS(los), :, 1)

			# Derivative of Green function about receiver location
			read_Gfn_file_at_index!(drGobs2, Gfn_fits_files_obs2,
				(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_obs2, 1:1, srcindFITS(los), :, 2)

			radial_fn_isotropic_δc_firstborn!(fjj_r₂_rsrc,
				Gsrc, drGsrc, divGsrc, Gobs2, drGobs2, divGobs, ℓ)

			Hjₒjₛω!(H¹₁jj_r₂r₁, fjj_r₂_rsrc, Gγr_r₁_rsrc)
		end

		populatekernelvalidation!(SoundSpeed(), los, K,
		(ℓ, ω), Y12[ℓ], conjhω, H¹₁jj_r₁r₂, H¹₁jj_r₂r₁, dω)
	end

	map(closeGfnfits, (Gfn_fits_files_src, Gfn_fits_files_obs1, Gfn_fits_files_obs2))
	return K
end

function kernel_δc₀₀_partial(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	nobs1::Point2D, nobs2_arr::Vector{<:Point2D}, los::los_direction, hω_arr,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = p_Gobs1,
	r_src = r_src_default)

	hω_arr = permutedims(hω_arr) #  convert to (n2, ω)

	r_obs_ind = radial_grid_index(nobs1)

	@unpack p_Gsrc, p_Gobs1, Gfn_path_src, NGfn_files_src =
		unpackGfnparams(p_Gsrc, r_src, p_Gobs1, r_obs_default, p_Gobs2, r_obs_default)

	Gfn_path_obs, NGfn_files_obs = p_Gobs1.path, p_Gobs1.num_procs
	@unpack ℓ_arr, ω_arr, Nν_Gfn, dω = p_Gsrc

	Gfn_fits_files_src, Gfn_fits_files_obs =
		Gfn_fits_files.((Gfn_path_src, Gfn_path_obs),
			((ℓ_arr, 1:Nν_Gfn),), (ℓ_ωind_iter_on_proc,),
			(NGfn_files_src, NGfn_files_obs))

	K = zeros(nr, length(nobs2_arr))

	arrs = allocatearrays(SoundSpeed(), los, true)
	@unpack Gsrc, drGsrc, divGsrc, divGobs = arrs
	Gobs, drGobs = arrs.Gobs1, arrs.drGobs1
	f_robs_rsrc, Hjj_robs_rsrc = arrs.fjₒjₛ_r₁_rsrc, arrs.Hjₒjₛω_r₁r₂

	Y12 = los_projected_biposh_spheroidal(computeY₀₀, nobs1, nobs2_arr, los, ℓ_ωind_iter_on_proc)

	for (ℓ, ω_ind) in ℓ_ωind_iter_on_proc
		ω = ω_arr[ω_ind]
		conjhω = conj(@view hω_arr[:, ω_ind])

		# Green function about the source radius
		read_Gfn_file_at_index!(Gsrc, Gfn_fits_files_src,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 1)

		# Derivative of Green function about the source radius
		read_Gfn_file_at_index!(drGsrc, Gfn_fits_files_src,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_src, 1:1, srcindFITS(los), :, 2)

		Gγr_robs_rsrc = αrcomp(Gsrc, r_obs_ind, los)

		# Green function about receiver location
		read_Gfn_file_at_index!(Gobs, Gfn_fits_files_obs,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_obs, 1:2, srcindFITS(los), :, 1)

		# Derivative of Green function about receiver location
		read_Gfn_file_at_index!(drGobs, Gfn_fits_files_obs,
			(ℓ_arr, 1:Nν_Gfn), (ℓ, ω_ind), NGfn_files_obs, 1:1, srcindFITS(los), :, 2)

		radial_fn_isotropic_δc_firstborn!(f_robs_rsrc,
			Gsrc, drGsrc, divGsrc, Gobs, drGobs, divGobs, ℓ)

		Hjₒjₛω!(Hjj_robs_rsrc, f_robs_rsrc, Gγr_robs_rsrc)

		populatekernelvalidation!(SoundSpeed(), los, K,
			(ℓ, ω), (@view Y12[:, ℓ]), conjhω, Hjj_robs_rsrc, Hjj_robs_rsrc, dω)
	end

	map(closeGfnfits, (Gfn_fits_files_src, Gfn_fits_files_obs))
	return K
end

kernelfilenameδc₀₀(::TravelTimes, ::los_radial) = "K_δτ_δc₀₀.fits"
kernelfilenameδc₀₀(::TravelTimes, ::los_earth) = "K_δτ_δc₀₀_los.fits"
kernelfilenameδc₀₀(::Amplitudes, ::los_radial) = "K_A_δc₀₀.fits"
kernelfilenameδc₀₀(::Amplitudes, ::los_earth) = "K_A_δc₀₀_los.fits"

function kernel_δc₀₀(comm, m::SeismicMeasurement, xobs1, xobs2, los::los_direction = los_radial(); kwargs...)
	r_src, = read_rsrc_robs_c_scale(kwargs)
	p_Gsrc, p_Gobs1, p_Gobs2 = read_parameters_for_points(xobs1, xobs2; kwargs..., c_scale = 1)
	@unpack ν_arr, ℓ_arr = p_Gsrc
	iters = ℓ_and_ν_range(ℓ_arr,ν_arr; kwargs...)

	hω_arr = get(kwargs, :hω) do
		h = hω(comm, m, xobs1, xobs2, los; kwargs..., print_timings = false)
		_broadcast(h, 0, comm)
	end

	K = pmapsum(comm, kernel_δc₀₀_partial, iters,
		xobs1, xobs2, los, hω_arr, p_Gsrc, p_Gobs1, p_Gobs2, r_src)
	K === nothing && return nothing

	if get(kwargs, :save, true)
		filename = joinpath(SCRATCH_kerneldir[], kernelfilenameδc₀₀(m, los))
		FITS(filename,"w") do f
			write(f, reinterpret_as_float(K))
		end
	end

	return K
end

########################################################################################
# Sound-speed kernels
########################################################################################

function populatekernel!(::SoundSpeed, ::los_radial, K::StructArray{<:Complex}, SHModes, Yjₒjₛ_n1n2, Yjₒjₛ_n2n1,
	(l, m), (jₛ, jₒ, ω), pre, tworealconjhωHjₒjₛω_r₁r₂, tworealconjhωconjHjₒjₛω_r₂r₁)

	lm_ind = modeindex(firstshmodes(Yjₒjₛ_n1n2), (l, m))
	lm_ind_K = modeindex(SHModes, (l, m))

	_Yjₒjₛ_lm_n₁n₂ = Yjₒjₛ_n1n2[lm_ind]
	_Yjₒjₛ_lm_n₂n₁ = Yjₒjₛ_n2n1[lm_ind]

	@turbo for r_ind in eachindex(r)
		T₁₂ = pre * conj(_Yjₒjₛ_lm_n₁n₂) * tworealconjhωHjₒjₛω_r₁r₂[r_ind]
		T₂₁ = pre * conj(_Yjₒjₛ_lm_n₂n₁) * tworealconjhωconjHjₒjₛω_r₂r₁[r_ind]

		K[r_ind, lm_ind_K] += T₂₁ + T₁₂
	end

	return K
end

function populatekernel!(::SoundSpeed, ::los_earth, K::StructArray{<:Complex}, SHModes, Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₂n₁,
	(l, m), (jₛ, jₒ, ω), pre, tworealconjhωHjₒjₛω_r₁r₂, tworealconjhωconjHjₒjₛω_r₂r₁)

	lm_ind = modeindex(firstshmodes(Yjₒjₛ_lm_n₁n₂), (l, m))
	lm_ind_K = modeindex(SHModes, (l, m))

	_Yjₒjₛ_lm_n₁n₂ = parent(Yjₒjₛ_lm_n₁n₂[lm_ind])
	_Yjₒjₛ_lm_n₂n₁ = parent(Yjₒjₛ_lm_n₂n₁[lm_ind])

	@turbo for r_ind in eachindex(r)
		T₁₂ = pre * sum(conj(_Yjₒjₛ_lm_n₁n₂) .* tworealconjhωHjₒjₛω_r₁r₂[r_ind])
		T₂₁ = pre * sum(conj(_Yjₒjₛ_lm_n₂n₁) .* tworealconjhωconjHjₒjₛω_r₂r₁[r_ind])

		K[r_ind, lm_ind_K] += T₂₁ + T₁₂
	end
	return K
end

function kernel_δcₛₜ_partial(localtimer, ℓ_ωind_iter_on_proc::ProductSplit,
	xobs1::SphericalPoint, xobs2::SphericalPoint, los::los_direction, SHModes, hω_arr,
	p_Gsrc::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs1::Union{Nothing, ParamsGfn} = nothing,
	p_Gobs2::Union{Nothing, ParamsGfn} = nothing,
	r_src = r_src_default)

	@unpack p_Gsrc, p_Gobs1, p_Gobs2, Gfn_path_src, NGfn_files_src,
	Gfn_path_obs1, NGfn_files_obs1, Gfn_path_obs2, NGfn_files_obs2 =
		unpackGfnparams(p_Gsrc, r_src, p_Gobs1, radius(xobs1), p_Gobs2, radius(xobs2))

	@unpack ℓ_arr, ω_arr, Nν_Gfn, dω = p_Gsrc

	s_max = l_max(SHModes)

	r₁_ind, r₂_ind = radial_grid_index.((xobs1, xobs2))
	obs_at_same_height = r₁_ind == r₂_ind

	Gfn_fits_files_src = Gfn_fits_files(Gfn_path_src,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, NGfn_files_src)

	# Gℓ′ω(r, robs) files
	Gfn_fits_files_obs1 = Gfn_fits_files(Gfn_path_obs1,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, s_max, NGfn_files_obs1)

	Gfn_fits_files_obs2 = Gfn_fits_files(Gfn_path_obs2,
						(ℓ_arr, 1:Nν_Gfn), ℓ_ωind_iter_on_proc, s_max, NGfn_files_obs2)

	arrs = allocatearrays(SoundSpeed(), los, obs_at_same_height)
	@unpack Gsrc, drGsrc, Gobs1, drGobs1, Gobs2, drGobs2, divGsrc, divGobs = arrs;
	@unpack fjₒjₛ_r₁_rsrc, fjₒjₛ_r₂_rsrc, Hjₒjₛω_r₁r₂, Hjₒjₛω_r₂r₁ = arrs;
	@unpack Gobs1_cache, drGobs1_cache, Gobs2_cache, drGobs2_cache = arrs;
	@unpack tworealconjhωHjₒjₛω_r₁r₂, tworealconjhωconjHjₒjₛω_r₂r₁ = arrs;
	tworealconjhωconjHjₒjₛω_r₂r₁P, tworealconjhωHjₒjₛω_r₁r₂P = map(parent, (tworealconjhωconjHjₒjₛω_r₂r₁, tworealconjhωHjₒjₛω_r₁r₂));

	HT = HybridArray{Tuple{sizeindG(los)...,sizeindG(los)...,StaticArrays.Dynamic()}}
	H₂₁ = _structarrayparent(Hjₒjₛω_r₂r₁, HT)
	H₁₂ = _structarrayparent(Hjₒjₛω_r₁r₂, HT)

	tworealconjhωconjHjₒjₛω_r₂r₁S = reinterpretSMatrix(tworealconjhωconjHjₒjₛω_r₂r₁P, los)
	tworealconjhωHjₒjₛω_r₁r₂S = reinterpretSMatrix(tworealconjhωHjₒjₛω_r₁r₂P, los)

	ℓ_range_proc = UnitRange(extremaelement(ℓ_ωind_iter_on_proc, dims = 1)...)

	jₒjₛ_allmodes = L2L1Triangle(ℓ_range_proc, s_max, ℓ_arr)
	jₒrange = l2_range(jₒjₛ_allmodes)
	@unpack Gobs1_cache, Gobs2_cache = arrs
	for jₒ in jₒrange
		Gobs1_cache[jₒ] = zeros(ComplexF64, axes(Gsrc))
		drGobs1_cache[jₒ] = zeros(ComplexF64, axes(drGsrc))
		if !obs_at_same_height
			Gobs2_cache[jₒ] = zeros(ComplexF64, axes(Gsrc))
			drGobs2_cache[jₒ] = zeros(ComplexF64, axes(drGsrc))
		end
	end

	@timeit localtimer "CG" begin
		C = SHVector([zeros(abs(jₒ-jₛ):jₒ+jₛ) for (jₒ, jₛ) in jₒjₛ_allmodes], jₒjₛ_allmodes)
		for (jₒ, jₛ) in jₒjₛ_allmodes
			C_jₒjₛ = C[(jₒ, jₛ)]
			for l in abs(jₒ-jₛ):jₒ+jₛ
				if isodd(jₒ+jₛ+l)
					continue
				end
				C_jₒjₛ[l] = clebschgordan(Float64, jₒ, 0, jₛ, 0, l, 0)
			end
		end
	end

	_K = zeros(nr, length(SHModes))
	K = StructArray{ComplexF64}((_K, zero(_K)))

	@timeit localtimer "biposh" begin
		Y12, Y21 = los_projected_spheroidal_biposh_flippoints(xobs1, xobs2, los, SHModes, jₒjₛ_allmodes)
	end

	ω_ind_prev = -1
	# Loop over the Greenfn files
	for (jₛ, ω_ind) in ℓ_ωind_iter_on_proc
		ω = ω_arr[ω_ind]
		conjhω = conj(hω_arr[ω_ind])
		dωω²Pω = dω/2π * ω^2 * Powspec(ω)

		@timeit localtimer "FITS" begin

		# Green function about the source radius
		read_Gfn_file_at_index!(Gsrc, Gfn_fits_files_src,
			(ℓ_arr, 1:Nν_Gfn), (jₛ, ω_ind), NGfn_files_src, 1:2, srcindFITS(los), :, 1)

		Gαr_r₁_rsrc = αrcomp(Gsrc, r₁_ind, los)
		Gαr_r₂_rsrc = αrcomp(Gsrc, r₂_ind, los)

		# Derivative of Green function about the source radius
		read_Gfn_file_at_index!(drGsrc, Gfn_fits_files_src,
			(ℓ_arr, 1:Nν_Gfn), (jₛ, ω_ind), NGfn_files_src, 1:1, srcindFITS(los), :, 2)
		end #localtimer

		for jₒ in l2_range(jₒjₛ_allmodes, jₛ)

			jₒjₛ_ind = modeindex(jₒjₛ_allmodes, (jₒ, jₛ))

			Yjₒjₛ_lm_n₁n₂ = Y12[jₒjₛ_ind]
			Yjₒjₛ_lm_n₂n₁ = Y21[jₒjₛ_ind]

			(Yjₒjₛ_lm_n₁n₂ === nothing || Yjₒjₛ_lm_n₂n₁ === nothing) && continue

			Cljₒjₛ = C[(jₒ, jₛ)]

			Gobs1 = Gobs1_cache[jₒ]
			drGobs1 = drGobs1_cache[jₒ]
			Gobs2 = Gobs2_cache[jₒ]
			drGobs2 = drGobs2_cache[jₒ]

			@timeit localtimer "FITS" begin

			if ω_ind != ω_ind_prev || jₒ ∉ l2_range(jₒjₛ_allmodes, jₛ-1)
				# Green functions based at the observation point for ℓ′
				read_Gfn_file_at_index!(Gobs1, Gfn_fits_files_obs1,
				(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs1, 1:2, srcindFITS(los), :, 1)

				read_Gfn_file_at_index!(drGobs1, Gfn_fits_files_obs1,
				(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs1, 1:1, srcindFITS(los), :, 2)
			end

			end # timer

			@timeit localtimer "radial term" begin

			# precompute the radial term in f
			radial_fn_δc_firstborn!(fjₒjₛ_r₁_rsrc, Gsrc, drGsrc, jₛ, divGsrc,
				Gobs1, drGobs1, jₒ, divGobs)

			end # timer

			@timeit localtimer "radial term 2" begin
				Hjₒjₛω!(Hjₒjₛω_r₁r₂, fjₒjₛ_r₁_rsrc, Gαr_r₂_rsrc)
			end
			@timeit localtimer "radial term 3" begin
				@turbo for I in eachindex(H₁₂)
					tworealconjhωHjₒjₛω_r₁r₂P[I] = 2real(conjhω * H₁₂[I])
				end
			end

			if !obs_at_same_height

				@timeit localtimer "FITS" begin
				if ω_ind != ω_ind_prev || jₒ ∉ l2_range(jₒjₛ_allmodes, jₛ-1)
					read_Gfn_file_at_index!(Gobs2, Gfn_fits_files_obs2,
					(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs2, 1:2, srcindFITS(los), :, 1)

					read_Gfn_file_at_index!(drGobs2, Gfn_fits_files_obs2,
					(ℓ_arr, 1:Nν_Gfn), (jₒ, ω_ind), NGfn_files_obs2, 1:1, srcindFITS(los), :, 2)
				end
				end # timer

				@timeit localtimer "radial term" begin
				radial_fn_δc_firstborn!(fjₒjₛ_r₂_rsrc, Gsrc, drGsrc, jₛ, divGsrc,
					Gobs2, drGobs2, jₒ, divGobs)
				end # timer

				@timeit localtimer "radial term 2" begin
				Hjₒjₛω!(Hjₒjₛω_r₂r₁, fjₒjₛ_r₂_rsrc, Gαr_r₁_rsrc)
				end
			end
			@timeit localtimer "radial term 3" begin
				@turbo for I in eachindex(H₂₁)
					tworealconjhωconjHjₒjₛω_r₂r₁P[I] = 2real(conjhω * conj(H₂₁[I]))
				end
			end

			for (l, m) in firstshmodes(Yjₒjₛ_lm_n₁n₂)

				# The Clebsch-Gordan coefficients imply the selection
				# rule that only even ℓ+ℓ′+s modes contribute
				isodd(jₒ + jₛ + l) && continue

				# Check if triangle condition is satisfied
				# The loop is over all possible ℓ and ℓ′ for a given s_max
				# Not all combinations would contribute towards a specific s
				δ(jₛ, jₒ, l) || continue

				pre = dωω²Pω * Njₒjₛs(jₒ, jₛ, l) * Cljₒjₛ[l]

				@timeit localtimer "kernel" begin

				populatekernel!(SoundSpeed(), los, K, SHModes,
					Yjₒjₛ_lm_n₁n₂, Yjₒjₛ_lm_n₂n₁,
					(l, m), (jₛ, jₒ, ω), pre,
					tworealconjhωHjₒjₛω_r₁r₂S,
					tworealconjhωconjHjₒjₛω_r₂r₁S)
				end
			end # lm
		end # jₒ

		ω_ind_prev = ω_ind
	end # (jₛ, ω)

	map(closeGfnfits, (Gfn_fits_files_src, Gfn_fits_files_obs1, Gfn_fits_files_obs2))
	Array(K)
end

function kernelfilenameδcₛₜ(m::SeismicMeasurement, los::los_direction, j_range, SHModes)
	mtag = m isa TravelTimes ? "δτ" : "A"
	lostag = los isa los_radial ? "" : "_los"
	"Kst_$(mtag)_c_$(modetag(j_range, SHModes))$(lostag).fits"
end

function kernel_δcₛₜ(comm, m::SeismicMeasurement, xobs1, xobs2,
	los::los_direction = los_radial(); kwargs...)

	SHModes = getkernelmodes(; kwargs...)

	r_src, = read_rsrc_robs_c_scale(kwargs)
	p_Gsrc, p_Gobs1, p_Gobs2 = read_parameters_for_points(xobs1, xobs2; kwargs..., c_scale = 1)
	@unpack ν_arr, ℓ_arr = p_Gsrc
	iters = ℓ_and_ν_range(ℓ_arr,ν_arr; kwargs...)
	j_range, ν_ind_range = iters

	hω_arr = get(kwargs, :hω) do
		h = hω(comm, m, xobs1, xobs2, los; kwargs..., print_timings = false)
		_broadcast(h, 0, comm)
	end

	K_δcₛₜ = pmapsum(comm, kernel_δcₛₜ_partial, iters,
		xobs1, xobs2, los, SHModes, hω_arr, p_Gsrc, p_Gobs1, p_Gobs2, r_src)

	K_δcₛₜ === nothing && return nothing

	if get(kwargs, :save, true)
		filename = joinpath(SCRATCH_kerneldir[], kernelfilenameδcₛₜ(m, los, j_range, SHModes))
		header = generatefitsheader(xobs1, xobs2, SHModes, j_range, ν_ind_range)
		FITS(filename,"w") do f
			FITSIO.write(f, reinterpret_as_float(K_δcₛₜ), header = header)
		end
	end

	SHArray(K_δcₛₜ, (axes(K_δcₛₜ, 1), SHModes))
end


using LearnBase
using DataFrames
import Base: cat, vcat, hcat, _cat, lastindex, getindex

abstract type AbstractNode end
abstract type AbstractProductNode <: AbstractNode end
abstract type AbstractBagNode <: AbstractNode end

# FIXME: this alias would better be Union{AbstractVector{T}, Tuple{Vararg{T}}}
# and method signatures should do AbstractVecOrTuple{<:T} when they want covariance,
# but that solution currently fails (see #27188 and #27224)
AbstractVecOrTuple{T} = Union{AbstractVector{<:T}, Tuple{Vararg{T}}}

"""
    data(x::AbstractNode)

    return data hold by the datanode
"""
data(x::AbstractNode) = x.data
data(x) = x

"""
    catobs(as...)

    concatenates `as...` into a single datanode while preserving their structure
"""
catobs(as...) = reduce(catobs, collect(as))

# reduction of common datatypes the way we like it
reduce(::typeof(catobs), as::Vector{<: AbstractMatrix}) = reduce(hcat, as)
@adjoint function reduce(::typeof(catobs), as::Vector{<: AbstractMatrix})
  sz = cumsum(size.(as, 2))
  return reduce(hcat, as), Δ -> (nothing, map(n -> Zygote.pull_block_horz(sz[n], Δ, as[n]), eachindex(as)))
end
reduce(::typeof(catobs), as::Vector{<: AbstractVector}) = reduce(vcat, as)
reduce(::typeof(catobs), as::Vector{<: DataFrame}) = reduce(vcat, as)
reduce(::typeof(catobs), as::Vector{<: Missing}) = missing
reduce(::typeof(catobs), as::Vector{<: Nothing}) = nothing
reduce(::typeof(catobs), as::Vector{<: Union{Missing, Nothing}}) = nothing
function reduce(::typeof(catobs), as::Vector{T}) where {T <: Union{Missing, AbstractNode}}
    reduce(catobs, [a for a in as if !ismissing(a)])
end

function reduce(::typeof(catobs), as::Vector{<: Any})
    isempty(as) && return(as)
    T = mapreduce(typeof, typejoin, as)
    T == Any && @error "cannot reduce Any"
    reduce(catobs, Vector{T}(as))
end

Base.cat(as::AbstractNode...; dims = :) = reduce(catobs, collect(as))

_cattrees(as::Vector{T}) where T <: Union{Tuple, Vector}  = tuple([reduce(catobs, [a[i] for a in as]) for i in 1:length(as[1])]...)
function _cattrees(as::Vector{T}) where T <: NamedTuple
    ks = keys(as[1])
    vs = [k => reduce(catobs, [a[k] for a in as]) for k in ks]
    (;vs...)
end

mapdata(f, x) = f(x)

# functions to make datanodes compatible with getindex and with MLDataPattern
Base.getindex(x::T, i::BitArray{1}) where T <: AbstractNode = x[findall(i)]
Base.getindex(x::T, i::Vector{Bool}) where T <: AbstractNode = x[findall(i)]
Base.getindex(x::AbstractNode, i::Int) = x[i:i]
Base.lastindex(ds::AbstractNode) = nobs(ds)
MLDataPattern.getobs(x::AbstractNode, i) = x[i]
MLDataPattern.getobs(x::AbstractNode, i, ::LearnBase.ObsDim.Undefined) = x[i]
MLDataPattern.getobs(x::AbstractNode, i, ::LearnBase.ObsDim.Last) = x[i]

#subset of common datatypes the way we like them
subset(x::AbstractMatrix, i) = x[:, i]
subset(x::AbstractVector, i) = x[i]
subset(x::AbstractNode, i) = x[i]
subset(x::DataFrame, i) = x[i, :]
subset(::Missing, i) = missing
subset(::Nothing, i) = nothing
subset(xs::Tuple, i) = tuple(map(x -> x[i], xs)...)
subset(xs::NamedTuple, i) = (; [k => xs[k][i] for k in keys(xs)]...)

include("arraynode.jl")

# definitions needed for all types of bag nodes
_len(a::UnitRange) = max(a.stop - a.start + 1, 0)
_len(a::Vector) = length(a)
LearnBase.nobs(a::AbstractBagNode) = length(a.bags)
LearnBase.nobs(a::AbstractBagNode, ::Type{ObsDim.Last}) = nobs(a)
Base.ndims(x::AbstractBagNode) = Colon()

include("bagnode.jl")
include("weighted_bagnode.jl")
include("ngrams.jl")
include("productnode.jl")
include("missingnode.jl")
include("lazynode.jl")


# Restricted for a single non zero block
struct BlockFieldArrayCoo{T,N,A,X} <: AbstractBlockArray{T,N}
  axes::X
  blockids::Vector{NTuple{N,Int}}
  block::A
  function BlockFieldArrayCoo(
    _axes::NTuple{N},
    blockids::Vector{NTuple{N,Int}},
    block::A) where {T,N,A<:AbstractArray{T,N}}

    @assert length(blockids) == 1
    #I = first(blockids)
    #@check blocks_equal(axes(block),map(local_range,_axes,I)) "The given block and axes are incompatible."

    X = typeof(_axes)
    new{T,N,A,X}(_axes,blockids,block)
  end
end

struct BlockFieldArrayCooMap{N} <: Map
  blocksize::NTuple{N,Int}
  blockids::Vector{NTuple{N,Int}}
  ptrs::Array{Int,N}
  function BlockFieldArrayCooMap(blocksize::NTuple{N,Int}, blockids::Vector{NTuple{N,Int}}) where N
    ptrs = fill(-1,blocksize)
    for (p,I) in enumerate(blockids)
      ptrs[I...] = p
      for i in 1:N
        @check blocksize[i] >= I[i]
      end
    end
    new{N}(blocksize,blockids,ptrs)
  end
end

@inline function lazy_map(k::BlockFieldArrayCooMap,T::Type,f::AbstractArray...)
  s = Arrays._common_size(f...)
  N = length(s)
  LazyArray(T,Val(N),Fill(k,s),f...)
end

BlockArrays.blocksize(a::BlockFieldArrayCooMap) = a.blocksize
is_zero_block(a::BlockFieldArrayCooMap,i::Integer) = a.ptrs[i] < 0
is_zero_block(a::BlockFieldArrayCooMap{N},i::Vararg{Integer,N}) where N = a.ptrs[i...] < 0
is_zero_block(a::BlockFieldArrayCooMap{N},i::Vararg{Block,N}) where N = is_zero_block(a,map(Int,i)...)
is_zero_block(a::BlockFieldArrayCooMap,i::Block) = is_zero_block(a,convert(Tuple,i)...)
is_zero_block(a::BlockFieldArrayCooMap,i::CartesianIndex) = is_zero_block(a,Tuple(i)...)

function evaluate!(cache,k::BlockFieldArrayCooMap,axes,block)
  @check map(i->first(blocksize(i)),axes) == k.blocksize "The given axes are not compatible with the given BlockFieldArrayCooMap"
  BlockFieldArrayCoo(axes,k.blockids,block)
end

testitem(a::BlockFieldArrayCoo) = testitem(a.block)

# Specific API

function is_zero_block(a::BlockFieldArrayCoo{T,N},i::Vararg{Integer,N}) where {T,N}
  i != first(a.blockids)
end

# AbstractBlockArray

@inline function BlockArrays.getblock(a::BlockFieldArrayCoo{T,N}, i::Vararg{Integer, N}) where {T,N}
  if i == first(a.blockids)
    a.block
  else
    @notimplemented "Cannot get a zero block from a BlockFieldArrayCoo"
  end
end

# AbstractArray

Base.size(a::BlockFieldArrayCoo) = map(length,Base.axes(a))
Base.axes(a::BlockFieldArrayCoo) = a.axes
Base.IndexStyle(::Type{<:BlockFieldArrayCoo}) = IndexCartesian()

function Base.getindex(a::BlockFieldArrayCoo{T,N},i::Vararg{Integer,N}) where {T,N}
  s = map(findblockindex,a.axes,i)
  ai = a[s...]
  ai
end

# Evaluation

function return_cache(a::BlockFieldArrayCoo,x::Point)
  fc = return_cache(a.block,x)
  fx = return_value(a.block,x)
  bsize = map(i->first(blocksize(i)),a.axes)
  k = BlockArrayCooMap(bsize,a.blockids)
  cr = return_cache(k,a.axes,fx)
  (k,fc,cr)
end

@inline function evaluate!(cache,a::BlockFieldArrayCoo,x::Point)
  k,fc,cr = cache
  fx = evaluate!(fc,a.block,x)
  evaluate!(cr,k,a.axes,fx)
end

function return_cache(a::BlockFieldArrayCoo,x::AbstractVector{<:Point})
  fc = return_cache(a.block,x)
  fx = return_value(a.block,x)
  pr = similar_range(first(a.axes),length(x))
  axs = (pr,a.axes...)
  blockids = map(i->(1,i...),a.blockids)
  bsize = map(i->first(blocksize(i)),axs)
  k = BlockArrayCooMap(bsize,blockids)
  cr = return_cache(k,axs,fx)
  (k,fc,axs,cr)
end

@inline function evaluate!(cache,a::BlockFieldArrayCoo,x::AbstractVector{<:Point})
  k,fc,axs,cr = cache
  fx = evaluate!(fc,a.block,x)
  evaluate!(cr,k,axs,fx)
end

# Gradient

function evaluate!(cache,k::Broadcasting{typeof(∇)},a::BlockFieldArrayCoo)
  g = k(a.block)
  BlockFieldArrayCoo(a.axes,a.blockids,g)
end

function evaluate!(cache,k::Broadcasting{typeof(∇∇)},a::BlockFieldArrayCoo)
  g = k(a.block)
  BlockFieldArrayCoo(a.axes,a.blockids,g)
end

# Transpose

function Base.transpose(a::BlockFieldArrayCoo{T,1} where T)
  r = similar_range(first(axes(a)),1)
  axs = (r,axes(a)...)
  blockids = map(i->(1,i...),a.blockids)
  BlockFieldArrayCoo(axs,blockids,transpose(a.block))
end

# Global optimizations

# Evaluation

function lazy_map(
  ::typeof(evaluate),
  a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}},
  x::AbstractArray{<:Point})

  m = a.maps.value
  cell_axs, cell_ai = a.args
  cell_aix = lazy_map(evaluate,cell_ai,x)
  lazy_map(BlockArrayCooMap(m.blocksize,m.blockids),cell_axs,cell_aix)
end

function lazy_map(
  ::typeof(evaluate),
  a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}},
  cell_x::AbstractArray{<:AbstractVector{<:Point}})

  m = a.maps.value
  cell_axs, cell_ai = a.args
  cell_aix = lazy_map(evaluate,cell_ai,cell_x)

  function result_axes_on_point_vector(axs,x)
    pr = similar_range(first(axs),length(x))
    axs = (pr,axs...)
  end

  cell_axs_new = lazy_map(result_axes_on_point_vector,cell_axs,cell_x)

  bsize_new = (1,m.blocksize...)
  bids_new = map(i->(1,i...),m.blockids)

  r = lazy_map(BlockArrayCooMap(bsize_new,bids_new),cell_axs_new, cell_aix)
end

# Gradient

function lazy_map(
  k::Broadcasting{typeof(∇)}, a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}})

  m = a.maps.value
  cell_axs, cell_ai = a.args

  cell_gi = lazy_map(k,cell_ai)
  lazy_map(m,cell_axs, cell_gi)
end

function lazy_map(
  k::Broadcasting{typeof(∇∇)}, a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}})

  m = a.maps.value
  cell_axs, cell_ai = a.args
  cell_gi = lazy_map(k,cell_ai)
  lazy_map(m,cell_axs,cell_gi)
end

function lazy_map(::typeof(axes),a::LazyArray{<:Fill{Broadcasting{typeof(∇)}}})
  b = a.args[1]
  lazy_map(axes,b)
end

function lazy_map(::typeof(axes),a::LazyArray{<:Fill{Broadcasting{typeof(∇∇)}}})
  b = a.args[1]
  lazy_map(axes,b)
end

# Composition

function lazy_map(
  k::Broadcasting{typeof(∘)}, a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}},b::AbstractArray{<:Field})

  m = a.maps.value
  cell_axs, cell_ai = a.args
  cell_gi = lazy_map(k,cell_ai,b)
  lazy_map(m,cell_axs,cell_gi)
end

function lazy_map(::typeof(axes),a::LazyArray{<:Fill{Broadcasting{typeof(∘)}}})
  b = a.args[1]
  lazy_map(axes,b)
end

# Transpose

function lazy_map(
  k::typeof(transpose), a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}})

  m = a.maps.value
  cell_axs, cell_ai = a.args

  cell_ait = lazy_map(transpose,cell_ai)

  cell_axs_new = lazy_map(cell_axs) do axs
    @check length(axs) == 1
    r = similar_range(first(axs),1)
    (r,axs...)
  end

  bsize_new = (1,m.blocksize...)
  bids_new = map(i->(1,i...),m.blockids)

  lazy_map(BlockFieldArrayCooMap(bsize_new,bids_new),cell_axs_new, cell_ait)
end

# Operations before evaluating

function lazy_map(
  k::Broadcasting{<:Operation}, a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}})
  m = a.maps.value
  cell_axs, cell_ai = a.args
  cell_aif = lazy_map(k,cell_ai)
  lazy_map(m,cell_axs,cell_aif)
end

function lazy_map(
  k::Broadcasting{<:Operation}, a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}},f::AbstractArray{<:Field})
  m = a.maps.value
  cell_axs, cell_ai = a.args
  cell_aif = lazy_map(k,cell_ai,f)
  lazy_map(m,cell_axs,cell_aif)
end

function lazy_map(
  k::Broadcasting{<:Operation}, f::AbstractArray{<:Field}, a::LazyArray{<:Fill{<:BlockFieldArrayCooMap}})
  m = a.maps.value
  cell_axs, cell_ai = a.args
  cell_aif = lazy_map(k,f,cell_ai)
  lazy_map(m,cell_axs,cell_aif)
end

# Operations on values

function _get_axes_and_blocks(f)
  f[1], f[2:end]
end
#
# Unary operations
# Assumption: op is a scaling of a
function lazy_map(
  k::BroadcastingFieldOpMap, a::LazyArray{<:Fill{<:BlockArrayCooMap}})
  m = a.maps.value
  cell_axs, cell_blocks = _get_axes_and_blocks(a.args)
  cell_blocks_new = map(b->lazy_map(k,b),cell_blocks)
  lazy_map(m,cell_axs,cell_blocks_new...)
end

# Binary test/field or trial/field

# Assumption: op is a scaling of a
function _op_basis_vs_field(k,a,f)
  @check ndims(eltype(a)) > ndims(eltype(f))
  m = a.maps.value
  cell_axs, cell_blocks = _get_axes_and_blocks(a.args)
  cell_blocks_new = map(b->lazy_map(k,b,f),cell_blocks)
  lazy_map(m,cell_axs,cell_blocks_new...)
end

function lazy_map(
  k::BroadcastingFieldOpMap,
  a::LazyArray{<:Fill{BlockArrayCooMap{1}}},
  f::AbstractArray{<:Number})
  _op_basis_vs_field(k,a,f)
end

function lazy_map(
  k::BroadcastingFieldOpMap,
  a::LazyArray{<:Fill{BlockArrayCooMap{2}}},
  f::AbstractArray{<:Number})
  _op_basis_vs_field(k,a,f)
end

function lazy_map(
  k::BroadcastingFieldOpMap,
  a::LazyArray{<:Fill{BlockArrayCooMap{2}}},
  f::AbstractArray{<:AbstractVector})
  _op_basis_vs_field(k,a,f)
end

function lazy_map(
  k::BroadcastingFieldOpMap,
  a::LazyArray{<:Fill{BlockArrayCooMap{3}}},
  f::AbstractArray{<:AbstractVector})
  _op_basis_vs_field(k,a,f)
end

# Assumption: op is a scaling of a
function _op_field_vs_basis(k,f,a)
  @check ndims(eltype(a)) > ndims(eltype(f))
  m = a.maps.value
  cell_axs, cell_blocks = _get_axes_and_blocks(a.args)
  cell_blocks_new = map(b->lazy_map(k,f,b),cell_blocks)
  lazy_map(m,cell_axs,cell_blocks_new...)
end

function lazy_map(
  k::BroadcastingFieldOpMap,
  f::AbstractArray{<:Number},
  a::LazyArray{<:Fill{BlockArrayCooMap{1}}})
  _op_field_vs_basis(k,f,a)
end

function lazy_map(
  k::BroadcastingFieldOpMap,
  f::AbstractArray{<:Number},
  a::LazyArray{<:Fill{BlockArrayCooMap{2}}})
  _op_field_vs_basis(k,f,a)
end

function lazy_map(
  k::BroadcastingFieldOpMap,
  f::AbstractArray{<:AbstractVector},
  a::LazyArray{<:Fill{BlockArrayCooMap{2}}})
  _op_field_vs_basis(k,f,a)
end

function lazy_map(
  k::BroadcastingFieldOpMap,
  f::AbstractArray{<:AbstractVector},
  a::LazyArray{<:Fill{BlockArrayCooMap{3}}})
  _op_field_vs_basis(k,f,a)
end

# Binary test/test or trial/trial
# Assumption: op is a linear combination of a and b
function lazy_map(
  k::BroadcastingFieldOpMap,
  a::LazyArray{<:Fill{BlockArrayCooMap{N}}},
  b::LazyArray{<:Fill{BlockArrayCooMap{N}}}) where N

  ma = a.maps.value
  mb = b.maps.value
  @assert blocksize(ma) == blocksize(mb)

  cell_axs, = a.args

  blocks = []
  blockids = eltype(ma.blockids)[]
  for I in eachblockid(ma)
    if is_nonzero_block(ma,I) || is_nonzero_block(mb,I)
      if is_nonzero_block(ma,I) && is_nonzero_block(mb,I)
        aI = _get_cell_block(ma,a,I)
        bI = _get_cell_block(mb,b,I)
      elseif is_nonzero_block(ma,I)
        aI = _get_cell_block(ma,a,I)
        bI = _lazy_array_of_zero_blocks(_block_type(eltype(b)),cell_axs,I,aI)
      else
        bI = _get_cell_block(mb,b,I)
        aI = _lazy_array_of_zero_blocks(_block_type(eltype(a)),cell_axs,I,bI)
      end
      block = lazy_map(k,aI,bI)
      push!(blocks,block)
      push!(blockids,I.n)
    end
  end

  @assert length(blocks) > 0
  m = BlockArrayCooMap(blocksize(ma),blockids)
  lazy_map(m,cell_axs,blocks...)
end

function _block_type(::Type{<:BlockArrayCoo{T,N,A}}) where {T,N,A}
  A
end

function _get_cell_block(
  m::BlockArrayCooMap,
  a::AbstractArray{<:BlockArrayCoo{T,N,A}},
  b::Block) where {T,N,A}
  cell_axs, cell_blocks = _get_axes_and_blocks(a.args)
  I = convert(Tuple,b)
  p = m.ptrs[I...]
  @assert p > 0
  cell_blocks[p]
end

function _lazy_array_of_zero_blocks(::Type{A},cell_axs,I,aI) where A
  lazy_map(cell_axs) do axs
    laxs = map( local_range, axs, I.n)
    Arrays._zero_block(A,laxs)
  end
end

function _lazy_array_of_zero_blocks(
  ::Type{A},cell_axs,I,aI::LazyArray{<:Fill{BlockArrayCooMap{N}}}) where {A,N}

  cell_axsI, = aI.args
  maI = aI.maps.value
  blocks = []
  for J in eachblockid(maI)
    if is_nonzero_block(maI,J)
      block = _lazy_array_of_zero_blocks(_block_type(A),cell_axsI,J,nothing)
      push!(blocks,block)
    end
  end

  lazy_map(maI,cell_axsI,blocks...)
end

# Binary test/trial
# Assumption: op is a product of a and b

#function lazy_map(
#  k::BroadcastingFieldOpMap,
#  a::LazyArray{<:Fill{BlockArrayCooMap{1}}},
#  b::LazyArray{<:Fill{BlockArrayCooMap{2}}})
#
#end

function lazy_map(
  k::BroadcastingFieldOpMap,
  a::LazyArray{<:Fill{BlockArrayCooMap{2}}},
  b::LazyArray{<:Fill{BlockArrayCooMap{3}}})

  cell_axs_a, = a.args
  cell_axs_b, = b.args
  cell_axs = lazy_map((a1,a2) -> (a1[1],a1[2],a2[3]),cell_axs_a,cell_axs_b)
  ma = a.maps.value
  mb = b.maps.value

  blocks = []
  blockids = NTuple{3,Int}[]
  nfield1 = size(ma.ptrs,2)
  nfield2 = size(mb.ptrs,3)
  for f1 in 1:nfield1
    I1 = Block(1,f1)
    for f2 in 1:nfield2
      I2 = Block(1,1,f2)
      if is_nonzero_block(ma,I1) && is_nonzero_block(mb,I2)
        aI1 = _get_cell_block(ma,a,I1)
        bI2 = _get_cell_block(mb,b,I2)
        block = lazy_map(k,aI1,bI2)
        push!(blocks,block)
        push!(blockids,(1,f1,f2))
      end
    end
  end

  @assert length(blocks) > 0
  bs = (ma.blocksize[1],ma.blocksize[2],mb.blocksize[3])
  m = BlockArrayCooMap(bs,blockids)
  lazy_map(m,cell_axs,blocks...)
end

#function lazy_map(
#  k::BroadcastingFieldOpMap,
#  a::LazyArray{<:Fill{BlockArrayCooMap{2}}},
#  b::LazyArray{<:Fill{BlockArrayCooMap{1}}})
#
#end

function lazy_map(
  k::BroadcastingFieldOpMap,
  a::LazyArray{<:Fill{BlockArrayCooMap{3}}},
  b::LazyArray{<:Fill{BlockArrayCooMap{2}}})

  cell_axs_a, = a.args
  cell_axs_b, = b.args
  cell_axs = lazy_map((a1,a2) -> (a2[1],a2[2],a1[3]),cell_axs_a,cell_axs_b)
  ma = a.maps.value
  mb = b.maps.value

  blocks = []
  blockids = NTuple{3,Int}[]
  nfield1 = size(mb.ptrs,2)
  nfield2 = size(ma.ptrs,3)
  for f1 in 1:nfield1
    I1 = Block(1,f1)
    for f2 in 1:nfield2
      I2 = Block(1,1,f2)
      if is_nonzero_block(mb,I1) && is_nonzero_block(ma,I2)
        bI1 = _get_cell_block(mb,b,I1)
        aI2 = _get_cell_block(ma,a,I2)
        block = lazy_map(k,bI1,aI2)
        push!(blocks,block)
        push!(blockids,(1,f1,f2))
      end
    end
  end

  @assert length(blocks) > 0
  bs = (mb.blocksize[1],mb.blocksize[2],ma.blocksize[3])
  m = BlockArrayCooMap(bs,blockids)
  lazy_map(m,cell_axs,blocks...)
end

# Integration of elem vectors
function lazy_map(k::IntegrationMap,a::LazyArray{<:Fill{BlockArrayCooMap{2}}},w::AbstractArray,j::AbstractArray)
  ma = a.maps.value
  cell_axs, cell_blocks = _get_axes_and_blocks(a.args)
  cell_axs_new = lazy_map(a->(a[2],),cell_axs)
  blocks = map(block->lazy_map(k,block,w,j),cell_blocks)
  blockids = [ (ids[2],) for ids in ma.blockids ]
  m = BlockArrayCooMap(ma.blocksize[2:end],blockids)
  lazy_map(m,cell_axs_new,blocks...)
end

## Integration of elem matrices
function lazy_map(k::IntegrationMap,a::LazyArray{<:Fill{BlockArrayCooMap{3}}},w::AbstractArray,j::AbstractArray)
  ma = a.maps.value
  cell_axs, cell_blocks = _get_axes_and_blocks(a.args)
  cell_axs_new = lazy_map(a->(a[2],a[3]),cell_axs)
  blocks = map(block->lazy_map(k,block,w,j),cell_blocks)
  blockids = [ (ids[2],ids[3]) for ids in ma.blockids ]
  m = BlockArrayCooMap(ma.blocksize[2:end],blockids)
  lazy_map(m,cell_axs_new,blocks...)
end
#function lazy_map(k::IntMap,f::VectorOfBlockArrayCoo{T,3} where T,w::AbstractArray,j::AbstractArray)
#  ax = lazy_map(a->(a[2],a[3]),f.axes)
#  blocks = map(block->lazy_map(k,block,w,j),f.blocks)
#  blockids = [ (ids[2], ids[3]) for ids in f.blockids ]
#  VectorOfBlockArrayCoo(blocks,blockids,ax)
#end


# Common XDR routines (for the XTC and TRR formats)

module XDR

export
		read_xdr_num, write_xdr_num, read_xdr_int, write_xdr_int,
		read_xdr_float, size_of_xdr_int, size_of_xdr_ints!, decode_xdr_bits!,
		decode_xdr_ints!

@inline read_xdr_num(io::IO, T::Type) = ntoh(read(io, T))

@inline write_xdr_num(io::IO, x::Number, T::Type) =
		write(io, hton(T(x)))

@inline read_xdr_int(io::IO) = read_xdr_num(io, Int32)

@inline write_xdr_int(io::IO, x::Integer) = write_xdr_num(io, x, Int32)

@inline read_xdr_float(io::IO) = read_xdr_num(io, Float32)

function size_of_xdr_int(size::T) where {T<:Integer}
	local num::T, num_of_bits::T
	num = 1
	num_of_bits = 0
	while (size >= num && num_of_bits < 32)
		num_of_bits += 1
		num <<= 1
	end
	num_of_bits
end

function size_of_xdr_ints!(bytes::AbstractVector{T},
		sizes::AbstractVector{T}) where {T<:Integer}
	local num_of_bytes::T, num_of_bits::T, tmp::T, bytecnt::T, num::T,
			last_byte::T
	num_of_bytes = 1
	num_of_bits = 0
	bytes[1] = 1
	for size in sizes
		tmp = 0
		bytecnt = 1
		while bytecnt <= num_of_bytes
			tmp = bytes[bytecnt] * size + tmp
			bytes[bytecnt] = tmp & 0xff
			tmp >>= 8
			bytecnt += 1
		end
		while tmp != 0
			bytes[bytecnt] = tmp & 0xff
			bytecnt += 1
			tmp >>= 8
		end
		num_of_bytes = bytecnt - 1
	end
	num = 1
	last_byte = bytes[num_of_bytes]
	num_of_bytes -= 1
	while last_byte >= num
		num_of_bits += 1
		num *= 2
	end
	num_of_bits + num_of_bytes * 8
end

function decode_xdr_bits!(state::AbstractVector{T}, buf::AbstractVector{UInt8},
		num_of_bits::T) where {T<:Integer}
	local mask::T, cnt::T, lastbits::T, lastbyte::T, num::T

	mask = (1 << num_of_bits) - 1
	cnt = state[1]
	lastbits = state[2]
	lastbyte = state[3]
	num = 0

	while num_of_bits >= 8
		lastbyte = (lastbyte << 8) | buf[cnt+1]
		cnt += 1
		num |= (lastbyte >> lastbits) << (num_of_bits - 8)
		num_of_bits -= 8
	end
	if num_of_bits > 0
		if lastbits < num_of_bits
			lastbits += 8
			lastbyte = (lastbyte << 8) | buf[cnt+1]
			cnt += 1
		end
		lastbits -= num_of_bits
		num |= (lastbyte >> lastbits) & ((1 << num_of_bits) - 1)
	end
	num &= mask
	state[1] = cnt
	state[2] = lastbits
	state[3] = lastbyte
	num
end

function decode_xdr_ints!(nums::AbstractVector{T}, bytes::AbstractVector{T},
		state::AbstractVector{T}, buf::AbstractVector{UInt8}, num_of_ints::T,
		num_of_bits::T, sizes::AbstractVector{T}, pos::T) where {T<:Integer}
	@inbounds begin
		local num_of_bytes::T, i::T, j::T, p::T
		bytes[2] = 0
		bytes[3] = 0
		bytes[4] = 0
		num_of_bytes = 0
		while num_of_bits > 8
			bytes[num_of_bytes+1] = decode_xdr_bits!(state, buf, T(8))
			num_of_bytes += 1
			num_of_bits -= T(8)
		end
		if num_of_bits > 0
			bytes[num_of_bytes+1] = decode_xdr_bits!(state, buf, num_of_bits)
			num_of_bytes += 1
		end
		i = num_of_ints - 1
		while i > 0
			num::Int = 0
			j = num_of_bytes - 1
			while j >= 0
				num = (num << 8) | bytes[j+1]
				p = unsafe_trunc(Int, num / sizes[i+1])
				bytes[j+1] = p
				num -= p * sizes[i+1]
				j -= 1
			end
			nums[i+pos] = num
			i -= 1
		end
		nums[pos] = bytes[1] | (bytes[2] << 8) | (bytes[3] << 16) |
				(bytes[4] << 24)
	end
	nothing
end

end # module


using Random
using Dates
using Printf
using MPI

mutable struct MonteCarloStatistics
    sweeps::Int
    attemptedLocalUpdates::Int
    acceptedLocalUpdates::Int
    attemptedReplicaExchanges::Int
    acceptedReplicaExchanges::Int
    initializationTime::Float64

    MonteCarloStatistics() = new(0, 0, 0, 0, 0, time())
end

mutable struct MonteCarlo{T<:Lattice}
    lattice::T

    beta::Float64
    thermalizationSweeps::Int
    measurementSweeps::Int
    measurementRate::Int
    replicaExchangeRate::Int
    reportInterval::Int
    checkpointInterval::Int

    rng::MersenneTwister
    seed::UInt
    sweep::Int

    observables::Observables
end

function MonteCarlo(
    lattice::T,
    beta::Float64,
    thermalizationSweeps::Int,
    measurementSweeps::Int;
    measurementRate::Int = 1,
    replicaExchangeRate::Int = 10,
    reportInterval::Int = round(Int, 0.05 * (thermalizationSweeps + measurementSweeps)),
    checkpointInterval::Int = 3600,
    rng::MersenneTwister = copy(Random.GLOBAL_RNG),
    seed::UInt = rand(Random.RandomDevice(),UInt)
    ) where T<:Lattice

    mc = MonteCarlo(deepcopy(lattice), beta, thermalizationSweeps, measurementSweeps, measurementRate, replicaExchangeRate, reportInterval, checkpointInterval, rng, seed, 0, Observables(lattice))
    Random.seed!(mc.rng, mc.seed)

    return mc
end

function run!(mc::MonteCarlo{T}; outfile::Union{String,Nothing}=nothing) where T<:Lattice
    #init MPI
    rank = 0
    commSize = 1
    allBetas = zeros(0)
    enableMPI = false
    if MPI.Initialized()
        commSize = MPI.Comm_size(MPI.COMM_WORLD)
        rank = MPI.Comm_rank(MPI.COMM_WORLD)
        if commSize > 1
            allBetas = zeros(commSize)
            allBetas[rank + 1] = mc.beta
            MPI.Allgather!(UBuffer(allBetas, 1), MPI.COMM_WORLD)
            enableMPI = true
            rank == 0 && @printf("MPI detected. Enabling replica exchanges across %d simulations.\n", commSize)
        end
    end

    #init IO
    enableOutput = typeof(outfile) != Nothing
    if enableOutput
        enableMPI && (outfile *= "." * string(rank))
        isfile(outfile) && error("File ", outfile, " already exists. Terminating.")
    end

    #init spin configuration
    if mc.sweep == 0
        for i in 1:length(mc.lattice)
            setSpin!(mc.lattice, i, uniformOnSphere(mc.rng))
        end
    end

    #init Monte Carlo run
    totalSweeps = mc.thermalizationSweeps + mc.measurementSweeps
    partnerSpinConfiguration = deepcopy(mc.lattice.spins)
    energy = getEnergy(mc.lattice)

    #launch Monte Carlo run
    lastCheckpointTime = time()
    statistics = MonteCarloStatistics()
    rank == 0 && @printf("Simulation started on %s.\n\n", Dates.format(Dates.now(), "dd u yyyy HH:MM:SS"))

    while mc.sweep < totalSweeps
        #perform local sweep
        for i in 1:length(mc.lattice)
            #select random spin
            site = rand(mc.rng, 1:length(mc.lattice))

            #propose new spin configuration
            newSpinState = uniformOnSphere(mc.rng)
            energyDifference = getEnergyDifference(mc.lattice, site, newSpinState)

            #check acceptance of new configuration
            statistics.attemptedLocalUpdates += 1
            p = exp(-mc.beta * energyDifference)
            if (rand(mc.rng) < min(1.0, p))
                setSpin!(mc.lattice, site, newSpinState)
                energy += energyDifference
                statistics.acceptedLocalUpdates += 1
            end
        end
        statistics.sweeps += 1

        #perform replica exchange
        if enableMPI && mc.sweep % mc.replicaExchangeRate == 0
            #determine MPI rank to exchagne configuration with
            if iseven(mc.sweep ÷ mc.replicaExchangeRate)
                partnerRank = iseven(rank) ? rank + 1 : rank - 1
            else
                partnerRank = iseven(rank) ? rank - 1 : rank + 1
            end

            if partnerRank >= 0 && partnerRank < commSize
                #obtain energy of new configuration
                partnerEnergy = MPISendrecvFloat(energy, partnerRank, MPI.COMM_WORLD)

                #check acceptance of new configuration
                statistics.attemptedReplicaExchanges += 1
                exchangeAccepted = false
                if iseven(rank)
                    p = exp(-(allBetas[rank + 1] - allBetas[partnerRank + 1]) * (partnerEnergy - energy))
                    exchangeAccepted = (rand(mc.rng) < min(1.0, p)) ? true : false
                    MPISendBool(exchangeAccepted, partnerRank, MPI.COMM_WORLD)
                else
                    exchangeAccepted = MPIRecvBool(partnerRank, MPI.COMM_WORLD)
                end
                if (exchangeAccepted)
                    energy = partnerEnergy
                    MPI.Sendrecv!(mc.lattice.spins, partnerRank, 0, partnerSpinConfiguration, partnerRank, 0, MPI.COMM_WORLD)
                    (mc.lattice.spins, partnerSpinConfiguration) = (partnerSpinConfiguration, mc.lattice.spins)
                    statistics.acceptedReplicaExchanges += 1
                end
            end
        end

        #perform measurement
        if mc.sweep >= mc.thermalizationSweeps
            if mc.sweep % mc.measurementRate == 0
                performMeasurements!(mc.observables, mc.lattice, energy)
            end
        end

        #increment sweep
        statistics.sweeps += 1
        mc.sweep += 1

        #runtime statistics
        t = time()
        if mc.sweep % mc.reportInterval == 0
            #collect statistics
            progress = 100.0 * mc.sweep / totalSweeps
            thermalized = (mc.sweep >= mc.thermalizationSweeps) ? "YES" : "NO"
            sweeprate = statistics.sweeps / (t - statistics.initializationTime)
            sweeptime = 1.0 / sweeprate
            eta = (totalSweeps - mc.sweep) / sweeprate

            localUpdateAcceptanceRate = 100.0 * statistics.acceptedLocalUpdates / statistics.attemptedLocalUpdates
            if enableMPI
                replicaExchangeAcceptanceRate = 100.0 * statistics.acceptedReplicaExchanges / statistics.attemptedReplicaExchanges
                allLocalAppectanceRate = zeros(commSize)
                allLocalAppectanceRate[rank + 1] = localUpdateAcceptanceRate
                MPI.Allgather!(UBuffer(allLocalAppectanceRate, 1), MPI.COMM_WORLD)
                allReplicaExchangeAcceptanceRate = zeros(commSize)
                allReplicaExchangeAcceptanceRate[rank + 1] = replicaExchangeAcceptanceRate
                MPI.Allgather!(UBuffer(allReplicaExchangeAcceptanceRate, 1), MPI.COMM_WORLD)
            end

            #print statistics
            if rank == 0
                str = ""
                str *= @sprintf("Sweep %d / %d (%.1f%%)", mc.sweep, totalSweeps, progress)
                str *= @sprintf("\t\tETA : %s\n", Dates.format(Dates.now() + Dates.Second(round(Int64,eta)), "dd u yyyy HH:MM:SS"))
                str *= @sprintf("\t\tthermalized : %s\n", thermalized)
                str *= @sprintf("\t\tsweep rate : %.1f sweeps/s\n", sweeprate)
                str *= @sprintf("\t\tsweep duration : %.3f ms\n", sweeptime * 1000)

                if enableMPI
                    for n in 1:commSize
                        str *= @sprintf("\t\tsimulation %d update acceptance rate: %.2f%%\n", n - 1, allLocalAppectanceRate[n])
                        str *= @sprintf("\t\tsimulation %d replica exchange acceptance rate : %.2f%%\n", n - 1, allReplicaExchangeAcceptanceRate[n])
                    end
                else
                    str *= @sprintf("\t\tupdate acceptance rate: %.2f%%\n", localUpdateAcceptanceRate)
                end
                str *= @sprintf("\n")
                print(str)
            end

            #reset statistics
            statistics = MonteCarloStatistics()
        end

        #write checkpoint
        if enableOutput
            checkpointPending = time() - lastCheckpointTime >= mc.checkpointInterval
            enableMPI && (checkpointPending = MPIBcastBool(checkpointPending, 0, MPI.COMM_WORLD))
            if checkpointPending
                writeMonteCarlo(outfile, mc)
                lastCheckpointTime = time()
                rank == 0 && @printf("Checkpoint written on %s.\n", Dates.format(Dates.now(), "dd u yyyy HH:MM:SS"))
            end
        end
    end

    #write final checkpoint
    if enableOutput
        writeMonteCarlo(outfile, mc)
        rank == 0 && @printf("Checkpoint written on %s.\n", Dates.format(Dates.now(), "dd u yyyy HH:MM:SS"))
    end

    #return
    rank == 0 && @printf("Simulation finished on %s.\n", Dates.format(Dates.now(), "dd u yyyy HH:MM:SS"))
    return nothing
end