module Day17

using ..ReTest, ..DocStringExtensions

import ..solve, ..PuzzleInfo, ..TEST_DATA_DIRECTORY

abstract type Day17PuzzleInfo <: PuzzleInfo end

"""
Day 17 part 1 puzzle information.

$(FIELDS)
"""
struct Part1PuzzleInfo <: Day17PuzzleInfo
    """Path to input data file."""
    datapath::String
end

"""
Day 17 part 2 puzzle information.

$(FIELDS)
"""
struct Part2PuzzleInfo <: Day17PuzzleInfo
    """Path to input data file."""
    datapath::String
end

"""
    $(FUNCTIONNAME)(input)

Read target area limits from input stream `input`.
"""
function readtargetarealimits(input::IO)
    targetareastring = readline(input)
    @assert targetareastring[1:13] == "target area: "
    xlimitstring, ylimitstring = split(targetareastring[14:end], ", ")
    @assert xlimitstring[1:2] == "x="
    @assert ylimitstring[1:2] == "y="
    xlimits = [parse(Int, lim) for lim in split(xlimitstring[3:end], "..")]
    ylimits = [parse(Int, lim) for lim in split(ylimitstring[3:end], "..")]
    xlimits, ylimits
end

"""
    $(FUNCTIONNAME)(xlimits, ylimits, initialvelocity)

Whether probe launched with `initialvelocity` will enter target area with horizontal
limits `xlimits` and vertical limits `ylimits` in any step.
"""
function hitstargetarea(xlimits, ylimits, initialvelocity)
    pos = [0, 0]
    velocity = copy(initialvelocity)
    while pos[1] <= xlimits[2] && pos[2] >= ylimits[1]
        if xlimits[1] <= pos[1] <= xlimits[2] && ylimits[1] <= pos[2] <= ylimits[2]
            return true
        end
        pos += velocity
        velocity[1] = max(0, velocity[1] - 1)
        velocity[2] -= 1
    end
    false
end

function solve(info::Day17PuzzleInfo)
    xlimits, ylimits = open(readtargetarealimits, info.datapath)
    ypos(velocity, nstep) = nstep * velocity - (nstep * (nstep - 1) ÷ 2)
    if isa(info, Part1PuzzleInfo)
        if ylimits[1] > 0
            ypos(ylimits[1], ylimit[1])d
        elseif ylimits[1] < 0
            ypos(-ylimits[1] - 1, -ylimits[1] - 1)
        else
            error("Target area lower boundary allows infinite velocity")
        end
    else
        numvel = 0
        # Lower bound on x-velocity corresponds to just hitting left limit of target
        # area as x-velocity reaches 0: vx * (vx + 1) ÷ 2 >= xlimits[1] for integer vx
        # Upper bound on x-velocity correspondings to hitting right limit of target area
        # in one step
        for vx in ceil(Int, (sqrt(1 + 8 * xlimits[1]) - 1) / 2):xlimits[2]
            # Lower bound on y-velocity corresponds to hitting lower limit in one step
            # Upper bound on y-velocity corresponds to hitting lower limit in
            # (2 * vy + 1) steps
            for vy in ylimits[1]:(-ylimits[1] - 1)
                hitstargetarea(xlimits, ylimits, [vx, vy]) && (numvel += 1)
            end
        end
        numvel
    end
end

@testset begin
    @test solve(Part1PuzzleInfo(joinpath(TEST_DATA_DIRECTORY, "day17.txt"))) == 45
    @test solve(Part2PuzzleInfo(joinpath(TEST_DATA_DIRECTORY, "day17.txt"))) == 112
end

end # module


function rhosp_mixed(probs::Array{Float64,1}, states::Union{Array{State,1},Array{State_fixed,1}})
    if round(sum(probs), digits=15) != 1
        throw(ArgumentError("Your probabilities must add up to 1!"))
    end

    ls = length(states)
    if length(probs) != ls
        throw(ArgumentError("There must be the same ammount of probabilities and states!"))
    end

    #for i in 1:length(states)
    #    stat = st(states[i])
    #    if round(stat'*stat, digits=15) != 1
    #        throw(ArgumentError("All states must be normalized to 1!"))
    #    end
    #end

    rhos = [probs[i]*rhosp(states[i]) for i in 1:ls]
    rho = sum(rhos)
end

function eigensp_mixed(probs::Array{Float64,1}, states::Union{Array{State,1},Array{State_fixed,1}})
    if round(sum(probs), digits=15) != 1
        throw(ArgumentError("Your probabilities must add up to 1!"))
    end

    ls = length(states)
    if length(probs) != ls
        throw(ArgumentError("There must be the same ammount of probabilities and states!"))
    end

    #for i in 1:length(states)
    #    stat = st(states[i])
    #    if round(stat'*stat, digits=15) != 1
    #        throw(ArgumentError("All states must be normalized to 1!"))
    #    end
    #end

    eig = sort(eigvals(Matrix(rhosp_mixed(probs, states))), rev=true)
    return eig
end


module Fu_Price_plate_examples
using FinEtools
using FinEtoolsDeforLinear
using FinEtoolsDeforLinear.AlgoDeforLinearModule
using FinEtoolsVibInFluids.LaplBEM
using FinEtoolsVibInFluids.AlgoVibInFluidsModule
using LinearAlgebra
using Arpack

# Free-vibration solution for a clamped plate partially or fully submerged in inviscid fluid.
#
# Reference: Fu, Y., and Price, W. G., 1987, “Interactions Between a Partially
# or Totally Immersed Vibrating Cantilever Plate and the Surrounding Fluid,”
# J. Sound Vib., 118(3), pp. 495–513.

E = 206000*phun("MPa");# Young's modulus
nu = 0.3;# Poisson ratio
rho = 7850*phun("KG*M^-3");# mass density
Length= 10e3*phun("MM"); Width= 10e3*phun("MM"); Height= 0.238e3*phun("MM");
rhow = 1000*phun("KG*M^-3");
# E = 206000.0;# Young's modulus
# nu = 0.3;# Poisson ratio
# rho = 7.850e-9;# mass density
# Length= 10.0e3; Width= 10.0e3; Height= 0.238e3;
# rhow = 1.000e-9;
nHeight = 4; nLength  = 4*10; nWidth = nLength;
tolerance = Height/nHeight/100;
OmegaShift = (2*pi*0.0) ^ 2; # to resolve rigid body modes
neigvs = 5;

function plate_free_vibration_solver()
	# Free-vibration solution for a clamped plate
	# Reference: Fu, Y., and Price, W. G., 1987, “Interactions Between a Partially
	# or Totally Immersed Vibrating Cantilever Plate and the Surrounding Fluid,”
	# J. Sound Vib., 118(3), pp. 495–513.
	# Dry Natural frequencies [radians per second] (nLength=10,nWidth=10)
	# 12.45153      29.44122      75.04792      94.26498      106.7881

	Mshift =0;

    MR = DeforModelRed3D
    fens,fes  = T4block(Length, Width, Height, nLength, nWidth, nHeight)

    geom = NodalField(fens.xyz)
    u = NodalField(zeros(size(fens.xyz,1),3)) # displacement field
    nl = selectnode(fens, box=[0.0 0.0 -Inf Inf -Inf Inf], inflate=tolerance)
    setebc!(u, nl, true, collect(1:3))
    applyebc!(u)
    numberdofs!(u)

    material = MatDeforElastIso(MR, rho, E, nu, 0.0)

    femm = FEMMDeforLinearESNICET4(MR, IntegDomain(fes, NodalSimplexRule(3)), material)
    associategeometry!(femm,  geom)
    K = stiffness(femm, geom, u)
    M = mass(femm, geom, u)

    d,v,nev,nconv = eigs(K+OmegaShift*M, M; nev=neigvs, which=:SM,  explicittransform=:none)
    d = d .- OmegaShift;

    return Dict("fens"=>fens, "femm"=>femm, "geom"=>geom, "u"=>u, "eigenvectors"=>v, "eigenvalues"=>d)
end # plate_free_vibration_solver

function plate_dry()
	model = plate_free_vibration_solver()

	fens = model["fens"]
	femm = model["femm"]
	d = model["eigenvalues"]
	v = model["eigenvectors"]

	fs = real(sqrt.(complex(d)))/(2*pi)
	println("Eigenvalues: $(2*pi*fs) [radians per second]")

	vectors = []
	for mode in 1:length(fs)
		scattersysvec!(u, v[:,mode])
		push!(vectors, ("mode$mode", deepcopy(u.values)))
	end
	File =  "plate_dry.vtk"
	vtkexportmesh(File, fens, femm.integdomain.fes; vectors=vectors)
	@async run(`"paraview.exe" $File`)

    true
end # plate_dry

