# stuff done once per exucution--not depending on r
function InitWRE!(o::StrBootTest{T}) where T
	o.LIML && o.Repl.kZ==1 && o.Nw==1 && (o.As = o.β̂s = zeros(1, o.B+1))
	o.S✻UZperpinvZperpZperp = Vector{Matrix{T}}(undef, o.Repl.kZ+1)
	o.S✻UZperp              = Vector{Matrix{T}}(undef, o.Repl.kZ+1)
	o.S✻UY                  = Matrix{Vector{T}}(undef, o.Repl.kZ+1, o.Repl.kZ+1)
	o.S✻UXinvXX             = Vector{Matrix{T}}(undef, o.Repl.kZ+1)
	o.S✻UX                  = Vector{Matrix{T}}(undef, o.Repl.kZ+1)
	o.S✻UU                  = Matrix{Matrix{T}}(undef, o.Repl.kZ+1, o.Repl.kZ+1)
	o.T1L = isone(o.Nw) ? [Matrix{T}(undef, o.Repl.kX, ncols(o.v))] :
												[Matrix{T}(undef, o.Repl.kX, length(o.WeightGrp[1])), Matrix{T}(undef, o.Repl.kX, length(o.WeightGrp[end]))]
	o.T1R = deepcopy(o.T1L)

	S✻⋂XZperp = panelcross21(o, o.Repl._X₁, o.X₂, o.DGP.Zperp, o.info✻⋂)
	S✻⋂ZperpZperp = panelcross11(o, o.Repl.Zperp, o.DGP.Zperp, o.info✻⋂)
	S✻⋂XY₂ = panelcross21(o, o.Repl._X₁, o.X₂, o.Y₂, o.info✻⋂)
	S✻⋂ZperpY₂ = panelcross11(o, o.Repl.Zperp, o.Y₂, o.info✻⋂)
	S✻⋂XX = panelcross22(o, o.Repl._X₁, o.X₂, o.DGP._X₁, o.X₂, o.info✻⋂)  # XXX X₂-X₂ part same for DGP & Repl
	S✻⋂ZperpX = panelcross12(o, o.Repl.Zperp, o.DGP._X₁, o.X₂, o.info✻⋂)
	S✻⋂Zperpy₁ = panelcross11(o, o.Repl.Zperp, o.y₁, o.info✻⋂)
	S✻⋂ZperpZR₁ = panelcross11(o, o.Repl.Zperp, o.DGP._ZR₁, o.info✻⋂)
	S✻⋂ZperpZ = panelcross11(o, o.Repl.Zperp, o.DGP._Z, o.info✻⋂)
	S✻⋂Xy₁ = panelcross21(o, o.Repl._X₁, o.X₂, o.y₁, o.info✻⋂)
	S✻⋂XZ = panelcross21(o, o.Repl._X₁, o.X₂, o.DGP._Z, o.info✻⋂)
	S✻⋂XZR₁ = panelcross21(o, o.Repl._X₁, o.X₂, o.DGP._ZR₁, o.info✻⋂)

	if o.robust && o.bootstrapt && !o.granular
		o.S✻⋂u₁XinvXX0    = o.Repl.invXX * (S✻⋂Xy₁  - S✻⋂XZperp * o.DGP.invZperpZperpZperpy₁  - o.Repl.invZperpZperpZperpX' * (S✻⋂Zperpy₁  - S✻⋂ZperpZperp * o.DGP.invZperpZperpZperpy₁))
		o.∂S✻⋂u₁XinvXX∂β̂  = o.Repl.invXX * (S✻⋂XZ   - S✻⋂XZperp * o.DGP.invZperpZperpZperpZ   - o.Repl.invZperpZperpZperpX' * (S✻⋂ZperpZ   - S✻⋂ZperpZperp * o.DGP.invZperpZperpZperpZ))
		o.∂S✻⋂u₁XinvXX∂γ̈  = o.Repl.invXX * (S✻⋂XY₂  - S✻⋂XZperp * o.DGP.invZperpZperpZperpY₂  - o.Repl.invZperpZperpZperpX' * (S✻⋂ZperpY₂  - S✻⋂ZperpZperp * o.DGP.invZperpZperpZperpY₂))
		o.∂S✻⋂u₁XinvXX∂γ̈Π̂ = o.Repl.invXX * (S✻⋂XX   - S✻⋂XZperp * o.DGP.invZperpZperpZperpX   - o.Repl.invZperpZperpZperpX' * (S✻⋂ZperpX   - S✻⋂ZperpZperp * o.DGP.invZperpZperpZperpX))
		o.∂S✻⋂u₁XinvXX∂r  = o.Repl.invXX * (S✻⋂XZR₁ - S✻⋂XZperp * o.DGP.invZperpZperpZperpZR₁ - o.Repl.invZperpZperpZperpX' * (S✻⋂ZperpZR₁ - S✻⋂ZperpZperp * o.DGP.invZperpZperpZperpZR₁))
		o.S✻⋂U₂XinvXX0 = o.∂S✻⋂u₁XinvXX∂γ̈
		o.∂S✻⋂U₂XinvXX0∂Π̂ = o.∂S✻⋂u₁XinvXX∂γ̈Π̂
	end

	info✻_✻⋂, _ = panelsetupID(o.ID✻⋂, 1:o.nbootclustvar)

	S✻XZperp = @panelsum(o, S✻⋂XZperp, info✻_✻⋂)
	S✻ZperpZperp = @panelsum(o, S✻⋂ZperpZperp, info✻_✻⋂)
	S✻XY₂ = @panelsum(o, S✻⋂XY₂, info✻_✻⋂)
	S✻ZperpY₂ = @panelsum(o, S✻⋂ZperpY₂, info✻_✻⋂)
	S✻XX = @panelsum(o, S✻⋂XX, info✻_✻⋂)
	S✻ZperpX = @panelsum(o, S✻⋂ZperpX, info✻_✻⋂)
	S✻Zperpy₁ = @panelsum(o, S✻⋂Zperpy₁, info✻_✻⋂)
	S✻ZperpZR₁ = @panelsum(o, S✻⋂ZperpZR₁, info✻_✻⋂)
	S✻ZperpZ = @panelsum(o, S✻⋂ZperpZ, info✻_✻⋂)
	S✻Xy₁ = @panelsum(o, S✻⋂Xy₁, info✻_✻⋂)
	S✻XZ = @panelsum(o, S✻⋂XZ, info✻_✻⋂)
	S✻XZR₁ = @panelsum(o, S✻⋂XZR₁, info✻_✻⋂)

	o.S✻Zperp_DGPy₁ = S✻Zperpy₁  - S✻ZperpZperp * o.DGP.invZperpZperpZperpy₁
	o.S✻Zperp_DGPZ  = S✻ZperpZ   - S✻ZperpZperp * o.DGP.invZperpZperpZperpZ
	o.S✻Zperp_DGPY₂ = S✻ZperpY₂  - S✻ZperpZperp * o.DGP.invZperpZperpZperpY₂
	o.S✻Zperp_DGPX = S✻ZperpX   - S✻ZperpZperp * o.DGP.invZperpZperpZperpX
	o.S✻Zperp_DGPZR₁ = S✻ZperpZR₁ - S✻ZperpZperp * o.DGP.invZperpZperpZperpZR₁

	o.S✻u₁X0    = S✻Xy₁  - S✻XZperp * o.DGP.invZperpZperpZperpy₁ - o.Repl.invZperpZperpZperpX' * o.S✻Zperp_DGPy₁
	o.∂S✻u₁X∂β̂  = S✻XZ   - S✻XZperp * o.DGP.invZperpZperpZperpZ  - o.Repl.invZperpZperpZperpX' * o.S✻Zperp_DGPZ
	o.∂S✻u₁X∂γ̈  = S✻XY₂  - S✻XZperp * o.DGP.invZperpZperpZperpY₂ - o.Repl.invZperpZperpZperpX' * o.S✻Zperp_DGPY₂
	o.∂S✻u₁X∂γ̈Π̂ = S✻XX   - S✻XZperp * o.DGP.invZperpZperpZperpX  - o.Repl.invZperpZperpZperpX' * o.S✻Zperp_DGPX
	o.∂S✻u₁X∂r  = S✻XZR₁ - S✻XZperp * o.DGP.invZperpZperp * o.DGP.Zperp_ZR₁ - o.Repl.invZperpZperpZperpX' * o.S✻Zperp_DGPZR₁
	o.S✻U₂X0 = o.∂S✻u₁X∂γ̈ * o.Repl.RparY
	o.∂S✻U₂X∂Π̂ = o.∂S✻u₁X∂γ̈Π̂

	if o.LIML || !o.robust || !isone(o.κ)
		# S✻y₁y₁ = panelcross11(o, o.y₁, o.y₁, o.info✻)
		# S✻y₁Y₂ = panelcross11(o, o.y₁, o.Y₂, o.info✻)
		# S✻y₁Zperp = panelcross11(o, o.y₁, o.DGP.Zperp, o.info✻)
		# S✻ZZ = panelcross11(o, o.Repl._Z, o.DGP._Z, o.info✻)
		# S✻ZZperp = panelcross11(o, o.Repl._Z, o.DGP.Zperp, o.info✻)
		# S✻y₁Z = panelcross11(o, o.y₁, o.DGP._Z, o.info✻)
		# S✻ZY₂ = panelcross11(o, o.Repl._Z, o.Y₂, o.info✻)
		# S✻ZX = panelcross12(o, o.Repl._Z, o.DGP._X₁, o.X₂, o.info✻)
		# S✻ZZR₁ = panelcross11(o, o.Repl._Z, o.DGP._ZR₁, o.info✻)
		# S✻y₁X = panelcross12(o, o.y₁, o.DGP._X₁, o.X₂, o.info✻)
		# S✻y₁ZR₁ = panelcross11(o, o.y₁, o.DGP._ZR₁, o.info✻)

		o.Reply₁_DGPy₁  = panelcross11(o, o.Repl.y₁, o.DGP.y₁, o.info✻)
		o.Reply₁_DGPZR₁ = panelcross11(o, o.Repl.y₁, o.DGP.ZR₁, o.info✻)
		o.Reply₁_DGPZ   = panelcross11(o, o.Repl.y₁, o.DGP.Z, o.info✻)
		o.Reply₁_DGPY₂  = panelcross11(o, o.Repl.y₁, o.DGP.Y₂, o.info✻)
		o.Reply₁_DGPX   = panelcross11(o, o.Repl.y₁, [o.DGP.X₁ o.DGP.X₂], o.info✻)

		o.ReplZ_DGPy₁  = panelcross11(o, o.Repl.Z, o.DGP.y₁, o.info✻)
		o.ReplZ_DGPZR₁ = panelcross11(o, o.Repl.Z, o.DGP.ZR₁, o.info✻)
		o.ReplZ_DGPZ   = panelcross11(o, o.Repl.Z, o.DGP.Z, o.info✻)
		o.ReplZ_DGPY₂  = panelcross11(o, o.Repl.Z, o.DGP.Y₂, o.info✻)
		o.ReplZ_DGPX   = panelcross11(o, o.Repl.Z, [o.DGP.X₁ o.DGP.X₂], o.info✻)

		if o.REst
			ZR₁r₁ = o.Repl.ZR₁ * o.r₁
			o.Reply₁_DGPy₁ .-= panelcross11(o, ZR₁r₁, o.DGP.y₁, o.info✻)
			o.Reply₁_DGPZR₁ .-= panelcross11(o, ZR₁r₁, o.DGP.ZR₁, o.info✻)
			o.Reply₁_DGPZ .-= panelcross11(o, ZR₁r₁, o.DGP.Z, o.info✻)
			o.Reply₁_DGPY₂ .-= panelcross11(o, ZR₁r₁, o.DGP.Y₂, o.info✻)
			o.Reply₁_DGPX .-= panelcross11(o, ZR₁r₁, [o.DGP.X₁ o.DGP.X₂], o.info✻)
		end

		# o.∂S✻y₁U₂∂γ̈ = S✻y₁Y₂ - o.Repl.invZperpZperpZperpy₁' * S✻ZperpY₂ - (S✻y₁Zperp - o.Repl.invZperpZperpZperpy₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpY₂
		# o.S✻y₁u₁0 = -o.∂S✻y₁U₂∂γ̈ * o.Repl.RparY
		# o.∂S✻y₁u₁∂β̂ = S✻ZZ - S✻ZZperp * o.DGP.invZperpZperpZperpZ - (S✻y₁Z - S✻y₁Zperp * o.DGP.invZperpZperpZperpZ)
		# o.∂S✻y₁u₁∂γ̈ = S✻ZY₂ - o.Repl.invZperpZperpZperpy₁' * S✻ZperpY₂ - (S✻ZZperp - o.Repl.invZperpZperpZperpy₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpY₂
		# o.∂S✻y₁u₁∂γ̈Π̂ = S✻ZX   - S✻ZZperp * o.DGP.invZperpZperpZperpX - (S✻y₁X - S✻y₁Zperp * o.DGP.invZperpZperpZperpX)
		# o.∂S✻y₁u₁∂r = S✻ZZR₁  - S✻ZZperp * o.DGP.invZperpZperpZperpZR₁ - (S✻y₁ZR₁ - S✻y₁Zperp * o.DGP.invZperpZperpZperpZR₁)
		# o.S✻y₁U₂0 = S✻y₁y₁ - o.Repl.invZperpZperpZperpy₁' * S✻Zperpy₁ - o.∂S✻y₁U₂∂γ̈
		# o.∂S✻y₁U₂∂β̂ = S✻y₁Z - S✻y₁Zperp * o.DGP.invZperpZperpZperpZ - o.Repl.invZperpZperpZperpy₁' * S✻ZperpZ
		# o.∂S✻y₁U₂∂γ̈Π̂ = S✻y₁X - o.Repl.invZperpZperpZperpy₁' * S✻ZperpX - (S✻y₁Zperp - o.Repl.invZperpZperpZperpy₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpX
		# o.∂S✻y₁U₂∂Π̂ =  o.∂S✻y₁U₂∂γ̈Π̂
		# o.∂S✻y₁U₂∂r = S✻y₁ZR₁ - o.Repl.invZperpZperpZperpy₁' * S✻ZperpZR₁ - (S✻y₁Zperp - o.Repl.invZperpZperpZperpy₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpZR₁

		# if o.REst
		# 	o.∂S✻y₁u₁∂β̂ .-= o.r₁' * (panelcross11(o, o.Repl._ZR₁, o.DGP._Z, o.info✻) - panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) * o.DGP.invZperpZperpZperpZ -
		# 											o.Repl.invZperpZperpZperpZR₁' * (S✻ZperpZ -
		# 																												S✻ZperpZperp * o.DGP.invZperpZperpZperpZ) -
		# 																												(panelcross11(o, o.Repl._ZR₁, o.DGP._Z, o.info✻) -
		# 																												panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) * o.DGP.invZperpZperpZperpZ -
		# 																												o.Repl.invZperpZperpZperpZR₁' * (S✻ZperpZ -
		# 																																												 S✻ZperpZperp * o.DGP.invZperpZperpZperpZ)))
		# 	o.∂S✻y₁u₁∂γ̈ .-= o.r₁' * ((panelcross11(o, o.Repl._ZR₁, o.Y₂, o.info✻) -
		# 										o.Repl.invZperpZperpZperpZR₁' * panelcross11(o, o.Repl.Zperp, o.Y₂, o.info✻)) -
		# 									(panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 										o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpY₂ -
		# 										((panelcross11(o, o.Repl._ZR₁, o.Y₂, o.info✻) -
		# 										 o.Repl.invZperpZperpZperpZR₁' * panelcross11(o, o.Repl.Zperp, o.Y₂, o.info✻)) -
		# 											(panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 										 o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpY₂))
		# 	o.∂S✻y₁u₁∂γ̈Π̂ .-= o.r₁' * ((panelcross12(o, o.Repl._ZR₁, o.DGP._X₁, o.X₂, o.info✻) -
		# 										panelcross12(o, o.Repl.Zperp * o.Repl.invZperpZperpZperpZR₁, o.DGP._X₁, o.X₂, o.info✻)) -
		# 										(panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 										o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpX -
		# 										((panelcross12(o, o.Repl._ZR₁, o.DGP._X₁, o.X₂, o.info✻) -
		# 									 panelcross12(o, o.Repl.Zperp * o.Repl.invZperpZperpZperpZR₁, o.DGP._X₁, o.X₂, o.info✻)) -
		# 										 (panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 									 o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpX))
		# 	o.∂S✻y₁u₁∂r .-= o.r₁' * ((panelcross11(o, o.Repl._ZR₁, o.DGP._ZR₁, o.info✻) -
		# 									o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZR₁) -
		# 									(panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 									o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpZR₁ -
		# 									((panelcross11(o, o.Repl._ZR₁, o.DGP._ZR₁, o.info✻) -
		# 									o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZR₁) -
		# 									 (panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 									o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpZR₁))
		# 	o.S✻y₁U₂0 .-= o.r₁' * (panelcross11(o, o.Repl._ZR₁, o.y₁, o.info✻) -
		# 	                        o.Repl.invZperpZperpZperpZR₁' * panelcross11(o, o.Repl.Zperp, o.y₁, o.info✻) +
		# 													(panelcross11(o, o.Repl._ZR₁, o.Y₂, o.info✻) -
		# 													o.Repl.invZperpZperpZperpZR₁' * panelcross11(o, o.Repl.Zperp, o.Y₂, o.info✻)) -
		# 													(panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 													o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpY₂) * o.Repl.RparY
		# 	o.∂S✻y₁U₂∂β̂ .-= o.r₁' * (panelcross11(o, o.Repl._ZR₁, o.DGP._Z, o.info✻) - panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) * o.DGP.invZperpZperpZperpZ -
		# 														o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZ)
		# 	o.∂S✻y₁U₂∂γ̈ .-= o.r₁' * ((panelcross11(o, o.Repl._ZR₁, o.Y₂, o.info✻) -
    #                       o.Repl.invZperpZperpZperpZR₁' * panelcross11(o, o.Repl.Zperp, o.Y₂, o.info✻)) -
    #                      (panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
    #                       o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpY₂)
		# 	o.∂S✻y₁U₂∂γ̈Π̂ .-= o.r₁' * ((panelcross12(o, o.Repl._ZR₁, o.DGP._X₁, o.X₂, o.info✻) -
		# 										 panelcross12(o, o.Repl.Zperp * o.Repl.invZperpZperpZperpZR₁, o.DGP._X₁, o.X₂, o.info✻)) -
    #                       (panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
    #                       o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpX)
		# 	o.∂S✻y₁U₂∂Π̂ .+= o.r₁' * ((panelcross11(o, o.Repl._ZR₁, o.Y₂, o.info✻) -
		# 														o.Repl.invZperpZperpZperpZR₁' * panelcross11(o, o.Repl.Zperp, o.Y₂, o.info✻)) -
		# 														(panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 														o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpY₂)
		# 	o.∂S✻y₁U₂∂r .-= o.r₁' * ((panelcross11(o, o.Repl._ZR₁, o.DGP._ZR₁, o.info✻) -
		# 														o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZR₁) -
		# 													(panelcross11(o, o.Repl._ZR₁, o.DGP.Zperp, o.info✻) -
		# 														o.Repl.invZperpZperpZperpZR₁' * S✻ZperpZperp) * o.DGP.invZperpZperpZperpZR₁)
		# end
	end
end

function PrepWRE!(o::StrBootTest{T}) where T
  r₁ = o.null ? [o.r₁ ; o.r] : o.r₁
	EstimateIV!(o.DGP, o, r₁)
  MakeResidualsIV!(o.DGP, o)
  Ü₂par = view(o.DGP.Ü₂ * o.Repl.RparY,:,:)

	_S✻U₂X = o.S✻U₂X0 - o.∂S✻U₂X∂Π̂ * o.DGP.Π̂ * o.Repl.RparY  # panelsum2(o, o.Repl.X₁, o.Repl.X₂, uwt, o.info✻)'
	(o.LIML || o.bootstrapt || !isone(o.κ)) &&
		(_S✻U₂Zperp = (o.S✻Zperp_DGPY₂ - o.S✻Zperp_DGPX * o.DGP.Π̂) * o.Repl.RparY)
	o.robust && o.bootstrapt && !o.granular &&
		(_S✻⋂U₂XinvXX = o.S✻⋂U₂XinvXX0 - o.∂S✻⋂U₂XinvXX0∂Π̂ * o.DGP.Π̂ * o.Repl.RparY)
	if o.LIML || !o.robust || !isone(o.κ)
		Reply₁_DGPU₂ = o.Reply₁_DGPY₂ - o.Reply₁_DGPX * o.DGP.Π̂
		ReplZ_DGPU₂  = o.ReplZ_DGPY₂  - o.ReplZ_DGPX  * o.DGP.Π̂
		Reply₁_DGPU₂par = Reply₁_DGPU₂ * o.Repl.RparY
		ReplZ_DGPU₂par = ReplZ_DGPU₂ * o.Repl.RparY
	end

  @inbounds for i ∈ 0:o.Repl.kZ  # precompute various clusterwise sums
		uwt = i>0 ? view(Ü₂par,:,i) : view(o.DGP.u⃛₁,:)::Union{Vector{T}, SubArray{T, 1}}

		# S_✻(u .* X), S_✻(u .* Zperp) for residuals u for each endog var; store transposed
		if iszero(i)  # panelsum2(o, o.Repl.X₁, o.Repl.X₂, uwt, o.info✻)'
			o.S✻UX[1] = panelsum2(o, o.Repl.X₁, o.Repl.X₂, uwt, o.info✻)'  #dropdims(o.S✻u₁X0 - o.∂S✻u₁X∂β̂ * o.DGP.β̈ + (o.∂S✻u₁X∂γ̈ - o.∂S✻u₁X∂γ̈Π̂ * o.DGP.Π̂) * o.DGP.γ̈; dims=3)
			# o.DGP.restricted &&
			# 	(o.S✻UX[1] .-= dropdims(o.∂S✻u₁X∂r * r₁; dims=3))
		else
			o.S✻UX[i+1] = panelsum2(o, o.Repl.X₁, o.Repl.X₂, uwt, o.info✻)'  # view(_S✻U₂X,:,:,i)
		end

		o.S✻UXinvXX[i+1] = o.Repl.invXX * o.S✻UX[i+1]

		if o.LIML || !isone(o.κ) || o.bootstrapt
			if iszero(i)  # panelsum(o, o.Repl.Zperp, uwt, o.infoBootData)'
				o.S✻UZperp[i+1] = view(o.S✻Zperp_DGPy₁ - o.S✻Zperp_DGPZ * o.DGP.β̈ + (o.S✻Zperp_DGPY₂ - o.S✻Zperp_DGPX * o.DGP.Π̂) * o.DGP.γ̈,:,:,1)
				o.DGP.restricted &&
					(o.S✻UZperp[i+1] .-= view(o.S✻Zperp_DGPZR₁ * r₁,:,:,1))
			else
				o.S✻UZperp[i+1] = view(_S✻U₂Zperp,:,:,i)
			end
			o.S✻UZperpinvZperpZperp[i+1] = o.Repl.invZperpZperp * o.S✻UZperp[i+1]
			o.NFE>0 && (o.CTFEU[i+1] = crosstabFE(o, uwt, o.info✻))
		end

		if o.LIML || !isone(o.κ) || !o.robust
			if iszero(i)  # panelsum2(o, o.Repl.y₁par, o.Repl.Z, uwt, o.info✻)
				o.S✻UY[1,1] = dropdims(o.Reply₁_DGPy₁ - o.Reply₁_DGPZR₁ * r₁ - o.Reply₁_DGPZ * o.DGP.β̈ - Reply₁_DGPU₂ * o.DGP.γ̈; dims=(1,3))
				for j ∈ 1:o.Repl.kZ
					o.S✻UY[1,j+1] = @view (o.ReplZ_DGPy₁ - o.ReplZ_DGPZR₁ * r₁ - o.ReplZ_DGPZ * o.DGP.β̈ - ReplZ_DGPU₂ * o.DGP.γ̈)[j,:,i]
				end
      else
				o.S✻UY[i+1,1] = @view Reply₁_DGPU₂par[1,:,i]
				for j ∈ 1:o.Repl.kZ
					o.S✻UY[i+1,j+1] = @view ReplZ_DGPU₂par[j,:,i]
				end
			end
			for j ∈ 0:i
				o.S✻UU[i+1,j+1] = vec(@panelsum(o, j>0 ? view(Ü₂par,:,j) : view(o.DGP.u⃛₁,:), uwt, o.info✻))
			end
		end

		if o.robust && o.bootstrapt
			if !o.granular  # Within each bootstrap cluster, groupwise sum by all-error-cluster-intersections of u.*X and u.Zperp (and times invXX or invZperpZperp)
				_S✻⋂U₂XinvXX = view(o.S✻⋂U₂XinvXX0 - o.∂S✻⋂U₂XinvXX0∂Π̂ * o.DGP.Π̂ * o.Repl.RparY,:,:,i)
				if iszero(i)
					_S✻⋂UXinvXX = dropdims(o.S✻⋂u₁XinvXX0 - o.∂S✻⋂u₁XinvXX∂β̂ * o.DGP.β̈ + (o.∂S✻⋂u₁XinvXX∂γ̈  - o.∂S✻⋂u₁XinvXX∂γ̈Π̂ * o.DGP.Π̂) * o.DGP.γ̈; dims=3)
					o.DGP.restricted
						(_S✻⋂UXinvXX .-= dropdims(o.∂S✻⋂u₁XinvXX∂r * r₁; dims=3))
				else
					_S✻⋂UXinvXX = view(_S✻⋂U₂XinvXX,:,:,i)
				end
				for g ∈ 1:o.N✻
					o.S✻⋂u₁XinvXX[i+1,g] = view(_S✻⋂UXinvXX,:,o._ID✻⋂[o.info✻[g]],1)'  # panelsum(o, o.Repl.XinvXX, u, o.infoCT⋂✻[g])
				end
			end

			i>0 && (o.S✻UPX[i+1] = o.Repl.XinvXX * o.S✻UX[i+1])
			o.S✻UMZperp[i+1] = o.Repl.Zperp * o.S✻UZperpinvZperpZperp[i+1]  # S_* diag(U ̈_(∥j) ) Z_⊥ (Z_⊥^' Z_⊥ )^(-1) Z_(⊥g)^'  over all g

			if iszero(i)  # subtract crosstab of observation by ∩-group of u
				o.S✻UMZperp[  1][o.crosstabBootind] .-= o.DGP.u⃛₁
			else
				o.S✻UMZperp[i+1][o.crosstabBootind] .-= view(Ü₂par,:,i)
			end

			o.NFE>0 &&
				(o.S✻UMZperp[i+1] .+= view(o.invFEwt .* o.CTFEU[i+1], o._FEID, :))  # CT_(*,FE) (U ̈_(parj) ) (S_FE S_FE^' )^(-1) S_FE
		end
  end
	nothing
end

# For WRE, and with reference to Y = [y₁ Z], given 0-based columns indexes within it, ind1, ind2, return all bootstrap realizations of
# Y[:,ind1]'((1-κ)*M_Zperp-κ*M_Xpar)*Y[:,ind2] for κ constant across replications
# ind1 can be a rowvector
# (only really the Hessian when we narrow Y to Z)
function HessianFixedkappa(o::StrBootTest{T}, ind1::Vector{S} where S<:Integer, ind2::Integer, κ::Number, w::Integer) where T
  dest = Matrix{T}(undef, length(ind1), ncols(o.v))
  @inbounds for i ∈ eachindex(ind1, axes(dest,1))
		_HessianFixedkappa!(o, view(dest,i:i,:), ind1[i], ind2, κ, w)
  end
  dest
end

function _HessianFixedkappa!(o::StrBootTest{T}, dest::AbstractMatrix, ind1::Integer, ind2::Integer, κ::Number, w::Integer) where T
  if !(o.Repl.Yendog[ind1+1] || o.Repl.Yendog[ind2+1])  # if both vars exog, result = order-0 term only, same for all draws
		!iszero(κ) &&
			coldot!(o, dest, view(o.Repl.XZ,:,ind1), view(o.Repl.invXXXZ,:,ind2))
		if !isone(κ)
			if iszero(κ)
				fill!(dest, o.Repl.YY[ind1+1,ind2+1])
			else
				dest .= κ .* dest .+ (1 - κ) .* o.Repl.YY[ind1+1,ind2+1]
			end
		end
	else
		if !iszero(κ)  # repititiveness in this section to maintain type stability
			if o.Repl.Yendog[ind1+1]
				T1L = o.T1L[isone(o.Nw) || w<o.Nw ? 1 : 2]  # use preallocated destinations
				mul!(T1L, o.S✻UX[ind1+1], o.v)
				if iszero(ind1)
					T1L .+= o.Repl.Xy₁par
				else
					T1L .+= view(o.Repl.XZ,:,ind1)
				end
				if o.Repl.Yendog[ind2+1]
					T1R = o.T1R[isone(o.Nw) || w<o.Nw ? 1 : 2]  # use preallocated destinations
					mul!(T1R, o.S✻UXinvXX[ind2+1], o.v)
					if iszero(ind2)
						T1R .+=  o.Repl.invXXXy₁par
					else
						T1R .+= view(o.Repl.invXXXZ,:,ind2)
					end
					coldot!(o, dest, T1L, T1R)
				else
					coldot!(o, dest, T1L, view(o.Repl.invXXXZ,:,ind2))
				end
			else
				if o.Repl.Yendog[ind2+1]
					T1R = o.T1R[isone(o.Nw) || w<o.Nw ? 1 : 2]  # use preallocated destinations
					mul!(T1R, o.S✻UXinvXX[ind2+1], o.v)
					if iszero(ind2)
						T1R .+=  o.Repl.invXXXy₁par
					else
						T1R .+= view(o.Repl.invXXXZ,:,ind2)
					end
					coldot!(o, dest, view(o.Repl.XZ,:,ind1), T1R)
				else
					dest .= coldot(o, view(o.Repl.XZ,:,ind1), view(o.Repl.invXXXZ,:,ind2))
				end
			end
		end
		if !isone(κ)
			if o.Repl.Yendog[ind1+1]
				T2 = o.S✻UZperpinvZperpZperp[ind1+1]'o.S✻UZperp[ind2+1]  # quadratic term
				T2[diagind(T2)] .-= ind1 ≤ ind2 ? o.S✻UU[ind2+1, ind1+1] : o.S✻UU[ind1+1, ind2+1]  # minus diagonal crosstab
				o.NFE>0 &&
					(T2 .+= o.CTFEU[ind1+1]'(o.invFEwt .* o.CTFEU[ind2+1]))
					if iszero(κ)
						dest .= o.Repl.YY[ind1+1,ind2+1] .+ colquadformminus!(o, (                            o.S✻UY[ind2+1,ind1+1] .+ o.S✻UY[ind1+1,ind2+1])'o.v, T2, o.v)
					else
						dest .=   κ .* dest .+ (1 - κ)   .* colquadformminus!(o, (o.Repl.YY[ind1+1,ind2+1] .+ o.S✻UY[ind2+1,ind1+1] .+ o.S✻UY[ind1+1,ind2+1])'o.v, T2, o.v)
					end
					# if iszero(ind1) || iszero(ind2)
				# 	tmp = iszero(ind1) ?
				# 					iszero(ind2) ? o.S✻UY[1] * 2 : o.S✻UY[ind2+1] :
				# 					o.S✻UY[ind1+1]
				# 	if iszero(κ)
				# 		dest .= o.Repl.YY[ind1+1,ind2+1] .+ colquadformminus!(o, (                            tmp)'o.v, T2, o.v)
				# 	else
				# 		dest .=  κ .* dest .+ (1 - κ)    .* colquadformminus!(o, (o.Repl.YY[ind1+1,ind2+1] .+ tmp)'o.v, T2, o.v)
				# 	end
				# else
				# 	if iszero(κ)
				# 		dest .= o.Repl.YY[ind1+1,ind2+1]
				# 	else
				# 		dest .= κ .* dest .+ (1 - κ) .* colquadformminus!(o, o.Repl.YY[ind1+1,ind2+1]'o.v, T2, o.v)
				# 	end
				# end
			elseif iszero(κ)
				dest .= o.Repl.YY[ind1+1,ind2+1]
			else
				dest .= κ .* dest .+ (1 - κ) .* o.Repl.YY[ind1+1,ind2+1]
			end
		end
  end
	nothing
end

# put threaded loops in functions to prevent type instability https://discourse.julialang.org/t/type-inference-with-threads/2004/3
function FillingLoop1!(o::StrBootTest{T}, dest::Matrix{T}, ind1::Integer, ind2::Integer, _β̂::AbstractMatrix{T}) where T
	Threads.@threads for i ∈ 1:o.clust[1].N
		PXY✻ = reshape(o.Repl.PXZ[i,ind1], :, 1)
		o.Repl.Yendog[ind1+1] && (PXY✻ = PXY✻ .+ view(o.S✻UPX[ind1+1],i,:)'o.v)

		if iszero(ind2)
			dest[i,:]   = colsum(PXY✻ .* (o.Repl.y₁[i] .- view(o.S✻UMZperp[1],i,:))'o.v)
		elseif o.Repl.Yendog[ind2+1]
			dest[i,:] .-= colsum(PXY✻ .* (o.Repl.Z[i,ind2] * _β̂ .- view(o.S✻UMZperp[ind2+1],i,:)'β̂v))
		else
			dest[i,:] .-= colsum(PXY✻ .* (o.Repl.Z[i,ind2] * _β̂))
		end
	end
	nothing
end
function FillingLoop2!(o::StrBootTest{T}, dest::Matrix{T}, ind1::Integer, ind2::Integer, _β̂::AbstractMatrix{T}) where T
	Threads.@threads for i ∈ 1:o.clust[1].N
		S = o.info⋂[i]
		PXY✻ = o.Repl.Yendog[ind1+1] ? view(o.Repl.PXZ,S,ind1) .+ view(o.S✻UPX[ind1+1],S,:) * o.v :
																	reshape(o.Repl.PXZ[S,ind1], :, 1)

		if iszero(ind2)
			dest[i,:]   = colsum(PXY✻ .* (o.Repl.y₁[S] .- view(o.S✻UMZperp[1],S,:) * o.v))
		else
			dest[i,:] .-= colsum(PXY✻ .* (o.Repl.Yendog[ind2+1] ? o.Repl.Z[S,ind2] * _β̂ .- view(o.S✻UMZperp[ind2+1],S,:) * β̂v :
																																o.Repl.Z[S,ind2] * _β̂                                       ))
		end
	end
	nothing
end
function FillingLoop3!(o::StrBootTest{T}, T₁::Matrix{T}, ind1::Integer, ind2::Integer) where T
	Threads.@threads for i ∈ 1:o.N✻
		T₁[o.IDCT⋂✻[i], i] .+= o.S✻⋂u₁XinvXX[ind2+1,i] * view(o.Repl.XZ,:,ind1)
	end
	nothing
end

# Workhorse for WRE CRVE sandwich filling
# Given a column index within it, ind1, and a matrix β̂s of all the bootstrap estimates,
# return all bootstrap realizations of P_X * Z[:,ind1]_g ' û₁g^*b
# for all groups in the intersection of all error clusterings
# return value has one row per ⋂ cluster, one col per bootstrap replication
function Filling(o::StrBootTest{T}, ind1::Integer, β̂s::AbstractMatrix) where T
	if o.granular
   	if o.Nw == 1  # create or avoid NxB matrix?
			PXY✻ = reshape(o.Repl.PXZ[:,ind1], :, 1)  # store as matrix to reduce compiler confusion
			o.Repl.Yendog[ind1+1] && (PXY✻ = PXY✻ .+ o.S✻UPX[ind1+1] * o.v)

			dest = @panelsum(o, PXY✻ .* (o.Repl.y₁ .- o.S✻UMZperp[1] * o.v), o.info⋂)

			@inbounds for ind2 ∈ 1:o.Repl.kZ
				_β̂ = view(β̂s,ind2,:)'
				dest .-= @panelsum(o, PXY✻ .* (o.Repl.Yendog[ind2+1] ?  view(o.Repl.Z,:,ind2) * _β̂ .- o.S✻UMZperp[ind2+1] * (o.v .* _β̂) :
															                              (view(o.Repl.Z,:,ind2) * _β̂)                                      ), o.info⋂)
			end
		else  # create pieces of each N x B matrix one at a time rather than whole thing at once
			dest = Matrix{T}(undef, o.clust[1].N, ncols(o.v))  # XXX preallocate this & turn Filling into Filling! ?
			@inbounds for ind2 ∈ 0:o.Repl.kZ
				ind2>0 && (β̂v = o.v .* (_β̂ = view(β̂s,ind2,:)'))

				if o.purerobust
					FillingLoop1!(o, dest, ind1, ind2, _β̂)
				else
					FillingLoop2!(o, dest, ind1, ind2, _β̂)
				end
			end
    end
  else  # coarse error clustering
		@inbounds for ind2 ∈ 0:o.Repl.kZ
			β̂v = iszero(ind2) ? o.v : o.v .* (_β̂ = -view(β̂s,ind2,:)')

			# T1 * o.v will be 1st-order terms
			T₁ = o.Repl.Yendog[ind1+1] ? o.Repl.S⋂YX[ind2+1]'o.S✻UXinvXX[ind1+1] : Matrix{T}(undef,0,0)  #  S_∩ (Y_(∥j):*X_∥) (X_∥^' X_∥)^(-1) [S_* (U ̈_(∥i):*X_∥)]^'

			if o.Repl.Yendog[ind2+1]  # add CT_(⋂,*) (P_(X_par ) Y_(pari).*U ̈_(parj) )
				if o.NClustVar == o.nbootclustvar && iszero(o.subcluster)  # simple case of one clustering: full crosstab is diagonal
					tmp = view(o.Repl.XZ,:,ind1)
					if length(T₁)>0
						T₁[diagind(T₁)] .+=         o.S✻UXinvXX[ind2+1]'tmp
					else
						T₁                = reshape(o.S✻UXinvXX[ind2+1]'tmp, :, 1)  # keep T₁ as vector rather than Diagonal matrix; probably better for fusion loop
					end
				else
					!o.Repl.Yendog[ind1+1] && (T₁ = o.JN⋂N✻)
					FillingLoop3(o, T₁, ind1, ind2)
				end
				ncols(o.Repl.Zperp) > 0 && (T₁ = T₁ .- o.Repl.S⋂PXYZperp[ind1+1] * o.S✻UZperpinvZperpZperp[ind2+1])  # subtract S_∩ (P_(X_∥ ) Y_(∥i):*Z_⊥ ) (Z_⊥^' Z_⊥ )^(-1) [S_* (U ̈_(∥j):*Z_⊥ )]^'
				o.NFE               > 0 && (T₁ = T₁ .- o.Repl.CT_FE⋂PY[ind1+1] * o.CTFEU[ind2+1])
			end

			if iszero(ind2)  # order-0 and -1 terms
				if iszero(ncols(T₁))
					dest = o.Repl.FillingT₀[ind1+1,1]
				elseif isone(ncols(T₁))
					dest = o.Repl.FillingT₀[ind1+1,1] .+ T₁ .* β̂v
				else
					dest = T₁ * o.v; dest .+= o.Repl.FillingT₀[ind1+1,1]
				end
			else  # y component
				if iszero(ncols(T₁))  # - x*β̈ components
					dest .+= o.Repl.FillingT₀[ind1+1,ind2+1] * _β̂
				elseif isone(ncols(T₁))
					dest .+= o.Repl.FillingT₀[ind1+1,ind2+1] * _β̂ .+ T₁ .* β̂v
				else
					dest .+= o.Repl.FillingT₀[ind1+1,ind2+1] * _β̂
					o.matmulplus!(dest, T₁, β̂v)
				end
			end

			if o.Repl.Yendog[ind1+1] && o.Repl.Yendog[ind2+1]
				for i ∈ 1:o.clust[1].N
					S = o.info⋂[i]
					o.colquadformminus!(dest, i, o.v, view(o.S✻UPX[ind1+1],S,:)'view(o.S✻UMZperp[ind2+1],S,:), β̂v)
				end
			end
		end
  end
  dest
end
function MakeWREStats!(o::StrBootTest{T}, w::Integer) where T
  if isone(o.Repl.kZ)  # optimized code for 1 coefficient in bootstrap regression
		if o.LIML
			YY₁₁   = HessianFixedkappa(o, [0], 0, zero(T), w)  # κ=0 => Y*MZperp*Y
			YY₁₂   = HessianFixedkappa(o, [0], 1, zero(T), w)
			YY₂₂   = HessianFixedkappa(o, [1], 1, zero(T), w)
			YPXY₁₁ = HessianFixedkappa(o, [0], 0, one(T) , w)  # κ=1 => Y*PXpar*Y
			YPXY₁₂ = HessianFixedkappa(o, [0], 1, one(T) , w)
			YPXY₂₂ = HessianFixedkappa(o, [1], 1, one(T) , w)
			YY₁₂YPXY₁₂ = YY₁₂ .* YPXY₁₂
			x₁₁ = YY₂₂ .* YPXY₁₁ .- YY₁₂YPXY₁₂      # elements of YY✻^-1 * YPXY✻ up to factor of det(YY✻)
			x₁₂ = YY₂₂ .* YPXY₁₂ .- YY₁₂ .* YPXY₂₂
			x₂₁ = YY₁₁ .* YPXY₁₂ .- YY₁₂ .* YPXY₁₁
			x₂₂ = YY₁₁ .* YPXY₂₂ .- YY₁₂YPXY₁₂
			κs = (x₁₁ .+ x₂₂)./2; κs = 1 ./ (1 .- (κs .- sqrt.(κs.^2 .- x₁₁ .* x₂₂ .+ x₁₂ .* x₂₁)) ./ (YY₁₁ .* YY₂₂ .- YY₁₂ .* YY₁₂))  # solve quadratic equation for smaller eignenvalue; last term is det(YY✻)
			!iszero(o.Fuller) && (κs .-= o.Fuller / (o._Nobs - o.kX))
			o.As = κs .* (YPXY₂₂ .- YY₂₂) .+ YY₂₂
			_β̂s = (κs .* (YPXY₁₂ .- YY₁₂) .+ YY₁₂) ./ o.As
		else
			o.As = HessianFixedkappa(o, [1], 1, o.κ, w)
			_β̂s  = HessianFixedkappa(o, [1], 0, o.κ, w) ./ o.As
		end

		if o.null
			o.numerw = _β̂s .+ (o.Repl.Rt₁ - o.r) / o.Repl.RRpar
		else
			o.numerw = _β̂s .- o.DGP.β̂₀
			isone(w) && (o.numerw[1] = _β̂s[1] + (o.Repl.Rt₁ - o.r) / o.Repl.RRpar)
		end

		@storeWtGrpResults!(o.numer, o.numerw)

		if o.bootstrapt
			if o.robust
				J⋂s = Filling(o, 1, _β̂s) ./ o.As
				@inbounds for c ∈ 1:o.NErrClustCombs  # sum sandwich over error clusterings
					nrows(o.clust[c].order)>0 &&
						(J⋂s = J⋂s[o.clust[c].order,:])
					@clustAccum!(denom, c, coldot(o, @panelsum(o, J⋂s, o.clust[c].info)))
				end
			else
				denom = (HessianFixedkappa(o, [0], 0, zero(T), w) .- 2 .* _β̂s .* HessianFixedkappa(o, [0], 1, zero(T), w) .+ _β̂s.^2 .* HessianFixedkappa(o, [1], 1, zero(T), w)) ./ o._Nobs ./ o.As  # classical error variance
			end
			@storeWtGrpResults!(o.dist, o.sqrt ? o.numerw ./ sqrt.(denom) : o.numerw .^ 2 ./ denom)
			denom *= o.Repl.RRpar[1]^2
		end

  else  # WRE bootstrap for more than 1 retained coefficient in bootstrap regression

		β̂s = zeros(T, o.Repl.kZ, ncols(o.v))
		A = Vector{Matrix{T}}(undef, ncols(o.v))

		if o.LIML
			YY✻   = [HessianFixedkappa(o, collect(0:i), i, zero(T), w) for i ∈ 0:o.Repl.kZ] # κ=0 => Y*MZperp*Y
			o.YPXY✻ = [HessianFixedkappa(o, collect(0:i), i,  one(T), w) for i ∈ 0:o.Repl.kZ] # κ=1 => Y*PXpar*Y

			@inbounds for b ∈ axes(o.v,2)
				for i ∈ 0:o.Repl.kZ
					o.YY✻_b[1:i+1,i+1]   = YY✻[i+1][:,b]  # fill uppper triangles, which is all that invsym() looks at
					o.YPXY✻_b[1:i+1,i+1] = o.YPXY✻[i+1][:,b]
				end
				o.κ = 1/(1 - eigvals(invsym(o.YY✻_b) * Symmetric(o.YPXY✻_b))[1])
				!iszero(o.Fuller) && (o.κ -= o.Fuller / (o._Nobs - o.kX))
				β̂s[:,b] = (A[b] = invsym(o.κ*o.YPXY✻_b[2:end,2:end] + (1-o.κ)*o.YY✻_b[2:end,2:end])) * (o.κ*o.YPXY✻_b[1,2:end]' + (1-o.κ)*YY✻_b[1,2:end]')
			end
		else
			δnumer =  HessianFixedkappa(o, collect(1:o.Repl.kZ), 0, o.κ, w)
			δdenom = [HessianFixedkappa(o, collect(1:i), i, o.κ, w) for i ∈ 1:o.Repl.kZ]

			@inbounds Threads.@threads for b ∈ axes(o.v,2)
				for i ∈ 1:o.Repl.kZ
					o.δdenom_b[1:i,i] = view(δdenom[i],:,b)  # fill uppper triangle
				end
				β̂s[:,b] = (A[b] = invsym(o.δdenom_b)) * view(δnumer,:,b)
			end
		end

		if o.bootstrapt
			if o.robust
				@inbounds for i ∈ 1:o.Repl.kZ  # avoid list comprehension construction because of compiler type detection issue
					o.Zyg[i] = Filling(o, i, β̂s)
				end
			else
				YY✻ = [HessianFixedkappa(o, collect(i:o.Repl.kZ), i, zero(T), w) for i ∈ 0:o.Repl.kZ]  # κ=0 => Y*MZperp*Y
			end
		end

		@inbounds for b ∈ reverse(axes(o.v,2))
			if o.null || w==1 && b==1
				o.numer_b .= o.Repl.RRpar * view(β̂s,:,b) + o.Repl.Rt₁ - o.r
			else
				o.numer_b .= o.Repl.RRpar * (view(β̂s,:,b) - o.DGP.β̂₀)
			end

			if o.bootstrapt
				if o.robust  # Compute denominator for this WRE test stat
					for i ∈ 1:o.Repl.kZ  # XXX replace with 3-D array?
						o._J⋂[:,i] = view(o.Zyg[i],:,b)
					end
					J⋂ = o._J⋂ * (A[b] * o.Repl.RRpar')

					for c ∈ 1:o.NErrClustCombs
						(!isone(o.NClustVar) && nrows(o.clust[c].order)>0) &&
							(J⋂ = J⋂[o.clust[c].order,:])
						J_b = @panelsum(o, J⋂, o.clust[c].info)
						@clustAccum!(denom, c, J_b'J_b)
					end
				else  # non-robust
					for i ∈ 0:o.Repl.kZ
						o.YY✻_b[i+1,i+1:o.Repl.kZ+1] = view(YY✻[i+1],:,b)  # fill upper triangle
					end
					denom = (o.Repl.RRpar * A[b] * o.Repl.RRpar') * [-one(T) ; β̂s[:,b]]'Symmetric(o.YY✻_b) * [-one(T) ; β̂s[:,b]] / o._Nobs  # 2nd half is sig2 of errors
				end
				if o.sqrt
					o.dist[b+first(o.WeightGrp[w])-1] = o.numer_b[1] / sqrt(denom[1])
				else
					o.dist[b+first(o.WeightGrp[w])-1] = o.numer_b'invsym(denom)*o.numer_b  # hand-code for 2-dimensional?
				end
			end
			o.numer[:,b+first(o.WeightGrp[w])-1] = o.numer_b  # slight inefficiency: in usual bootstrap-t case, only need to save numerators in numer if getdist("numer") is coming because of svmat(numer)
		end
	end
	w==1 && o.bootstrapt && (o.statDenom = denom)  # original-sample denominator
	nothing
end


@hascuda using CUDAnative, CuArrays

import GPUifyLoops: @launch, @loop, @unroll, @synchronize

using Oceananigans.Operators

const Tx = 16 # CUDA threads per x-block
const Ty = 16 # CUDA threads per y-block

"""
    time_step!(model, Nt, Δt)

Step forward `model` `Nt` time steps using a second-order Adams-Bashforth
method with step size `Δt`.
"""
function time_step!(model::Model{A}, Nt, Δt) where A <: Architecture
    clock = model.clock
    model_start_time = clock.time
    model_end_time = model_start_time + Nt*Δt

    if clock.iteration == 0
        for output_writer in model.output_writers
            write_output(model, output_writer)
        end
        for diagnostic in model.diagnostics
            run_diagnostic(model, diagnostic)
        end
    end

    arch = A()

    Nx, Ny, Nz = model.grid.Nx, model.grid.Ny, model.grid.Nz
    Lx, Ly, Lz = model.grid.Lx, model.grid.Ly, model.grid.Lz
    Δx, Δy, Δz = model.grid.Δx, model.grid.Δy, model.grid.Δz

    grid = model.grid
    cfg = model.configuration
    bcs = model.boundary_conditions
    clock = model.clock

    G = model.G
    Gp = model.Gp
    constants = model.constants
    eos =  model.eos
    U = model.velocities
    tr = model.tracers
    pr = model.pressures
    forcing = model.forcing
    poisson_solver = model.poisson_solver

    RHS = model.stepper_tmp.fCC1
    ϕ = model.stepper_tmp.fCC2

    gΔz = model.constants.g * model.grid.Δz
    fCor = model.constants.f

    uvw = U.u.data, U.v.data, U.w.data
    TS = tr.T.data, tr.S.data
    Guvw = G.Gu.data, G.Gv.data, G.Gw.data

    # Source terms at current (Gⁿ) and previous (G⁻) time steps.
    Gⁿ = G.Gu.data, G.Gv.data, G.Gw.data, G.GT.data, G.GS.data
    G⁻ = Gp.Gu.data, Gp.Gv.data, Gp.Gw.data, Gp.GT.data, Gp.GS.data

    Bx, By, Bz = floor(Int, Nx/Tx), floor(Int, Ny/Ty), Nz  # Blocks in grid

    tb = (threads=(Tx, Ty), blocks=(Bx, By, Bz))

    for n in 1:Nt
        χ = ifelse(model.clock.iteration == 0, -0.5, 0.125) # Adams-Bashforth (AB2) parameter.

        @launch device(arch) store_previous_source_terms!(grid, Gⁿ..., G⁻..., threads=(Tx, Ty), blocks=(Bx, By, Bz))
        @launch device(arch) update_buoyancy!(grid, constants, eos, tr.T.data, pr.pHY′.data, threads=(Tx, Ty), blocks=(Bx, By))
        @launch device(arch) calculate_interior_source_terms!(grid, constants, eos, cfg, uvw..., TS..., pr.pHY′.data, Gⁿ..., forcing, threads=(Tx, Ty), blocks=(Bx, By, Bz))
                             calculate_boundary_source_terms!(model)
        @launch device(arch) adams_bashforth_update_source_terms!(grid, Gⁿ..., G⁻..., χ, threads=(Tx, Ty), blocks=(Bx, By, Bz))
        @launch device(arch) calculate_source_term_divergence!(arch, grid, Guvw..., RHS.data, threads=(Tx, Ty), blocks=(Bx, By, Bz))

        if arch == CPU()
            solve_poisson_3d_ppn_planned!(poisson_solver, grid, RHS, ϕ)
            @. pr.pNHS.data = real(ϕ.data)
        elseif arch == GPU()
            solve_poisson_3d_ppn_gpu_planned!(Tx, Ty, Bx, By, Bz, poisson_solver, grid, RHS, ϕ)
            @launch device(arch) idct_permute!(grid, ϕ.data, pr.pNHS.data, threads=(Tx, Ty), blocks=(Bx, By, Bz))
        end

        @launch device(arch) update_velocities_and_tracers!(grid, uvw..., TS..., pr.pNHS.data, Gⁿ..., G⁻..., Δt, threads=(Tx, Ty), blocks=(Bx, By, Bz))

        clock.time += Δt
        clock.iteration += 1

        for diagnostic in model.diagnostics
            (clock.iteration % diagnostic.diagnostic_frequency) == 0 && run_diagnostic(model, diagnostic)
        end

        for output_writer in model.output_writers
            (clock.iteration % output_writer.output_frequency) == 0 && write_output(model, output_writer)
        end
    end

    return nothing
end

time_step!(model; Nt, Δt) = time_step!(model, Nt, Δt)

"""Store previous source terms before updating them."""
function store_previous_source_terms!(grid::Grid, Gu, Gv, Gw, GT, GS, Gpu, Gpv, Gpw, GpT, GpS)
    @loop for k in (1:grid.Nz; blockIdx().z)
        @loop for j in (1:grid.Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
            @loop for i in (1:grid.Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
                @inbounds Gpu[i, j, k] = Gu[i, j, k]
                @inbounds Gpv[i, j, k] = Gv[i, j, k]
                @inbounds Gpw[i, j, k] = Gw[i, j, k]
                @inbounds GpT[i, j, k] = GT[i, j, k]
                @inbounds GpS[i, j, k] = GS[i, j, k]
            end
        end
    end
    @synchronize
end

"Update the hydrostatic pressure perturbation pHY′ and buoyancy δρ."
function update_buoyancy!(grid::Grid, constants, eos, T, pHY′)
    gΔz = constants.g * grid.Δz

    @loop for j in (1:grid.Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
        @loop for i in (1:grid.Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
            @inbounds pHY′[i, j, 1] = 0.5 * gΔz * δρ(eos, T, i, j, 1)
            @unroll for k in 2:grid.Nz
                @inbounds pHY′[i, j, k] = pHY′[i, j, k-1] + gΔz * 0.5 * (δρ(eos, T, i, j, k-1) + δρ(eos, T, i, j, k))
            end
        end
    end

    @synchronize
end

"Store previous value of the source term and calculate current source term."
function calculate_interior_source_terms!(grid::Grid, constants, eos, cfg, u, v, w, T, S, pHY′, Gu, Gv, Gw, GT, GS, F)
    Nx, Ny, Nz = grid.Nx, grid.Ny, grid.Nz
    Δx, Δy, Δz = grid.Δx, grid.Δy, grid.Δz

    fCor = constants.f
    ρ₀ = eos.ρ₀
    𝜈h, 𝜈v, κh, κv = cfg.𝜈h, cfg.𝜈v, cfg.κh, cfg.κv

    @loop for k in (1:grid.Nz; blockIdx().z)
        @loop for j in (1:grid.Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
            @loop for i in (1:grid.Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
                # u-momentum equation
                @inbounds Gu[i, j, k] = (-u∇u(grid, u, v, w, i, j, k)
                                            + fCor*avg_xy(grid, v, i, j, k)
                                            - δx_c2f(grid, pHY′, i, j, k) / (Δx * ρ₀)
                                            + 𝜈∇²u(grid, u, 𝜈h, 𝜈v, i, j, k)
                                            + F.u(grid, u, v, w, T, S, i, j, k))

                # v-momentum equation
                @inbounds Gv[i, j, k] = (-u∇v(grid, u, v, w, i, j, k)
                                            - fCor*avg_xy(grid, u, i, j, k)
                                            - δy_c2f(grid, pHY′, i, j, k) / (Δy * ρ₀)
                                            + 𝜈∇²v(grid, v, 𝜈h, 𝜈v, i, j, k)
                                            + F.v(grid, u, v, w, T, S, i, j, k))

                # w-momentum equation: comment about how pressure and buoyancy are handled
                @inbounds Gw[i, j, k] = (-u∇w(grid, u, v, w, i, j, k)
                                            + 𝜈∇²w(grid, w, 𝜈h, 𝜈v, i, j, k)
                                            + F.w(grid, u, v, w, T, S, i, j, k))

                # temperature equation
                @inbounds GT[i, j, k] = (-div_flux(grid, u, v, w, T, i, j, k)
                                            + κ∇²(grid, T, κh, κv, i, j, k)
                                            + F.T(grid, u, v, w, T, S, i, j, k))

                # salinity equation
                @inbounds GS[i, j, k] = (-div_flux(grid, u, v, w, S, i, j, k)
                                            + κ∇²(grid, S, κh, κv, i, j, k)
                                            + F.S(grid, u, v, w, T, S, i, j, k))
            end
        end
    end

    @synchronize
end

function adams_bashforth_update_source_terms!(grid::Grid, Gu, Gv, Gw, GT, GS, Gpu, Gpv, Gpw, GpT, GpS, χ)
    @loop for k in (1:grid.Nz; blockIdx().z)
        @loop for j in (1:grid.Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
            @loop for i in (1:grid.Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
                @inbounds Gu[i, j, k] = (1.5 + χ)*Gu[i, j, k] - (0.5 + χ)*Gpu[i, j, k]
                @inbounds Gv[i, j, k] = (1.5 + χ)*Gv[i, j, k] - (0.5 + χ)*Gpv[i, j, k]
                @inbounds Gw[i, j, k] = (1.5 + χ)*Gw[i, j, k] - (0.5 + χ)*Gpw[i, j, k]
                @inbounds GT[i, j, k] = (1.5 + χ)*GT[i, j, k] - (0.5 + χ)*GpT[i, j, k]
                @inbounds GS[i, j, k] = (1.5 + χ)*GS[i, j, k] - (0.5 + χ)*GpS[i, j, k]
            end
        end
    end
    @synchronize
end

"Store previous value of the source term and calculate current source term."
function calculate_source_term_divergence!(::CPU, grid::Grid, Gu, Gv, Gw, RHS)
    @loop for k in (1:grid.Nz; blockIdx().z)
        @loop for j in (1:grid.Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
            @loop for i in (1:grid.Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
                # Calculate divergence of the RHS source terms (Gu, Gv, Gw).
                @inbounds RHS[i, j, k] = div_f2c(grid, Gu, Gv, Gw, i, j, k)
            end
        end
    end

    @synchronize
end

function calculate_source_term_divergence!(::GPU, grid::Grid, Gu, Gv, Gw, RHS)
    Nx, Ny, Nz = grid.Nx, grid.Ny, grid.Nz
    @loop for k in (1:Nz; blockIdx().z)
        @loop for j in (1:Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
            @loop for i in (1:Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
                # Calculate divergence of the RHS source terms (Gu, Gv, Gw) and applying a permutation
                # which is the first step in the DCT.
                if CUDAnative.ffs(k) == 1  # isodd(k)
                    @inbounds RHS[i, j, convert(UInt32, CUDAnative.floor(k/2) + 1)] = div_f2c(grid, Gu, Gv, Gw, i, j, k)
                else
                    @inbounds RHS[i, j, convert(UInt32, Nz - CUDAnative.floor((k-1)/2))] = div_f2c(grid, Gu, Gv, Gw, i, j, k)
                end
            end
        end
    end

    @synchronize
end

function idct_permute!(grid::Grid, ϕ, pNHS)
    Nx, Ny, Nz = grid.Nx, grid.Ny, grid.Nz
    @loop for k in (1:Nz; blockIdx().z)
        @loop for j in (1:Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
            @loop for i in (1:Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
                if k <= Nz/2
                    @inbounds pNHS[i, j, 2k-1] = real(ϕ[i, j, k])
                else
                    @inbounds pNHS[i, j, 2(Nz-k+1)] = real(ϕ[i, j, k])
                end
            end
        end
    end

    @synchronize
end

function update_velocities_and_tracers!(grid::Grid, u, v, w, T, S, pNHS, Gu, Gv, Gw, GT, GS, Gpu, Gpv, Gpw, GpT, GpS, Δt)
    @loop for k in (1:grid.Nz; blockIdx().z)
        @loop for j in (1:grid.Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
            @loop for i in (1:grid.Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
                @inbounds u[i, j, k] = u[i, j, k] + (Gu[i, j, k] - (δx_c2f(grid, pNHS, i, j, k) / grid.Δx)) * Δt
                @inbounds v[i, j, k] = v[i, j, k] + (Gv[i, j, k] - (δy_c2f(grid, pNHS, i, j, k) / grid.Δy)) * Δt
                @inbounds w[i, j, k] = w[i, j, k] + (Gw[i, j, k] - (δz_c2f(grid, pNHS, i, j, k) / grid.Δz)) * Δt
                @inbounds T[i, j, k] = T[i, j, k] + (GT[i, j, k] * Δt)
                @inbounds S[i, j, k] = S[i, j, k] + (GS[i, j, k] * Δt)
            end
        end
    end

    @synchronize
end

#
# Boundary condition physics specification
#

"Apply boundary conditions by modifying the source term G."
function calculate_boundary_source_terms!(model::Model{A}) where A <: Architecture
    arch = A()

    Nx, Ny, Nz = model.grid.Nx, model.grid.Ny, model.grid.Nz
    Lx, Ly, Lz = model.grid.Lx, model.grid.Ly, model.grid.Lz
    Δx, Δy, Δz = model.grid.Δx, model.grid.Δy, model.grid.Δz

    grid = model.grid
    clock = model.clock
    eos =  model.eos
    cfg = model.configuration
    bcs = model.boundary_conditions
    U = model.velocities
    tr = model.tracers
    G = model.G

    t, iteration = clock.time, clock.iteration
    u, v, w, T, S = U.u.data, U.v.data, U.w.data, tr.T.data, tr.S.data
    Gu, Gv, Gw, GT, GS = G.Gu.data, G.Gv.data, G.Gw.data, G.GT.data, G.GS.data

    Bx, By, Bz = floor(Int, Nx/Tx), floor(Int, Ny/Ty), Nz  # Blocks in grid

    coord = :z #for coord in (:x, :y, :z) when we are ready to support more coordinates.
    𝜈 = cfg.𝜈v
    κ = cfg.κv

    u_x_bcs = getproperty(bcs.u, coord)
    v_x_bcs = getproperty(bcs.v, coord)
    w_x_bcs = getproperty(bcs.w, coord)
    T_x_bcs = getproperty(bcs.T, coord)
    S_x_bcs = getproperty(bcs.S, coord)

    # Apply boundary conditions. We assume there is one molecular 'diffusivity'
    # value, which is passed to apply_bcs.
    apply_bcs!(arch, Val(coord), Bx, By, Bz, u_x_bcs.left, u_x_bcs.right, u, Gu, 𝜈, u, v, w, T, S, t, iteration, grid)
    apply_bcs!(arch, Val(coord), Bx, By, Bz, v_x_bcs.left, v_x_bcs.right, v, Gv, 𝜈, u, v, w, T, S, t, iteration, grid)
    apply_bcs!(arch, Val(coord), Bx, By, Bz, w_x_bcs.left, w_x_bcs.right, w, Gw, 𝜈, u, v, w, T, S, t, iteration, grid)
    apply_bcs!(arch, Val(coord), Bx, By, Bz, T_x_bcs.left, T_x_bcs.right, T, GT, κ, u, v, w, T, S, t, iteration, grid)
    apply_bcs!(arch, Val(coord), Bx, By, Bz, S_x_bcs.left, S_x_bcs.right, S, GS, κ, u, v, w, T, S, t, iteration, grid)

    return nothing
end

# Do nothing if both boundary conditions are default.
apply_bcs!(::CPU, ::Val{:x}, Bx, By, Bz, left_bc::BC{<:Default, T}, right_bc::BC{<:Default, T}, args...) where {T} = nothing
apply_bcs!(::CPU, ::Val{:y}, Bx, By, Bz, left_bc::BC{<:Default, T}, right_bc::BC{<:Default, T}, args...) where {T} = nothing
apply_bcs!(::CPU, ::Val{:z}, Bx, By, Bz, left_bc::BC{<:Default, T}, right_bc::BC{<:Default, T}, args...) where {T} = nothing

apply_bcs!(::GPU, ::Val{:x}, Bx, By, Bz, left_bc::BC{<:Default, T}, right_bc::BC{<:Default, T}, args...) where {T} = nothing
apply_bcs!(::GPU, ::Val{:y}, Bx, By, Bz, left_bc::BC{<:Default, T}, right_bc::BC{<:Default, T}, args...) where {T} = nothing
apply_bcs!(::GPU, ::Val{:z}, Bx, By, Bz, left_bc::BC{<:Default, T}, right_bc::BC{<:Default, T}, args...) where {T} = nothing

# First, dispatch on coordinate.
apply_bcs!(arch, ::Val{:x}, Bx, By, Bz, args...) = @launch device(arch) apply_x_bcs!(args..., threads=(Tx, Ty), blocks=(Bx, By, Bz))
apply_bcs!(arch, ::Val{:y}, Bx, By, Bz, args...) = @launch device(arch) apply_y_bcs!(args..., threads=(Tx, Ty), blocks=(Bx, By, Bz))
apply_bcs!(arch, ::Val{:z}, Bx, By, Bz, args...) = @launch device(arch) apply_z_bcs!(args..., threads=(Tx, Ty), blocks=(Bx, By))

"Apply a top and/or bottom boundary condition to variable ϕ. Note that this kernel
MUST be launched with blocks=(Bx, By). If launched with blocks=(Bx, By, Bz), the
boundary condition will be applied Bz times!"
function apply_z_bcs!(top_bc, bottom_bc, ϕ, Gϕ, κ, u, v, w, T, S, t, iteration, grid::RegularCartesianGrid)
    @loop for j in (1:grid.Ny; (blockIdx().y - 1) * blockDim().y + threadIdx().y)
        @loop for i in (1:grid.Nx; (blockIdx().x - 1) * blockDim().x + threadIdx().x)
            apply_z_top_bc!(top_bc, ϕ, Gϕ, κ, t, grid, u, v, w, T, S, iteration, i, j)
            apply_z_bottom_bc!(bottom_bc, ϕ, Gϕ, κ, t, grid, u, v, w, T, S, iteration, i, j)
        end
    end
    @synchronize
end


# This file is a part of RunStatistics.jl, licensed under the MIT License (MIT).

using RunStatistics
using Test

@testset "t_obs" begin
    @test RunStatistics.t_obs([-1, 1, 3, -2], 0, 1) == (10.0, Integer[2,3])
end


module TestOptics

using Accessors
using Test

@testset "Properties" begin
    pt = (x=1, y=2, z=3)
    @test (x=0, y=1, z=2) === @set pt |> Properties() -= 1
end

@testset "Elements" begin

    @test [0,0,0] == @set 1:3 |> Elements() = 0

    arr = 1:3
    @test 2:4 == (@set arr |> Elements() += 1)
    @test map(cos, arr) == modify(cos, arr, Elements())

    @test modify(cos, (), Elements()) === ()

    @inferred modify(cos, arr, Elements())
    @inferred modify(cos, (), Elements())
end

@testset "Recursive" begin
    obj = (a=1, b=(1,2), c=(A=1, B=(1,2,3), D=4))
    rp = Recursive(x -> !(x isa Tuple), Properties())
    @test modify(collect, obj, rp) == (a = 1, b = [1, 2], c = (A = 1, B = [1, 2, 3], D = 4))

    arr = [1,2,[3,4], [5, 6:7,8, 9,]]
    oc = Recursive(x -> x isa AbstractArray, Elements())
    expected = [0,1,[2,3], [4, 5:6,7, 8,]]
    @test modify(x-> x - 1, arr, oc) == expected
end

@testset "If" begin
    @test 10 === @set(1 |> If(>=(0)) = 10)
    @test -1 === @set(-1 |> If(>=(0)) = 10)
    @inferred set(1, If(iseven), 2)
    @inferred modify(x -> 0, 1, If(iseven))

    arr = 1:6
    @test [1, 0, 3, 0, 5, 0] == @set(arr |> Elements() |> If(iseven) = 0)
    @inferred modify(x -> 0, arr, @optic _ |> Elements() |> If(iseven))
end

end#module


# This file was generated, do not modify it. # hide
# hideall
c = """
3.26;0.829;1.676;0;1;1.453;3.770
2.189;0.58;0.863;0;0;1.348;3.115
2.125;0.638;0.831;0;0;1.348;3.531
3.027;0.331;1.472;1;0;1.807;3.510
2.094;0.827;0.86;0;0;1.886;5.390
3.222;0.331;2.177;0;0;0.706;1.819
3.179;0;1.063;0;0;2.942;3.947
3;0;0.938;1;0;2.851;3.513
2.62;0.499;0.99;0;0;2.942;4.402
2.834;0.134;0.95;0;0;1.591;3.021
2.405;0.134;0.843;0;0;1.769;3.210
2.728;0.223;0.953;0;0;1.591;2.371
2.512;0.223;0.929;1;0;1.769;3.919
2.834;0.134;1.237;0;0;1.859;3.030
2.819;0.331;1.271;0;1;0.981;2.736
2.126;0.251;1.114;0;0;0.143;2.157
2.834;0.134;1.322;0;0;1.199;2.413
3.014;0.56;1.781;0;0;-0.115;0.898
3.024;0.452;2.698;0;0;1.107;0.450
3.036;0.405;1.205;1;0;1.807;3.733
2.707;0.972;1.889;0;3;-1.169;2.976
2.978;1.246;1.103;0;1;3.988;6.535
3.111;0.732;0.923;0;0;4.068;5.643
"""
fpath, = mktemp()
write(fpath, c);


using CUDA

codee = """
define dso_local i16 @"?addToTp@@YAG_N0G@Z"(i1 zeroext %0, i1 zeroext %1, i16 %2) #0 {
  %4 = alloca i16, align 2
  %5 = alloca i8, align 1
  %6 = alloca i8, align 1
  store i16 %2, i16* %4, align 2
  %7 = zext i1 %1 to i8
  store i8 %7, i8* %5, align 1
  %8 = zext i1 %0 to i8
  store i8 %8, i8* %6, align 1
  %9 = load i8, i8* %6, align 1
  %10 = trunc i8 %9 to i1
  %11 = zext i1 %10 to i32
  %12 = load i8, i8* %5, align 1
  %13 = trunc i8 %12 to i1
  %14 = zext i1 %13 to i32
  %15 = and i32 %11, %14
  %16 = load i16, i16* %4, align 2
  %17 = zext i16 %16 to i32
  %18 = add nsw i32 %17, %15
  %19 = trunc i32 %18 to i16
  store i16 %19, i16* %4, align 2
  ret i16 %19
}
"""

    # intrinsic = "llvm.nvvm.vote.$mode"
    # @eval begin
    #     @inline $fname(pred) =
    #         Base.llvmcall($("""
    #             declare i32 @$intrinsic(i1)
    #             define i32 @entry(i8) #0 {
    #                 %predicate = icmp eq i8 %0, 1
    #                 %ret = call i32 @$intrinsic(i1 %predicate)
    #                 ret i32 %ret
    #             }
    #             attributes #0 = { alwaysinline }""", "entry"),
    #         UInt32, Tuple{Bool}, pred)
    # end

    # # warp-synchronous
    # intrinsic = "llvm.nvvm.vote.$mode.sync"

    fname = Symbol("addToTp")
    @eval export $fname
   @eval begin
        @inline addToTp(boolGold, boolSegm,tp) =
            Base.llvmcall($("""
            $codee
                attributes #0 = { alwaysinline }""", "addToTp"),
            UInt32, Tuple{Bool, Bool, UInt32}, boolGold, boolSegm,tp)
    end
#    @eval begin
#         @inline addToTp(boolGold, boolSegm,tp) =
#             Base.llvmcall($("""
#             define i32 @addToTp(i32 %tp) #0 {
#                 ret i32 %tp
#             }
#                 attributes #0 = { alwaysinline }""", "addToTp"),
#             UInt32, Tuple{Bool, Bool, UInt32}, boolGold, boolSegm,tp)
#     end
addToTp(true,true,UInt32(1))

# using Test

# function do_the_call()
#     Base.llvmcall(
#         ("""declare double @jl_test_returns_float()
#             define double @entry() #0 {
#             0:
#                 %1 = call double @jl_test_returns_float()
#                 ret double %1
#             }
#             attributes #0 = { alwaysinline }
#         """, "entry"),Float64,Tuple{})
# end
# @test do_the_call() === 42.0

# @eval export $fname, $fname_sync
# using Test
# function zz(x::UInt16, y::UInt16)
#     Base.llvmcall("""%3 = add i16 %1, %0
#                 ret i16 %3""", UInt16, Tuple{UInt16, UInt16}, x, y)
# end
# @test zz(UInt16(1),UInt16(2))==UInt16(3)

using Test
function addToFp(boolGold::Bool, boolSegm::Bool,tp::UInt16)
    Base.llvmcall("""
    %4 = xor i8 %0, %1
    %5 = and i8 %4, %1
    %6 = zext i8 %5 to i16
    %7 = add i16 %2,%6
    ret i16 %7""", UInt16, Tuple{Bool,Bool, UInt16}, boolGold, boolSegm,tp)
end

@test addToFp(false,true,UInt16(2))==UInt16(3)
@test addToFp(true,true,UInt16(2))==UInt16(2)
@test addToFp(false,false,UInt16(2))==UInt16(2)
@test addToFp(true,false,UInt16(2))==UInt16(2)

function addToFn(boolGold::Bool, boolSegm::Bool,tp::UInt16)
    Base.llvmcall("""
    %4 = xor i8 %0, %1
    %5 = and i8 %4, %0
    %6 = zext i8 %5 to i16
    %7 = add i16 %2,%6
    ret i16 %7""", UInt16, Tuple{Bool,Bool, UInt16}, boolGold, boolSegm,tp)
end

@test addToFn(false,true,UInt16(2))==UInt16(2)
@test addToFn(true,true,UInt16(2))==UInt16(2)
@test addToFn(false,false,UInt16(2))==UInt16(2)
@test addToFn(true,false,UInt16(2))==UInt16(3)

x=UInt16(1)
zz(true,true,x)
x


# Rectangle methods

RectanglePoints(x1::Int, y1::Int, x2::Int, y2::Int)  = RectanglePoints(Point(x1, y1), Point(x2, y2))
RectanglePoints(p1::CartesianIndex{2}, p2::CartesianIndex{2}) = RectanglePoints(Point(p1[1], p1[2]), Point(p2[1], p2[2]))

draw!(img::AbstractArray{T, 2}, rectangle::RectanglePoints, color::T) where {T<:Colorant} = draw!(img, Polygon(rectangle), color)


# ---------- #
# State type #
# ---------- #

struct State
    board::Board
    player::Int
    round::Int
end
State() = State(Board(0, 0), 1, 0)

function Base.show(io::IO, mime::MIME"text/plain", s::State)
    if s.player == 1 || s.player == 2
        msg = "It is Player $(s.player)'s ($(MARKERS[s.player])) turn"
    else
        msg = "It is player $(s.player)'s turn"
    end

    println(io, msg, " on round $(s.round)")
    println(io, "The score is $(score(s)) and the board looks like:\n")
    show(io, mime, s.board)
end
Base.show(io::IO, s::State) = show(io, MIME"text/plain"(), s)

# --------------------------- #
# bitboard api for State type #
# --------------------------- #

function legal_actions_bits(s::State)
    if s.player == 1
        return legal_actions_bits(s.board.p1_placed, s.board.p2_placed)
    else
        return legal_actions_bits(s.board.p2_placed, s.board.p1_placed)
    end
end

score(s::State) = score(s.board.p1_placed), score(s.board.p2_placed)
legal_actions(s::State) = decode(legal_actions_bits(s))
function game_over(s::State)
    if s.round  < 4
        return false, 0, 0
    end
    game_over(s.board.p1_placed, s.board.p2_placed)
end

function do_action(s::State, action::UInt64)::State
    p1 = s.board.p1_placed
    p2 = s.board.p2_placed
    if s.player == 1
        new_p1, new_p2 = next_state_bits(action, p1, p2)
        new_player = 2
    else
        new_p2, new_p1 = next_state_bits(action, p2, p1)
        new_player = 1
    end
    State(Board(new_p1, new_p2), new_player, s.round+1)
end

do_action(s::State, a::Tuple{Int,Int}) = do_action(s, encode(a))
do_pass(s::State) = State(s.board, (s.player == 1) + 1, s.round)

opponent(i::Integer) = (i == 1) + 1
opponent(s::State) = opponent(state.player)


module UInt12s

using BitIntegers
using ..UInt24s

export UInt12

# From BitIntegers:

## convenient abbreviations, only for internal use

const BBS = Base.BitSigned
const BBU = Base.BitUnsigned
const BBI = Base.BitInteger

# eXtended
const XBS = BitIntegers.AbstractBitSigned
const XBU = BitIntegers.AbstractBitUnsigned
const XBI = Union{XBS,XBU}

# Union, Unified
const UBS = Union{BBS,XBS}
const UBU = Union{BBU,XBU}
const UBI = Union{BBI,XBI}

# End from BitIntegers

# Abstract UInt12

abstract type AbstractUInt12 <: Unsigned end

const UINT12_TYPEMAX = 2^12 - 1

Base.typemin(::Type{T}) where T <: AbstractUInt12 = convert(T, 0)
Base.typemax(::Type{T}) where T <: AbstractUInt12 = convert(T, UINT12_TYPEMAX)

# Concrete base UInt12 type

struct UInt12{T <: Union{Bool, Unsigned}} <: AbstractUInt12
    data::T
    function UInt12{T}(data::Union{Bool, UBI}) where T
        if data > 0xfff
            throw( InexactError(:UInt12, UInt12, data))
        else
            new{T}(data)
        end
    end
end

# Default UInt12 constructor uses UInt16
UInt12(x::Union{Bool, UBU}) = UInt12{UInt16}(x)
UInt12(x::UBS) = UInt12{UInt16}(UInt16(x))
UInt12(x::Union{Float16, Float32, Float64}) = UInt12(UInt16(x))
#UInt12(x::Union{Bool, Base.BitInteger, BitIntegers.AbstractBitSigned, BitIntegers.AbstractBitUnsigned}) = UInt12{UInt16}(x)
#UInt12{UInt16}(x::Union{Bool, Base.BitInteger, BitIntegers.AbstractBitSigned, BitIntegers.AbstractBitUnsigned}) = UInt12{UInt16}(x)

Base.uinttype(::Type{UInt12{T}}) where T = UInt12{T}
Base.widen(::Type{UInt12{T}}) where T = UInt24

macro uint12_str(s)
    return UInt12(parse(UInt16, s))
end

Core.UInt16(x::UInt12) = UInt16(x.data)
Core.Int64(x::UInt12) = Int64(x.data)
Core.UInt64(x::UInt12) = UInt64(x.data)

Base.show(io::IO, n::UInt12) = print(io, "0x", string(n, pad = 3, base = 16))
Base.hex(x::UInt12, pad::Integer, neg::Bool) = Base.hex(x.data, pad, neg)
Base.bin(x::UInt12, pad::Integer, neg::Bool) = Base.bin(x.data, pad, neg)
Base.promote_rule(::Type{UInt12{T}}, o::Type{S}) where {T,S} = promote_type(T, o)
Base.:+(x::UInt12{T}, y::Union{Bool, UBU}) where T = UInt12{T}( (x.data + y) % 0x1000 )

Base.:<(x::UInt12, y::UInt12) = x.data < y.data
Base.:<=(x::UInt12, y::UInt12) = x.data <= y.data

Base.:(==)(x::UInt12, y::UInt12) = x.data == y.data

# bit operations

Base.:(~)(x::UInt12{T}) where T = UInt12{T}(~x.data & 0xfff)
Base.:(&)(x::UInt12{T}, y::UInt12{T}) where {T} = UInt12{T}(x.data & y.data)
Base.:(|)(x::UInt12{T}, y::UInt12{T}) where {T} = UInt12{T}(x.data | y.data)
Base.xor(x::UInt12{T}, y::UInt12{T}) where {T} = UInt12{T}( xor(x.data, y.data) )

Base.:>>(x::UInt12{T}, y::UBU) where T = UInt12{T}(x.data >> y)
Base.:>>>(x::UInt12{T}, y::UBU) where T = UInt12{T}(x.data >>> y)
Base.:<<(x::UInt12{T}, y::UBU) where T = UInt12{T}( (x.data << y) & 0xfff )

Base.count_ones(x::UInt12) = count_ones(x.data)
Base.leading_zeros(x::UInt12{UInt16}) = leading_zeros(x.data) - 4
Base.leading_zeros(x::UInt12{T}) where T = leading_zeros(x.data) - sizeof(T)*8 + 12
Base.trailing_zeros(x::UInt12) = trailing_zeros(x.data)

Base.isodd(x::UInt12) = isodd(x.data)

# arithmetic operations
Base.:(-)(x::UInt12{T}) where T = UInt12{T}( (-x.data) & 0xfff )
Base.:(-)(x::UInt12{T}, y::UInt12{T}) where T = UInt12{T}( (x.data - y.data) & 0xfff )
Base.:(+)(x::UInt12{T}, y::UInt12{T}) where T = UInt12{T}( (x.data + y.data) & 0xfff )
Base.:(*)(x::UInt12{T}, y::UInt12{T}) where T = UInt12{T}( (x.data * y.data) & 0xfff )

Base.div(x::UInt12{T}, y::UInt12{T}) where T = UInt12{T}( x.data ÷ y.data )
Base.rem(x::UInt12{T}, y::UInt12{T}) where T = UInt12{T}( rem(x.data, y.data) )

Base.checked_abs(x::UInt12) = x

Base.bitstring(x::UInt12) = string(x, pad=12, base = 2)

end


#Any plotting functionality should be here

"""
Function to make plot of positioners
Takes input a POSRobotBody structure and plots the on_target location of positioner
"""
function plot_moved_positioner(pos;linecolor=:black,bodycolor=:blue,headcolor=:red,alpha=0.2,hold=false)
    nT=pos.nseg[1]
    nP=pos.nseg[2]

    #plots center
    if(hold)
        plot!([pos.xy[1]],[pos.xy[2]],seriestype=:scatter,color=:black,markersize = 5)
    else
        plot([pos.xy[1]],[pos.xy[2]],seriestype=:scatter,color=:black,markersize = 5)
    end
    #plot([pos.xy[1],pos.axis_on_target[1]],[pos.xy[2],pos.axis_on_target[2]],seriestype=:scatter,color=:black,markersize = 5)

    #plot(pos.body_on_target[1:nT,1],pos.body_on_target[1:nT,2],linestyle=:solid)
    #plots body
    plot!(pos.body_on_target[1:nT,1],pos.body_on_target[1:nT,2],
        seriestype=[:shape,],lw=0.5,c=bodycolor,linecolor=linecolor,
        legend=false,fillalpha=alpha)

    plot!(pos.head_on_target[1:nP,1],pos.head_on_target[1:nP,2],
        seriestype=[:shape,],lw=0.5,c=headcolor,linecolor=linecolor,
        legend=false,fillalpha=alpha)

    #plot!(pos.head_orig[1:nP,1],pos.head_orig[1:nP,2],
    #    seriestype=[:shape,],lw=0.5,c=:green,linecolor=linecolor,
    #    legend=false,fillalpha=alpha)

    plot_circle(pos.xy[1],pos.xy[2],pos.arms[1],color=:black,alpha=0.1)
    plot_circle(pos.xy[1],pos.xy[2],pos.arms[1]+pos.arms[2],color=:green,alpha=0.05)
end

"""plots a circle with given properties
"""
function plot_circle(x,y,rad;color=:blue,linecolor=:black,alpha=0.2)
    theta=LinRange(0,2*pi,500)
    plot!(x .+ rad.*sin.(theta), y.+ rad.*cos.(theta),
        seriestype=[:shape,],lw=0.5,c=color,linecolor=linecolor,
        legend=false,fillalpha=alpha,aspect_ratio=1)
end

"""
plots the target in the zone and positioner
as as any auxiliary informatio
"""
function plot_zones(pos,zone_target,fp_dic)
    nc=zone_target.ntarget[1]
    plot(zone_target.X_FP[1:nc],zone_target.Y_FP[1:nc],seriestype=:scatter,color=:black)
    ifp=fp_dic["zone_map"][zone_target.zones[3]]
    #plot!(fp_dic["OFFSET_X"][ifp[1]:ifp[2]],fp_dic["OFFSET_Y"][ifp[1]:ifp[2]],
    #    seriestype=:scatter,color=:green,markersize=10)

    iass=findall(zone_target.pot_ass[1:nc])
    plot!(zone_target.X_FP[iass],zone_target.Y_FP[iass],seriestype=:scatter,color=:red)
    icoll=findall(zone_target.pot_coll[1:nc])
    plot!(zone_target.X_FP[icoll],zone_target.Y_FP[icoll],seriestype=:scatter,color=:blue)

end

function plot_tile_assignment(position_assigned,targets_dic,fp_dic;ri=1,outfile="test.png")

    iass=position_assigned[:,ri] .>0
    iass=sort(position_assigned[iass,ri])

    bool_fib=zeros(Bool,size(targets_dic["X_FP"],1))
    bool_fib[iass] .=true

    #plot all target and assigned objects
    p1=plot(targets_dic["X_FP"][bool_fib],targets_dic["Y_FP"][bool_fib],seriestype=:scatter,color=:black,
        ma=0.5,size=(1500,1500),axis=nothing,title="Assigned targets")
    #plot un-assigned object
    p2=plot(targets_dic["X_FP"][.!bool_fib],targets_dic["Y_FP"][.!bool_fib],seriestype=:scatter,color=:black,
        ma=0.1,size=(1500,1500),axis=nothing,title="Un-Assigned targets")
    #plot un-assigned positioner
    pos_un_ass=position_assigned[:,ri] .== 0
    p3=plot(fp_dic["OFFSET_X"][pos_un_ass],fp_dic["OFFSET_Y"][pos_un_ass],seriestype=:scatter,color=:red,
        ma=0.5,size=(1500,1500),axis=nothing,title="Un-Assigned Positioners")

    #split by priority for top three priority
    usp=sort(unique(targets_dic["PRIORITY"]),rev=:true)
    bp=findall(bool_fib .& (targets_dic["PRIORITY"] .== usp[1]))
    ncount=size(bp,1)
    #@show size(bp),sum(targets_dic["PRIORITY"] .== usp[1])
    p4=plot(targets_dic["X_FP"][bp],targets_dic["Y_FP"][bp],seriestype=:scatter,color=:red,
        ma=1.0,size=(1500,1500),axis=nothing,title="PRIORITY=$(usp[1]) ,count=$(ncount)")

    bp=findall(bool_fib .& (targets_dic["PRIORITY"] .== usp[2]))
    ncount=size(bp,1)
    p5=plot(targets_dic["X_FP"][bp],targets_dic["Y_FP"][bp],seriestype=:scatter,color=:blue,
        ma=1.0,size=(1500,1500),axis=nothing,title="PRIORITY=$(usp[2]) ,count=$(ncount)")
    #@show size(bp),sum(targets_dic["PRIORITY"] .== usp[2])

    bp=findall(bool_fib .& (targets_dic["PRIORITY"] .== usp[3]))
    ncount=size(bp,1)
    p6=plot(targets_dic["X_FP"][bp],targets_dic["Y_FP"][bp],seriestype=:scatter,color=:green,
        ma=0.5,size=(1500,1500),axis=nothing,title="PRIORITY=$(usp[3]) ,count=$(ncount)")
    #@show size(bp),sum(targets_dic["PRIORITY"] .== usp[3])

    plot(p1, p2,p3,p4,p5,p6, layout = (2, 3), #title=["Assigned","UnAssigned"],
        legend = false,aspect_ratio=1)
    savefig(outfile)
    println("Generated: $(outfile)")

end


using KiteModels, KiteUtils
using Test

cd("..")
KiteUtils.set_data_path("")
include("test_kps3.jl")
include("test_kps4.jl")
include("bench3.jl")
include("bench4.jl")


# mri/z-list.jl

include("kspace.jl")
include("sensemap-sim.jl")


function get_date(obj::TimeStamped)
    return jcall(obj, "getDate", AbsoluteDate, ())
end

function get_observed_value(obj::ComparableMeasurement)
    return jcall(obj, "getObservedValue", Vector{jdouble}, ())
end


"""
    q = query_reference2image(sectiondataset, x, y, z)

Given a `sectiondataset`, find a point in a 2d slice image closest to
the specified `x`, `y`, `z` location in the reference atlas (specified
in microns).
"""
function query_reference2image(sectiondataset::Dict, x, y, z)
    id = sectiondataset["id"]
    refspace = sectiondataset["reference_space_id"]
    xr, yr, zr = inmicrons(x), inmicrons(y), inmicrons(z)
    str = "http://api.brain-map.org/api/v2/reference_to_image/$refspace.json?x=$xr&y=$yr&z=$zr&section_data_set_ids=$id"
    rq = HTTP.request("GET", str)
    data = JSON.parse(String(rq.data))
    data["msg"][1]["image_sync"]
end

"""
    p, i, r = query_image2reference(sectionimageid, x, y)

Given a particular slice `sectionimageid` and pixel location `x`, `y`,
return the posterior, inferior, right coordinates `p`, `i`, `r` in microns.
"""
function query_image2reference(sectionimageid::Integer, x, y)
    str = "http://api.brain-map.org/api/v2/image_to_reference/$sectionimageid.json?x=$x&y=$y"
    rq = HTTP.request("GET", str)
    data = JSON.parse(String(rq.data))
    ret = data["msg"]["image_to_reference"]
    ret["x"], ret["y"], ret["z"]
end

"""
    sectionimage(secdata, w, h, filename; downsample=0)

Download a section image specified by `secdata`, where `secdata` comes
from [`query_reference2image`](@ref). Width `w` and height `h` are expressed
in pixels prior to downsampling (shrinking horizontal and vertical by
a factor of `2^downsample`). Image is saved to `filename`.
"""
function sectionimage(secdata::Dict, w, h, filename; downsample=0)
    id = secdata["section_image_id"]
    x, y = secdata["x"], secdata["y"]
    download("http://api.brain-map.org/api/v2/image_download/$id?downsample=$downsample&left=$x&top=$y&width=$w&height=$h", filename)
end

"""
    splice_sectionimages(dirname, Δplane)

Combine a series of section images into a 3d volume.  Adjacent planes
will be spaced by `Δplane`, expressed in microns. The resolution
within the slice plane is determined by the raw images.

Uses interpolation along the slicing axis where appropriate, but
otherwise this takes a conservative approach and marks pixels with NaN
whenever data are missing. Data can be missing for at least two
reasons:

* Adjacent available slices are separated by more than `2Δplane`, so
  that some intermediate plane is more distant than `Δplane` from any
  available image;

* Some individual images do not cover the entire region expressed by
  the within-plane components of the BoundingBox used to extract the
  slices (stored in the metadata file in `dirname`). This can happen
  when the relevant structure lies near the tissue boundary.

## Note

Currently this assumes that the image is in PIR coordinates and that
these are coronal slices, so that the first axis is the slicing axis.
"""
function splice_sectionimages(dirname, Δplane)
    cd(dirname) do
        md = load("metadata.jld2")
        pos = md["sectionposition"]
        ds = md["downsample"]
        fn = md["filenames"]
        bb = md["boundingbox"]
        res = md["sectiondataset"]["section_images"][1]["resolution"]*μm*2^ds
        ivx, ivy, ivz = bb.intervals               # in "Allen Coordinates"
        xrange = minimum(ivx):Δplane:maximum(ivx)  # the slice axis (assumes coronal)
        sz2d = map(x->round(Int, x/res), IntervalSets.width.((ivy, ivz)))
        img1 = load(first(fn))
        frames = Vector{typeof(img1)}(length(fn))
        frames[1] = img1
        for i = 2:length(fn)
            frames[i] = load(fn[i])
        end
        C = base_colorant_type(eltype(img1)){Float32}
        img = Array{C}(sz2d..., length(xrange))
        fill!(img, nan(RGB{Float32}))
        computeframes!(img, bb, xrange, frames, pos, ds)
        imgaa = AxisArray(img, (:I, :R, :P), (res, res, Δplane))
        save("spliced.nhdr", imgaa; props=Dict("datafile"=>"spliced.raw"))
    end
end

function computeframes!(img, bb, xrange, frames, pos, ds)
    slicepos = [p[1]*μm for p in pos]
    for (j, x) in enumerate(xrange)
        # Find the nearest slices to this particular "x" (slice) position
        im = findfirst(y->y>=x, slicepos) # nearest slice "below"
        ip = im+1                         # nearest slice "above"
        # The available data might be too far away to reasonably
        # interpolate, in which case it's better to leave this plane
        # as NaN to mark it as missing data.
        m_inrange = isinrange(im, slicepos, x, step(xrange))
        p_inrange = isinrange(ip, slicepos, x, step(xrange))
        m_inrange || p_inrange || continue
        if !m_inrange
            computeframe!(view(img, :, :, j), bb, ds, frames[ip], pos[ip])
        elseif !p_inrange
            computeframe!(view(img, :, :, j), bb, ds, frames[im], pos[im])
        else  # both are in range, use linear interpolation along the slice dimension
            f = (x - slicepos[im])/(slicepos[ip] - slicepos[im])
            computeframe!(view(img, :, :, j), bb, ds, frames[im], pos[im], frames[ip], pos[ip], f)
        end
    end
    img
end

"Copy frame data, compensating if needed for a shift in the slice plane"
function computeframe!(dest, bb, downsample, src, pos)
    sa = extrapolant(eltype(dest), bb, downsample, src, pos)
    for i in CartesianRange(indices(dest))
        dest[i] = sa[i]
    end
    dest
end

"Interpolate frame data, compensating if needed for a shift in the slice plane"
function computeframe!(dest, bb, downsample, src1, pos1, src2, pos2, f)
    sa1 = extrapolant(eltype(dest), bb, downsample, src1, pos1)
    sa2 = extrapolant(eltype(dest), bb, downsample, src2, pos2)
    for i in CartesianRange(indices(dest))
        dest[i] = (1-f)*sa1[i] + f*sa2[i]
    end
    dest
end

"""
Correct for an integer-pixel shift in the frame. We could use subpixel
interpolation, but given that the resolution in the slice plane is so
much better than the resolution along the slice axis, there seems
little point in being more sophisticated.
"""
function extrapolant(::Type{T}, bb, downsample, src, pos) where T
    d1, d2 = inmicrons(minimum(bb.intervals[2])), inmicrons(minimum(bb.intervals[3]))
    s1, s2 = pos[2], pos[3]
    mag = 2^downsample
    Δ1, Δ2 = round(Int, (s1-d1)/mag), round(Int, (s2-d2)/mag)
    inds1, inds2 = offsetindex(indices(src, 1), Δ1), offsetindex(indices(src, 2), Δ2)
    extrapolate(interpolate(view(src, inds1, inds2), NoInterp(), OnGrid()), nan(T))
end

offsetindex(inds, Δ) = OffsetArray(inds, inds+Δ)

function isinrange(idx, pos, x, Δx)
    1 <= idx <= length(pos) || return false
    abs(pos[idx] - x) <= Δx
end

function imagesize(filename)
    wand = ImageMagick.MagickWand()
    ImageMagick.readimage(wand, filename)
    size(wand)
end


"""

Given the index of the atom in the vector of coordinates of the solute or the solvent,
returns the type of the atom, that is, the index of this atom within the molecule
(goes from 1 to natomspermol)

"""
function itype(iatom::Int, natomspermol::Int)
  itype = iatom%natomspermol
  if itype == 0
    itype = natomspermol
  end
  return itype
end

# Calling using the structures of Solute and Solvent, to clear up the code above
itype(iatom::Int, s::Selection) = itype(iatom,s.natomspermol)


using Test

using MagicUnderscores
using MagicUnderscores: lower_underscores

@testset "MagicUnderscores" begin
    @test @_(map(_^2, [1,2])) == [1,4]
    @test @_(map(sqrt(abs(_)) + 1, [-1,2,3])) == sqrt.(abs.([-1,2,3])) .+ 1
    @test @_(map(map(x->x^2, _), [[1,2]])) == [[1,4]]

    @test @_(filter(_>2, [1,2])) == []

    @test @_([1,2] |> length) == 2
    @test @_([1,2,3,4] |> filter(_>2, _)) == [3,4]
    @test @_([1,2,3,4] |> filter(_>2, _) |> length) == 2

    @test @_(map(_.a == 0, [(a=1, b=2), (a=0, b=2)])) == [false, true]
    @test @_(findall(2*_ in (4,6), [1,2,3])) == [2,3]
end


struct SubDiskArray{T,N} <: AbstractDiskArray{T,N}
  v::SubArray{T,N}
end
replace_colon(s,::Colon) = Base.OneTo(s)
replace_colon(s,r) = r
function Base.view(a::AbstractDiskArray,i...)
  i2 = replace_colon.(size(a),i)
  SubDiskArray(SubArray(a,i2))
end
Base.view(a::AbstractDiskArray, i::CartesianIndices) = view(a,i.indices...)
function Base.view(a::SubDiskArray,i...)
  SubDiskArray(view(a.v,i...))
end
Base.view(a::SubDiskArray, i::CartesianIndices) = view(a,i.indices...)
function readblock!(a::SubDiskArray,aout,i::OrdinalRange...)
  pinds = parentindices(view(a.v,i...))
  inds,resh = interpret_indices_disk(parent(a.v),pinds)
  readblock!(parent(a.v),reshape(aout,map(length,inds)...),inds...)
end
function writeblock!(a::SubDiskArray,v,i::OrdinalRange...)
  pinds = parentindices(view(a.v,i...))
  inds,resh = interpret_indices_disk(parent(a.v),pinds)
  writeblock!(parent(a.v),reshape(v,map(length,inds)...),inds...)
end
Base.size(a::SubDiskArray) = size(a.v)
eachchunk(a::SubDiskArray) = eachchunk_view(haschunks(a.v.parent),a.v)
Base.parent(a::SubDiskArray) = a.v.parent
function eachchunk_view(::Chunked, vv)
  pinds = parentindices(vv)
  iomit = findints(pinds)
  chunksparent = eachchunk(parent(vv))
  #TODO this currently only works if the parent chunks are GridChunks, should be generalized in the future
  parentoffset, parentsize = chunksparent.offset, chunksparent.chunksize
  offsets = ([mod1(first(pinds[i]),parentsize[i])+parentoffset[i]-1 for i in 1:length(pinds) if !in(i,iomit)]...,)
  newcs = ([parentsize[i] for i in 1:length(pinds) if !in(i,iomit)]...,)
  GridChunks(vv,newcs,offset = offsets)
end
eachchunk_view(::Unchunked, a) = GridChunks(a,estimate_chunksize(a))
haschunks(a::SubDiskArray) = haschunks(parent(a.v))


include("1.jl")
function standart(r::Robot,n::Int64)
    flag1 = 1
    while (!isborder(r,Nord))
        y = n
        while (y > 0)
            putmarkersus(r,flag1,n,Ost)
            move!(r,Nord)
            y -= 1
        end
        flag1 = mod(flag1 + 1, 2)
    end
    if (mod(flag1 + 1, 2) == 1)
        putmarkersus(r,flag1,n,Ost)
    end
end

function putmarkersus(r::Robot,flag::Int64,n::Int64,side::HorizonSide)
    flag0 = flag
    while (!isborder(r,side))
        y = n
        while (y > 0)
            if ((flag0 == 0) && (!isborder(r,side)))
                move!(r,side)
            end
            if ((flag0 == 1) && (!isborder(r,side)))
                putmarker!(r)
                move!(r,side)
            end
            y -= 1
        end
        flag0 = mod(flag0 + 1, 2)
    end
    if (mod(flag0 + 1, 2) == 1)
        putmarker!(r)
    end
    while (!isborder(r,inverse(side)))
        move!(r,inverse(side))
    end
end


# #############################################################################
# #############################################################################
#Cavity analysis functions and global variables

#This file contains the main driving functions for evaluating a cavity, as well as the state variables and setting/getting functions for the simulation and cavity parameters. Typically, a control file will run on a multiprocess julia instance

#   shell-prompt> julia -p <num of parallel procs> <ctrlfile>

#The file starts with the declaration

#   @everywhere using CavChaos

#to initiate the module's methods into the namespace on every process. The following functions can then be used in a control file to set up a simulation run:

#>> Cavity properties

#   set_cavity_bnd!(bnd::Boundary)
#   Sets the desired cavity boundary to <bnd>.

#   bnd = get_cavity_bnd()
#   Returns the current cavity boundary.

#   set_cavity_idx!(idx::RefractiveIndex)
#   Sets the desired cavity index distribution to <idx>.

#   idx = get_cavity_idx()
#   Returns the current cavity index distribution.

#>> Solver parameters

#   set_solver_params!(;kwargs...)
#   Sets the solver parameter represented by keywords to the numerical value associated to it in <kwargs>. Appropriate keywords are: maxpathlength, maxbounces, relativetolerance, absolutetolerance.

#   value = get_solver_param(key::Union(AbstractString,Symbol))
#   Returns the current value of the solver parameter represented by the symbol or string <key>.

#>> Initial conditions

#   set_init_params!(;kwargs...)
#   Sets the initial conditions autogeneration parameter represented by keywords to the numerical value associated to it in <kwargs>. Appropriate keywords are: symmetry, thetares, chires.

#   value = get_init_param(key::Union(AbstractString,Symbol))
#   Returns the current value of the initial conditions autogeneration parameter represented by the symbol or string <key>.

#   set_pssinitarray!(A::Array{Float64,2})
#   Sets the custom initial conditions as an array <A> of initial (theta,chi) values as its rows. If nonempty, the autogeneration parameters will be ignored, and the initial conditions fed to the solver will be computed from <A> directly. This is useful for choosing points on the Poincare Surface of Section for performing simulation runs.

#   pssinitarray = get_pssinitarray()
#   Returns the current custom/autogenerated (theta,chi)-based initial conditions array <pssinitarray>.

#   set_initarray!(A::Array{Float64,2})
#   Sets the custom initial conditions as an array <A> of initial (r,theta,phi) values as its rows. If nonempty, the autogeneration parameters and the (theta,chi)-based custom initial conditions array will be ignored, and the initial conditions fed to the solver will be taken from <A> directly. This is useful for fine control over the initial ray position and direction when performing simulation runs.

#   initarray = get_initarray()
#   Returns the current custom/autogenerated (r,theta,phi)-based initial conditions array <initarray>.

#>> Results directories

#   set_resultsroot!(dir::AbstractString)
#   Sets the results root directory (in which a "results" subdirectory stores all output data) to <dir>.

#   get_resultsroot()
#   Returns the results root directory.

#   get_resultsdir()
#   Returns the configuration-specific results directory

#>> Results to record

#   set_results_params!(;kwargs...)
#   Sets the result represented by keywords to a boolean value associated to it in <kwargs>, indicating whether that result will be recorded. Appropriate keywords are: pssinitarray, initarray, initialconditions, raypath, bounceindices, bouncepoints, cavityimage, rayimage, pathlengths, actions, modeproperties, farfield.

#   set_results_params!(args...)
#   unset_results_params!(args...)
#   For a convenient syntax, if the above results keywords are passed as regular symbol or string arguments to set_results!, then they will be recorded. If passed to unset_results!, they will not.

#   boolean = get_results_param(key::Union(AbstractString,Symbol))
#   Returns whether the result represented by the keyword represented by the string or symbol <key> will be recorded.

#Finally, the actual run is started by a call to the function run_cavity().

# #############################################################################
# #############################################################################
#Initiate

#Dependencies
#require("util.jl")
#require("boundary.jl")
#require("refractive_index.jl")
#require("solver.jl")
#require("io.jl")
#require("ray_plot.jl")
#require("ray_analysis.jl")
#require("ray_combined.jl")

# #############################################################################
# #############################################################################
#Settings as global state

#Cavity objects
cavity_bnd = FourierBnd()
cavity_idx = UniformIdx()

#Solver parameters
solver = Dict{Symbol,Number}()
solver[:maxpathlength] = 200.0
solver[:maxbounces] = 500
solver[:relativetolerance] = 1.0e-12
solver[:absolutetolerance] = 1.0e-12

#Initial conditions array
init = Dict{Symbol,Number}()
init[:symmetry] = 1
init[:thetares] = 25
init[:chires]= 25
pssinitarray = Array(Float64,0,2)
initarray = Array(Float64,0,3)

#Root directory to store results
resultsroot = "."

#Results to record
results = Dict{Symbol,Bool}()
results[:pssinitarray] = false
results[:initarray] = true
results[:initialconditions] = false
results[:raypath] = false
results[:bounceindices] = false
results[:bouncepoints] = true
results[:cavityimage] = true
results[:rayimage] = true
results[:pathlengths] = true
results[:actions] = true
results[:modeproperties] = false
results[:farfield] = false

# #############################################################################
# #############################################################################
#Setters and unsetters and getters

#Cavity parameters
function set_cavity_bnd!(bnd::Boundary)
    global cavity_bnd = bnd
    return nothing
end

function get_cavity_bnd()
    return cavity_bnd::Boundary
end

function set_cavity_idx!(idx::RefractiveIndex)
    global cavity_idx = idx
    return nothing
end

function get_cavity_idx()
    return cavity_idx::RefractiveIndex
end

#Solver parameters
function set_solver_params!(;kwargs...)
    for i=1:length(kwargs)
        if haskey(solver,kwargs[i][1])
            solver[kwargs[i][1]] = kwargs[i][2]
        else
            error("No solver parameter $(kwargs[i][1]).")
        end
    end
    return nothing
end

get_solver_param(key::Symbol) = solver[key]::Number
get_solver_param(key::AbstractString) = solver[symbol(key)]::Number

#Initial conditions parameters
function set_init_params!(;kwargs...)
    for i=1:length(kwargs)
        if haskey(init,kwargs[i][1])
            init[kwargs[i][1]] = kwargs[i][2]
        else
            error("No initial conditions parameter $(kwargs[i][1]).")
        end
    end
    return nothing
end

get_init_param(key::Symbol) = init[key]::Number
get_init_param(key::AbstractString) = init[symbol(key)]::Number

function set_pssinitarray!(A::Array{Float64,2})
    if size(A,2) != 2
        error("PSS initial conditions array must have (theta,chi) pairs as rows.")
    end
    global pssinitarray = A
    return nothing
end

function get_pssinitarray()
    return pssinitarray::Array{Float64,2}
end

function set_initarray!(A::Array{Float64,2})
    if size(A,2) != 3
        error("Initial conditions array must have (r,theta,phi) values as rows.")
    end
    global initarray = A
    return nothing
end

function get_initarray()
    return initarray::Array{Float64,2}
end

#Root directory to store results
function set_resultsroot!(directory::AbstractString)
    global resultsroot = directory
    return nothing
end

function get_resultsroot()
    return resultsroot::AbstractString
end

#Results directory for the current configuration
function get_resultsdir()
    #Autogenerate initarray
    initarray = get_initarray()
    if isempty(initarray)
        pssinitarray = get_pssinitarray()
        if isempty(pssinitarray)
            pssinitarray = gen_pssinitarray(get_init_param(:thetares),
                                            get_init_param(:chires),
                                            get_init_param(:symmetry))
        end
        initarray = pss2initarray(get_cavity_bnd(),pssinitarray)
    end

    #Get run parameter hash
    run_params_hash::UInt64 =
        get_runparamshash(  initarray,
                            get_solver_param(:maxpathlength),
                            get_solver_param(:maxbounces),
                            get_solver_param(:relativetolerance),
                            get_solver_param(:absolutetolerance),
                            get_results_param(:pssinitarray),get_results_param(:initarray),
                            get_results_param(:initialconditions),get_results_param(:raypath),
                            get_results_param(:bounceindices),get_results_param(:bouncepoints),
                            get_results_param(:cavityimage),get_results_param(:rayimage),
                            get_results_param(:pathlengths),get_results_param(:actions),
                            get_results_param(:modeproperties),get_results_param(:farfield))

    return getresultsdir(   run_params_hash,get_cavity_bnd(),get_cavity_idx(),
                            get_resultsroot(),makedir=false)
end

#Results parameters
function set_results_params!(;kwargs...)
    for i=1:length(kwargs)
        if haskey(results,kwargs[i][1])
            results[kwargs[i][1]] = kwargs[i][2]
        else
            error("No result named $(kwargs[i][1]).")
        end
    end
    return nothing
end

function set_results_params!(args...)
    set_results!(;[(symbol(arg),true) for arg in args]...)
end

function unset_results_params!(args...)
    set_results!(;[(symbol(arg),false) for arg in args]...)
end

get_results_param(key::Symbol) = results[key]::Bool
get_results_param(key::AbstractString) = results[symbol(key)]::Bool

# #############################################################################
# #############################################################################
#Main driver

#Profile a single cavity by running multiple rays in it
function run_cavity()

    run_rays(   #Cavity parameters
                get_cavity_bnd(),get_cavity_idx();

                #Initial conditions
                pssinitarray = get_pssinitarray(),
                initarray = get_initarray(),

                #Initial conditions generator parameters
                symmetry = get_init_param(:symmetry),
                thetares = get_init_param(:thetares),
                chires= get_init_param(:chires),

                #Solver parameters
                maxpathlength = get_solver_param(:maxpathlength),
                maxbounces = get_solver_param(:maxbounces),
                relativetolerance = get_solver_param(:relativetolerance),
                absolutetolerance = get_solver_param(:absolutetolerance),

                #Results root directory
                resultsroot = get_resultsroot(),

                #Results to record
                record_pssinitarray = get_results_param(:pssinitarray),
                record_initarray = get_results_param(:initarray),
                record_initialconditions = get_results_param(:initialconditions),
                record_raypath = get_results_param(:raypath),
                record_bounceindices = get_results_param(:bounceindices),
                record_bouncepoints = get_results_param(:bouncepoints),
                record_cavityimage = get_results_param(:cavityimage),
                record_rayimage = get_results_param(:rayimage),
                record_pathlengths = get_results_param(:pathlengths),
                record_actions = get_results_param(:actions),
                record_modeproperties = get_results_param(:modeproperties),
                record_farfield = get_results_param(:farfield),
            )

end


make_globals(s) = s
lift_global(s::Symbol) = GlobalRef(@__MODULE__, s)
function lift_global(ex::Expr)
    if ex.head == :.
        return GlobalRef(
                         CodeInfoTools.resolve(lift_global(ex.args[1])),
                         ex.args[2].value)
    end
    return ex
end
function make_globals(ex::Expr)
    if ex.head == :call
        return Expr(ex.head,
                    lift_global(ex.args[1]),
                    map(make_globals, ex.args[2 : end])...)
    elseif ex.head == :(::)
        return Expr(ex.head, ex.args[1],
                    lift_global(ex.args[2]))
    elseif ex.head == :.
        return lift_global(ex)
    else
        return Expr(ex.head, map(make_globals, ex.args)...)
    end
end

make_patterm(s::Number) = PatLiteral(s)
make_patterm(s::Symbol) = PatVar(s)
function make_patterm(ex::Expr)
    if ex.head == :call
        return PatTerm(:call, [PatLiteral(ex.args[1]), map(make_patterm, ex.args[2 : end])...])
    elseif ex.head == :(::)
        @assert(ex.args[1] isa Symbol)
        return PatTypeAssertion(PatVar(ex.args[1]), ex.args[2])
    end
end

create_patterm(e) = e
create_patterm(e::Int) = PatLiteral(e)
function create_patterm(expr::Expr)
    resolved = make_globals(expr)
    resolved = walk(CodeInfoTools.resolve, resolved)
    return make_patterm(resolved)
end

function _methodtheory(ex::Expr)
    @assert(@capture(ex, begin body__ end))
    new = map(body) do sub
        if @capture(sub, lhs_ => rhs_)
            return Expr(:call, :push!, :arr,
                        RewriteRule(create_patterm(lhs), create_patterm(rhs)))
        elseif @capture(sub, lhs_ |> rhs_)
            return Expr(:call, :push!, :arr,
                        DynamicRule(create_patterm(lhs), rhs))
        end
    end
    return quote
        arr = Rule[]
        $(new...)
        arr
    end
end

macro methodtheory(ex)
    new = _methodtheory(ex)
    new = postwalk(rmlines ∘ unblock, new)
    esc(new)
end


function init_pseudo_observable_mappings!(m::Model1002)

    pseudo_names = [:y_t, :y_f_t, :NaturalRate, :π_t, :OutputGap, :ExAnteRealRate, :LongRunInflation,
                    :MarginalCost, :Wages, :FlexibleWages, :Hours, :FlexibleHours, :z_t,
                    :Expected10YearRateGap, :NominalFFR, :Expected10YearRate, :Expected10YearNaturalRate,
                    :ExpectedNominalNaturalRate, :NominalRateGap, :LaborProductivityGrowth]

    # Create PseudoObservable objects
    pseudo = OrderedDict{Symbol,PseudoObservable}()
    for k in pseudo_names
        pseudo[k] = PseudoObservable(k)
    end

    # Fill in names and reverse transforms
    pseudo[:y_t].name = "Output Growth"
    pseudo[:y_t].longname = "Output Growth Per Capita"

    pseudo[:y_f_t].name = "Flexible Output Growth"
    pseudo[:y_f_t].longname = "Output that would prevail in a flexible-price economy."

    pseudo[:NaturalRate].name = "Real Natural Rate"
    pseudo[:NaturalRate].longname = "The real interest rate that would prevail in a flexible-price economy."
    pseudo[:NaturalRate].rev_transform = quartertoannual

    pseudo[:π_t].name = "Inflation"
    pseudo[:π_t].longname = "Inflation"
    pseudo[:π_t].rev_transform = quartertoannual

    pseudo[:OutputGap].name = "Output Gap"
    pseudo[:OutputGap].longname = "Output Gap"

    pseudo[:ExAnteRealRate].name = "Ex Ante Real Rate"
    pseudo[:ExAnteRealRate].longname = "Ex Ante Real Rate"
    pseudo[:ExAnteRealRate].rev_transform = quartertoannual

    pseudo[:LongRunInflation].name = "Long Run Inflation"
    pseudo[:LongRunInflation].longname = "Long Run Inflation"
    pseudo[:LongRunInflation].rev_transform = quartertoannual

    pseudo[:MarginalCost].name = "Marginal Cost"
    pseudo[:MarginalCost].longname = "Marginal Cost"

    pseudo[:Wages].name = "Wages"
    pseudo[:Wages].longname = "Wages"

    pseudo[:FlexibleWages].name = "Flexible Wages"
    pseudo[:FlexibleWages].longname = "Wages that would prevail in a flexible-wage economy"

    pseudo[:Hours].name = "Hours"
    pseudo[:Hours].longname = "Hours"

    pseudo[:FlexibleHours].name     = "Flexible Hours"
    pseudo[:FlexibleHours].longname = "Flexible Hours"

    pseudo[:z_t].name     = "z_t"
    pseudo[:z_t].longname = "z_t"

    pseudo[:Expected10YearRateGap].name     = "Expected 10-Year Rate Gap"
    pseudo[:Expected10YearRateGap].longname = "Expected 10-Year Rate Gap"
    pseudo[:Expected10YearRateGap].rev_transform = quartertoannual

    pseudo[:NominalFFR].name     = "Nominal FFR"
    pseudo[:NominalFFR].longname = "Nominal FFR at an annual rate"
    pseudo[:NominalFFR].rev_transform = quartertoannual

    pseudo[:Expected10YearRate].name     = "Expected 10-Year Rate"
    pseudo[:Expected10YearRate].longname = "Expected 10-Year Interest Rate"
    pseudo[:Expected10YearRate].rev_transform = quartertoannual

    pseudo[:Expected10YearNaturalRate].name     = "Expected 10-Year Natural Rate"
    pseudo[:Expected10YearNaturalRate].longname = "Expected 10-Year Natural Rate of Interest"
    pseudo[:Expected10YearNaturalRate].rev_transform = quartertoannual

    pseudo[:ExpectedNominalNaturalRate].name     = "Expected Nominal Natural Rate"
    pseudo[:ExpectedNominalNaturalRate].longname = "Natural Rate + Expected Inflation"
    pseudo[:ExpectedNominalNaturalRate].rev_transform = quartertoannual

    pseudo[:NominalRateGap].name     = "Nominal Rate Gap"
    pseudo[:NominalRateGap].longname = "Nominal FFR - Nominal Natural Rate"
    pseudo[:NominalRateGap].rev_transform = quartertoannual

    pseudo[:LaborProductivityGrowth].name     = "Labor Productivity Growth"
    pseudo[:LaborProductivityGrowth].longname = "Labor Productivity Growth Rate"
    pseudo[:LaborProductivityGrowth].rev_transform = quartertoannual

    # Add to model object
    m.pseudo_observable_mappings = pseudo
end


# Physical constants
const BohrInA=1/0.52918
# 0.5291772083 # GSL value