function plate_one_quarter_wet()
	model = plate_free_vibration_solver()

	fens = model["fens"]
	femm = model["femm"]
	d = model["eigenvalues"]
	v = model["eigenvectors"]
	u = model["u"]

	neigvs = size(v, 2)

	fes = femm.integdomain.fes

	bfes = meshboundary(fes)
	tolerance = Height/1000;
	rl = vcat(selectelem(fens, bfes, box = [3*Length/4 Length -Inf Inf Height Height], inflate = tolerance),
		selectelem(fens, bfes, box = [3*Length/4 Length -Inf Inf 0.0 0.0], inflate = tolerance));
	# These are the wet surface panels
	wbfes = subset(bfes, rl);
	nnperel = nodesperelem(wbfes)

	pnormals = LaplBEM.allnormals(LaplBEM.allelxs(fens.xyz, connasarray(wbfes)))
	vn = fill(0.0, count(wbfes), neigvs)
	uk = fill(0.0, 3)
	for mode in 1:neigvs
		for i in 1:count(wbfes)
			vn[i, mode] = 0.0
			for k in wbfes.conn[i]
				for m in 1:3
					dof = u.dofnums[k, m]
					uk[m] = (dof != 0 ? v[dof, mode] : 0.0)
				end
				vn[i, mode] += dot(pnormals[i], uk)
			end
			vn[i, mode] /= nnperel
		end
	end

	scalars = []
	for mode in 1:neigvs
		push!(scalars, ("vn$mode", deepcopy(vn[:, mode])))
	end
	File =  "plate_wet-vn.vtk"
	vtkexportmesh(File, fens, wbfes; scalars=scalars)
	@async run(`"paraview.exe" $File`)

	A = fill(0.0, count(wbfes), count(wbfes))
	A = LaplBEM.doublelayer!(A, fens.xyz, connasarray(wbfes), TriRule(3))
	B = fill(0.0, count(wbfes), count(wbfes))
	B = LaplBEM.singlelayer!(B, fens.xyz, connasarray(wbfes), TriRule(3), TriRule(3))
	pp = A \ (B * vn)

	areas = fill(0.0, count(wbfes))
	n = fill(0.0, 3)
	for i in 1:count(wbfes)
		ix = collect(wbfes.conn[i])
		LaplBEM.trinml!(n, @view fens.xyz[ix, :])
		areas[i] = norm(n) / 2
	end

	rK = diagm(vec(d))
	rM = I
	raM = fill(0.0, neigvs, neigvs);
	for i in 1:neigvs
	    for j in 1:neigvs
	        raM[i,j] = rhow*dot(vn[:,i].*areas, pp[:,j]);
	    end
	end
	raM = (raM +raM')/2;# it should be symmetric, but make sure

	decomp = eigen(rK, rM+raM)
	wv = v*decomp.vectors;
	@show sqrt.(decomp.values)

    true
end # plate_wet

function plate_completely_wet()
	# Reference 7.35, 20.2, 50.45, 70.41, 78.85 [angular frequency, radians per second]

	model = plate_free_vibration_solver()

	fens = model["fens"]
	femm = model["femm"]
	d = model["eigenvalues"]  # squares of angular frequencies
	v = model["eigenvectors"]
	u = model["u"]

	neigvs = size(v, 2)

	fes = femm.integdomain.fes

	bfes = meshboundary(fes)
	tolerance = Height/1000;
	rl = vcat(selectelem(fens, bfes, box = [-Inf Inf -Inf Inf Height Height], inflate = tolerance),
		selectelem(fens, bfes, box = [-Inf Inf -Inf Inf 0.0 0.0], inflate = tolerance));
	# These are the wet surface panels
	wbfes = subset(bfes, rl);
	nnperel = nodesperelem(wbfes)

	pnormals = LaplBEM.allnormals(LaplBEM.allelxs(fens.xyz, connasarray(wbfes)))
	vn = fill(0.0, count(wbfes), neigvs)
	uk = fill(0.0, 3)
	for mode in 1:neigvs
		for i in 1:count(wbfes)
			vn[i, mode] = 0.0
			for k in wbfes.conn[i]
				for m in 1:3
					dof = u.dofnums[k, m]
					uk[m] = (dof != 0 ? v[dof, mode] : 0.0)
				end
				vn[i, mode] += dot(pnormals[i], uk)
			end
			vn[i, mode] /= nnperel
		end
	end

	scalars = []
	for mode in 1:neigvs
		push!(scalars, ("vn$mode", deepcopy(vn[:, mode])))
	end
	File =  "plate_wet-vn.vtk"
	vtkexportmesh(File, fens, wbfes; scalars=scalars)
	@async run(`"paraview.exe" $File`)

	A = fill(0.0, count(wbfes), count(wbfes))
	A = LaplBEM.doublelayer!(A, fens.xyz, connasarray(wbfes), TriRule(3))
	B = fill(0.0, count(wbfes), count(wbfes))
	B = LaplBEM.singlelayer!(B, fens.xyz, connasarray(wbfes), TriRule(3), TriRule(3))
	pp = A \ (B * vn)

	areas = fill(0.0, count(wbfes))
	n = fill(0.0, 3)
	for i in 1:count(wbfes)
		ix = collect(wbfes.conn[i])
		LaplBEM.trinml!(n, @view fens.xyz[ix, :])
		areas[i] = norm(n) / 2
	end

	rK = diagm(vec(d))
	rM = I
	raM = fill(0.0, neigvs, neigvs);
	for i in 1:neigvs
	    for j in 1:neigvs
	        raM[i,j] = rhow*dot(vn[:,i].*areas, pp[:,j]);
	    end
	end
	raM = (raM +raM')/2;# it should be symmetric, but make sure

	decomp = eigen(rK, rM+raM)
	wv = v*decomp.vectors;
	@show sqrt.(decomp.values)

    true
end # plate_completely_wet

function plate_completely_wet_algo()
	# Reference 7.35, 20.2, 50.45, 70.41, 78.85 [angular frequency, radians per second]
	tolerance = Height/nHeight/1000;

	fens,fes  = T4block(Length, Width, Height, nLength, nWidth, nHeight)

	MR = DeforModelRed3D
	material = MatDeforElastIso(MR, rho, E, nu, 0.0)

	femm = FEMMDeforLinearESNICET4(MR, IntegDomain(fes, NodalSimplexRule(3)), material)

	nl1 = selectnode(fens, box=[0.0 0.0 -Inf Inf -Inf Inf], inflate=tolerance)
	ebc1 = FDataDict("node_list"=>nl1, "component"=>1, "displacement"=>0.0)
	ebc2 = FDataDict("node_list"=>nl1, "component"=>2, "displacement"=>0.0)
	ebc3 = FDataDict("node_list"=>nl1, "component"=>3, "displacement"=>0.0)

	# Make the region
	region1 = FDataDict("femm"=>femm, "femm_mass"=>femm)

	# Make model data
	modeldata =  FDataDict("fens"=> fens, "regions"=>  [region1],
		"essential_bcs"=>[ebc1 ebc2 ebc3], "omega_shift"=>0.0, "neigvs"=>neigvs)

	# Solve for the in-vacuo free vibration modes
	modeldata = AlgoDeforLinearModule.modal(modeldata)

	println("Dry angular frequencies: $(modeldata["omega"]) [rad/s]")

	# Determine the wet boundary faces
	bfes = meshboundary(fes)
	rl = vcat(selectelem(fens, bfes, box = [-Inf Inf -Inf Inf Height Height], inflate = tolerance),
		selectelem(fens, bfes, box = [-Inf Inf -Inf Inf 0.0 0.0], inflate = tolerance));
	# These are the wet surface panels
	modeldata["wet_boundary_fes"] = subset(bfes, rl);
	modeldata["rhow"] = rhow

	# Solve for the wet-vibration modes
	modeldata = AlgoVibInFluidsModule.modal(modeldata)

	println("Wet angular frequencies: $(modeldata["wet_omega"]) [rad/s]")

	modeldata = AlgoVibInFluidsModule.exportfluidpressuremode(modeldata)
	modeldata["postprocessing"]["mode"] = 1:neigvs
	modeldata = AlgoVibInFluidsModule.exportmode(modeldata)
    true
end # plate_completely_wet_algo

function allrun()
    println("#####################################################")
    println("# plate_dry ")
    plate_dry()
    println("#####################################################")
    println("# plate_completely_wet ")
    plate_completely_wet()
    println("#####################################################")
    println("# plate_completely_wet_algo ")
    plate_completely_wet_algo()
    return true
end # function allrun

end # module Fu_Price_plate_examples


using Infinities, Base64, Base.Checked, Test

@testset "InfiniteCardinal" begin
    @testset "basics" begin
        @test !isone(ℵ₀)
        @test !iszero(ℵ₀)
        @test sign(ℵ₀) ≡ 1 && !signbit(ℵ₀)
        @test angle(ℵ₀) ≡ 0
        @test Integer(∞) ≡ convert(Integer,∞) ≡ Integer(ℵ₀) ≡ convert(Integer, ℵ₀) ≡ ℵ₀
        @test abs(ℵ₀) ≡ ℵ₀
        @test zero(ℵ₀) ≡ 0
        @test one(ℵ₀) ≡ one(InfiniteCardinal{0}) ≡ oneunit(ℵ₀) ≡ oneunit(InfiniteCardinal{0}) ≡ 1
        @test isinf(ℵ₀) && !isfinite(ℵ₀)
        @test Integer(RealInfinity()) ≡ Integer(ComplexInfinity()) ≡ ℵ₀
        @test_throws InexactError Integer(-∞)
        @test_throws InexactError Integer(exp(0.1im)*∞)
    end

    @testset "equality" begin
        @test ℵ₀ == ℵ₀
        @test ℵ₀ ≠ ℵ₁
        @test ℵ₀ == ∞ && ∞ == ℵ₀
        @test ℵ₁ ≠ ∞ && ∞ ≠ ℵ₁
        @test ℵ₀ ≠ -∞ && -∞ ≠ ℵ₀
        @test ℵ₀ == Inf && Inf == ℵ₀
        @test ℵ₀ ≠ 5 && 5 ≠ ℵ₀
    end

    @testset "inequalities" begin
        @test !isless(ℵ₀, ℵ₀)
        @test isless(ℵ₀, ℵ₁)
        @test isless(Inf, ℵ₁) && !isless(ℵ₁, Inf)
        @test !isless(Inf, ℵ₀) && !isless(ℵ₀, Inf)
        @test isless(5, ℵ₀) && !isless(ℵ₀, 5)
        @test isless(5, ℵ₁) && !isless(ℵ₁, 5)

        @test !(ℵ₀ < ℵ₀) && !(ℵ₀ > ℵ₀)
        @test ℵ₀ ≤ ℵ₀ && ℵ₀ ≥ ℵ₀
        @test ℵ₀ < ℵ₁ && ℵ₀ ≤ ℵ₁
        @test !(ℵ₀ > ℵ₁) && !(ℵ₀ ≥ ℵ₁)
        @test ℵ₁ ≤ ℵ₁ && ℵ₁ ≥ ℵ₁ && !(ℵ₁ > ℵ₁) && !(ℵ₁ < ℵ₁)

        @test !(∞ < ℵ₀) && ∞ ≤ ℵ₀
        @test !(∞ > ℵ₀) && ∞ ≥ ℵ₀
        @test ∞ < ℵ₁ && ∞ ≤ ℵ₁
        @test !(∞ > ℵ₁) && !(∞ ≥ ℵ₁)
        @test ℵ₀ ≤ ∞
        @test !(ℵ₁ < ∞) && !(ℵ₁ ≤ ∞)
        @test !(ℵ₀ > ∞)
        @test ℵ₁ > ∞ && ℵ₁ ≥ ∞

        @test -∞ < ℵ₀ && -∞ ≤ ℵ₀
        @test -∞ < ℵ₁ && -∞ ≤ ℵ₁
        @test !(-∞ > ℵ₀) && RealInfinity() ≥ ℵ₀
        @test !(-∞ > ℵ₁) && !(RealInfinity() ≥ ℵ₁)
        @test !(-∞ > ℵ₀) && !(-∞ ≥ ℵ₀)
        @test ℵ₀ > -∞
        @test ℵ₁ > -∞ && ℵ₁ ≥ -∞
        @test ℵ₀ ≤ RealInfinity()
        @test !(ℵ₀ ≤ -∞)
        @test !(ℵ₁ ≤ RealInfinity())

        @test Inf < ℵ₁ && !(ℵ₁ < Inf)
        @test !(Inf < ℵ₀) && !(ℵ₀ < Inf)
        @test (5 < ℵ₀) && !(ℵ₀ < 5)
        @test 5 ≤ ℵ₁ && !(5 ≥ ℵ₁) && !(ℵ₁ ≤ 5) && ℵ₁ > 5
        @test 5 < ℵ₀ && 5 ≤ ℵ₀
        @test !(ℵ₀ < 5) && !(ℵ₀ ≤ 5)
        @test ℵ₀ > 5 && ℵ₀ ≥ 5
        @test !(5 > ℵ₀) && !(5 ≥ ℵ₀)
    end

    @testset "min/max" begin
        @test @inferred(min(ℵ₀,ℵ₁)) ≡ ℵ₀
        @test @inferred(min(ℵ₀,ℵ₁)) ≡ min(ℵ₁,ℵ₀) ≡ ℵ₀
        @test @inferred(max(ℵ₀,ℵ₁)) ≡ max(ℵ₁,ℵ₀) ≡ ℵ₁
        @test min(∞,ℵ₀) ≡ min(ℵ₀,∞) ≡ ∞
        @test max(∞,ℵ₀) ≡ max(ℵ₀,∞) ≡ ℵ₀
        @test min(-∞,ℵ₀) ≡ min(ℵ₀,-∞) ≡ -∞
        @test max(-∞,ℵ₀) ≡ max(ℵ₀,-∞) ≡ ℵ₀
        @test min(5,ℵ₀) ≡ min(ℵ₀,5) ≡ 5
        @test max(5,ℵ₀) ≡ max(ℵ₀,5) ≡ ℵ₀
    end

    @testset "algebra" begin
        @test +(ℵ₀) ≡ ℵ₀
        @test ℵ₀ + ℵ₁ ≡ ℵ₁ + ℵ₀ ≡ ℵ₁
        @test 5 + ℵ₀ ≡ ℵ₀ + 5 ≡ ℵ₀
        @test ℵ₀ - 5 ≡ ℵ₀
        @test 5 - ℵ₀ ≡ -∞
        @test ℵ₀ - ℵ₀ ≡ NotANumber()
        @test -ℵ₀ ≡ -∞

        @test *(ℵ₀) ≡ ℵ₀
        @test ℵ₀ * ℵ₀ ≡ ℵ₀
        @test ℵ₀ * ∞ ≡ ∞ * ℵ₀ ≡ ℵ₀
        @test 5 * ℵ₀ ≡ ℵ₀ * 5 ≡ ℵ₀
        @test 5.0ℵ₀ ≡ ℵ₀*5.0 ≡ RealInfinity()
        @test_throws ArgumentError (-5) * ℵ₀

        @test ℵ₀ - 5.1 ≡ ∞
        @test ℵ₀ + 5.1 ≡ ∞
        @test 5.1 + ℵ₀ ≡ ∞
        @test 5.1 - ℵ₀ ≡ -∞

        @test ℵ₀ + ∞ == ∞
        @test ∞ + ℵ₀ == ∞
        @test ℵ₀ + RealInfinity() == ∞
        @test ℵ₀ - (-∞) == ∞
        @test  RealInfinity() + ℵ₀ == ∞
    end

    @testset "fld/cld/div/mod" begin
        @test ℵ₀ ÷ 5 ≡ ℵ₀
        @test ℵ₀ ÷ ℵ₀ ≡ NotANumber()
        @test 5 ÷ ℵ₀ ≡ 0
        @test fld(-5, ℵ₀) ≡ -1
        @test cld(5, ℵ₀) ≡ 1
        @test mod(ℵ₀,ℵ₀) ≡ NotANumber()
        @test mod(ℵ₀,6) ≡ NotANumber()
        @test mod(5,ℵ₀) ≡ 5
        @test_throws ArgumentError mod(-1,ℵ₀)
    end

    @test string(ℵ₀) == stringmime("text/plain", ℵ₀) == "ℵ₀"
    @test string(ℵ₁) == stringmime("text/plain", ℵ₁) == "ℵ₁"
    @test Base.to_index(ℵ₀) ≡ Base.to_shape(ℵ₀) ≡ ℵ₀
    @test Base.to_shape((∞,)) ≡ (ℵ₀,)

    @testset "Set" begin
        s = Set([ℵ₀,ℵ₁,∞,1])
        @test 1 ∈ s
        @test ∞ ∈ s
        @test ℵ₀ ∈ s
        @test ℵ₁ ∈ s
        @test 2 ∉ s
    end

    @testset "checked" begin
        @test checked_add(5, ℵ₀) ≡ checked_add(ℵ₀, 5) ≡ ℵ₀
        @test checked_sub(ℵ₀, 5) ≡ ℵ₀
        @test checked_sub(5, ℵ₀) ≡ -∞
        @test checked_mul(-5, ℵ₀) ≡ checked_mul(ℵ₀, -5) ≡ -∞
    end

    @testset "indexing" begin
        @test_throws BoundsError randn(3)[ℵ₀]
        @test_throws ErrorException Base._unsafe_getindex(IndexCartesian(),permutedims(1:3)',ℵ₀)
        @test_throws BoundsError view(randn(3),1:2)[ℵ₀]
        # check ambiguity is not introduced
        @test view(collect(1:5),2)[] == 2
    end
end


function compute_tss(y::Vector{Float64}, hasintercept::Bool, sqrtw::AbstractVector)
    if hasintercept
        tss = zero(Float64)
        m = (mean(y) / sum(sqrtw) * length(y))::Float64
        @inbounds @simd for i in eachindex(y)
            tss += abs2(y[i] - sqrtw[i] * m)
        end
    else
        tss = sum(abs2, y)
    end
    return tss
end


# SPDX-License-Identifier: X11
# 2020-10-21
# Cream puff (300pt)

N = parse(Int, readline())
A = []

push!(A, 1)
for i ∈ 2:floor(Int, sqrt(N))
  if N % i == 0
    push!(A, i)
    i != N ÷ i && push!(A, N ÷ i)
  end
end
N != 1 && push!(A, N)
sort!(A)

for i ∈ A
  println(i)
end


# License for this file: MIT (expat)
# Copyright 2017-2018, DLR Institute of System Dynamics and Control

"""
    module Modia3D.Frames

This module contains functions for **frames** that is coordinate systems in 3D.
The orientation of a frame is described either with a 3x3 **rotation matrix**
or with a **quaternion vector** and its origin is described with a **Vector3D**:

- `const `[`Modia3D.RotationMatrix`](@ref) = SMatrix{3,3,Float64,9}`:
  Type of a Rotation matrix to rotate from a frame 1 into a frame 2.

- `const `[`Modia3D.Quaternion`](@ref) = SVector{4,Float64}`:
  Type of a Quaternion vector to rotate from a frame 1 into a frame 2.

- `const `[`Modia3D.Vector3D`](@ref) = SVector{3,Float64}`:
  Type of a vector in 3D space (e.g. position vector of the origin of a frame).

The following constants are defined

- `const `[`Modia3D.NullRotation`](@ref):
  RotationMatrix with no rotation from a frame 1 into a frame 2.

- `const `[`Modia3D.NullQuaternion`](@ref):
  Quaternion vector with no rotation from a frame 1 into a frame 2.

- `const `[`Modia3D.ZeroVector3D`](@ref):
  Vector3D with only zero elements.

If an angle is given as an argument to one of the functions below, it might be a
number (interpreted as having unit `rad`) or a number with a unit
(for example: `using Unitful; angle = 90u"°"`).

# Constructors for a RotationMatrix R

The following functions return a [`Modia3D.RotationMatrix`](@ref)` R`
to rotate a frame 1 into a frame 2.

| Function                                         | Description                                        |
|:-------------------------------------------------|:---------------------------------------------------|
| [`Modia3D.rot1`](@ref)(angle)                    | Rotate around `angle` along x-axis                 |
| [`Modia3D.rot2`](@ref)(angle)                    | Rotate around `angle` along y-axis                 |
| [`Modia3D.rot3`](@ref)(angle)                    | Rotate around `angle` along z-axis                 |
| [`Modia3D.rot123`](@ref)(angle1, angle2, angle3) | Rotate around angles along x,y,z-axes              |
| [`Modia3D.rotAxis`](@ref)(axis,angle)            | Rotate around `angle` along `axis` (= 1,2,3)       |
| [`Modia3D.rotAxis`](@ref)(axis,positive,angle)   | Rotate around `angle` if `positive`, else `-angle` |
| [`Modia3D.rot_e`](@ref)(e, angle)                | Rotate around `angle` along unit vector `e`        |
| [`Modia3D.rot_nxy`](@ref)(nx, ny)                | `nx`/`ny` are in x/y-direction of frame 2          |
| [`Modia3D.from_q`](@ref)(q)                      | Return `R` from Quaternion `q`                     |

# Constructors for a Quaternion q

The following functions return a [`Modia3D.Quaternion`](@ref)` q`
to rotate a frame 1 into a frame 2.
Since `q` and `-q` define the same rotation the constructor functions have
a keyword argument `q_guess::Quaternion = NullQuaternion`.
From the two possible solutions `q` the one is returned that is closer to `q_guess`.

| Function                                          | Description                                |
|:--------------------------------------------------|:-------------------------------------------|
| [`Modia3D.qrot1`](@ref)(angle)                    | Rotate around `angle` along x-axis         |
| [`Modia3D.qrot2`](@ref)(angle)                    | Rotate around `angle` along y-axis         |
| [`Modia3D.qrot3`](@ref)(angle)                    | Rotate around `angle` along z-axis         |
| [`Modia3D.qrot123`](@ref)(angle1, angle2, angle3) | Rotate around angles along x,y,z-axes      |
| [`Modia3D.qrot_e`](@ref)(e, angle)                | Rotate around `angle` along unit vector `e`|
| [`Modia3D.qrot_nxy`](@ref)(nx, ny)                | `nx`/`ny` are in x/y-direction of frame 2  |
| [`Modia3D.from_R`](@ref)(R)                       | Return `q` from RotationMatrix `R`         |

# Operations on Frames

The following functions provide operations on frames. The orientation of a frame is
defined with argument `Rq` meaning it can be either a
[`Modia3D.RotationMatrix`](@ref) ` R` or a
[`Modia3D.Quaternion`](@ref) ` q` (to rotate a frame 1 into a frame 2).

| Function                                       | Description                                   |
|:-----------------------------------------------|:----------------------------------------------|
| [`Modia3D.resolve1`](@ref)(Rq, v2)             | Transform vector `v` from frame 2 to frame 1  |
| [`Modia3D.resolve2`](@ref)(Rq, v1)             | Transform vector `v` from frame 1 to frame 2  |
| [`Modia3D.absoluteRotation`](@ref)(Rq01, Rq12) | Return rotation 0->2 from rot. 0->1 and 1->2  |
| [`Modia3D.relativeRotation`](@ref)(Rq01, Rq02) | Return rotation 1->2 from rot. 0->1 and 0->2  |
| [`Modia3D.inverseRotation`](@ref)(Rq01)        | Return rotation 1->0 from rot, 0->1           |
| [`Modia3D.planarRotationAngle`](@ref)(e,v1,v2) | Return angle of planar rotation along `e`     |
| [`Modia3D.eAxis`](@ref)(axis)                  | Return unit vector `e` in direction of `axis` |
| [`Modia3D.skew`](@ref)(v)                      | Return skew-symmetric matrix of vector v      |

# Interpolation of Frames

Given a set of frames by a vector `r` of position vectors to their origins and
and an optional vector `q` of Quaternions of their absolute orientations, then
the following functions interpolate linearly in these frames:

| Function                                        | Description                                    |
|:------------------------------------------------|:-----------------------------------------------|
| [`Modia3D.Path`](@ref)(r,q)                     | Return path defined by a vector of frames      |
| [`Modia3D.t_pathEnd`](@ref)(path)               | Return path parameter `t_end` of last frame    |
| [`Modia3D.interpolate`](@ref)(path,t)           | Return `(rt,qt)` of Path at path parameter `t` |
| [`Modia3D.interpolate_r`](@ref)(path,t)         | Return `rt` of Path at path parameter `t`      |

# Examples

```julia
using Modia3D
using Unitful

# R1,R2,R3 are the same RotationMatrices
R1 = Modia3D.rot1(pi/2)
R2 = Modia3D.rot1(90u"°")
R3 = Modia3D.rot_e([1,0,0], pi/2)
```

# Main developer

[Martin Otter](https://rmc.dlr.de/sr/de/staff/martin.otter/),
[DLR - Institute of System Dynamics and Control](https://www.dlr.de/sr/en)

*The functions of this module are mostly a mapping of some of the functions
of the Modelica Standard Library from Modelica
([Modelica.Mechanics.MultiBody.Frames](https://doc.modelica.org/help/Modelica_Mechanics_MultiBody_Frames.html#Modelica.Mechanics.MultiBody.Frames))
to Julia (taking advantage of Julia features
such as multiple dispatch and unit package Unitful)*.
"""
module Frames

export RotationMatrix, NullRotation  , assertRotationMatrix
export Quaternion    , NullQuaternion, assertQuaternion
export Vector3D      , ZeroVector3D  , axisValue

export  rot1,  rot2,  rot3,  rot123, rotAxis,  rot_e,  rot_nxy, from_q
export qrot1, qrot2, qrot3, qrot123,          qrot_e, qrot_nxy, from_R

export resolve1, resolve2, absoluteRotation, relativeRotation, inverseRotation
export planarRotationAngle, eAxis

export Path, t_pathEnd, interpolate, interpolate_r
export skew

using StaticArrays
using LinearAlgebra

include("vector3D.jl")
include("rotationMatrix.jl")
include("quaternion.jl")
include("interpolation.jl")

end


using LinearAlgebra

@testset "CUBLAS" begin

using CuArrays.CUBLAS
using CuArrays.CUBLAS: band, bandex

m = 20
n = 35
k = 13

@test_throws ArgumentError CUBLAS.cublasop('V')
@test_throws ArgumentError CUBLAS.cublasfill('V')
@test_throws ArgumentError CUBLAS.cublasdiag('V')
@test_throws ArgumentError CUBLAS.cublasside('V')

#################
# level 1 tests #
#################

@testset "Level 1 with element type $T" for T in [Float32, Float64, ComplexF32, ComplexF64]
    A = CuArrays.rand(T, m)
    B = CuArray{T}(undef, m)
    CuArrays.CUBLAS.blascopy!(m,A,1,B,1)
    @test Array(A) == Array(B)

    @test testf(rmul!, rand(T, 6, 9, 3), Ref(rand()))
    @test testf(dot, rand(T, m), rand(T, m))
    @test testf(*, transpose(rand(T, m)), rand(T, m))
    @test testf(*, rand(T, m)', rand(T, m))
    @test testf(norm, rand(T, m))
    @test testf(BLAS.asum, rand(T, m))
    @test testf(BLAS.axpy!, Ref(rand()), rand(T, m), rand(T, m))

    if T <: Complex
        @test testf(BLAS.dotu, rand(T, m), rand(T, m))
        x = rand(T, m)
        y = rand(T, m)
        dx = CuArray(x)
        dy = CuArray(y)
        dz = BLAS.dot(dx, dy)
        z = BLAS.dotc(x, y)
        @test dz ≈ z
    end
end # level 1 testset

@testset "element type $elty" for elty in [Float32, Float64, ComplexF32, ComplexF64]
    alpha = convert(elty,2)
    beta = convert(elty,3)

    @testset "Level 2" begin
        @testset "gemv" begin
            @test testf(*, rand(elty, m, n), rand(elty, n))
            @test testf(*, transpose(rand(elty, m, n)), rand(elty, m))
            @test testf(*, rand(elty, m, n)', rand(elty, m))
            x = rand(elty, m)
            A = rand(elty, m, m + 1 )
            y = rand(elty, m)
            dx = CuArray(x)
            dA = CuArray(A)
            dy = CuArray(y)
            @test_throws DimensionMismatch mul!(dy, dA, dx)
            A = rand(elty, m + 1, m )
            dA = CuArray(A)
            @test_throws DimensionMismatch mul!(dy, dA, dx)
        end
        @testset "banded methods" begin
            # bands
            ku = 2
            kl = 3
            # generate banded matrix
            A = rand(elty,m,n)
            A = bandex(A,kl,ku)
            # get packed format
            Ab = band(A,kl,ku)
            d_Ab = CuArray(Ab)
            x = rand(elty,n)
            d_x = CuArray(x)
            @testset "gbmv!" begin
                # test y = alpha*A*x + beta*y
                y = rand(elty,m)
                d_y = CuArray(y)
                CuArrays.CUBLAS.gbmv!('N',m,kl,ku,alpha,d_Ab,d_x,beta,d_y)
                BLAS.gbmv!('N',m,kl,ku,alpha,Ab,x,beta,y)
                h_y = Array(d_y)
                @test y ≈ h_y
                # test y = alpha*transpose(A)*x + beta*y
                x = rand(elty,n)
                d_x = CuArray(x)
                y = rand(elty,m)
                d_y = CuArray(y)
                CuArrays.CUBLAS.gbmv!('T',m,kl,ku,alpha,d_Ab,d_y,beta,d_x)
                BLAS.gbmv!('T',m,kl,ku,alpha,Ab,y,beta,x)
                h_x = Array(d_x)
                @test x ≈ h_x
                # test y = alpha*A'*x + beta*y
                x = rand(elty,n)
                d_x = CuArray(x)
                y = rand(elty,m)
                d_y = CuArray(y)
                CuArrays.CUBLAS.gbmv!('C',m,kl,ku,alpha,d_Ab,d_y,beta,d_x)
                BLAS.gbmv!('C',m,kl,ku,alpha,Ab,y,beta,x)
                h_x = Array(d_x)
                @test x ≈ h_x
            end
            x = rand(elty,n)
            d_x = CuArray(x)
            @testset "gbmv" begin
                # test y = alpha*A*x
                d_y = CuArrays.CUBLAS.gbmv('N',m,kl,ku,alpha,d_Ab,d_x)
                y = zeros(elty,m)
                y = BLAS.gbmv('N',m,kl,ku,alpha,Ab,x)
                h_y = Array(d_y)
                @test y ≈ h_y
            end
            A = rand(elty,m,m)
            A = A + A'
            nbands = 3
            @test m >= 1+nbands
            A = bandex(A,nbands,nbands)
            # convert to 'upper' banded storage format
            AB = band(A,0,nbands)
            # construct x
            x = rand(elty,m)
            d_AB = CuArray(AB)
            d_x = CuArray(x)
            if elty <: Real
                @testset "sbmv!" begin
                    y = rand(elty,m)
                    d_y = CuArray(y)
                    # sbmv!
                    CuArrays.CUBLAS.sbmv!('U',nbands,alpha,d_AB,d_x,beta,d_y)
                    y = alpha*(A*x) + beta*y
                    # compare
                    h_y = Array(d_y)
                    @test y ≈ h_y
                end
                @testset "sbmv" begin
                    d_y = CuArrays.CUBLAS.sbmv('U',nbands,d_AB,d_x)
                    y = A*x
                    # compare
                    h_y = Array(d_y)
                    @test y ≈ h_y
                end
            else
                @testset "hbmv!" begin
                    y = rand(elty,m)
                    d_y = CuArray(y)
                    # hbmv!
                    CuArrays.CUBLAS.hbmv!('U',nbands,alpha,d_AB,d_x,beta,d_y)
                    y = alpha*(A*x) + beta*y
                    # compare
                    h_y = Array(d_y)
                    @test y ≈ h_y
                end
                @testset "hbmv" begin
                    d_y = CuArrays.CUBLAS.hbmv('U',nbands,d_AB,d_x)
                    y = A*x
                    # compare
                    h_y = Array(d_y)
                    @test y ≈ h_y
                end
            end
            # generate triangular matrix
            A = rand(elty,m,m)
            # restrict to 3 bands
            nbands = 3
            @test m >= 1+nbands
            A = bandex(A,0,nbands)
            # convert to 'upper' banded storage format
            AB = band(A,0,nbands)
            d_AB = CuArray(AB)
            @testset "tbmv!" begin
                y = rand(elty, m)
                # move to host
                d_y = CuArray(y)
                # tbmv!
                CuArrays.CUBLAS.tbmv!('U','N','N',nbands,d_AB,d_y)
                y = A*y
                # compare
                h_y = Array(d_y)
                @test y ≈ h_y
            end
            @testset "tbmv" begin
                # tbmv
                d_y = CuArrays.CUBLAS.tbmv('U','N','N',nbands,d_AB,d_x)
                y = A*x
                # compare
                h_y = Array(d_y)
                @test y ≈ h_y
            end
            @testset "tbsv!" begin
                d_y = copy(d_x)
                #tbsv!
                CuArrays.CUBLAS.tbsv!('U','N','N',nbands,d_AB,d_y)
                y = A\x
                # compare
                h_y = Array(d_y)
                @test y ≈ h_y
            end
            @testset "tbsv" begin
                d_y = CuArrays.CUBLAS.tbsv('U','N','N',nbands,d_AB,d_x)
                y = A\x
                # compare
                h_y = Array(d_y)
                @test y ≈ h_y
            end
        end
        A = rand(elty,m,n)
        dA = CuArray(A)
        sA = rand(elty,m,m)
        sA = sA + transpose(sA)
        dsA = CuArray(sA)
        hA = rand(elty,m,m)
        hA = hA + hA'
        dhA = CuArray(hA)
        x = rand(elty,m)
        dx = CuArray(x)
        @testset "symv!" begin
            # generate vectors
            y = rand(elty,m)
            # copy to device
            dy = CuArray(y)
            # execute on host
            BLAS.symv!('U',alpha,sA,x,beta,y)
            # execute on device
            CuArrays.CUBLAS.symv!('U',alpha,dsA,dx,beta,dy)
            # compare results
            hy = Array(dy)
            @test y ≈ hy
        end

        @testset "symv" begin
            y = BLAS.symv('U',sA,x)
            # execute on device
            dy = CuArrays.CUBLAS.symv('U',dsA,dx)
            # compare results
            hy = Array(dy)
            @test y ≈ hy
        end
        if elty <: Complex
            @testset "hemv!" begin
                y = rand(elty,m)
                dy = CuArray(y)
                # execute on host
                BLAS.hemv!('U',alpha,hA,x,beta,y)
                # execute on device
                CuArrays.CUBLAS.hemv!('U',alpha,dhA,dx,beta,dy)
                # compare results
                hy = Array(dy)
                @test y ≈ hy
            end
            @testset "hemv" begin
                y = BLAS.hemv('U',hA,x)
                # execute on device
                dy = CuArrays.CUBLAS.hemv('U',dhA,dx)
                # compare results
                hy = Array(dy)
                @test y ≈ hy
            end
        end
        A = triu(sA)
        dA = CuArray(A)
        @testset "trmv!" begin
            d_y = copy(dx)
            # execute trmv!
            CuArrays.CUBLAS.trmv!('U','N','N',dA,d_y)
            y = A*x
            # compare
            h_y = Array(d_y)
            @test y ≈ h_y
        end

        @testset "trmv" begin
            d_y = CuArrays.CUBLAS.trmv('U','N','N',dA,dx)
            y = A*x
            # compare
            h_y = Array(d_y)
            @test y ≈ h_y
        end

        @testset "trsv!" begin
            d_y = copy(dx)
            # execute trsv!
            CuArrays.CUBLAS.trsv!('U','N','N',dA,d_y)
            y = A\x
            # compare
            h_y = Array(d_y)
            @test y ≈ h_y
        end

        @testset "trsv" begin
            d_y = CuArrays.CUBLAS.trsv('U','N','N',dA,dx)
            y = A\x
            # compare
            h_y = Array(d_y)
            @test y ≈ h_y
        end

        @testset "ldiv!(::UpperTriangular, ::CuVector)" begin
            A = copy(sA)
            dA = CuArray(A)
            dy = copy(dx)
            ldiv!(UpperTriangular(dA), dy)
            y = UpperTriangular(A) \ x
            @test y ≈ Array(dy)
        end
        @testset "ldiv!(::AdjointUpperTriangular, ::CuVector)" begin
            A = copy(sA)
            dA = CuArray(A)
            dy = copy(dx)
            ldiv!(adjoint(UpperTriangular(dA)), dy)
            y = adjoint(UpperTriangular(A)) \ x
            @test y ≈ Array(dy)
        end
        @testset "ldiv!(::TransposeUpperTriangular, ::CuVector)" begin
            A = copy(sA)
            dA = CuArray(A)
            dy = copy(dx)
            ldiv!(transpose(UpperTriangular(dA)), dy)
            y = transpose(UpperTriangular(A)) \ x
            @test y ≈ Array(dy)
        end
        @testset "ldiv!(::UpperTriangular, ::CuVector)" begin
            A = copy(sA)
            dA = CuArray(A)
            dy = copy(dx)
            ldiv!(LowerTriangular(dA), dy)
            y = LowerTriangular(A) \ x
            @test y ≈ Array(dy)
        end
        @testset "ldiv!(::AdjointUpperTriangular, ::CuVector)" begin
            A = copy(sA)
            dA = CuArray(A)
            dy = copy(dx)
            ldiv!(adjoint(LowerTriangular(dA)), dy)
            y = adjoint(LowerTriangular(A)) \ x
            @test y ≈ Array(dy)
        end
        @testset "ldiv!(::TransposeUpperTriangular, ::CuVector)" begin
            A = copy(sA)
            dA = CuArray(A)
            dy = copy(dx)
            ldiv!(transpose(LowerTriangular(dA)), dy)
            y = transpose(LowerTriangular(A)) \ x
            @test y ≈ Array(dy)
        end

        A = rand(elty,m,m)
        x = rand(elty,m)
        y = rand(elty,m)
        dA = CuArray(A)
        dx = CuArray(x)
        dy = CuArray(y)
        @testset "ger!" begin
            # perform rank one update
            dB = copy(dA)
            CuArrays.CUBLAS.ger!(alpha,dx,dy,dB)
            B = (alpha*x)*y' + A
            # move to host and compare
            hB = Array(dB)
            @test B ≈ hB
        end

        @testset "syr!" begin
            dB = copy(dsA)
            CuArrays.CUBLAS.syr!('U',alpha,dx,dB)
            B = (alpha*x)*transpose(x) + sA
            # move to host and compare upper triangles
            hB = Array(dB)
            B = triu(B)
            hB = triu(hB)
            @test B ≈ hB
        end
        if elty <: Complex
            @testset "her!" begin
                dB = copy(dhA)
                # perform rank one update
                CuArrays.CUBLAS.her!('U',alpha,dx,dB)
                B = (alpha*x)*x' + hA
                # move to host and compare upper triangles
                hB = Array(dB)
                B = triu(B)
                hB = triu(hB)
                @test B ≈ hB
            end

            @testset "her2!" begin
                dB = copy(dhA)
                CuArrays.CUBLAS.her2!('U',alpha,dx,dy,dB)
                B = (alpha*x)*y' + y*(alpha*x)' + hA
                # move to host and compare upper triangles
                hB = Array(dB)
                B = triu(B)
                hB = triu(hB)
                @test B ≈ hB
            end
        end
    end
    @testset "Level 3" begin
        A = rand(elty,m,k)
        B = rand(elty,k,n)
        Bbad = rand(elty,k+1,n+1)
        C1 = rand(elty,m,n)
        C2 = copy(C1)
        d_A = CuArray(A)
        d_B = CuArray(B)
        d_Bbad = CuArray(Bbad)
        d_C1 = CuArray(C1)
        d_C2 = CuArray(C2)
        hA = rand(elty,m,m)
        hA = hA + hA'
        dhA = CuArray(hA)
        sA = rand(elty,m,m)
        sA = sA + transpose(sA)
        dsA = CuArray(sA)
        @testset "gemm!" begin
            CuArrays.CUBLAS.gemm!('N','N',alpha,d_A,d_B,beta,d_C1)
            mul!(d_C2, d_A, d_B)
            h_C1 = Array(d_C1)
            h_C2 = Array(d_C2)
            C1 = (alpha*A)*B + beta*C1
            C2 = A*B
            # compare
            @test C1 ≈ h_C1
            @test C2 ≈ h_C2
            @test_throws ArgumentError mul!(dhA, dhA, dsA)
            @test_throws DimensionMismatch mul!(d_C1, d_A, dsA)
        end

        @testset "gemm" begin
            d_C = CuArrays.CUBLAS.gemm('N','N',d_A,d_B)
            C = A*B
            C2 = d_A * d_B
            # compare
            h_C = Array(d_C)
            h_C2 = Array(C2)
            @test C ≈ h_C
            @test C ≈ h_C2
        end
        @testset "xt_gemm!" begin
            CuArrays.CUBLAS.xt_gemm!('N','N',alpha,d_A,d_B,beta,d_C1)
            mul!(d_C2, d_A, d_B)
            h_C1 = Array(d_C1)
            h_C2 = Array(d_C2)
            C1 = (alpha*A)*B + beta*C1
            C2 = A*B
            # compare
            @test C1 ≈ h_C1
            @test C2 ≈ h_C2
            @test_throws DimensionMismatch CuArrays.CUBLAS.xt_gemm!('N','N',alpha,d_A,d_Bbad,beta,d_C1)
        end

        @testset "xt_gemm" begin
            d_C = CuArrays.CUBLAS.xt_gemm('N','N',d_A,d_B)
            C = A*B
            C2 = d_A * d_B
            # compare
            h_C = Array(d_C)
            h_C2 = Array(C2)
            @test C ≈ h_C
            @test C ≈ h_C2
        end
        # generate matrices
        bA = [rand(elty,m,k) for i in 1:10]
        bB = [rand(elty,k,n) for i in 1:10]
        bC = [rand(elty,m,n) for i in 1:10]
        # move to device
        bd_A = CuArray{elty, 2}[]
        bd_B = CuArray{elty, 2}[]
        bd_C = CuArray{elty, 2}[]
        bd_bad = CuArray{elty, 2}[]
        for i in 1:length(bA)
            push!(bd_A,CuArray(bA[i]))
            push!(bd_B,CuArray(bB[i]))
            push!(bd_C,CuArray(bC[i]))
            if i < length(bA) - 2
                push!(bd_bad,CuArray(bC[i]))
            end
        end
        @testset "gemm_batched!" begin
            # C = (alpha*A)*B + beta*C
            CuArrays.CUBLAS.gemm_batched!('N','N',alpha,bd_A,bd_B,beta,bd_C)
            for i in 1:length(bd_C)
                bC[i] = (alpha*bA[i])*bB[i] + beta*bC[i]
                h_C = Array(bd_C[i])
                #compare
                @test bC[i] ≈ h_C
            end
            @test_throws DimensionMismatch CuArrays.CUBLAS.gemm_batched!('N','N',alpha,bd_A,bd_bad,beta,bd_C)
        end

        @testset "gemm_batched" begin
            bd_C = CuArrays.CUBLAS.gemm_batched('N','N',bd_A,bd_B)
            for i in 1:length(bA)
                bC = bA[i]*bB[i]
                h_C = Array(bd_C[i])
                @test bC ≈ h_C
            end
            @test_throws DimensionMismatch CuArrays.CUBLAS.gemm_batched('N','N',alpha,bd_A,bd_bad)
        end

        nbatch = 10
        bA = rand(elty, m, k, nbatch)
        bB = rand(elty, k, n, nbatch)
        bC = rand(elty, m, n, nbatch)
        bbad = rand(elty, m+1, n+1, nbatch)
        # move to device
        bd_A = CuArray{elty, 3}(bA)
        bd_B = CuArray{elty, 3}(bB)
        bd_C = CuArray{elty, 3}(bC)
        bd_bad = CuArray{elty, 3}(bbad)
        @testset "gemm_strided_batched!" begin
            CuArrays.CUBLAS.gemm_strided_batched!('N', 'N', alpha, bd_A, bd_B, beta, bd_C)
            for i in 1:nbatch
                bC[:, :, i] = (alpha * bA[:, :, i]) * bB[:, :, i] + beta * bC[:, :, i]
            end
            h_C = Array(bd_C)
            @test bC ≈ h_C
            @test_throws DimensionMismatch CuArrays.CUBLAS.gemm_strided_batched!('N', 'N', alpha, bd_A, bd_B, beta, bd_bad)
        end

        @testset "gemm_strided_batched" begin
            bd_C = CuArrays.CUBLAS.gemm_strided_batched('N', 'N', bd_A, bd_B)

            for i in 1:nbatch
                bC[:, :, i] = bA[:, :, i] * bB[:, :, i]
            end
            h_C = Array(bd_C)
            @test bC ≈ h_C
            # generate matrices
            bA = rand(elty, k, m, nbatch)
            bB = rand(elty, k, n, nbatch)
            bC = zeros(elty, m, n, nbatch)
            # move to device
            bd_A = CuArray{elty, 3}(bA)
            bd_B = CuArray{elty, 3}(bB)

            bd_C = CuArrays.CUBLAS.gemm_strided_batched('T', 'N', bd_A, bd_B)
            for i in 1:nbatch
                bC[:, :, i] = transpose(bA[:, :, i]) * bB[:, :, i]
            end
            h_C = Array(bd_C)
            @test bC ≈ h_C
            @test_throws DimensionMismatch CuArrays.CUBLAS.gemm_strided_batched('N', 'N', alpha, bd_A, bd_bad)
        end

        B = rand(elty,m,n)
        C = rand(elty,m,n)
        Bbad = rand(elty,m+1,n+1)
        d_B = CuArray(B)
        d_C = CuArray(C)
        d_Bbad = CuArray(Bbad)
        @testset "symm!" begin
            CuArrays.CUBLAS.symm!('L','U',alpha,dsA,d_B,beta,d_C)
            C = (alpha*sA)*B + beta*C
            # compare
            h_C = Array(d_C)
            @test C ≈ h_C
            @test_throws DimensionMismatch CuArrays.CUBLAS.symm!('L','U',alpha,dsA,d_Bbad,beta,d_C)
        end

        @testset "symm" begin
            d_C = CuArrays.CUBLAS.symm('L','U',dsA,d_B)
            C = sA*B
            # compare
            h_C = Array(d_C)
            @test C ≈ h_C
            @test_throws DimensionMismatch CuArrays.CUBLAS.symm('L','U',dsA,d_Bbad)
        end
        @testset "xt_symm!" begin
            CuArrays.CUBLAS.xt_symm!('L','U',alpha,dsA,d_B,beta,d_C)
            C = (alpha*sA)*B + beta*C
            # compare
            h_C = Array(d_C)
            @test C ≈ h_C
        end

        @testset "xt_symm" begin
            d_C = CuArrays.CUBLAS.xt_symm('L','U',dsA,d_B)
            C = sA*B
            # compare
            h_C = Array(d_C)
            @test C ≈ h_C
        end
        A = triu(rand(elty, m, m))
        B = rand(elty,m,n)
        C = zeros(elty,m,n)
        dA = CuArray(A)
        dB = CuArray(B)
        dC = CuArray(C)
        @testset "trmm!" begin
            C = alpha*A*B
            CuArrays.CUBLAS.trmm!('L','U','N','N',alpha,dA,dB,dC)
            # move to host and compare
            h_C = Array(dC)
            @test C ≈ h_C
        end
        @testset "trmm" begin
            C = alpha*A*B
            d_C = CuArrays.CUBLAS.trmm('L','U','N','N',alpha,dA,dB)
            # move to host and compare
            h_C = Array(d_C)
            @test C ≈ h_C
        end
        @testset "xt_trmm!" begin
            C = alpha*A*B
            CuArrays.CUBLAS.xt_trmm!('L','U','N','N',alpha,dA,dB,dC)
            # move to host and compare
            h_C = Array(dC)
            @test C ≈ h_C
        end
        @testset "xt_trmm" begin
            C = alpha*A*B
            d_C = CuArrays.CUBLAS.xt_trmm('L','U','N','N',alpha,dA,dB)
            # move to host and compare
            h_C = Array(d_C)
            @test C ≈ h_C
        end
        @testset "trsm!" begin
            C = alpha*(A\B)
            dC = copy(dB)
            CuArrays.CUBLAS.trsm!('L','U','N','N',alpha,dA,dC)
            # move to host and compare
            h_C = Array(dC)
            @test C ≈ h_C
        end
        @testset "xt_trsm!" begin
            C = alpha*(A\B)
            dC = copy(dB)
            CuArrays.CUBLAS.xt_trsm!('L','U','N','N',alpha,dA,dC)
            # move to host and compare
            h_C = Array(dC)
            @test C ≈ h_C
        end
        @testset "trsm" begin
            Br = rand(elty,m,n)
            Bl = rand(elty,n,m)
            d_Br = CuArray(Br)
            d_Bl = CuArray(Bl)
            # compute
            @testset "adjtype=$adjtype, uplotype=$uplotype" for
                adjtype in (identity, adjoint, transpose),
                    uplotype in (UpperTriangular, UnitUpperTriangular, LowerTriangular, UnitLowerTriangular)

                @test adjtype(uplotype(A))\Br ≈ Array(adjtype(uplotype(dA))\d_Br)
                @test Bl/adjtype(uplotype(A)) ≈ Array(d_Bl/adjtype(uplotype(dA)))
            end
            # Check also that scaling parameter works
            @test BLAS.trsm('L','U','N','N',alpha,A,Br) ≈ Array(CuArrays.CUBLAS.trsm('L','U','N','N',alpha,dA,d_Br))
        end

        @testset "trsm_batched!" begin
            bA = [rand(elty,m,m) for i in 1:10]
            map!((x) -> triu(x), bA, bA)
            bB = [rand(elty,m,n) for i in 1:10]
            bBbad = [rand(elty,m,n) for i in 1:9]
            # move to device
            bd_A = CuArray{elty, 2}[]
            bd_B = CuArray{elty, 2}[]
            bd_Bbad = CuArray{elty, 2}[]
            for i in 1:length(bA)
                push!(bd_A,CuArray(bA[i]))
                push!(bd_B,CuArray(bB[i]))
            end
            for i in 1:length(bBbad)
                push!(bd_Bbad,CuArray(bBbad[i]))
            end
            # compute
            CuArrays.CUBLAS.trsm_batched!('L','U','N','N',alpha,bd_A,bd_B)
            @test_throws DimensionMismatch CuArrays.CUBLAS.trsm_batched!('L','U','N','N',alpha,bd_A,bd_Bbad)
            # move to host and compare
            for i in 1:length(bd_B)
                bC = alpha*(bA[i]\bB[i])
                h_C = Array(bd_B[i])
                #compare
                @test bC ≈ h_C
            end
        end

        @testset "trsm_batched" begin
            # generate parameter alpha = rand(elty)
            # generate matrices
            bA = [rand(elty,m,m) for i in 1:10]
            map!((x) -> triu(x), bA, bA)
            bB = [rand(elty,m,n) for i in 1:10]
            # move to device
            bd_A = CuArray{elty, 2}[]
            bd_B = CuArray{elty, 2}[]
            for i in 1:length(bA)
                push!(bd_A,CuArray(bA[i]))
                push!(bd_B,CuArray(bB[i]))
            end
            # compute
            bd_C = CuArrays.CUBLAS.trsm_batched('L','U','N','N',alpha,bd_A,bd_B)
            # move to host and compare
            for i in 1:length(bd_C)
                bC = alpha*(bA[i]\bB[i])
                h_C = Array(bd_C[i])
                @test bC ≈ h_C
            end
        end
        B = rand(elty,m,n)
        C = rand(elty,m,n)
        d_B = CuArray(B)
        d_C = CuArray(C)
        if elty <: Complex
            @testset "hemm!" begin
                # compute
                C = alpha*(hA*B) + beta*C
                CuArrays.CUBLAS.hemm!('L','L',alpha,dhA,d_B,beta,d_C)
                # move to host and compare
                h_C = Array(d_C)
                @test C ≈ h_C
            end
            @testset "hemm" begin
                C = hA*B
                d_C = CuArrays.CUBLAS.hemm('L','U',dhA,d_B)
                # move to host and compare
                h_C = Array(d_C)
                @test C ≈ h_C
            end
            @testset "xt_hemm!" begin
                # compute
                C = alpha*(hA*B) + beta*C
                CuArrays.CUBLAS.xt_hemm!('L','L',alpha,dhA,d_B,beta,d_C)
                # move to host and compare
                h_C = Array(d_C)
                @test C ≈ h_C
            end
            @testset "xt_hemm" begin
                C   = hA*B
                d_C = CuArrays.CUBLAS.xt_hemm('L','U',dhA, d_B)
                # move to host and compare
                h_C = Array(d_C)
                @test C ≈ h_C
            end
        end
        A = rand(elty,m,n)
        d_A = CuArray(A)
        @testset "geam!" begin
            # compute
            D = alpha*A + beta*B
            CuArrays.CUBLAS.geam!('N','N',alpha,d_A,beta,d_B,d_C)
            # move to host and compare
            h_C = Array(d_C)
            @test D ≈ h_C

            #test in place versions too
            d_C = CuArray(C)
            D = alpha*C + beta*B
            CuArrays.CUBLAS.geam!('N','N',alpha,d_C,beta,d_B,d_C)
            # move to host and compare
            h_C = Array(d_C)
            @test D ≈ h_C

            d_C = CuArray(C)
            D = alpha*A + beta*C
            CuArrays.CUBLAS.geam!('N','N',alpha,d_A,beta,d_C,d_C)
            # move to host and compare
            h_C = Array(d_C)
            @test D ≈ h_C

            #test setting C to zero
            CuArrays.CUBLAS.geam!('N','N',zero(elty),d_A,zero(elty),d_B,d_C)
            h_C = Array(d_C)
            @test h_C ≈ zeros(elty,m,n)

            # bounds checking
            @test_throws DimensionMismatch CuArrays.CUBLAS.geam!('N','T',alpha,d_A,beta,d_B,d_C)
            @test_throws DimensionMismatch CuArrays.CUBLAS.geam!('T','T',alpha,d_A,beta,d_B,d_C)
            @test_throws DimensionMismatch CuArrays.CUBLAS.geam!('T','N',alpha,d_A,beta,d_B,d_C)
        end

        @testset "geam" begin
            D = alpha*A + beta*B
            d_C = CuArrays.CUBLAS.geam('N','N',alpha,d_A,beta,d_B)
            # move to host and compare
            h_C = Array(d_C)
            @test D ≈ h_C
        end
        A = rand(elty,m,k)
        d_A = CuArray(A)
        @testset "syrk!" begin
            # generate matrices
            d_C = CuArray(sA)
            # C = (alpha*A)*transpose(A) + beta*C
            CuArrays.CUBLAS.syrk!('U','N',alpha,d_A,beta,d_C)
            C = (alpha*A)*transpose(A) + beta*sA
            C = triu(C)
            # move to host and compare
            h_C = Array(d_C)
            h_C = triu(C)
            @test C ≈ h_C
        end
        @testset "xt_syrk!" begin
            # generate matrices
            d_C = CuArray(sA)
            # C = (alpha*A)*transpose(A) + beta*C
            CuArrays.CUBLAS.xt_syrk!('U','N',alpha,d_A,beta,d_C)
            C = (alpha*A)*transpose(A) + beta*sA
            C = triu(C)
            # move to host and compare
            h_C = Array(d_C)
            h_C = triu(C)
            @test C ≈ h_C
        end
        @testset "syrk" begin
            # C = A*transpose(A)
            d_C = CuArrays.CUBLAS.syrk('U','N',d_A)
            C = A*transpose(A)
            C = triu(C)
            # move to host and compare
            h_C = Array(d_C)
            h_C = triu(C)
            @test C ≈ h_C
        end
        @testset "xt_syrk" begin
            # C = A*transpose(A)
            d_C = CuArrays.CUBLAS.xt_syrk('U','N',d_A)
            C = A*transpose(A)
            C = triu(C)
            # move to host and compare
            h_C = Array(d_C)
            h_C = triu(C)
            @test C ≈ h_C
        end
        @testset "syrkx!" begin
            # generate matrices
            syrkx_A = rand(elty, n, k)
            syrkx_B = rand(elty, n, k)
            syrkx_C = rand(elty, n, n)
            syrkx_C += syrkx_C'
            d_syrkx_A = CuArray(syrkx_A)
            d_syrkx_B = CuArray(syrkx_B)
            d_syrkx_C = CuArray(syrkx_C)
            # C = (alpha*A)*transpose(B) + beta*C
            d_syrkx_C = CuArrays.CUBLAS.syrkx!('U','N',alpha,d_syrkx_A,d_syrkx_B,beta,d_syrkx_C)
            final_C = (alpha*syrkx_A)*transpose(syrkx_B) + beta*syrkx_C
            # move to host and compare
            h_C = Array(d_syrkx_C)
            @test triu(final_C) ≈ triu(h_C)
            badC = rand(elty, m, n)
            d_badC = CuArray(badC)
            @test_throws DimensionMismatch CuArrays.CUBLAS.syrkx!('U','N',alpha,d_syrkx_A,d_syrkx_B,beta,d_badC)
            badC = rand(elty, n+1, n+1)
            d_badC = CuArray(badC)
            @test_throws DimensionMismatch CuArrays.CUBLAS.syrkx!('U','N',alpha,d_syrkx_A,d_syrkx_B,beta,d_badC)
        end
        @testset "xt_syrkx!" begin
            # generate matrices
            syrkx_A = rand(elty, n, k)
            syrkx_B = rand(elty, n, k)
            syrkx_C = rand(elty, n, n)
            syrkx_C += syrkx_C'
            d_syrkx_A = CuArray(syrkx_A)
            d_syrkx_B = CuArray(syrkx_B)
            d_syrkx_C = CuArray(syrkx_C)
            # C = (alpha*A)*transpose(B) + beta*C
            d_syrkx_C = CuArrays.CUBLAS.xt_syrkx!('U','N',alpha,d_syrkx_A,d_syrkx_B,beta,d_syrkx_C)
            final_C = (alpha*syrkx_A)*transpose(syrkx_B) + beta*syrkx_C
            # move to host and compare
            h_C = Array(d_syrkx_C)
            @test triu(final_C) ≈ triu(h_C)
            badC = rand(elty, m, n)
            d_badC = CuArray(badC)
            @test_throws DimensionMismatch CuArrays.CUBLAS.xt_syrkx!('U','N',alpha,d_syrkx_A,d_syrkx_B,beta,d_badC)
            badC = rand(elty, n+1, n+1)
            d_badC = CuArray(badC)
            @test_throws DimensionMismatch CuArrays.CUBLAS.xt_syrkx!('U','N',alpha,d_syrkx_A,d_syrkx_B,beta,d_badC)
        end
        @testset "syrk" begin
            # C = A*transpose(A)
            d_C = CuArrays.CUBLAS.syrk('U','N',d_A)
            C = A*transpose(A)
            C = triu(C)
            # move to host and compare
            h_C = Array(d_C)
            h_C = triu(C)
            @test C ≈ h_C
        end
        @testset "xt_syrk" begin
            # C = A*transpose(A)
            d_C = CuArrays.CUBLAS.xt_syrk('U','N',d_A)
            C = A*transpose(A)
            C = triu(C)
            # move to host and compare
            h_C = Array(d_C)
            h_C = triu(C)
            @test C ≈ h_C
        end
        if elty <: Complex
            @testset "herk!" begin
                d_C = CuArray(dhA)
                CuArrays.CUBLAS.herk!('U','N',alpha,d_A,beta,d_C)
                C = alpha*(A*A') + beta*hA
                C = triu(C)
                # move to host and compare
                h_C = Array(d_C)
                h_C = triu(C)
                @test C ≈ h_C
            end
            @testset "herk" begin
                d_C = CuArrays.CUBLAS.herk('U','N',d_A)
                C = A*A'
                C = triu(C)
                # move to host and compare
                h_C = Array(d_C)
                h_C = triu(C)
                @test C ≈ h_C
            end
            @testset "xt_herk!" begin
                d_C = CuArray(dhA)
                CuArrays.CUBLAS.xt_herk!('U','N',alpha,d_A,beta,d_C)
                C = alpha*(A*A') + beta*C
                C = triu(C)
                # move to host and compare
                h_C = Array(d_C)
                h_C = triu(C)
                @test C ≈ h_C
            end
            @testset "xt_herk" begin
                d_C = CuArrays.CUBLAS.xt_herk('U','N',d_A)
                C = A*A'
                C = triu(C)
                # move to host and compare
                h_C = Array(d_C)
                h_C = triu(C)
                @test C ≈ h_C
            end
        end
        A = rand(elty,m,k)
        B = rand(elty,m,k)
        Bbad = rand(elty,m+1,k+1)
        C = rand(elty,m,m)
        C = C + transpose(C)
        # move to device
        d_A = CuArray(A)
        d_B = CuArray(B)
        d_Bbad = CuArray(Bbad)
        d_C = CuArray(C)
        @testset "syr2k!" begin
            # compute
            C = alpha*(A*transpose(B) + B*transpose(A)) + beta*C
            CuArrays.CUBLAS.syr2k!('U','N',alpha,d_A,d_B,beta,d_C)
            # move back to host and compare
            C = triu(C)
            h_C = Array(d_C)
            h_C = triu(h_C)
            @test C ≈ h_C
            @test_throws DimensionMismatch CuArrays.CUBLAS.syr2k!('U','N',alpha,d_A,d_Bbad,beta,d_C)
        end

        @testset "syr2k" begin
            C = alpha*(A*transpose(B) + B*transpose(A))
            d_C = CuArrays.CUBLAS.syr2k('U','N',alpha,d_A,d_B)
            # move back to host and compare
            C = triu(C)
            h_C = Array(d_C)
            h_C = triu(h_C)
            @test C ≈ h_C
        end
        if elty <: Complex
            @testset "her2k!" begin
                elty1 = elty
                elty2 = real(elty)
                # generate parameters
                α = rand(elty1)
                β = rand(elty2)
                C = C + C'
                d_C = CuArray(C)
                C = α*(A*B') + conj(α)*(B*A') + β*C
                CuArrays.CUBLAS.her2k!('U','N',α,d_A,d_B,β,d_C)
                # move back to host and compare
                C = triu(C)
                h_C = Array(d_C)
                h_C = triu(h_C)
                @test C ≈ h_C
                @test_throws DimensionMismatch CuArrays.CUBLAS.her2k!('U','N',α,d_A,d_Bbad,β,d_C)
            end

            @testset "her2k" begin
                C = A*B' + B*A'
                d_C = CuArrays.CUBLAS.her2k('U','N',d_A,d_B)
                # move back to host and compare
                C = triu(C)
                h_C = Array(d_C)
                h_C = triu(h_C)
                @test C ≈ h_C
            end
            @testset "xt_her2k!" begin
                elty1 = elty
                elty2 = real(elty)
                # generate parameters
                α = rand(elty1)
                β = rand(elty2)
                C = C + C'
                d_C = CuArray(C)
                C = α*(A*B') + conj(α)*(B*A') + β*C
                CuArrays.CUBLAS.xt_her2k!('U','N',α,d_A,d_B,β,d_C)
                # move back to host and compare
                C = triu(C)
                h_C = Array(d_C)
                h_C = triu(h_C)
                @test C ≈ h_C
            end
            @testset "her2k" begin
                C = A*B' + B*A'
                d_C = CuArrays.CUBLAS.her2k('U','N',d_A,d_B)
                # move back to host and compare
                C = triu(C)
                h_C = Array(d_C)
                h_C = triu(h_C)
                @test C ≈ h_C
            end
        end
    end
    @testset "extensions" begin
        @testset "getrf_batched!" begin
            Random.seed!(1)
            local k
            # generate matrices
            A = [rand(elty,m,m) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
            end
            pivot, info = CuArrays.CUBLAS.getrf_batched!(d_A, false)
            h_info = Array(info)
            for As in 1:length(d_A)
                C   = lu!(copy(A[As]), Val(false)) # lu(A[As],pivot=false)
                h_A = Array(d_A[As])
                #reconstruct L,U
                dL = Matrix(one(elty)*I, m, m)
                dU = zeros(elty,(m,m))
                k = h_info[As]
                if( k >= 0 )
                    dL += tril(h_A,-k-1)
                    dU += triu(h_A,k)
                end
                #compare
                @test C.L ≈ dL rtol=1e-2
                @test C.U ≈ dU rtol=1e-2
            end
            for i in 1:length(A)
                d_A[ i ] = CuArray(A[i])
            end
            pivot, info = CuArrays.CUBLAS.getrf_batched!(d_A, true)
            h_info = Array(info)
            h_pivot = Array(pivot)
            for As in 1:length(d_A)
                C   = lu(A[As])
                h_A = Array(d_A[As])
                #reconstruct L,U
                dL = Matrix(one(elty)*I, m, m)
                dU = zeros(elty,(m,m))
                k = h_info[As]
                if( k >= 0 )
                    dL += tril(h_A,-k-1)
                    dU += triu(h_A,k)
                end
                #compare pivots
                @test length(setdiff(h_pivot[:,As],C.p)) == 0
                #make device pivot matrix
                P = Matrix(1.0*I, m, m)
                for row in 1:m
                    temp = copy(P[row,:])
                    P[row,:] = P[h_pivot[row,As],:]
                    P[h_pivot[row,As],:] = temp
                end
                @test inv(P)*dL*dU ≈ inv(C.P) * C.L * C.U
            end
        end

        @testset "getrf_batched" begin
            local k
            # generate matrices
            A = [rand(elty,m,m) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
            end
            pivot, info, d_B = CuArrays.CUBLAS.getrf_batched(d_A, false)
            h_info = Array(info)
            for Bs in 1:length(d_B)
                C   = lu!(copy(A[Bs]),Val(false)) # lu(A[Bs],pivot=false)
                h_B = Array(d_B[Bs])
                #reconstruct L,U
                dL = Matrix(one(elty)*I, m, m)
                dU = zeros(elty,(m,m))
                k = h_info[Bs]
                if( h_info[Bs] >= 0 )
                    dU += triu(h_B,k)
                    dL += tril(h_B,-k-1)
                end
                #compare
                @test C.L ≈ dL rtol=1e-2
                @test C.U ≈ dU rtol=1e-2
            end
        end

        @testset "getrf_strided_batched!" begin
            Random.seed!(1)
            local k
            # generate strided matrix
            A = rand(elty,m,m,10)
            # move to device
            d_A = CuArray(A)
            pivot, info = CuArrays.CUBLAS.getrf_strided_batched!(d_A, false)
            h_info = Array(info)
            for As in 1:size(d_A, 3)
                C   = lu!(copy(A[:,:,As]), Val(false)) # lu(A[As],pivot=false)
                h_A = Array(d_A[:,:,As])
                #reconstruct L,U
                dL = Matrix(one(elty)*I, m, m)
                dU = zeros(elty,(m,m))
                k = h_info[As]
                if( k >= 0 )
                    dL += tril(h_A,-k-1)
                    dU += triu(h_A,k)
                end
                #compare
                @test C.L ≈ dL rtol=1e-2
                @test C.U ≈ dU rtol=1e-2
            end
            d_A = CuArray(A)
            pivot, info = CuArrays.CUBLAS.getrf_strided_batched!(d_A, true)
            h_info = Array(info)
            h_pivot = Array(pivot)
            for As in 1:size(d_A, 3)
                C   = lu(A[:,:,As])
                h_A = Array(d_A[:,:,As])
                #reconstruct L,U
                dL = Matrix(one(elty)*I, m, m)
                dU = zeros(elty,(m,m))
                k = h_info[As]
                if( k >= 0 )
                    dL += tril(h_A,-k-1)
                    dU += triu(h_A,k)
                end
                #compare pivots
                @test length(setdiff(h_pivot[:,As],C.p)) == 0
                #make device pivot matrix
                P = Matrix(1.0*I, m, m)
                for row in 1:m
                    temp = copy(P[row,:])
                    P[row,:] = P[h_pivot[row,As],:]
                    P[h_pivot[row,As],:] = temp
                end
                @test inv(P)*dL*dU ≈ inv(C.P) * C.L * C.U
            end
        end

        @testset "getrf_strided_batched" begin
            local k
            # generate strided matrix
            A = rand(elty,m,m,10)
            # move to device
            d_A = CuArray(A)
            pivot, info, d_B = CuArrays.CUBLAS.getrf_strided_batched(d_A, false)
            h_info = Array(info)
            for Bs in 1:size(d_B, 3)
                C   = lu!(copy(A[:,:,Bs]),Val(false)) # lu(A[Bs],pivot=false)
                h_B = Array(d_B[:,:,Bs])
                #reconstruct L,U
                dL = Matrix(one(elty)*I, m, m)
                dU = zeros(elty,(m,m))
                k = h_info[Bs]
                if( h_info[Bs] >= 0 )
                    dU += triu(h_B,k)
                    dL += tril(h_B,-k-1)
                end
                #compare
                @test C.L ≈ dL rtol=1e-2
                @test C.U ≈ dU rtol=1e-2
            end
        end

        @testset "getri_batched" begin
            # generate matrices
            A = [rand(elty,m,m) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
            end
            pivot, info = CuArrays.CUBLAS.getrf_batched!(d_A, true)
            h_info = Array(info)
            for Cs in 1:length(h_info)
                @test h_info[Cs] == 0
            end
            pivot, info, d_C = CuArrays.CUBLAS.getri_batched(d_A, pivot)
            h_info = Array(info)
            for Cs in 1:length(d_C)
                C   = inv(A[Cs])
                h_C = Array(d_C[Cs])
                @test h_info[Cs] == 0
                @test C ≈ h_C rtol=1e-2
            end
        end

        @testset "matinv_batched" begin
            # generate matrices
            A = [rand(elty,m,m) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
            end
            info, d_C = CuArrays.CUBLAS.matinv_batched(d_A)
            for Cs in 1:length(d_C)
                C   = inv(A[Cs])
                h_C = Array(d_C[Cs])
                @test C ≈ h_C
            end
            push!(d_A, CuArray(rand(elty, m, m+1)))
            @test_throws DimensionMismatch CuArrays.CUBLAS.matinv_batched(d_A)
        end

        @testset "geqrf_batched!" begin
            # generate matrices
            A = [rand(elty,m,n) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
            end
            tau, d_A = CuArrays.CUBLAS.geqrf_batched!(d_A)
            for As in 1:length(d_A)
                C   = qr(A[As])
                h_A = Array(d_A[As])
                h_tau = Array(tau[As])
                # build up Q
                Q = Matrix(one(elty)*I, min(m,n), min(m,n))
                for i in 1:min(m,n)
                    v = zeros(elty,m)
                    v[i] = one(elty)
                    v[i+1:m] = h_A[i+1:m,i]
                    Q *= I - h_tau[i] * v * v'
                end
                @test Q ≈ Array(C.Q) rtol=1e-2
            end
        end

        @testset "geqrf_batched" begin
            # generate matrices
            A = [rand(elty,m,n) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
            end
            tau, d_B = CuArrays.CUBLAS.geqrf_batched!(d_A)
            for Bs in 1:length(d_B)
                C   = qr(A[Bs])
                h_B = Array(d_B[Bs])
                h_tau = Array(tau[Bs])
                # build up Q
                Q = Matrix(one(elty)*I, min(m,n), min(m,n))
                for i in 1:min(m,n)
                    v = zeros(elty,m)
                    v[i] = one(elty)
                    v[i+1:m] = h_B[i+1:m,i]
                    Q *= I - h_tau[i] * v * v'
                end
                @test Q ≈ Array(C.Q) rtol=1e-2
            end
        end

        @testset "gels_batched!" begin
            # generate matrices
            A = [rand(elty,n,n) for i in 1:10]
            C = [rand(elty,n,k) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            d_C = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
                push!(d_C,CuArray(C[i]))
            end
            d_A, d_C, info = CuArrays.CUBLAS.gels_batched!('N',d_A, d_C)
            for Cs in 1:length(d_C)
                X = A[Cs]\C[Cs]
                h_C = Array(d_C[Cs])
                @test X ≈ h_C rtol=1e-2
            end
            push!(d_C,CuArray(rand(elty,n,k)))
            @test_throws DimensionMismatch CuArrays.CUBLAS.gels_batched!('N',d_A, d_C)
            A = [rand(elty,n-1,n) for i in 1:10]
            C = [rand(elty,n,k) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            d_C = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
                push!(d_C,CuArray(C[i]))
            end
            # system is now not overdetermined
            @test_throws ArgumentError CuArrays.CUBLAS.gels_batched!('N',d_A, d_C)
        end

        @testset "gels_batched" begin
            # generate matrices
            A = [rand(elty,n,n) for i in 1:10]
            C = [rand(elty,n,k) for i in 1:10]
            # move to device
            d_A = CuArray{elty, 2}[]
            d_C = CuArray{elty, 2}[]
            for i in 1:length(A)
                push!(d_A,CuArray(A[i]))
                push!(d_C,CuArray(C[i]))
            end
            d_B, d_D, info = CuArrays.CUBLAS.gels_batched('N',d_A, d_C)
            for Ds in 1:length(d_D)
                X = A[Ds]\C[Ds]
                h_D = Array(d_D[Ds])
                @test X ≈ h_D rtol=1e-2
            end
        end
        # generate matrices
        A = rand(elty,m,n)
        C = rand(elty,m,n)
        x = rand(elty,m)
        # move to device
        d_A = CuArray(A)
        d_C = CuArray(C)
        d_x = CuArray(x)
        C = diagm(0 => x) * A
        @testset "dgmm!" begin
            # compute
            CuArrays.CUBLAS.dgmm!('L', d_A, d_x, d_C)
            # move to host and compare
            h_C = Array(d_C)
            @test C ≈ h_C
            # bounds checking
            @test_throws DimensionMismatch CuArrays.CUBLAS.dgmm!('R', d_A, d_x, d_C)
            E = rand(elty,m,m)
            d_E = CuArray(E)
            @test_throws DimensionMismatch CuArrays.CUBLAS.dgmm!('L', d_E, d_x, d_C)
        end
        @testset "dgmm" begin
            d_C = CuArrays.CUBLAS.dgmm('L', d_A, d_x)
            # move to host and compare
            h_C = Array(d_C)
            @test C ≈ h_C
        end
    end # extensions
end # elty

end # cublas testset


# filtering.jl
# this function contains all the functions to do filtering stabilization

@doc """
### EulerEquationMod.applyFilter

  This function multiplies a filter matrix by the variables at every
  node in the mesh.  The filter matrix is stored in eqn.params.filter_mat

  Inputs:
    mesh
    sbp
    eqn
    opts

  Keyword Args:
    trans=false  : transpose the filter matrix or not.

  Inputs/Outputs:
    arr: 3D array to multiply the filter matrix by

"""->
function applyFilter(mesh::AbstractMesh{Tmsh}, sbp::AbstractOperator,
         eqn::AbstractSolutionData{Tsol}, opts,
         arr::Abstract3DArray; trans=false) where {Tmsh, Tsol}
# applies filter to array arr
# trans determine whether or not to transpose the filter matrix

  if trans
    filter_mat = eqn.params.filter_mat.'
  else
    filter_mat = eqn.params.filter_mat
  end

  # holds the filtered results
  q_filt = zeros(Tsol, mesh.numNodesPerElement, mesh.numDofPerNode)
  len = mesh.numNodesPerElement*mesh.numDofPerNode
  for i=1:mesh.numEl
    q_vals = sview(arr, :, :, i)  # mesh.numDof x sbp.numnodes
    # apply filter matrix to q_vals transposed, so it is applied to
    # all the rho components, then all the x momentum, etc.
    smallmatmatT!(filter_mat, q_vals, q_filt)

    # copy values back into eqn.q, remembering to transpose q_filt
    for j=1:mesh.numNodesPerElement
      for k=1:mesh.numDofPerNode
        arr[k, j, i] = q_filt[j, k]
      end
    end

  end  # end loop over elements

  return nothing
end

@doc """
### EulerEquationMod.calcfilter

  This function calculates the filter used by applyFilter().
  The filter is calculated as V*filter_kernel*inv(V), where V is a matrix
  that converts the interpolating SBP basis to a modal basis and filter_kernal
  is a matrix (typically diagonal) that performs the filtering of the modal
  basis coefficients.

  Inputs:
    sbp: SBP operator used to compute the solutions
    filter_name: and String that matches the name of a filter function.
                 This string is used to retrieve the function from a dictionary
                 of all supported filter function.
    opts:  options dictonary

  Outputs:
    F_ret: a numNodesPerElement x numNodesPerElement filter operator

"""->
function calcFilter(sbp::AbstractOperator, filter_name::String, opts)
# calc the filter specified by filter_name

  filter_func = filter_dict[filter_name]
  filt_mat = filter_func(sbp, opts)

  # testing only
#  (m,n) = size(filt_mat)
#  filt_mat = eye(m)

  V = calcModalTransformationOp(sbp)
  println("filt_mat = \n", filt_mat)
  println("V = \n", V)
  println("cond(V) = ", cond(V))
  # calculate V*filter_mat*inv(V)
  # which converts from interpolating to modal, applies filter, then
  # converts back to interpolating
  F_t = filt_mat.'
  V_t = V.'
  F_ret = (V_t\F_t).'
  F_ret = V*F_ret

  F_ret = V*filt_mat*inv(V)
  # for testing, return identity matrix
#  (m,n) = size(filt_mat)
#  F_ret = eye(m)

  for i=1:length(F_ret)
    if abs(F_ret[i]) < 1e-15
      F_ret[i] = 0
    end
  end

  println("F_ret = \n", F_ret)
  return F_ret

end

@doc """
### EulerEquationMod.calcModalTransformationOp

  This function calculates a matrix operator V that transforms from a modal
  basis to an interpolating basis for SBP operators.  Because the transformation
  itself is rank deficient, V is augmented with the last n vector of its Q
  matrix (from the full QR decomposition), where numNodesPerElement - rank(V)
  = n

  Inputs:
    sbp:  SBP operator

  Outputs:
    V_full: a numNodesPerElement x numNodesPerElement generalized
            Vandermonde matrix

"""->
function calcModalTransformationOp(sbp::AbstractOperator)

  vtx = [-1. -1; 1 -1; -1 1]  # reference element
  x, y = SummationByParts.SymCubatures.calcnodes(sbp.cub, vtx)
  # loop over ortho polys up to degree d
  d = sbp.degree
  n = convert(Int, (d+1)*(d+2)/2)
  V = zeros(Float64, (sbp.numnodes, n))
  Vx = zeros(V)
  Vy = zeros(V)
  ptr = 0
  for r = 0:d
    for j = 0:r
      i = r-j
      V[:,ptr+1] = SummationByParts.OrthoPoly.proriolpoly(x, y, i, j)
      Vx[:,ptr+1], Vy[:,ptr+1] = SummationByParts.OrthoPoly.diffproriolpoly(x, y, i, j)
      ptr += 1
    end
  end

  Q, R = qr(V, thin=false)
  Qx, Rx = qr(Vx, thin=false)
  Qy, Ry = qr(Vy, thin=false)

  # make the V matrix square
  # for this to work, the QR decomposition *cannot* do column pivoting
  # if it does we will have to do a little more bookkepping
  V_full = zeros(sbp.numnodes, sbp.numnodes)
  V_full[:, 1:n] = V
  V_full[:, (n+1):end] = Q[:, (n+1):end]

  # make Vx, Vy square
  # also make them full rank by replacing linearly dependend columns with vectors from Q

  Vx_full = zeros(sbp.numnodes, sbp.numnodes)
  Vy_full = zeros(sbp.numnodes, sbp.numnodes)

  Vx_full[:, 1:n] = Vx
  Vy_full[:, 1:n] = Vy

  Vx_full[:, (n+1):end] = Qx[:, (n+1):end]
  Vy_full[:, (n+1):end] = Qy[:, (n+1):end]

  # also replace linearly dependent columns here?

  return V_full
end

@doc """
### EulerEquationMod.calcRaisedCosineFilter

  This function constructs a diagonal matrix filt for a 2 dimensional basis
  filter, using the raised cosine filter described in Spectral Methods for
  the Euler Equations: Part I by Hussaini, Kproiva, Salas, Zang

  Inputs
    sbp: SBP operator
    opts: options dictonary

  Outputs:
    filt: an numNodesPerElement x numNodesPerElement diagonal filter matrix

"""->
function calcRaisedCosineFilter(sbp::AbstractOperator, opts)
# calculates the 1D raised cosine filter
# from Spectral Methods for the Euler Equations: Part I - Fourier Methods and
# shock Capturing
# Hussaini, Koproiva, Salas, Zang

  filt = zeros(sbp.numnodes, sbp.numnodes)

  max_mode = getPascalLevel(sbp.numnodes)
  for i=1:sbp.numnodes
    mode_i = getPascalLevel(i)
    theta_i = (mode_i-1)/max_mode
    filt[i, i] = 0.5*(1 + cos(theta_i))
  end

  # get rid of nullspace component
  filt[sbp.numnodes, sbp.numnodes] = 0.0

  # increase the steepness of the filter
  for i=1:sbp.numnodes
    diff =  1 - filt[i, i]
    filt[i, i] -= 2*diff
  end

  for i=1:sbp.numnodes
    if filt[i,i] < 0
      filt[i,i] = 0
    end
  end

  return filt
end

@doc """
### EulerEquationMod.getPascalLevel

  This function returns the level of Pascals Triangle a particular node
  lies in.

  Inputs:
    node:  node index

  Outputs:
    level: integer describing level of Pascals triangle.

"""->
function getPascalLevel(node::Integer)
# get the current polynomial order of some entry node in
# Pascals triangle
# this assumes the tree is being traversed in order, from 1 to n

  level = 1
  for i=1:(node+1) # loop over level of triangles
            # looping all the way to i is excessive
    # get the maximum node in current level of triangle
#    println("Level = ", level)
    max_n = div(level*(1 + level), 2)
#    println("max_n = ", max_n)
    # break when we are at the right level
    if  node <= max_n
#      println("breaking")
      break
    end

    level += 1  # increment level
  end

  return level
end

@doc """
### EulerEquationMod.calcLowPassFilter

  This function calculates a low pass filter diagonal filter matrix.

  Inputs:
    sbp: SBP operator
    opts: options dictonary

  Outputs:
    filt: numNodesPerElement x numNodesPerElement diagonal filter matrix

"""->
function calcLowPassFilter(sbp::AbstractOperator, opts)

  filt = zeros(sbp.numnodes, sbp.numnodes)
  for i=1:sbp.numnodes
    filt[i, i] = 1
  end

  filt[end, end] = 0.99

  return filt
end

global const filter_dict = Dict{String, Function}(
"raisedCosineFilter" => calcRaisedCosineFilter,
"lowPassFilter" => calcLowPassFilter,
)


# addition

# function (+){T<:Float120}(a::T,b::T)
function (+)(a::Float120,b::Float120)
    s1, s2 = eftAdd(a.hi,b.hi)
    t1, t2 = eftAdd(a.lo,b.lo)
    s2 += t1
    s1, s2 = eftAddGTE(s1,s2)
    s2 += t2
    s1, s2 = eftAddGTE(s1,s2)
    Float120(s1,s2)
end

function (+)(a::Float120,b::Float64)
    s1, s2 = eftAdd(a.hi,b)
    s2 += a.lo
    s1, s2 = eftAddGTE(s1,s2)
    Float120(s1,s2)
end

(+)(a::Float64,b::Float120) = (+)(b,a)
(+)(a::Float120,b::Signed) = (+)(a,convert(Float64,b))
(+)(a::Signed,b::Float120) = (+)(b,a)

(+)(a::Float120, b::Real) = (+)(promote(a,b)...)
(+)(a::Real, b::Float120) = (+)(promote(a,b)...)

# subtraction

function (-){T<:Float120}(a::T,b::T)
    s1, s2 = eftSub(a.hi,b.hi)
    t1, t2 = eftSub(a.lo,b.lo)
    s2 += t1
    s1, s2 = eftAddGTE(s1,s2)
    s2 += t2
    s1, s2 = eftAddGTE(s1,s2)
    Float120(s1,s2)
end

function (-)(a::Float120,b::Float64)
    s1, s2 = eftSub(a.hi,b)
    s2 += a.lo
    s1, s2 = eftAddGTE(s1,s2)
    Float120(s1,s2)
end

function (-)(a::Float64,b::Float120)
    s1, s2 = eftSub(a,b.hi)
    s2 -= b.lo
    s1, s2 = eftAddGTE(s1,s2)
    Float120(s1,s2)
end

(-)(a::Float120,b::Signed) = (-)(a,convert(Float64,b))
(-)(a::Signed,b::Float120) = (-)(convert(Float64,a),b)

(-)(a::Float120, b::Real) = (-)(promote(a,b)...)
(-)(a::Real, b::Float120) = (-)(promote(a,b)...)

# multiplication

function (sqr)(a::Float120)
  t1,t2 = eftMul(a.hi,a.hi)
  t3 = a.hi * a.lo
  t5 = t3 + t3
  t6 = t2 + t5
  t1,t6 = eftAddGTE(t1,t6)
  Float120(t1,t6)
end

function (*){T<:Float120}(a::T,b::T)
  t1,t2 = eftMul(a.hi,b.hi)
  t3 = a.hi * b.lo
  t4 = a.lo * b.hi
  t5 = t3 + t4
  t6 = t2 + t5
  t1,t6 = eftAddGTE(t1,t6)
  Float120(t1,t6)
end

function (*)(a::Float120,b::Float64)
  t1,t2 = eftMul(a.hi,b)
  t4 = a.lo * b
  t6 = t2 + t4
  t1,t6 = eftAddGTE(t1,t6)
  Float120(t1,t6)
end

(*)(a::Float64,b::Float120) = (*)(b,a)
(*)(a::Float120,b::Signed) = (*)(a,convert(Float64,b))
(*)(a::Signed,b::Float120) = (*)(convert(Float64,a),b)

(*)(a::Float120, b::Real) = (*)(promote(a,b)...)
(*)(a::Real, b::Float120) = (*)(promote(a,b)...)

function fma{T<:Float120}(a::T,b::T,c::T)
    hi,lo = eftFMAas2(a,b,c)
    Float120(hi,lo)
end

function fms{T<:Float120}(a::T,b::T,c::T)
    hi,lo = eftFMSas2(a,b,c)
    Float120(hi,lo)
end

# reciprocation

function (recip)(b::Float120)
  q1 = one(Float64) / b.hi
  r  = one(Float120) - q1*b

  q2 = r.hi / b.hi
  r = r - (q2 * b)

  q2 += r.hi / b.hi

  q3 = r.hi / b.hi

  q1,q2 = eftAdd(q1, q2)
  q1,q2 = eftAddAs2(q1,q2,q3)
  Float120(q1,q2)
end

# division

function (/){T<:Float120}(a::T,b::T)
  q1 = a.hi / b.hi
  r  = a - (q1 * b)

  q2 = r.hi / b.hi
  r = r - (q2 * b)

  q3 = r.hi / b.hi

  q1,q2 = eftAddGTE(q1, q2)
  q1,q2 = eftAddAs2(q1,q2,q3)
  Float120(q1,q2)
end

(/)(a::Float120, b::Real) = (/)(promote(a,b)...)
(/)(a::Real, b::Float120) = (/)(promote(a,b)...)

# powers

# roots

#=
      for a in [1e-15..1e18]
      relerr ~1.3e-32  (106 bits)
=#

function sqrt(a::Float120)
    if a.hi <= 0.0
       if a.hi == 0.0
           return zero(Float120)
       else
           throw(ArgumentError("sqrt expects a nonnegative base"))
       end
    #elseif (a.hi < 1.0e-18) | (a.hi > 1.0e18)
    #    throw(ArgumentError("sqrt arg ($a) outside domain"))
    end

    if (a.hi < 1.0e-7)  # -log2(1.0e-7) < (1/2) Float64 significand bits
        r = recip(a)
        s = sqrt(r)
        return recip(s)
    end
    # initial approximation to 1/sqrt(a)
    r = Float120(1.0/sqrt(a.hi), 0.0)
    r = r + divby2( r * (one(Float120) - (a*(r*r))) )
    r = r + divby2( r * (one(Float120) - (a*(r*r))) )
    r = r + divby2( r * (one(Float120) - (a*(r*r))) )

    r = a*r
    #divby2(r + a/r)
    r += a/r
    divby2(r)
end

function hypot(a::Float120, b::Float120)
    a = abs(a)
    b = abs(b)
    t, x = minmax(a,b)
    t = t/a
    t = sqr(t)
    t = sqrt(one(Digits30) + t)
    x * t
end

(hypot)(a::Float120, b::Real) = (hypot)(a, convert(Float120,b))
(hypot)(a::Real, b::Float120) = (hypot)(convert(Float120,a), b)


"""
	solve_particle_scattering(k0, kin, P, sp::ScatteringProblem, u::Einc; get_inner = true, verbose = true) -> beta, inner

Solve the scattering problem `sp` with outer wavenumber `k0`, inner wavenumber
`kin`, `2P+1` cylindrical harmonics per inclusion and incident TM field
`u`. Solves multiple-scattering equation directly.
Returns the cylindrical harmonics basis `beta` along with potential
densities (in case of arbitrary inclusion) or inner cylindrical coefficients (in
case of circular). By default, incident wave propagates left->right.

Inner coefficients are only calculated if `get_inner` is true, and timing is
printed if `verbose` is true.
"""
function solve_particle_scattering(k0, kin, P, sp::ScatteringProblem, u::Einc;
								get_inner = true, verbose = true)
	#TODO: incorporate matrix-lexx rotation rotateMultipole!
	shapes = sp.shapes;	ids = sp.ids; centers = sp.centers; φs = sp.φs
	Ns = size(sp)
	#first solve for single scatterer densities
	dt1 = @elapsed begin
		scatteringMatrices,innerExpansions = particleExpansion(k0, kin, shapes, P, ids)
	end
	dt2 = @elapsed begin
		T = Array{Complex{Float64}}(undef, Ns*(2*P+1), Ns*(2*P+1))
		a = u2α(k0, u, centers, P)
		Btemp = Array{Complex{Float64}}(undef, 2*P+1, 2*P+1)
		for ic1 = 1:Ns
			rng1 = (ic1-1)*(2*P+1) .+ (1:2*P+1)
			if φs[ic1] == 0.0 || typeof(shapes[ids[ic1]]) == CircleParams
				RotScatMat = scatteringMatrices[ids[ic1]]
			else
				Rot = sparse(Diagonal(Complex{Float64}[exp(-1.0im*φs[ic1]*l) for l=-P:P])) #rotation matrix
				RotScatMat = Rot*(scatteringMatrices[ids[ic1]]*conj(Rot))
			end
			for ic2 = 1:Ns
				rng2 = (ic2-1)*(2*P+1) .+ (1:2*P+1)
				if ic1 == ic2
					T[rng1,rng2] = Matrix{Complex{Float64}}(I, 2*P+1, 2*P+1)
				else
					M2Lmatrix!(Btemp, k0, P, centers[ic1,:] - centers[ic2,:])
					T[rng1,rng2] = -RotScatMat*Btemp
				end
			end
			a[rng1] = RotScatMat*a[rng1]
		end
	end
	dt3 = @elapsed begin
		beta = T\a
	end
	#recover full incoming expansion - in sigma_mu terms for parametrized shape,
	#in multipole expansion for circle
	if get_inner
		#find LU factorization once for each shape
		dt4 = @elapsed begin
	        scatteringLU = [lu(scatteringMatrices[iid]) for iid = 1:length(shapes)]
			inner = Array{Vector{Complex{Float64}}}(undef, Ns)
			α_c = Array{Complex{Float64}}(undef, 2*P+1)
			for ic = 1:Ns
				rng = (ic-1)*(2*P+1) .+ (1:2*P+1)
				if typeof(shapes[ids[ic]]) == ShapeParams
					if φs[ic] == 0.0
						α_c[:] = scatteringLU[ids[ic]]\beta[rng]
					else
						Rot = sparse(Diagonal(Complex{Float64}[exp(-1.0im*φs[ic]*l) for l=-P:P])) #rotation matrix
						α_c[:] = scatteringLU[ids[ic]]\(conj(Rot)*beta[rng])
					end
					inner[ic] = innerExpansions[ids[ic]]*α_c
				else
					inner[ic] = innerExpansions[ids[ic]]*beta[rng]
				end
			end
		end
	end
	verbose && begin
		println("Direct solution timing:")
		println("Scattering matrix solution: $dt1 s")
		println("Matrix construction: $dt2 s")
		println("Matrix solution: $dt3 s")
        get_inner && println("Retrieving inner coefficients: $dt4 s")
    end
	get_inner ? (return beta, inner) : (return beta)
end

function M2Lmatrix!(T, k, P, d)
	#builds off-diagonal translation matrix from particle 2 to particle 1 (d=1-2)
	#only calculates 2*(2*P+1) values
	kd = k*sqrt(sum(abs2,d))
	td = atan(d[2],d[1])
	bess = besselh.(0:2*P,1,kd)
	for ix = 1:2*P #lower diagonals
		rng = ix+1:1+(2*P+1):(2*P+1)^2-(2*P+1)*ix
		T[rng] .= exp(-1im*td*ix)*(-1)^(ix)*bess[ix+1]
	end
	for ix = 0:2*P #central and upper diagonals
		rng = ix*(2*P+1)+1:1+(2*P+1):(2*P+1)^2-ix
		T[rng] .= exp(1im*td*ix)*bess[ix+1]
	end
end

function scattered_field_multipole(k0, beta, centers::Array{Float64,2}, points::Array{Float64,2}; recurrence = false)
	Ns = size(centers,1)
	P = div(div(length(beta),Ns)-1,2)
	len = size(points,1)
	Esc = zeros(Complex{Float64}, len)

	if recurrence
		scattered_field_multipole_recurrence!(Esc, k0, beta, P, centers, 1:Ns, points, 1:len)
	else
		scattered_field_multipole!(Esc, k0, beta, P, centers, 1:Ns, points, 1:len)
	end
	return Esc
end

function scattered_field_multipole!(Esc::Array{Complex{Float64},1}, k0, beta, P, centers::Array{Float64,2}, ind_centers, points::Array{Float64,2}, ind_points)
	for ic in ind_centers
		ind = (ic-1)*(2*P+1) + P + 1
		for ip in ind_points
			points_moved1 = points[ip,1] - centers[ic,1]
			points_moved2 = points[ip,2] - centers[ic,2]

			rs_moved = hypot(points_moved1, points_moved2)
			ts_moved = atan(points_moved2, points_moved1)
			if rs_moved == 0
				error("rs_moved == 0, center=$(centers[ic,:]), point=$(points[ip,:]), k0 = $k0, ic=$ic, ip=$ip")
			end
			Esc[ip] += beta[ind]*besselh(0, 1, k0*rs_moved)
			for p = 1:P
				Esc[ip] += besselh(p, 1, k0*rs_moved)*(beta[p + ind]*exp(1im*p*ts_moved) + (-1)^p*beta[-p + ind]*exp(-1im*p*ts_moved))
			end
		end
	end
end

function scattered_field_multipole_recurrence!(Esc::Array{Complex{Float64},1}, k0, beta, P, centers::Array{Float64,2}, ind_centers, points::Array{Float64,2}, ind_points)
	for ic in ind_centers
		ind = (ic-1)*(2*P+1) + P + 1
		for ip in ind_points
			points_moved1 = points[ip,1] - centers[ic,1]
			points_moved2 = points[ip,2] - centers[ic,2]

			rs_moved = hypot(points_moved1, points_moved2)
			ts_moved = atan(points_moved2, points_moved1)
			if rs_moved == 0
				error("rs_moved == 0, center=$(centers[ic,:]), point=$(points[ip,:]), k0 = $k0, ic=$ic, ip=$ip")
			end
			Hₚ₋₂ = besselh(-1, 1, k0*rs_moved)
			Hₚ₋₁ = besselh(0, 1, k0*rs_moved)
			Esc[ip] += beta[ind]*Hₚ₋₁
			for p = 1:P
				Hₚ = (2*(p-1)/(k0*rs_moved))*Hₚ₋₁ - Hₚ₋₂
				Esc[ip] += Hₚ*(beta[p + ind]*exp(1im*p*ts_moved) + (-1)^p*beta[-p + ind]*exp(-1im*p*ts_moved))
				Hₚ₋₂ = Hₚ₋₁; Hₚ₋₁ = Hₚ
			end
		end
	end
end

function circleScatteringMatrix(kout, kin, R, P; gamma = false)
    #non-vectorized, reuses bessel
    S = Array{Complex{Float64}}(undef, 2*P+1)
    gamma && (G = Array{Complex{Float64}}(undef, 2*P+1))

    pre_J0 = besselj(-P-1,kout*R)
    pre_J1 = besselj(-P-1,kin*R)
    pre_H = besselh(-P-1,kout*R)
    for p = -P:0
        J0 = besselj(p,kout*R)
        J1 = besselj(p,kin*R)
        H = besselh(p,kout*R)

        dJ0 = kout*(pre_J0 - (p/kout/R)*J0)
        dJ1 = kin*(pre_J1 - (p/kin/R)*J1)
        dH = kout*(pre_H - (p/kout/R)*H)

        S[p+P+1] = -(dJ0*J1 - J0*dJ1)/(dH*J1 - H*dJ1)
        p != 0 && (S[P+1-p] = S[p+P+1])

        if gamma #also returns gamma_n as a function of beta_n
            G[p+P+1] = (J0*dH - dJ0*H)/(J0*dJ1 - dJ0*J1)
            p != 0 && (G[P+1-p] = G[p+P+1])
        end
        pre_J0 = J0
        pre_J1 = J1
        pre_H = H
    end
    ScatMat = sparse(Diagonal(S))
    if gamma
        InnerMat = sparse(Diagonal(G))
        return ScatMat,InnerMat
    else
        return ScatMat
    end
end

function innerFieldCircle(kin, gamma, center::Array{Float64,1}, points::Array{Float64,2})
    #non-vectorized, reuses bessel
    P = div(length(gamma)-1,2)

	len = size(points,1)
	points_moved = similar(points)
    points_moved[:,1] = points[:,1] .- center[1]
	points_moved[:,2] = points[:,2] .- center[2]

	rs_moved = sqrt.(sum(abs2,points_moved, dims=2))
	ts_moved = atan.(points_moved[:,2], points_moved[:,1])

	bess = [besselj(p,kin*rs_moved[ii]) for ii=1:len, p=0:P]
	E = gamma[P + 1]*bess[:,1]
	for p = 1:P
		E += bess[:,p+1].*(gamma[p + P + 1]*exp.(1.0im*p*ts_moved) +
							(-1)^p*gamma[-p + P + 1]*exp.(-1.0im*p*ts_moved))
	end
	return E
end

function innerFieldCircle(kin, gamma, center::Array{Float64,1}, points::Array{Float64,1})
    #non-vectorized, reuses bessel
    P = div(length(gamma)-1,2)

    points_moved = points - center
	rs_moved = sqrt(sum(abs2,points_moved))
	ts_moved = atan(points_moved[2], points_moved[1])

	bess = [besselj(p,kin*rs_moved) for p=0:P]
	E = gamma[P + 1]*bess[1]
	for p = 1:P
		E += bess[p+1]*(gamma[p + P + 1]*exp(1.0im*p*ts_moved) + (-1)^p*gamma[-p + P + 1]*exp(-1.0im*p*ts_moved))
	end
	return E
end

function particleExpansion(k0, kin, shapes, P, ids)
    scatteringMatrices = Array{Any}(undef, 0)
	innerExpansions = Array{Any}(undef, 0)
	for i = 1:length(shapes)
        #no use in computing matrices if shape doesn't actually show up! push garbage to maintain order
		#TODO: don't even try to compute LU for these
		if all(ids .!= i)
			push!(scatteringMatrices, [NaN NaN])
			push!(innerExpansions, [NaN NaN])
			continue
		end
		if typeof(shapes[i]) == ShapeParams
            AB = shapeMultipoleExpansion(k0, shapes[i].t, shapes[i].ft, shapes[i].dft, P)
    		sigma_mu_mult = get_potential(k0, kin, P, shapes[i].t, shapes[i].ft, shapes[i].dft)
    		ScatMat = AB*sigma_mu_mult
            push!(scatteringMatrices, ScatMat)
            push!(innerExpansions, sigma_mu_mult)
		else
			ScatMat, inner = circleScatteringMatrix(k0, kin, shapes[i].R, P, gamma = true)
			push!(scatteringMatrices, ScatMat)
			push!(innerExpansions, inner)
		end
	end
    return scatteringMatrices,innerExpansions
end

function rotateMultipole!(arr,phi,P)
    #applies e^{-i \phi l}, p=-P...P
    #if slow, change from view to direct indexing (should be negligible despite higher memory use)
    for p = -P:P
        arr[P + 1 + p] *= exp(-1.0im*phi*p)
    end
end

function rotateMultipole!(dest_arr,source_arr,phi,P)
    #applies e^{-i \phi l}, p=-P...P
    #if slow, change from view to direct indexing (should be negligible despite higher memory use)
    for p = -P:P
        dest_arr[P + 1 + p] = source_arr[P + 1 + p]*exp(-1.0im*phi*p)
    end
end


export EmbeddingPatch

"""
    EmbeddingPatch()

Holds an `PVector` onto its affine patch. With this the effect
is basically the same as tracking in affine space.
"""
struct EmbeddingPatch <: AbstractAffinePatch end

struct EmbeddingPatchState{N} <: AbstractAffinePatchState{N} end

function state(::EmbeddingPatch, x::PVector{T,N}) where {T,N}
    EmbeddingPatchState{N}()
end

function onpatch!(x::PVector, state::EmbeddingPatchState)
    ProjectiveVectors.affine_chart!(x)
end

function evaluate!(u, state::EmbeddingPatchState{N}, x::PVector{T, N}) where {T, N}
    homvars = ProjectiveVectors.homvars(x)
    n = length(u) - N
    for i in 1:N
        u[n + i] = x[homvars[i]] - one(eltype(x))
    end
    nothing
end

function jacobian!(U, state::EmbeddingPatchState{N}, x::PVector{T, N}) where {T, N}
    homvars = ProjectiveVectors.homvars(x)
    n = size(U,1) - N
    for j in 1:size(U, 2), i in 1:N
        U[n + i, j] = j == homvars[i] ? one(eltype(x)) : zero(eltype(x))
    end
    nothing
end


test_str1 = "mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X
mem[8] = 11
mem[7] = 101
mem[8] = 0"

test_str2 = "mask = 000000000000000000000000000000X1001X
mem[42] = 100
mask = 00000000000000000000000000000000X0XX
mem[26] = 1"

test_list1 = split(test_str1, '\n')

test_list2 = split(test_str2, '\n')

list = readlines("day14_input.txt")

function mask_value(list)
    lines = map(x -> split(x, " = "), list)
    memory = Dict()
    mask = "X"^36
    for i in 1:length(list)
        if lines[i][1] == "mask"
            mask = lines[i][2]
        else
            value = bitstring(parse(Int, lines[i][2]))[end-35:end]
            masked_value = ""
            for j=1:36
                if mask[j] == 'X'
                    masked_value *= value[j]
                elseif mask[j] == '0'
                    masked_value *= '0'
                else
                    masked_value *= '1'
                end
            end
            if lines[i][1] in keys(memory)
                memory[lines[i][1]] = parse(Int, masked_value, base=2)
            else
                push!(memory, lines[i][1] => parse(Int, masked_value, base=2))
            end
        end
    end
    return sum(values(memory))
end

@show mask_value(test_list1) == 165
@show mask_value(list) == 15919415426101

function mask_address(list)
    lines = map(x -> split(x, " = ") |> y -> y[1] == "mask" ? (y[1], y[2]) :
        (parse(Int,(split(y[1], r"mem\[|\]")[2])), parse(Int, y[2])), list)
    memory = Dict()
    mask = "X"^36
    for i in 1:length(list)
        if lines[i][1] == "mask"
            mask = lines[i][2]
        else
            address = bitstring(lines[i][1])[end-35:end]
            value = lines[i][2]
            masked_address = map(x -> x[1] == '0' ? x[2] : x[1], zip(mask, address))
            num_x = count(==('X'), masked_address)
            pos_x = [i for (i,c) in enumerate(masked_address) if c=='X']
            num_comb = 2^num_x
            comb = [collect(bitstring(j)[end-num_x+1:end]) for j=1:num_comb]
            for j=1:num_comb
                x_address = deepcopy(masked_address)
                x_address[pos_x] = comb[j]
                if x_address in keys(memory)
                    memory[x_address] = value
                else
                    push!(memory, x_address => value)
                end
            end
        end
    end
    return sum(values(memory))
end

@show mask_address(test_list2) == 208
@show mask_address(list) == 3443997590975

nothing


function absdiff(x::T,y::T) where {T<:Unsigned}
    d = max(x,y) - min(x,y)
    d, d
end
function absdiff(x::T,y::T) where {T<:Signed}
    d = x - y
    abs(d), d
end
# binary GCD (aka Stein's) algorithm
# about 1.7x (2.1x) faster for random Int64s (Int128s)
# Unfortunately, we need to manually annotate this as `@assume_effects :terminates_locally` to work around #41694.
# Since this is used in the Rational constructor, constant folding is something we do care about here.
function _gcd(ain::T, bin::T) where T<:Integer
    zb = trailing_zeros(bin)
    za = trailing_zeros(ain)
    a = abs(ain)
    b = abs(bin >> zb)
    k = min(za, zb)
    while a != 0
        a >>= za
        absd, diff = absdiff(a, b)
        za = trailing_zeros(diff)
        b = min(a, b)
        a = absd
    end
    r = b << k
    return r % T
end