@testset "minimal_mulRevToPair_test" begin
    @test mul_rev_to_pair(emptyinterval(), interval(1.0, 2.0))[1] == emptyinterval() && mul_rev_to_pair(emptyinterval(), interval(1.0, 2.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(1.0, 2.0), emptyinterval())[1] == emptyinterval() && mul_rev_to_pair(interval(1.0, 2.0), emptyinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(emptyinterval(), emptyinterval())[1] == emptyinterval() && mul_rev_to_pair(emptyinterval(), emptyinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(-2.1, -0.4))[1] == interval(0x1.999999999999AP-3, 0x1.5P+4) && mul_rev_to_pair(interval(-2.0, -0.1), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(-2.1, -0.4))[1] == interval(0x1.999999999999AP-3, Inf) && mul_rev_to_pair(interval(-2.0, 0.0), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(-2.1, -0.4))[1] == interval(-Inf, -0x1.745D1745D1745P-2) && mul_rev_to_pair(interval(-2.0, 1.1), interval(-2.1, -0.4))[2] == interval(0x1.999999999999AP-3, Inf)
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(-2.1, -0.4))[1] == interval(-Inf, -0x1.745D1745D1745P-2) && mul_rev_to_pair(interval(0.0, 1.1), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(-2.1, -0.4))[1] == interval(-0x1.A400000000001P+7, -0x1.745D1745D1745P-2) && mul_rev_to_pair(interval(0.01, 1.1), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(-2.1, -0.4))[1] == emptyinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(-2.1, -0.4))[1] == interval(0.0, 0x1.5P+4) && mul_rev_to_pair(interval(-Inf, -0.1), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(-2.1, -0.4))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(-Inf, 0.0), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(-2.1, -0.4))[1] == interval(-Inf, -0x1.745D1745D1745P-2) && mul_rev_to_pair(interval(-Inf, 1.1), interval(-2.1, -0.4))[2] == interval(0.0, Inf)
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(-2.1, -0.4))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-2.0, Inf), interval(-2.1, -0.4))[2] == interval(0x1.999999999999AP-3, Inf)
    @test mul_rev_to_pair(interval(0.0, Inf), interval(-2.1, -0.4))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(0.0, Inf), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(-2.1, -0.4))[1] == interval(-0x1.A400000000001P+7, 0.0) && mul_rev_to_pair(interval(0.01, Inf), interval(-2.1, -0.4))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(-2.1, -0.4))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(entireinterval(), interval(-2.1, -0.4))[2] == interval(0.0, Inf)
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(-2.1, 0.0))[1] == interval(0.0, 0x1.5P+4) && mul_rev_to_pair(interval(-2.0, -0.1), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(-2.1, 0.0))[1] == interval(-0x1.A400000000001P+7, 0.0) && mul_rev_to_pair(interval(0.01, 1.1), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(-2.1, 0.0))[1] == interval(0.0, 0x1.5P+4) && mul_rev_to_pair(interval(-Inf, -0.1), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(-2.1, 0.0))[1] == interval(-0x1.A400000000001P+7, 0.0) && mul_rev_to_pair(interval(0.01, Inf), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(-2.1, 0.0))[1] == entireinterval() && mul_rev_to_pair(entireinterval(), interval(-2.1, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(-2.1, 0.12))[1] == interval(-0x1.3333333333333P+0, 0x1.5P+4) && mul_rev_to_pair(interval(-2.0, -0.1), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(-2.1, 0.12))[1] == interval(-0x1.A400000000001P+7 , 0x1.8P+3) && mul_rev_to_pair(interval(0.01, 1.1), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(-2.1, 0.12))[1] == interval(-0x1.3333333333333P+0, 0x1.5P+4) && mul_rev_to_pair(interval(-Inf, -0.1), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(-2.1, 0.12))[1] == interval(-0x1.A400000000001P+7 , 0x1.8P+3) && mul_rev_to_pair(interval(0.01, Inf), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(-2.1, 0.12))[1] == entireinterval() && mul_rev_to_pair(entireinterval(), interval(-2.1, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(0.0, 0.12))[1] == interval(-0x1.3333333333333P+0, 0.0) && mul_rev_to_pair(interval(-2.0, -0.1), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(0.0, 0.12))[1] == interval(0.0, 0x1.8P+3) && mul_rev_to_pair(interval(0.01, 1.1), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(0.0, 0.12))[1] == interval(-0x1.3333333333333P+0, 0.0) && mul_rev_to_pair(interval(-Inf, -0.1), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(0.0, 0.12))[1] == interval(0.0, 0x1.8P+3) && mul_rev_to_pair(interval(0.01, Inf), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(0.0, 0.12))[1] == entireinterval() && mul_rev_to_pair(entireinterval(), interval(0.0, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(0.01, 0.12))[1] == interval(-0x1.3333333333333P+0, -0x1.47AE147AE147BP-8) && mul_rev_to_pair(interval(-2.0, -0.1), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(0.01, 0.12))[1] == interval(-Inf, -0x1.47AE147AE147BP-8) && mul_rev_to_pair(interval(-2.0, 0.0), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(0.01, 0.12))[1] == interval(-Inf, -0x1.47AE147AE147BP-8) && mul_rev_to_pair(interval(-2.0, 1.1), interval(0.01, 0.12))[2] == interval(0x1.29E4129E4129DP-7, Inf)
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(0.01, 0.12))[1] == interval(0x1.29E4129E4129DP-7, Inf) && mul_rev_to_pair(interval(0.0, 1.1), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(0.01, 0.12))[1] == interval(0x1.29E4129E4129DP-7, 0x1.8P+3) && mul_rev_to_pair(interval(0.01, 1.1), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(0.01, 0.12))[1] == emptyinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(0.01, 0.12))[1] == interval(-0x1.3333333333333P+0, 0.0) && mul_rev_to_pair(interval(-Inf, -0.1), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(0.01, 0.12))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-Inf, 0.0), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(0.01, 0.12))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-Inf, 1.1), interval(0.01, 0.12))[2] == interval(0x1.29E4129E4129DP-7, Inf)
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(0.01, 0.12))[1] == interval(-Inf, -0x1.47AE147AE147BP-8) && mul_rev_to_pair(interval(-2.0, Inf), interval(0.01, 0.12))[2] == interval(0.0, Inf)
    @test mul_rev_to_pair(interval(0.0, Inf), interval(0.01, 0.12))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(0.0, Inf), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(0.01, 0.12))[1] == interval(0.0, 0x1.8P+3) && mul_rev_to_pair(interval(0.01, Inf), interval(0.01, 0.12))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(0.01, 0.12))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(entireinterval(), interval(0.01, 0.12))[2] == interval(0.0, Inf)
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(0.0, 0.0))[1] == interval(0.0, 0.0) && mul_rev_to_pair(interval(-2.0, -0.1), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(0.0, 0.0))[1] == interval(0.0, 0.0) && mul_rev_to_pair(interval(0.01, 1.1), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(0.0, 0.0))[1] == interval(0.0, 0.0) && mul_rev_to_pair(interval(-Inf, -0.1), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(0.0, 0.0))[1] == interval(0.0, 0.0) && mul_rev_to_pair(interval(0.01, Inf), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(0.0, 0.0))[1] == entireinterval() && mul_rev_to_pair(entireinterval(), interval(0.0, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(-Inf, -0.1))[1] == interval(0x1.999999999999AP-5, Inf) && mul_rev_to_pair(interval(-2.0, -0.1), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(-Inf, -0.1))[1] == interval(0x1.999999999999AP-5 , Inf) && mul_rev_to_pair(interval(-2.0, 0.0), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(-Inf, -0.1))[1] == interval(-Inf , -0x1.745D1745D1745P-4) && mul_rev_to_pair(interval(-2.0, 1.1), interval(-Inf, -0.1))[2] == interval(0x1.999999999999AP-5 , Inf)
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(-Inf, -0.1))[1] == interval(-Inf, -0x1.745D1745D1745P-4) && mul_rev_to_pair(interval(0.0, 1.1), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(-Inf, -0.1))[1] == interval(-Inf, -0x1.745D1745D1745P-4) && mul_rev_to_pair(interval(0.01, 1.1), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(-Inf, -0.1))[1] == emptyinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(-Inf, -0.1))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(-Inf, -0.1), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(-Inf, -0.1))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(-Inf, 0.0), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(-Inf, -0.1))[1] == interval(-Inf, -0x1.745D1745D1745P-4) && mul_rev_to_pair(interval(-Inf, 1.1), interval(-Inf, -0.1))[2] == interval(0.0, Inf)
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(-Inf, -0.1))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-2.0, Inf), interval(-Inf, -0.1))[2] == interval(0x1.999999999999AP-5 , Inf)
    @test mul_rev_to_pair(interval(0.0, Inf), interval(-Inf, -0.1))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(0.0, Inf), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(-Inf, -0.1))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(0.01, Inf), interval(-Inf, -0.1))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(-Inf, -0.1))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(entireinterval(), interval(-Inf, -0.1))[2] == interval(0.0, Inf)
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(-Inf, 0.0))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(-2.0, -0.1), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(-Inf, 0.0))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(0.01, 1.1), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(-Inf, 0.0))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(-Inf, -0.1), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(-Inf, 0.0))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(0.01, Inf), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(-Inf, 0.0))[1] == entireinterval() && mul_rev_to_pair(entireinterval(), interval(-Inf, 0.0))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(-Inf, 0.3))[1] == interval(-0x1.8P+1, Inf) && mul_rev_to_pair(interval(-2.0, -0.1), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(-Inf, 0.3))[1] == interval(-Inf, 0x1.EP+4) && mul_rev_to_pair(interval(0.01, 1.1), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(-Inf, 0.3))[1] == interval(-0x1.8P+1, Inf) && mul_rev_to_pair(interval(-Inf, -0.1), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(-Inf, 0.3))[1] == interval(-Inf, 0x1.EP+4) && mul_rev_to_pair(interval(0.01, Inf), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(-Inf, 0.3))[1] == entireinterval() && mul_rev_to_pair(entireinterval(), interval(-Inf, 0.3))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(-0.21, Inf))[1] == interval(-Inf , 0x1.0CCCCCCCCCCCDP+1) && mul_rev_to_pair(interval(-2.0, -0.1), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(-0.21, Inf))[1] == interval(-0x1.5P+4, Inf) && mul_rev_to_pair(interval(0.01, 1.1), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(-0.21, Inf))[1] == interval(-Inf, 0x1.0CCCCCCCCCCCDP+1) && mul_rev_to_pair(interval(-Inf, -0.1), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(-0.21, Inf))[1] == interval(-0x1.5P+4, Inf) && mul_rev_to_pair(interval(0.01, Inf), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(-0.21, Inf))[1] == entireinterval() && mul_rev_to_pair(entireinterval(), interval(-0.21, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(0.0, Inf))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-2.0, -0.1), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(0.0, Inf))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(0.01, 1.1), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(0.0, Inf))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-Inf, -0.1), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(0.0, Inf))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(0.01, Inf), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(0.0, Inf))[1] == entireinterval() && mul_rev_to_pair(entireinterval(), interval(0.0, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, -0.1), interval(0.04, Inf))[1] == interval(-Inf, -0x1.47AE147AE147BP-6) && mul_rev_to_pair(interval(-2.0, -0.1), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), interval(0.04, Inf))[1] == interval(-Inf, -0x1.47AE147AE147BP-6) && mul_rev_to_pair(interval(-2.0, 0.0), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), interval(0.04, Inf))[1] == interval(-Inf, -0x1.47AE147AE147BP-6) && mul_rev_to_pair(interval(-2.0, 1.1), interval(0.04, Inf))[2] == interval(0x1.29E4129E4129DP-5, Inf)
    @test mul_rev_to_pair(interval(0.0, 1.1), interval(0.04, Inf))[1] == interval(0x1.29E4129E4129DP-5, Inf) && mul_rev_to_pair(interval(0.0, 1.1), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), interval(0.04, Inf))[1] == interval(0x1.29E4129E4129DP-5, Inf) && mul_rev_to_pair(interval(0.01, 1.1), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), interval(0.04, Inf))[1] == emptyinterval() && mul_rev_to_pair(interval(0.0, 0.0), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), interval(0.04, Inf))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-Inf, -0.1), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), interval(0.04, Inf))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-Inf, 0.0), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), interval(0.04, Inf))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(interval(-Inf, 1.1), interval(0.04, Inf))[2] == interval(0x1.29E4129E4129DP-5, Inf)
    @test mul_rev_to_pair(interval(-2.0, Inf), interval(0.04, Inf))[1] == interval(-Inf, -0x1.47AE147AE147BP-6) && mul_rev_to_pair(interval(-2.0, Inf), interval(0.04, Inf))[2] == interval(0.0, Inf)
    @test mul_rev_to_pair(interval(0.0, Inf), interval(0.04, Inf))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(0.0, Inf), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), interval(0.04, Inf))[1] == interval(0.0, Inf) && mul_rev_to_pair(interval(0.01, Inf), interval(0.04, Inf))[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), interval(0.04, Inf))[1] == interval(-Inf, 0.0) && mul_rev_to_pair(entireinterval(), interval(0.04, Inf))[2] == interval(0.0, Inf)
    @test mul_rev_to_pair(interval(-2.0, -0.1), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, -0.1), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 0.0), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 0.0), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, 1.1), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, 1.1), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 1.1), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 1.1), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, 1.1), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(0.01, 1.1), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, 0.0), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(0.0, 0.0), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, -0.1), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, -0.1), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 0.0), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 0.0), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-Inf, 1.1), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(-Inf, 1.1), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(-2.0, Inf), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(-2.0, Inf), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.0, Inf), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(0.0, Inf), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(interval(0.01, Inf), entireinterval())[1] == entireinterval() && mul_rev_to_pair(interval(0.01, Inf), entireinterval())[2] == emptyinterval()
    @test mul_rev_to_pair(entireinterval(), entireinterval())[1] == entireinterval() && mul_rev_to_pair(entireinterval(), entireinterval())[2] == emptyinterval()
end


using PyPlot, JUDI,LinearAlgebra, FFTW, DSP, SpecialFunctions, JOLI
using IterativeSolvers

using Shampoo

n = (201, 201)   # (x,y,z) or (x,z)
d = (5f0, 5f0)
o = (0f0, 0f0)

extentx = (n[1]-1)*d[1]
extentz = (n[2]-1)*d[2]

v0 = 2f0*ones(Float32,n)
v = deepcopy(v0)
v[101,101] = 2.1f0  # dirac perturbation

# Slowness squared [s^2/km^2]
m = convert(Array{Float32,2},(1f0 ./ v).^2)	# true model
m0 = convert(Array{Float32,2},(1f0 ./ v0).^2)
dm = vec(m-m0)

model = Model(n, d, o, m; nb = 200)
model0 = Model(n, d, o, m0; nb = 200)

# Model structure

dtS = 2f0
timeS = 2000f0
nt = Int64(timeS/dtS)+1

fmin = 10f0
fmax = 60f0
cfreqs = (fmin, fmin+5f0, fmax-5f0, fmax) # corner frequencies1
wavelet = reshape(cfreq_wavelet(500, nt, dtS/1f3, cfreqs; edge=hanning),:,1)
#wavelet = ricker_wavelet(timeS, dtS, 0.03f0)

################################### Source/receiver geometry ######################################

idx_wb = 30
Tm = judiTopmute(model0.n, idx_wb, 10)  # Mute water column
S = judiDepthScaling(model0)
Mr = S*Tm

# Set up receiver geometry

nrec = n[1]

xrec = range(0f0, stop=Float32((n[1]-1)*d[1]), length=nrec)
yrec = 0f0
zrec = range(14f0, stop=14f0, length=nrec)

# receiver sampling and recording time
timeR = timeS   # receiver recording time [ms]
dtR   = dtS
# Set up receiver structure
nsrc = 8

xsrc = convertToCell(range(0f0, stop=Float32((n[1]-1)*d[1]), length=nsrc))
# xsrc = convertToCell(range((n[1]-1)*d[1]/2f0, stop=(n[1]-1)*d[1]/2f0, length=nsrc))
ysrc = convertToCell(range(0f0, stop=0f0, length=nsrc))
zsrc = convertToCell(range(9f0, stop=9f0, length=nsrc))

recGeometry = Geometry(xrec, yrec, zrec; dt=dtR, t=timeR, nsrc=nsrc)

srcGeometry = Geometry(xsrc, ysrc, zsrc; dt=dtS, t=timeS)

q = judiVector(srcGeometry, wavelet)

# Set up info structure for linear operators
ntComp = get_computational_nt(srcGeometry, recGeometry, model)
info = Info(prod(n), nsrc, ntComp)

opt = Options(free_surface=false,isic=false)
F0 = judiModeling(info, model0, srcGeometry, recGeometry; options=opt)
J = judiJacobian(F0, q)

PyPlot.rc("font", family="serif"); PyPlot.rc("xtick", labelsize=8); PyPlot.rc("ytick", labelsize=8)
figure(); imshow(reshape(sqrt.(1f0./m), n[1], n[2])',extent=(0,extentx,extentz,0));
PyPlot.scatter(xrec,zrec,marker=".",color="red",label="receivers",s=1);
PyPlot.scatter(xsrc,zsrc,marker=".",color="white",label="sources",s=1);
legend(loc=3,fontsize=8);
xlabel("Lateral position [m]", fontsize=8);ylabel("Horizontal position [m]", fontsize=8);
fig = PyPlot.gcf();
title("Experimental setup")

# left preconditioners
P = FractionalIntegrationOp(nt,nsrc,nrec,0.5)

d_obs = J*dm

dm1 = J'*d_obs
dm2 = J'*P'*P*d_obs

figure();
subplot(2,1,1);
PyPlot.plot(wavelet);title("zoomed-in wavelet in time domain");
subplot(2,1,2);
PyPlot.plot(abs.(fftshift(fft(wavelet))));title("wavelet in frequency domain");

figure();
subplot(2,2,1)
imshow(dm1.data');title("J'*J*dm")
subplot(2,2,2)
imshow(dm2.data');title("J'*P'*P*J*dm")
subplot(2,2,3)
imshow(abs.(fftshift(fft(dm1.data')))/norm(abs.(fftshift(fft(dm1.data'))),Inf),cmap="jet",vmin=0,vmax=1)
subplot(2,2,4)
imshow(abs.(fftshift(fft(dm2.data')))/norm(abs.(fftshift(fft(dm2.data'))),Inf),cmap="jet",vmin=0,vmax=1)

maxit = 5

x1 = 0f0 .* dm
x1,his1 = lsqr!(x1,P*J*Mr,P*d_obs,atol=0f0,btol=0f0,conlim=0f0,maxiter=maxit,log=true,verbose=true)

x2 = 0f0 .* dm
x2,his2 = lsqr!(x2,J*Mr,d_obs,atol=0f0,btol=0f0,conlim=0f0,maxiter=maxit,log=true,verbose=true)

res1 = his1[:resnorm]
res2 = his2[:resnorm]

u1 = 20*log.(res1./norm(d_obs))
u2 = 20*log.(res2./norm(P*d_obs))

figure();
xlabel("iterations")
ylabel("ratio")
PyPlot.plot(res1/norm(d_obs),label="LSQR")
PyPlot.plot(res2/norm(P*d_obs),label="P-LSQR")
title("normalized least squares residual")
legend()

figure();
xlabel("iterations")
ylabel("dB")
PyPlot.plot(u1,label="LSQR")
PyPlot.plot(u2,label="P-LSQR")
title("normalized log-based least squares residual")
legend()

figure();
subplot(2,2,1)
imshow(reshape(Mr*x1,n)');title("LSQR")
subplot(2,2,2)
imshow(reshape(Mr*x2,n)');title("P-LSQR")
subplot(2,2,3)
imshow(abs.(fftshift(fft(reshape(Mr*x1,n)')))/norm(abs.(fftshift(fft(reshape(Mr*x1,n)'))),Inf),cmap="jet",vmin=0,vmax=1)
subplot(2,2,4)
imshow(abs.(fftshift(fft(reshape(Mr*x2,n)')))/norm(abs.(fftshift(fft(reshape(Mr*x2,n)'))),Inf),cmap="jet",vmin=0,vmax=1)


using DynamicPolynomials
MP = DynamicPolynomials.MultivariatePolynomials
using GroupsCore
using PermutationGroups

struct VariablePermutation <: SymbolicWedderburn.ByPermutations end

function SymbolicWedderburn.action(a::VariablePermutation, g::PermutationGroups.AbstractPerm, m::Monomial)
    v = variables(m)
    return m(v => SymbolicWedderburn.action(a, g, v))
end

function SymbolicWedderburn.action(::VariablePermutation, g::PermutationGroups.AbstractPerm, v::AbstractVector)
    return map(i -> v[i^g], eachindex(v))
end

abstract type OnMonomials <: SymbolicWedderburn.ByLinearTransformation end

function SymbolicWedderburn.action(
    a::Union{VariablePermutation,OnMonomials},
    el::GroupElement,
    term::MP.AbstractTerm,
)
    return MP.coefficient(term) * SymbolicWedderburn.action(a, el, MP.monomial(term))
end
function SymbolicWedderburn.action(
    a::Union{VariablePermutation,OnMonomials},
    el::GroupElement,
    poly::MP.AbstractPolynomial,
)
    return sum([SymbolicWedderburn.action(a, el, term) for term in MP.terms(poly)])
end

function SymbolicWedderburn.decompose(
    k::MP.AbstractPolynomialLike,
    hom::SymbolicWedderburn.InducedActionHomomorphism,
)
    # correct only if features(hom) == monomials

    indcs = [hom[mono] for mono in MP.monomials(k)]
    coeffs = MP.coefficients(k)

    return indcs, coeffs
end


using PyPlot, HDF5, MRIReco, LinearAlgebra

filename = "rawdata_heart_radial_55proj_34ch.h5"
data = permutedims(h5read(filename, "rawdata"),[3,2,1,4])
traj = permutedims(h5read(filename, "trajectory"),[3,2,1])
N = 240
Nc = 34

#############################################
# load data and form Acquisition data object
##############################################
tr = Trajectory(reshape(traj[1:2,:,:],2,:) ./ N, 55, 320, circular=false)
dat = Array{Array{Complex{Float64},2},3}(undef,1,1,1)
dat[1,1,1] = 1.e8.*reshape(data,:,Nc)
acqData = AcquisitionData(tr, dat, encodingSize=[N,N,1])

################################
# generate coil sensitivity maps
################################
@info "Espirit"
acqDataCart = regrid2d(acqData, (N,N); cgnr_iter=3)
sensitivity = espirit(acqDataCart,(6,6), 30, eigThresh_1=0.02, eigThresh_2=0.96)

##############################
# undersampled reconstructions
##############################
@info "undersampled reco"
rf = [1, 5/3, 5/2, 5]
img_cg = Vector{Array{ComplexF64,5}}(undef,4)

params = Dict{Symbol, Any}()
params[:reconSize] = (N,N)
params[:regularization] = "L2"
params[:solver] = "cgnr"
params[:senseMaps] = reshape(sensitivity, N, N, 1, Nc)
params[:iterations] = 20
params[:relTol] = 1.e-3
params[:λ] = 1.e-2

for (i,r) in enumerate(rf)
  @info "r=$r"
  # undersample profiles
  acqDataSub = convertUndersampledData(sample_kspace(acqData, Float64.(r), "regular",step=1))
  # SENSE reconstruction while monitoring error
  params[:reco] = "multiCoil"
  img_cg[i] = reconstruction(acqDataSub, params).data
end

##############
# plot figures
##############

figure("cardiac reconstructions",figsize=(6,1.9))
clf()
subplot(1,4,1); title("11 shots")
subplot(1,4,2); title("22 shots")
subplot(1,4,3); title("33 shots")
subplot(1,4,4); title("55 shots")
for (i,r) in enumerate(rf)

  subplot(1,4,5-i)
  imshow(abs.(img_cg[i][end:-1:1,end:-1:1,1,1,1]),cmap="gray")
  xticks([]);yticks([])
end
subplots_adjust(wspace=0.04,hspace=0.04,left=0.01,bottom=0.02,right=0.99,top=0.85)
savefig("Fig6.png",dpi=300)


export ScaledEuclidean, ScaledFubiniStudy, FubiniStudyDistances, FubiniStudyDistances!, EllipsoidDistances

"""
EuclideanDistances(data::Array{T,2})

Returns the pairwise euclidean distances of the columns of data, and scales them, such that the maximal pairwise distances is 1.
"""
function EuclideanDistances(data::Array{T,2}) where {T<:Number}
    n = size(data, 2)
    D = zeros(T, n, n)
    D = map(Base.Iterators.product(1:n, 1:n)) do i
        if i[1] < i[2]
            return LinearAlgebra.norm(data[:,i[1]] - data[:,i[2]])
        else
            return 0.0
        end
    end

    return (D+transpose(D))
end
function ScaledEuclidean(data::Array{T,2}) where {T<:Number}
    D = EuclideanDistances(data)
    m = maximum(D)
    return D./m
end

"""
FubiniStudyDistances(data::Array{T,2})

Returns the pairwise Fubini-Study distance of the of the columns of data.
"""
function FubiniStudyDistances(data::Array{T,2}) where {T<:Number}
    m = size(data, 2)
    norms = [LinearAlgebra.norm(data[:,i]) for i in 1:m]

    D = map(Base.Iterators.product(1:m, 1:m)) do i
        if i[1] < i[2]
            p = abs(LinearAlgebra.dot(data[:,i[1]], data[:,i[2]]) / (norms[i[1]] * norms[i[2]]))
            if p > 1
                p = 1.0
            end
            return acos(p)
        else
            return 0.0
        end
    end
    return D+transpose(D)
end
function FubiniStudyDistances!(u::Array{T,2}, data::Array{S,2}) where {T,S<:Number}
    m = size(data, 2)
    norms = [LinearAlgebra.norm(data[:,i]) for i in 1:m]

    for i in Base.Iterators.product(1:m, 1:m)
        if i[1] < i[2]
            p = abs(LinearAlgebra.dot(data[:,i[1]], data[:,i[2]]) / (norms[i[1]] * norms[i[2]]))
            if p > 1
                p = 1.0
            end
            view(u, i[1], i[2]) .= acos(p)
        else
            view(u, i[1], i[2]) .= 0.0
        end
    end
    return u+transpose(u)
end

"""
ScaledFubiniStudy(data::Array{T,2})

Returns the pairwise Fubini-Study distance of the columns of data, and scales them, such that the maximal pairwise distances is 1.
"""
function ScaledFubiniStudy(data::Array{T,2}) where {T<:Number}
    D = FubiniStudyDistances(data)
    m = maximum(D)

    return D ./ m
end

"""
SafeDehomogenization(data::Array{T,2})

Provides affine coordinates for the colums of projective data. The affine patch is safely chosen so that no point lies at infinity.
"""
function SafeDehomogenization(data::Array{T,2}) where {T<:Number}
    n = size(data,1)
    m = size(data,2)
    dots = [data[n,i] for i in 1:m]
    if sum(dots .== 0) == 0
        return hcat([data[1:n-1,i]./dots[i] for i in 1:m]...)
    else
        dots = zeros(T, m)
        u = zeros(T,n)
        check = true
        while check
            u = LinearAlgebra.normalize(rand(n))
            dots = [LinearAlgebra.dot(u, data[:,i]) for i in 1:m]
            if sum(dots .== 0) == 0
                check = false
            end
        end

        S = LinearAlgebra.svd(Diagonal{T}(I, n) - u * u')
        data = hcat([data[:,i]./dots[i] for i in 1:m]...)

        return S.U[:,1:n-1]' * data
    end
end

"""
EllipsoidDistance(data::Array{T,2}, f, λ::Number)

Returns the matrix with distances between points weighted by the radii in direction data[:,i]-data[:,j] of ellipsoids that have radius 1 in tangent direction of {f=0} and radius λ in normal direction of {f=0}.
"""

function EllipsoidDistances(data::Array{T,2}, f::Vector{S}, λ::Number) where {T<:Number, S<:MP.AbstractPolynomialLike}
    n, m = size(data)
    F = FP.System(convert(Vector{FP.Polynomial{Float64}}, f))
    cfg = FP.JacobianConfig(F)
    Js = [FP.jacobian(F, data[:,i], cfg) for i in 1:m]
    Vs = [LinearAlgebra.svd(J, full = true).V for J in Js]
    r = LinearAlgebra.rank(Js[rand(1:m)])
    Σ = LinearAlgebra.diagm(0 => [λ .* ones(r); ones(n-r)])

    Qs = [V * Σ * V' for V in Vs]
    dists = ScaledEuclidean(data)

    D = map(Base.Iterators.product(1:m, 1:m)) do i
     if i[1] < i[2]
          Q1 = Qs[i[1]]
          Q2 = Qs[i[2]]
          u = data[:,i[1]]
          v = data[:,i[2]]

          h = LinearAlgebra.normalize(v-u)

          a1 = LinearAlgebra.transpose(h) * Q1 * h
          a2 = LinearAlgebra.transpose(h) * Q2 * h
          return 2 * dists[i[1],i[2]]/(sqrt(a1[1])+sqrt(a2[1]))
      else
          return 0.0
      end
    end

    for i in findall(isnan.(D))
        D[i[1],i[2]] = 0.0
    end

    return D+transpose(D)
end

function EllipsoidDistances(data::Array{T,2}, f::S, λ::Number) where {T<:Number, S<:MP.AbstractPolynomialLike}
        EllipsoidDistances(data, [f], λ)
end

#
# """
#
# barcode_plot(C::Dict{String,Any},
#     dims::Array{Int64,1},
#     how_many_bars::Array{Int64,1};
#     sorted_by::Symbol=:lower_limit,
#     lw=10,
#     upper_limit = Inf,
#     fontsize = 16)
#
# Plots the barcode associated to the dictionary C that was produced by the eirene() function from the Eirene package. dims is an array determining which dimensions should be plotted. how_many_bars is an array that determines how many bars in each dimension are plotted. sorted_by can be either :lower_limit or :length. It is also possible to pass an array of barcodes B instead of C.
# """
# function barcode_plot(B,
#      dims::Array{Int64,1}, how_many_bars::Array{Int64,1}; sorted_by::Symbol=:lower_limit, lw=10, upper_limit = Inf, fontsize = 16)
#
#         @assert length(dims) == length(how_many_bars) "Number of dimensions (was $(length(dims))) must be the same as the number of values specifying the number of bars that should be displayed for each dimension  (was $(length(how_many_bars)))."
#
#         # colors = Colors.distinguishable_colors(length(dims)+1,[RGB(1,1,1)])[2:end]
#         cols = Colors.colormap("Blues", mid = 0.5)
#         range = Int.(round.(collect(linspace(50,100,length(dims)+1))))
#         colors = map(r -> cols[r], range)
#
#         if upper_limit == Inf
#             upper_limit = 2 * maximum([maximum(b[b.< Inf]) for b in B])
#         end
#
#         traces = map(1:length(B)) do j
#              b = B[j]
#              # lengths = b[:,2]-b[:,1]
#              # b = b[lengths .> tol[j],:]
#              if size(b,1) == 0
#                  return [PlotlyJS.scatter(;x=[0,0], y=[0,0], mode="lines",  line_width = lw, line_color = colors[j], name = "dimension $(dims[j])")]
#              end
#
#              l = minimum([size(b,1), how_many_bars[j]])
#              s = sortperm(b[:,2]-b[:,1])
#              b = b[s[end-l+1:end],:]
#
#              i = find(x->x==Inf, b[:,2])
#              b[i,2] .= upper_limit
#
#              if sorted_by == :length
#              elseif sorted_by == :lower_limit
#                 s = sortperm(b[:,1])
#                 b = b[s,:]
#              else
#                 println("The second argument must be either :length or :lower_limit.")
#                 return 0
#              end
#              return [PlotlyJS.scatter(;x=b[1,:], y=[1,1], mode="lines",  line_width = lw, line_color = colors[j], name = "Dimension $(dims[j])");
#              [PlotlyJS.scatter(;x=b[i,:], y=[i,i], mode="lines",  line_width = lw, line_color = colors[j], showlegend=false) for i in 2:l]]
#
#          end
#
#          for i = 2:length(traces)
#              for j = 1:length(traces[i])
#                  traces[i][j][:y] = traces[i][j][:y] + traces[i-1][end][:y] + 1
#              end
#          end
#          traces = vcat(traces...)
#
#
#          x = maximum(vcat([t[:x] for t in traces]...))
#          y = maximum(vcat([t[:y] for t in traces]...))
#
#          layout = PlotlyJS.Layout(;
#          xaxis = attr(range = [-.001, upper_limit+0.001], showgrid=false, zeroline =false, title = "ϵ", titlefont_size=fontsize, tickfont_size=fontsize),
#          yaxis = attr(range = [0,y+1.1], showgrid=false, ticks = false, titlefont_size=fontsize, tickfont_size=fontsize),
#          legend=attr(font_size=fontsize)
#          )
#          PlotlyJS.Plot(traces[end:-1:1], layout)
# end
# function barcode_plot(C::Dict{String,Any}, dims::Array{Int64,1}, how_many_bars::Array{Int64,1}; sorted_by::Symbol=:lower_limit, lw=10, upper_limit = Inf, fontsize = 16)
#     B = [barcode(C, dim = d) for d in dims]
#     barcode_plot(B, dims, how_many_bars, sorted_by=sorted_by, lw=lw, upper_limit = upper_limit, fontsize = fontsize)
# end
# function barcode_plot(C::Dict{String,Any}, dims::Array{Int64,1}; sorted_by::Symbol=:lower_limit, lw=10, upper_limit = Inf, fontsize = 16)
#     l = size(C["symmat"],1)
#     B = [barcode(C, dim = d) for d in dims]
#     barcode_plot(B, dims, l .* ones(Int64,length(dims)), sorted_by=sorted_by, lw=lw, upper_limit = upper_limit, fontsize = fontsize)
# end


push!(LOAD_PATH, "./")
# if you want to experiment with the "pushed" matrix creation method,
# switch comments on the two following lines:
using Sparse23
#using Sparse23push
using Printf
using SparseArrays,LinearAlgebra

function dotest(dim::Int,size::Int)
    # 1) matrix creation:
    t1 = time_ns()
    if dim==2
        M,order,nc=PreLapl2(size)
        sizeV=size^2
    else
        M,order,nc=PreLapl3(size)
        sizeV=size^3
    end
    t1 = time_ns()-t1
    V=Vector{Float64}([1 for i in 1:sizeV])
    W=Vector{Float64}([0 for i in 1:sizeV])

    # 2) matrix x vector product.
    t2 = time_ns()
    #A_mul_B!(W,M,V)
    mul!(W,M,V)

    t2 = time_ns()-t2

    return order,nc,t1,t2
end

println()
@printf "%-5s %1s %-20s\n" "size" ":" "size of the grid."
@printf "%-5s %1s %-20s\n" "order" ":" "order of the matrix."
@printf "%-5s %1s %-20s\n" "nc" ":" "number of ceofficients."
@printf "%-5s %1s %-20s\n" "T.b" ":" "time to build the matrix (s)."
@printf "%-5s %1s %-20s\n" "T.p" ":" "time for matrix x vector product (s)."

# computation starts here:
for dim=2:3
    println("\n"*string(dim)*"d:")
    fw=open("RunningOn"*gethostname()*"-"*string(dim),"w")
    if dim==2
        size=32
        sizeM=2048
    else
        size=16
        sizeM=256
    end
    @printf "%4s %8s %9s %10s %10s \n" "size" "order" "nc" "T.b" "T.p"
    while size<=sizeM

        order,nc,t1,t2=dotest(dim,size)#warmup

        order,nc,t1,t2=dotest(dim,size)

        s=@sprintf "%4d %8d %9d %10.2e %10.2e \n" size order nc float(t1)*10.0^(-9) float(t2)*10.0^(-9)
        print(s)
        write(fw,s)
        size*=2
    end
    close(fw)
end


struct TemperatureProfile{AMP, FWHM, U, V}
    # amplitude and full-width half-max functions for generalized Gaussian temperature profile
    amplitude::AMP
    width::FWHM
    # parameters governing uncertainty calculation
    dT::U
    dx::V
end

# Constructors:
# defaults to parameters of Science Advances submission
function TemperatureProfile(dT::Real = 20, dx::Real = 10/1000)
    get_temperature_profile_Bi2O3_2021(dT, dx)
end

# template for t-profile constructor
function get_temperature_profile_template(dT::Real = 20, dx::Real = 10/1000)
    println("temperature_profile_template")
    amplitude(T_max, log10_τ) = 2
    function width(T, log10_τ)
        throw("not implemented - should be a functional expression from temperature and dwell time to the fwhm of the profile")
    end
    TemperatureProfile(amplitude, width, dT, dx)
end

# temperature profile for the Bi2O3 data in the Science Advances submission
function get_temperature_profile_Bi2O3_2021(dT::Real = 20, dx::Real = 10/1000)
    amplitude(T_max, log10_τ) = 2
    function width(T, log10_τ)
        fwhm_0 = 1.60742002e+03
        a = (-1.46900821e+00, -2.06060111e+02, 3.05703021e-01, 3.29193895e+02, -1.23361265e+13, 1.09165256e+01, -1.69547934e+04, -1.42207444e-04)
        fwhm_0 + a[1]*T + a[2]*log10_τ + a[3]*log10_τ*T + a[4]*log10_τ^4/T +
            a[5]/log10_τ^8/T^3 + a[6]*T/log10_τ^3 + a[7]/log10_τ^4 + a[8]*T^3/log10_τ^8
    end
    TemperatureProfile(amplitude, width, dT, dx)
end

# OLD temperature profile for the Bi2O3 data
function get_temperature_profile_Bi2O3_2020(dT::Real = 20, dx::Real = 10/1000)
    amplitude(T_max, log10_τ) = 2
    function width(T, log10_τ)
        fwhm_0 = 6.21599199e+01
        a = (-3.56990024e-01, 2.84613876e+02, -8.90507983e-03, -5.76367089e+02, -1.95522786e+13, 5.51350589e+00, -3.34684841e+03, -1.33972469e-04)
        fwhm_0 + a[1]*T + a[2]*log10_τ + a[3]*log10_τ*T + a[4]*log10_τ^4/T +
            a[5]/log10_τ^8/T^3 + a[6]*T/log10_τ^3 + a[7]/log10_τ^4 + a[8]*T^3/log10_τ^8
    end
    TemperatureProfile(amplitude, width, dT, dx)
end

# temperature profile for CHESS 2021
function get_temperature_profile_CHESS_2021(dT::Real = 20, dx::Real = 10/1000)
    amplitude(T_max, log10_τ) = 2
    # power = current in A
    # tau = dwell in us
    function _std(power, log10_τ)
        mm_per_pixel = 0.00153846153 # millimeters per pixel
        μm_per_pixel = mm_per_pixel * 1e3 # micrometers per pixel
        μm_per_pixel * (-6.0585 * power + 5.29285 * log10_τ + 1.432e-2 * power^2 - 1.59943e1 *log10_τ^2 + 7.6715e-1 * power*log10_τ + 726.0242)
    end
    _power(T_max::Int, log10_τ) = _power(float(T_max), float(log10_τ))
    _power(T_max, log10_τ) = (5000*sqrt(5)*sqrt(614000*T_max+1327591125*log10_τ^2 -8080505000*log10_τ+12142813538)-341554591*log10_τ+1065621710)/3070000
    _fwhm(power, log10_τ) = _std(power, log10_τ) / 2 # by the definitions of variables in the Gaussian-like function Max used
    width(T_peak, log10_τ) = _fwhm(_power(T_peak, log10_τ), log10_τ) # getting the power from the power profile
    TemperatureProfile(amplitude, width, dT, dx)
end

# Functor definitions:
# WARNING: temperature profile is a function of position in mm
function (P::TemperatureProfile)(T_max::Real, log10_τ::Real)
    a = P.amplitude(T_max, log10_τ)
    f = P.width(T_max, log10_τ)
    x->_T_helper(T_max, f, a, 1e3*x) # multiplying position by 1e3 to convert from mm to μm
end
function (P::TemperatureProfile)(T_max::Real, log10_τ::Real, x::Real)
    P(T_max, log10_τ)(x)
end
# WARNING: temperature profile is a function of position in μm
function _T_helper(T_max, fwhm, ampl, x::Real)
    return @. T_max * exp(-abs(2x / fwhm)^ampl)
end

profile(P::TemperatureProfile, T_max, log10_τ) = P(T_max, log10_τ)
# returns the positive side of the inverse mapping T to position in mm
function inverse_profile(P::TemperatureProfile, T_max::Real, log10_τ::Real)
    a = P.amplitude(T_max, log10_τ)
    f = P.width(T_max, log10_τ)
    function invprof(T)
        T > 0 || throw(DomainError("T is non-positive: $T"))
        T_max ≥ T || throw(DomainError("T exceeds T_max: T = $T > $T_max = T_max"))
        (T ≈ T_max) ? zero(eltype(T)) : log(T_max / T)^(1/a) * f/2 * (1e-3)
    end
end

# x is a position
# computes uncertainty of the temperature profile
# Two components:
# - multiplicative and
# - proportional to derivative
function temperature_uncertainty(P::TemperatureProfile, T_max, log10_τ, dT::Real = P.dT, dx::Real = P.dx)
    f = P(T_max, log10_τ)
    function temperature_variance(x::Real)
        T, dTdx = f(x), ForwardDiff.derivative(f, x)
        (dT * T / 1400)^2 + (dx * dTdx)^2 # error variance is additive
    end
end


#
# Computes the total potential energy of the system and
# the force on every particle. Modifies input vector f
#
function energyandforce!(p,f,data)
  utotal = 0.
  for v in f
    v .= 0.
  end
  for ip in 1:data.N-1
    for jp in ip+1:data.N
      wrap!(p[ip],p[jp],data.side)
      rij = sqrt( (p[ip][1]-p[jp][1])^2 + (p[ip][2]-p[jp][2])^2 )
      if rij < data.cutoff
        upair, fpair = forcepair(p[ip],p[jp],rij,data)
        utotal += upair
        f[ip] .= f[ip] .+ fpair
        f[jp] .= f[jp] .- fpair
      end
    end
  end
  return utotal, f
end


## SCALE TRANSFORMATIONS

#     Scale = SCALE()

# Object for dispatching on scales and functions when generating
# parameter ranges. We require different behaviour for scales and
# functions:

#      transform(Scale, scale(:log10), 100) = 2
#      inverse_transform(Scale, scale(:log10), 2) = 100

# but
#     transform(Scale, scale(log10), 100) = 100       # identity
#     inverse_transform(Scale, scale(log10), 100) = 2

struct SCALE end
Scale = SCALE()

scale(s::Symbol)   = Val(s)
scale(f::Function) = f

transform(::SCALE, ::Val{:linear}, x) = x
inverse_transform(::SCALE, ::Val{:linear}, x) = x

transform(::SCALE, ::Val{:log}, x) = log(x)
inverse_transform(::SCALE, ::Val{:log}, x) = exp(x)

transform(::SCALE, ::Val{:logminus}, x) = log(-x)
inverse_transform(::SCALE, ::Val{:logminus}, x) = -exp(x)

transform(::SCALE, ::Val{:log10minus}, x) = log10(-x)
inverse_transform(::SCALE, ::Val{:log10minus}, x) = -10^x

transform(::SCALE, ::Val{:log10}, x) = log10(x)
inverse_transform(::SCALE, ::Val{:log10}, x) = 10^x

transform(::SCALE, ::Val{:log2}, x) = log2(x)
inverse_transform(::SCALE, ::Val{:log2}, x) = 2^x

transform(::SCALE, f::Function, x) = x            # not a typo!
inverse_transform(::SCALE, f::Function, x) = f(x) # not a typo!

## SCALE INSPECTION (FOR EG PLOTTING)

"""
    scale(r::ParamRange)

Return the scale associated with a `ParamRange` object `r`. The possible
return values are: `:none` (for a `NominalRange`), `:linear`, `:log`, `:log10`,
`:log2`, or `:custom` (if `r.scale` is a callable object).
"""
scale(r::NominalRange) = :none
scale(r::NumericRange) = :custom
scale(r::NumericRange{T,B,Symbol}) where {B<:Boundedness,T} = r.scale

## ITERATOR METHOD (FOR GENERATING A 1D GRID)

"""
    iterator([rng, ], r::NominalRange, [,n])
    iterator([rng, ], r::NumericRange, n)

Return an iterator (currently a vector) for a `ParamRange` object `r`.
In the first case iteration is over all `values` stored in the range
(or just the first `n`, if `n` is specified). In the second case, the
iteration is over approximately `n` ordered values, generated as
follows:

(i) First, exactly `n` values are generated between `U` and `L`, with a
spacing determined by `r.scale` (uniform if `scale=:linear`) where `U`
and `L` are given by the following table:

| `r.lower`   | `r.upper`  | `L`                 | `U`                 |
|-------------|------------|---------------------|---------------------|
| finite      | finite     | `r.lower`           | `r.upper`           |
| `-Inf`      | finite     | `r.upper - 2r.unit` | `r.upper`           |
| finite      | `Inf`      | `r.lower`           | `r.lower + 2r.unit` |
| `-Inf`      | `Inf`      | `r.origin - r.unit` | `r.origin + r.unit` |

(ii) If a callable `f` is provided as `scale`, then a uniform spacing
is always applied in (i) but `f` is broadcast over the results. (Unlike
ordinary scales, this alters the effective range of values generated,
instead of just altering the spacing.)

(iii) If `r` is a discrete numeric range (`r isa NumericRange{<:Integer}`)
then the values are additionally rounded, with any duplicate values
removed. Otherwise all the values are used (and there are exacltly `n`
of them).

(iv) Finally, if a random number generator `rng` is specified, then the values are
returned in random order (sampling without replacement), and otherwise
they are returned in numeric order, or in the order provided to the
range constructor, in the case of a `NominalRange`.

"""
iterator(rng::AbstractRNG, r::ParamRange, args...) =
    Random.shuffle(rng, iterator(r, args...))

iterator(r::NominalRange, ::Nothing) = iterator(r)
iterator(r::NominalRange, n::Integer) =
    collect(r.values[1:min(n, length(r.values))])
iterator(r::NominalRange) = collect(r.values)

# numeric range, top level dispatch

function iterator(r::NumericRange{T,<:Bounded},
                  n::Int) where {T<:Real}
    L = r.lower
    U = r.upper
    return iterator(T, L, U, r.scale, n)
end

function iterator(r::NumericRange{T,<:LeftUnbounded},
                  n::Int) where {T<:Real}
    L = r.upper - 2r.unit
    U = r.upper
    return iterator(T, L, U, r.scale, n)
end

function iterator(r::NumericRange{T,<:RightUnbounded},
                  n::Int) where {T<:Real}
    L = r.lower
    U = r.lower + 2r.unit
    return iterator(T, L, U, r.scale, n)
end

function iterator(r::NumericRange{T,<:DoublyUnbounded},
                  n::Int) where {T<:Real}
    L = r.origin - r.unit
    U = r.origin + r.unit
    return iterator(T, L, U, r.scale, n)
end

# middle level

iterator(::Type{<:Real}, L, U, s, n) =
    iterator(L, U, s, n)

function iterator(I::Type{<:Integer}, L, U, s, n)
    raw = iterator(L, U, s, n)
    rounded = map(x -> round(I, x), raw)
    return unique(rounded)
end

# ground level

# if scale `s` is a callable (the fallback):
function iterator(L, U, s, n)
    return s.(range(L, stop=U, length=n))
end

# if scale is a symbol:
function iterator(L, U, s::Symbol, n)
    transformed = range(transform(Scale, scale(s), L),
                stop=transform(Scale, scale(s), U),
                        length=n)
    inverse_transformed = map(transformed) do value
        inverse_transform(Scale, scale(s), value)
    end
    return inverse_transformed
end

## FITTING DISTRIBUTIONS TO A RANGE

### Helper

function _truncated(d::Dist.Distribution, r::NumericRange)
    if minimum(d) >= r.lower && maximum(d) <= r.upper
        return d
    else
        return Dist.truncated(d, r.lower, r.upper)
    end
end

### Fallback and docstring

"""
    Distributions.fit(D, r::MLJBase.NumericRange)

Fit and return a distribution `d` of type `D` to the one-dimensional
range `r`.

Only types `D` in the table below are supported.

The distribution `d` is constructed in two stages. First, a
distributon `d0`, characterized by the conditions in the second column
of the table, is fit to `r`. Then `d0` is truncated between `r.lower`
and `r.upper` to obtain `d`.

Distribution type `D`  | Characterization of `d0`
:----------------------|:-------------------------
`Arcsine`, `Uniform`, `Biweight`, `Cosine`, `Epanechnikov`, `SymTriangularDist`, `Triweight` |   `minimum(d) = r.lower`, `maximum(d) = r.upper`
`Normal`, `Gamma`, `InverseGaussian`, `Logistic`, `LogNormal` | `mean(d) = r.origin`, `std(d) = r.unit`
`Cauchy`, `Gumbel`, `Laplace`, (`Normal`) | `Dist.location(d) = r.origin`, `Dist.scale(d)  = r.unit`
`Poisson` | `Dist.mean(d) = r.unit`

Here `Dist = Distributions`.

"""
Dist.fit(::Type{D}, r::NumericRange) where D<:Distributions.Distribution =
    throw(ArgumentError("Fitting distributions of type `$D` to "*
          "`NumericRange` objects is unsupported. "*
          "Try passing an explicit instance, or a supported type. "))

### Continuous support

##### bounded

for D in [:Arcsine, :Uniform]
    @eval Dist.fit(::Type{<:Dist.$D}, r::NumericRange) =
        Dist.$D(r.lower, r.upper)
end
for D in [:Biweight, :Cosine, :Epanechnikov, :SymTriangularDist, :Triweight]
    @eval Dist.fit(::Type{<:Dist.$D}, r::NumericRange) =
        Dist.$D(r.origin, r.unit)
end

##### doubly-unbounded

# corresponding to values of `Dist.location` and `Dist.scale`:
for D in [:Cauchy, :Gumbel, :Normal, :Laplace]
    @eval Dist.fit(::Type{<:Dist.$D}, r::NumericRange) =
        _truncated(Dist.$D(r.origin, r.unit), r)
end

# Logistic:
function Dist.fit(::Type{<:Dist.Logistic}, r::NumericRange)
    μ = r.origin
    θ = sqrt(3)*r.unit/pi
    return _truncated(Dist.Logistic(μ, θ), r)
end

#### right-unbounded

# Gamma:
function Dist.fit(::Type{<:Dist.Gamma}, r::NumericRange)
    α = (r.origin/r.unit)^2
    θ = r.origin/α
    _truncated(Dist.Gamma(α, θ), r)
end

# InverseGaussian:
function Dist.fit(::Type{<:Dist.InverseGaussian}, r::NumericRange)
    mu = r.origin
    lambda = mu^3/r.unit^2
    return _truncated(Dist.InverseGaussian(mu, lambda), r)
end

# LogNormal:
function Dist.fit(::Type{<:Dist.LogNormal}, r::NumericRange)
    sig2 = log((r.unit/r.origin)^2 + 1)
    sig = sqrt(sig2)
    mu = log(r.origin) - sig2/2
    return _truncated(Dist.LogNormal(mu, sig), r)
end

### Discrete support

# Poisson:
function Dist.fit(::Type{<:Dist.Poisson}, r::NumericRange)
    _truncated(Dist.Poisson(r.unit), r)
end

## SAMPLER (FOR RANDOM SAMPLING A 1D RANGE)

### Numeric case

struct NumericSampler{T,D<:Distributions.Sampleable,S}
    distribution::D
    scale::S
    NumericSampler(::Type{T}, d::D, s::S) where {T,D,S} = new{T,D,S}(d,s)
end

function Base.show(stream::IO,
                   s::NumericSampler{T,D}) where {T,D}
    repr = "NumericSampler{$T,$D}}"
    s.scale isa Symbol || (repr = "transformed "*repr)
    print(stream, repr)
    return nothing
end

# constructor for distribution *instances*:
"""
    sampler(r::NominalRange, probs::AbstractVector{<:Real})
    sampler(r::NominalRange)
    sampler(r::NumericRange{T}, d)

Construct an object `s` which can be used to generate random samples
from a `ParamRange` object `r` (a one-dimensional range) using one of
the following calls:

    rand(s)             # for one sample
    rand(s, n)          # for n samples
    rand(rng, s [, n])  # to specify an RNG

The argument `probs` can be any probability vector with the same
length as `r.values`. The second `sampler` method above calls the
first with a uniform `probs` vector.

The argument `d` can be either an arbitrary instance of
`UnivariateDistribution` from the Distributions.jl package, or one of
a Distributions.jl *types* for which `fit(d, ::NumericRange)` is
defined. These include: `Arcsine`, `Uniform`, `Biweight`, `Cosine`,
`Epanechnikov`, `SymTriangularDist`, `Triweight`, `Normal`, `Gamma`,
`InverseGaussian`, `Logistic`, `LogNormal`, `Cauchy`, `Gumbel`,
`Laplace`, and `Poisson`; but see the doc-string for
[`Distributions.fit`](@ref) for an up-to-date list.

If `d` is an *instance*, then sampling is from a truncated form of the
supplied distribution `d`, the truncation bounds being `r.lower` and
`r.upper` (the attributes `r.origin` and `r.unit` attributes are
ignored). For discrete numeric ranges (`T <: Integer`) the samples are
rounded.

If `d` is a *type* then a suitably truncated distribution is
automatically generated using `Distributions.fit(d, r)`.

*Important.* Values are generated with no regard to `r.scale`, except
in the special case `r.scale` is a callable object `f`. In that case,
`f` is applied to all values generated by `rand` as described above
(prior to rounding, in the case of discrete numeric ranges).

### Examples

    r = range(Char, :letter, values=collect("abc"))
    s = sampler(r, [0.1, 0.2, 0.7])
    samples =  rand(s, 1000);
    StatsBase.countmap(samples)
    Dict{Char,Int64} with 3 entries:
      'a' => 107
      'b' => 205
      'c' => 688

    r = range(Int, :k, lower=2, upper=6) # numeric but discrete
    s = sampler(r, Normal)
    samples = rand(s, 1000);
    UnicodePlots.histogram(samples)
               ┌                                        ┐
    [2.0, 2.5) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 119
    [2.5, 3.0) ┤ 0
    [3.0, 3.5) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 296
    [3.5, 4.0) ┤ 0
    [4.0, 4.5) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 275
    [4.5, 5.0) ┤ 0
    [5.0, 5.5) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 221
    [5.5, 6.0) ┤ 0
    [6.0, 6.5) ┤▇▇▇▇▇▇▇▇▇▇▇ 89
               └                                        ┘

"""
Distributions.sampler(r::NumericRange{T},
                      d::Distributions.UnivariateDistribution) where T =
                          NumericSampler(T, _truncated(d, r), r.scale)

# constructor for distribution *types*:
Distributions.sampler(r::NumericRange,
                      D::Type{<:Dist.UnivariateDistribution}) =
                          sampler(r, Dist.fit(D, r))

# rand fallbacks (non-integer ranges):
Base.rand(s::NumericSampler, dims::Integer...) =
    s.scale.(rand(s.distribution, dims...))
Base.rand(rng::AbstractRNG, s::NumericSampler, dims::Integer...) =
    s.scale.(rand(rng, s.distribution, dims...))
Base.rand(s::NumericSampler{<:Any,<:Dist.Sampleable,Symbol},
          dims::Integer...) = rand(s.distribution, dims...)
Base.rand(rng::AbstractRNG,
          s::NumericSampler{<:Any,<:Dist.Sampleable,Symbol},
          dims::Integer...) =
              rand(rng, s.distribution, dims...)

# rand for integer ranges:
Base.rand(s::NumericSampler{I}, dims::Integer...) where I<:Integer =
    map(x -> round(I, s.scale(x)), rand(s.distribution, dims...))
Base.rand(rng::AbstractRNG,
          s::NumericSampler{I},
          dims::Integer...) where I<:Integer =
    map(x -> round(I, s.scale(x)), rand(rng, s.distribution, dims...))
Base.rand(s::NumericSampler{I,<:Dist.Sampleable,Symbol},
          dims::Integer...) where I<:Integer  =
              map(x -> round(I, x), rand(s.distribution, dims...))
Base.rand(rng::AbstractRNG,
          s::NumericSampler{I,<:Dist.Sampleable,Symbol},
          dims::Integer...) where I<:Integer =
              map(x -> round(I, x), rand(rng, s.distribution, dims...))

## Nominal case:

struct NominalSampler{T,N,D<:Distributions.Sampleable} <: MLJType
    distribution::D
    values::NTuple{N,T}
    NominalSampler(::Type{T}, d::D, values::NTuple{N,T}) where {T,N,D} =
        new{T,N,D}(d, values)
end

function Base.show(stream::IO,
                   s::NominalSampler{T,N,D}) where {T,N,D}
    samples = round3.(s.values)
    seqstr = sequence_string(s.values)
    repr = "NominalSampler($seqstr)"
    print(stream, repr)
    return nothing
end

# constructor for probability vectors:
function Distributions.sampler(r::NominalRange{T},
                 probs::AbstractVector{<:Real}) where T
    length(probs) == length(r.values) ||
        error("Length of probability vector must match number "*
              "of range values. ")
    return NominalSampler(T, Distributions.Categorical(probs), r.values)
end

# constructor for uniform sampling:
function Distributions.sampler(r::NominalRange{T,N}) where {T, N}
    return sampler(r, fill(1/N, N))
end

Base.rand(s::NominalSampler, dims::Integer...) where I<:Integer =
    broadcast(idx -> s.values[idx], rand(s.distribution, dims...))
Base.rand(rng::AbstractRNG,
          s::NominalSampler,
          dims::Integer...) where I<:Integer =
    broadcast(idx -> s.values[idx], rand(rng, s.distribution, dims...))

## SCALE METHOD FOR SAMPLERS

# these mimick the definitions for 1D ranges above:
scale(::Any) = :none
scale(::NumericSampler) = :custom
scale(s::NumericSampler{<:Any,<:Distributions.Sampleable,Symbol}) = s.scale


# -------------------------------------------------------------
# Repurposed from https://github.com/tbreloff/OnlineAI.jl
# -------------------------------------------------------------

"""
Calculate the amount `Δwᵢⱼ` to adjust the parameter.  Signatures:

```
    param_change!(state::ParameterUpdaterState, updater::ParameterUpdater, ploss::ParameterLoss, gradient::Real, param::Real)
    param_change!(state::ParameterUpdaterState, updater::ParameterUpdater, gradient::Real)
```

The first allows for an additional `ParameterLoss` to be added to the final gradient.
"""
function param_change!(state::ParameterUpdaterState, updater::ParameterUpdater, ploss::ParameterLoss, gradient::Real, param::Real)
    ∇ = gradient + deriv(ploss, param)
    param_change!(state, updater, ∇)
end

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

"Stochastic Gradient Descent with Momentum"
type SGDUpdater <: ParameterUpdater
    η::Float64 # learning rate
    μ::Float64 # momentum
end
SGDUpdater(; η=0.1, μ=0.5) = SGDUpdater(η, μ)

type SGDState <: ParameterUpdaterState
    lastChange::Float64
end
SGDState() = SGDState(0.0)

function param_change!(state::SGDState, updater::SGDUpdater, gradient::Real)
    state.lastChange = -updater.η * gradient + updater.μ * state.lastChange
end

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

"Adaptive Gradient"
type AdagradUpdater <: ParameterUpdater
    ε::Float64  # try 0.01?
    η::Float64 # base learning rate (numerator)
end
AdagradUpdater(; ε=1e-8, η=1.0) = AdagradUpdater(ε, η)

type AdagradState <: ParameterUpdaterState
    G::Float64
end
AdagradState() = AdagradState(0.0)

function param_change!(state::AdagradState, updater::AdagradUpdater, gradient::Real)
    state.G += gradient^2
    η = updater.η / sqrt(updater.ε + state.G)
    -η * gradient
end

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

"""
See: ADADELTA: An Adaptive Learning Rate Method (Zeiler 2012)

Relatively parameter-free... can probably avoid changing ε and ρ
"""
type AdadeltaUpdater <: ParameterUpdater
    ε::Float64  # try 0.01?
    η::Float64
    ρ::Float64  # try 0.97?
end
AdadeltaUpdater(; ε=1e-8, η=0.1, ρ=0.95) = AdadeltaUpdater(ε, η, ρ)

type AdadeltaState <: ParameterUpdaterState
    dMean::Float64
    GMean::Float64
end
AdadeltaState() = AdadeltaState(0.0, 0.0)

function param_change!(state::AdadeltaState, updater::AdadeltaUpdater, gradient::Real)
    ε, ρ = updater.ε, updater.ρ

    # average g²
    state.GMean = ρ * state.GMean + (1.0 - ρ) * gradient^2

    # compute learning rate from previous average dw² and current average g²
    η = updater.η * sqrt(state.dMean + ε) / sqrt(state.GMean + ε)

    # compute change and update average dw²
    dij = -η * gradient
    state.dMean = ρ * state.dMean + (1.0 - ρ) * dij^2
    dij
end

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

"""
see: ADAM: A method for Stochastic Optimization (Kingma and Ba 2015)

Tracks an exponential moving average of the first and second moments of the gradient,
adjusting for zero-bias.  The defaults are those suggested in the paper.

TODO: AdaMax is similar, using the p-norm as p -> ∞
"""
type AdamUpdater <: ParameterUpdater
    ε::Float64  # small number so we don't divide by 0
    η::Float64  # learning rate... (this is α in the paper) maybe use around 1e-3?
    ρ1::Float64 # decay for first moment (β₁ in the paper)
    ρ2::Float64 # decay for second moment (β₂ in the paper)
end
AdamUpdater(; ε=1e-8, η=1e-3, ρ1=0.9, ρ2=0.999) = AdamUpdater(ε, η, ρ1, ρ2)

immutable AdamState{T<:Number, M<:AbstractArray{T}} <: ParameterUpdaterState
    m::M      # average first moment
    v::M      # average second moment
    ρ1t::M    # β₁ᵗ from the paper... t-th power of β₁
    ρ2t::M    # β₂ᵗ from the paper... t-th power of β₂
end
AdamState() = AdamState(0.0, 0.0, 1.0, 1.0)

function param_change!(state::AdamState{T,M}, updater::AdamUpdater, gradient::M)
    ρ1, ρ2 = updater.ρ1, updater.ρ2
    state.m = ρ1 * state.m + (1.0 - ρ1) * gradient
    state.v = ρ2 * state.v + (1.0 - ρ2) * gradient^2
    state.ρ1t *= updater.ρ1
    state.ρ2t *= updater.ρ2
    ηt = updater.η * (sqrt(1.0 - state.ρ2t) / (1.0 - state.ρ1t))
    -ηt * state.m / (sqrt(state.v + updater.ε))
end

function param_change!(state::AdamState{T,M}, updater::AdamUpdater, gradient::T, i::Int...)

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

"""
see: ADAM: A method for Stochastic Optimization (Kingma and Ba 2015)

AdaMax is similar to Adam, using the p-norm as p -> ∞
"""
type AdaMaxUpdater <: ParameterUpdater
    # ε::Float64  # small number so we don't divide by 0
    η::Float64  # learning rate... (this is α in the paper) maybe use around 1e-3?
    ρ1::Float64 # decay for first moment (β₁ in the paper)
    ρ2::Float64 # decay for second moment (β₂ in the paper)
end
AdaMaxUpdater(; η=1e-3, ρ1=0.9, ρ2=0.99) = AdaMaxUpdater(η, ρ1, ρ2)

type AdaMaxState <: ParameterUpdaterState
    m::Float64      # average first moment
    u::Float64      # average second moment
    ρ1t::Float64    # β₁ᵗ from the paper... t-th power of β₁
    # ρ2t::Float64  # β₂ᵗ from the paper... t-th power of β₂
end
AdaMaxState() = AdaMaxState(0.0, 0.0, 1.0)

function param_change!(state::AdaMaxState, updater::AdaMaxUpdater, gradient::Real)
    ρ1 = updater.ρ1
    mij = ρ1 * state.m + (1.0 - ρ1) * gradient
    state.m = mij
    uij = max(updater.ρ2 * state.u, abs(gradient))
    state.u = uij
    state.ρ1t *= ρ1
    -updater.η * mij / ((uij + 1e-10) * (1.0 - state.ρ1t[1]))
end

# -------------------------------------------------------------
# Constructors for updater states
# -------------------------------------------------------------

get_state_type(::Type{SGDUpdater})      = SGDState
get_state_type(::Type{AdagradUpdater})  = AdagradState
get_state_type(::Type{AdadeltaUpdater}) = AdadeltaState
get_state_type(::Type{AdamUpdater})     = AdamState
get_state_type(::Type{AdaMaxUpdater})   = AdaMaxState

# single state
ParameterUpdaterState{U<:ParameterUpdater}(::Type{U}) = get_state_type(U)()
ParameterUpdaterState(updater::ParameterUpdater) = ParameterUpdaterState(typeof(updater))

# arrays of states
function ParameterUpdaterState{U<:ParameterUpdater}(::Type{U}, dims::Integer...)
    T = get_state_type(U)
    states = Array(T, dims...)
    for i in eachindex(states)
        states[i] = T()
    end
    states
end
ParameterUpdaterState(updater::ParameterUpdater, dims::Integer...) = ParameterUpdaterState(typeof(updater), dims...)

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


@testset "NfAbsNS" begin

  Qx, x = FlintQQ["x"]
  K, (a, b) = @inferred number_field([x^2 - 2, x^3 - 3])

  @test K isa NfAbsNS
  @test elem_type(NfAbsNS) == NfAbsNSElem
  @test parent_type(NfAbsNSElem) == NfAbsNS
  @test show_minus_one(NfAbsNSElem) isa Bool
  @test !issimple(K)
  @test !issimple(NfAbsNS)

  @testset "Basics" begin
    @test FlintQQ == @inferred base_ring(K)
    @test 6 == @inferred degree(K)
    @test [2, 3] == @inferred degrees(K)
    @test 2 == @inferred ngens(K)
    @test [a, b] == @inferred gens(K)
    @test [one(K), a, b, a * b, b^2, a * b^2] == @inferred basis(K)
    @test string(K) isa String
    @test string(a) isa String
  end

  @testset "Elements" begin
    c = @inferred deepcopy(a)
    @test parent(c) === K
    d = @inferred rand(K, -10:10)
    @test parent(d) === K
    e = @inferred zero(K)
    @test @inferred iszero(e)
    e = @inferred one(K)
    @test @inferred isone(e)

    for i in 1:10
      z = @inferred rand(K, -10:10)
      @test z == @inferred K(data(z))
    end

    for i in 1:10
      z = rand(K, -10:10)

      @test @inferred ==(z, z)
      @test !(@inferred ==(z, z + 1))

      zm = @inferred -z
      @test iszero(zm + z)

      y = rand(K, -10:10)
      w = rand(K, -10:10)
      @test z * y + w * y == @inferred (z + w) *y

      while iszero(z)
        z = rand(K, -10:10)
      end
      zinv = @inferred inv(z)
      @test isone(@inferred z * zinv)

      for T in [Int, BigInt, fmpq, fmpz]
        u = rand(-10:10)
        while iszero(u)
          u = rand(-10:10)
        end
        @test y + u == y + u
        @test y - u == -(u - y)
        @test y * u == u * y
        @test y//u == inv(K(u)//y)
      end

      u = rand(-10:10)
      @test @inferred z^u == @inferred z^fmpz(u)

      M = zero_matrix(FlintQQ, 1, 6)
      Hecke.elem_to_mat_row!(M, 1, z)
      @test z == sum(M[1, j] * basis(K)[j] for j in 1:6)
      M = matrix(FlintQQ, 1, 6, [rand(-10:10) for j in 1:6])
      zz = Hecke.elem_from_mat_row(K, M, 1)
      @test zz == sum(M[1, j] * basis(K)[j] for j in 1:6)

      f = @inferred minpoly(z)
      @test length(factor(f)) == 1 && first(factor(f))[2] == 1
      @test ismonic(f)
      @test iszero(f(z))

      f = @inferred Hecke.minpoly_sparse(z)
      @test length(factor(f)) == 1 && first(factor(f))[2] == 1
      @test ismonic(f)
      @test iszero(f(z))

      f = @inferred Hecke.minpoly_dense(z)
      @test length(factor(f)) == 1 && first(factor(f))[2] == 1
      @test ismonic(f)
      @test iszero(f(z))

      f = @inferred charpoly(z)
      @test length(factor(f)) == 1
      @test ismonic(f)
      @test iszero(f(z))
      @test degree(f) == 6

      M = @inferred representation_matrix(z)
      for i in 1:nrows(M)
        @test Hecke.elem_from_mat_row(K, M, i) == z * basis(K)[i]
      end

      n = @inferred norm(z)
      @test n == det(M)
      t = @inferred tr(z)
      @test t == tr(M)
    end
  end

  @testset "Morphisms" begin
    L, m = @inferred simple_extension(K)
    LL,  = number_field(x^6-6*x^4-6*x^3+12*x^2-36*x+1)
    @test isisomorphic(L, LL)[1]
    for i in 1:100
      z = rand(L, -10:10)
      zz = rand(L, -10:10)
      while iszero(z)
        z = rand(L, -10:10)
      end

      @test @inferred m(z + zz) == m(z) + m(zz)
      @test @inferred m(z * zz) == m(z) * m(zz)
      @test @inferred m(-z) == -m(z)
      @test @inferred m(inv(z)) == inv(m(z))
      @test m\m(z) == z
    end

    for i in 1:100
      z = rand(K, -10:10)
      zz = rand(K, -10:10)
      while iszero(z)
        z = rand(K, -10:10)
      end

      @test @inferred m\(z + zz) == m\(z) + m\(zz)
      @test @inferred m\(z * zz) == m\(z) * (m\(zz))
      @test @inferred m\(-z) == -(m\(z))
      @test @inferred m\(inv(z)) == inv(m\z)
      @test m(m\z) == z
    end
  end

  @testset "Maximal order" begin
    K2,  = @inferred number_field([x^2 - 50, x^3 - 3])
    O = EquationOrder(K2)
    Omax = @inferred MaximalOrder(O)
    @test discriminant(Omax) == FlintZZ(30233088)
  end
end


abstract SalvagingAlgo
type NoneSalvagingAlgo <: SalvagingAlgo end

function normalize_pseudo_root!(matrix::Matrix, pseudo::Matrix)
  sz = size(matrix)[1]
  sz == size(pseudo)[1] || error("matrix / pseudo mismatch")

  pseudoCols = size(pseudo)[2]
  # row normalization
  for i = 1:sz
    norm = 0.0
    for j = 1:pseudoCols
      norm += pseudo[i,j] * pseudo[i, j]
    end

    if norm > 0.0
      normAdj = sqrt(matrix[i, i] / norm)
      for j = 1:pseudoCols
        pseudo[i, j] *= normAdj
      end
    end
  end

  return pseudo
end

function rank_reduced_sqrt(matrix::Matrix, maxRank::Int, componentRetainedPercentage::Float64, sa::NoneSalvagingAlgo)
  sz = size(matrix)[1]
  # Symmetric Schur decomp
  jd = schurfact(matrix)
  eigenVals = jd[:values]

  # eigen vals are sorted in decreasing order
  eigenVals[end] >= -1e-16 || error("negative eigenvalue(s)")

  # factor reduction
  enough = componentRetainedPercentage * sum(eigenVals)

  if componentRetainedPercentage == 1.0
    # numerical glitches might cause some factors to be discarded
    enough *= 1.1
  end

  # retain at least one factor
  components = eigenVals[1]
  retainedFactors = 1
  i = 2
  while components < enough && i <= sz
    components += eigenVals[i]
    retainedFactors += 1
    i += 1
  end

  # output is granted to have a rank <= maxrank
  retainedFactors = min(retainedFactors, maxRank)

  diagonal = zeros(sz, retainedFactors)
  for i = 1:retainedFactors
    diagonal[i, i] = sqrt(eigenVals[i])
  end

  result = jd[:vectors] * diagonal

  normalize_pseudo_root!(matrix, result)

  return result
end

function ql_norm_squared(v::Matrix{Float64}, row::Int)
  x = 0.0
  for i = 1:size(v)[2]
    x += v[row, i] * v[row, i]
  end

  return x
end

function ql_norm(v::Matrix{Float64}, row::Int)
  return sqrt(ql_norm_squared(v, row))
end

function inner_product(v::Matrix{Float64}, row1::Int, w::Matrix{Float64}, row2::Int)
  x = 0.0
  for i = 1:size(v)[2]
    x += v[row1, i] * w[row2, i]
  end

  return x
end

type OrthogonalProjection
  originalVectors::Matrix{Float64}
  multiplierCutoff::Float64
  numberVectors::Int
  numberValidVectors::Int
  dimension::Int
  validVectors::BitArray{1}
  projectedVectors::Vector{Vector{Float64}}
  orthoNormalizedVectors::Matrix{Float64}
end

function OrthogonalProjection(originalVectors::Matrix{Float64}, multiplierCutoff::Float64, tolerance::Float64)
  numberVectors, dimension = size(originalVectors)
  validVectors = trues(numberVectors)
  orthoNormalizedVectors = Matrix{Float64}(numberVectors, dimension)
  projectedVectors = Vector{Vector{Float64}}()

  currentVector = Vector{Float64}(dimension)
  for j in eachindex(validVectors)
    if validVectors[j]
      for k = 1:numberVectors
        for m = 1:dimension
          orthoNormalizedVectors[k, m] = originalVectors[k, m]
        end

        if k != j && validVectors[k]
          for l = 1:k
            if validVectors[l] && l != j
              dotProduct = inner_product(orthoNormalizedVectors, k, orthoNormalizedVectors, l)
              for n = 1:dimension
                orthoNormalizedVectors[k, n] -= dotProduct * orthoNormalizedVectors[l, n]
              end
            end
          end

          normBeforeScaling = ql_norm(orthoNormalizedVectors, k)

          if normBeforeScaling < tolerance
            validVectors[k] = false
          else
            normBeforeScalingRecip = 1.0/normBeforeScaling
            for m = 1:dimension
              orthoNormalizedVectors[k, m] *= normBeforeScalingRecip
            end
          end # end of else (norm < tolerance)
        end # end of if k != j && validVectors[k]
      end # end of for k = 1:numberVectors
      # we now have an o.n. basis for everything except j
      prevNormSquared = ql_norm_squared(originalVectors, j)

      for r = 1:numberVectors
        if validVectors[r] && r != j
          dotProduct = inner_product(orthoNormalizedVectors, j, orthoNormalizedVectors, r)
          for s = 1:dimension
            orthoNormalizedVectors[j, s] -= dotProduct * orthoNormalizedVectors[r, s]
          end
        end
      end

      projectionOnOriginalDirection = inner_product(originalVectors, j, orthoNormalizedVectors, j)
      sizeMultiplier = prevNormSquared / projectionOnOriginalDirection

      if abs(sizeMultiplier) < multiplierCutoff
        for t = 1:dimension
          currentVector[t] = orthoNormalizedVectors[j, t] * sizeMultiplier
        end
      else
        validVectors[j] = false
      end
    end # end of validVectors[j]

    push!(projectedVectors, copy(currentVector))
  end # end of j loop

  numberValidVectors = 0
  for i in eachindex(validVectors)
    numberValidVectors += validVectors[i] ? 1 : 0
  end

  return OrthogonalProjection(originalVectors, multiplierCutoff, numberVectors, numberValidVectors, dimension, validVectors, projectedVectors, orthoNormalizedVectors)
end

get_vector(op::OrthogonalProjection, i::Int) = op.projectedVectors[i]


using DSPopt
using StructJuMP

NS = 3;                        # number of scenarios
probability = [1/3, 1/3, 1/3]; # probability

CROPS  = 1:3 # set of crops (wheat, corn and sugar beets, resp.)
PURCH  = 1:2 # set of crops to purchase (wheat and corn, resp.)
SELL   = 1:4 # set of crops to sell (wheat, corn, sugar beets under 6K and those over 6K)

Cost     = [150., 230., 260.]    # cost of planting crops
Budget   = 500.                # budget capacity
Purchase = [238., 210.];        # purchase price
Sell     = [170., 150., 36., 10.] # selling price
Yield    = [3.0 3.6 24.0;
            2.5 3.0 20.0;
            2.0 2.4 16.0]
Minreq   = [200., 240., 0.]      # minimum crop requirement

# JuMP model
m = StructuredModel(num_scenarios = NS)

@variable(m, x[i=CROPS] >= 0, Int)
@objective(m, Min, sum(Cost[i] * x[i] for i=CROPS))
@constraint(m, sum(x[i] for i=CROPS) <= Budget)

for s in 1:NS
    blk = StructuredModel(parent = m, id = s, prob = probability[s])
    @variable(blk, y[j=PURCH] >= 0)
    @variable(blk, w[k=SELL] >= 0)
    @objective(blk, Min,
          sum(Purchase[j] * y[j] for j=PURCH)
        - sum(Sell[k] * w[k] for k=SELL))
    @constraint(blk, const_minreq[j=PURCH], Yield[s,j] * x[j] + y[j] - w[j] >= Minreq[j])
    @constraint(blk, const_minreq_beets, Yield[s,3] * x[3] - w[3] - w[4] >= Minreq[3])
    @constraint(blk, const_aux, w[3] <= 6000)

    # farmer.txt: quadratic constraints
    @constraint(blk, const_quad, w[1]^2 <= 1600)
end

@testset "optimize!: $j" for j in [DSPopt.Dual, DSPopt.ExtensiveForm] #instances(DSPopt.Methods)

    dsp.is_stochastic = true

    status = DSPopt.optimize!(m, is_stochastic = true, solve_type = j)
    @test DSPopt.termination_status(m) == MOI.OPTIMAL
    @test isapprox(dual_objective_value(m), -105093.3333333334, rtol=0.1)

    primsol = value()
    dualsol = dual()

    print("Optimal objective value: ", objective_value(m), "\n")
    print("Dual objective value: ", dual_objective_value(m), "\n")
    # print("Optimal primal solution: \n")
    # print(primsol, "\n")
    DSPopt.freeSolver(dsp)
    @testset "freeModel" begin
        DSPopt.freeModel(dsp)
        @test dsp.p != C_NULL
        @test length(dsp.numRows) == 0
        @test length(dsp.numCols) == 0
        @test isnan(dsp.primVal)
        @test isnan(dsp.dualVal)
        @test length(dsp.colVal) == 0
        @test length(dsp.rowVal) == 0
        @test dsp.nblocks == -1
        @test dsp.block_ids == []
        @test dsp.is_stochastic == false
        @test dsp.solve_type == DSPopt.Dual
        @test length(dsp.quadConstrs) == 0
        @test length(dsp.linConstrs) == 0
    end
end

# @testset "writeFile" begin
#     DSPopt.writeMps!(m, "farmer", is_stochastic = true)
#     @test isfile("farmer.mps")
# end


# rbf kernel: exp(-gamma*|u-v|^2)

type Rbfk <: Kernel; n; s; w; v; x; y; dy; gamma;
    function Rbfk(nclass;o...)
        l = setparam!(new(nclass); o...)
        isdefined(l,:gamma) || (l.gamma = 1)
        return l
    end
end

# copy(l::Rbfk; o...)=Rbfk(s=copy(l.s), w=copy(l.w), v=copy(l.v), gamma=l.gamma)
setparam!(l::Rbfk; o...)=(for (n,v) in o; l.(n)=v; end; l)

function kernel(l::Rbfk)
    s2 = sum(l.s.^2, 1)
    x2 = sum(l.x.^2, 1)
    exp(-l.gamma * broadcast(+, x2, broadcast(+, s2', -2*(l.s' * l.x))))
end


module MinComposite

using LinearAlgebra
using OptimPackNextGen
using Printf

import ..Utils: affine_model!

export ProblemParams, SubproblemOptParams, value_function!, min_composite

struct ProblemParams
    c::Function
    ∇c_adjoint::Function
    gauge_projection::Function
    dual_gauge::Union{Function, Nothing}
end

mutable struct IntermediateVars{T}
    r::Vector{T}
    z::Vector{T}
    tau::Real
end

struct SubproblemOptParams{T}
    mem::Integer
    lower::Union{T,Vector{T},Nothing}
    upper::Union{T,Vector{T},Nothing}
    ftol::Tuple{Real,Real}
    maxiter::Integer
    maxeval::Integer
    function SubproblemOptParams{T}(mem::Integer=10,
                                    lower::Union{T,Vector{T},Nothing}=nothing,
                                    upper::Union{T,Vector{T},Nothing}=nothing,
                                    ftol::Tuple{Real,Real}=(0.0,1e-3),
                                    maxiter::Integer=20,
                                    maxeval::Integer=100) where {T}
        mem > 0 || error("mem must be > 0, got $(mem)")
        ftol[1] ≥ 0.0 || error("ftol[1] must be >= 0, got $(ftol[1])")
        ftol[2] ≥ 0.0 || error("ftol[2] must be >= 0, got $(ftol[2])")
        maxiter > 0 || error("maxiter must be > 0, got $(maxiter)")
        maxeval > 0 || error("maxeval must be > 0, got $(maxeval)")
        new(mem,lower,upper,ftol,maxiter,maxeval)
    end
end

function value_function_objective!(x, df, params::ProblemParams, info::IntermediateVars{T})::Float64 where {T}
    params.c(info.r, x)
    params.gauge_projection(info.z, info.r, info.tau)
    info.r .= info.r .- info.z
    params.∇c_adjoint(df, x, info.r)
    return 0.5*norm(info.r)^2
end

function value_function!(x::Vector{T}, params::ProblemParams, info::IntermediateVars, opt_params::SubproblemOptParams{T}) where {T}
    obj! = (x,g)->value_function_objective!(x,g,params,info)
    x .= vmlmb(obj!, x, mem=opt_params.mem, ftol=opt_params.ftol, maxiter=opt_params.maxiter, maxeval=opt_params.maxeval)

    f = obj!(x,zeros(size(x)))
    if params.dual_gauge != nothing
        df = -params.dual_gauge(info.r)::T
    else
        df = convert(0.0, T)
    end
    return f, df
end

"""
min_composite(A, b, gauge_projection, x0, dims, opt_params, dual_gauge)

Value function approach to solve problems of the form
    min_{x} h(A*x - b)

for a non-smooth, often convex, function h(z) with easy to project on level sets.
"""
function min_composite(A::Matrix{T},
                       b::Vector{T},
                       gauge_projection::Function,
                       opt_params::SubproblemOptParams{T},
                       dual_gauge::Union{Function, Nothing}=nothing;
                       num_outer_iter::Integer = 20,
                       x0::Union{Vector{T},Nothing}=nothing,
                       initial_tau::Real=0.0,
                       use_newton::Bool=true,
                       verbose::Bool=false) where {T}
    c = (y,x)->affine_model!(y,A,x,b)
    ∇c_adjoint = (y,x,r)->mul!(y,A',r)
    return min_composite(c, ∇c_adjoint, gauge_projection, size(A), opt_params, dual_gauge,
                        num_outer_iter=num_outer_iter, x0=x0, initial_tau=initial_tau, use_newton=use_newton, verbose=verbose)
end

"""
min_composite(c, ∇c_adjoint, gauge_projection, x0, dims, opt_params, dual_gauge)

Value function approach to solve problems of the form
    min_{x} h(c(x))

"""
function min_composite(c::Function,
                       ∇c_adjoint::Function,
                       gauge_projection::Function,
                       dims::Tuple{Integer,Integer},
                       opt_params::SubproblemOptParams{T},
                       dual_gauge::Union{Function, Nothing}=nothing;
                       num_outer_iter::Integer=20,
                       x0::Union{Vector{T}, Nothing}=nothing,
                       initial_tau::Real=0.0,
                       use_newton::Bool=true,
                       verbose::Bool=false) where {T}
    if dual_gauge == nothing
        use_newton = false
    end
    problem_params = ProblemParams(c,∇c_adjoint,gauge_projection,dual_gauge)
    m,n = dims
    if x0 == nothing
        x = zeros(dims[2])
    else
        x = copy(x0)
    end
    τ1 = initial_tau
    info = IntermediateVars{T}(zeros(m),zeros(m),initial_tau)
    vf! = function(x,τ)
        info.tau = τ
        f,df = value_function!(x,problem_params, info, opt_params)
        return f, df
    end

    for i in 1:num_outer_iter
        if use_newton
            f1, df1 = vf!(x,τ1)
            verbose && @printf("itr %3d - τ: %3.3e f: %3.3e df: %3.3e\n",i,τ1,f1,df1)
            τ1 = τ1 - f1/df1
        else
            if i==1
                f1, df1 = vf!(x,τ1)
                τ2 = max((1.001)*τ1,τ1 + 0.001)
            end
            verbose && @printf("itr %3d - τ: %3.3e f: %3.3e \n",i,τ1,f1)
            f2, df2 = vf!(x,τ2)
            τ_new = τ2 - f2*(τ2 - τ1)/(f2 - f1)
            τ1, f1, df1 = τ2, f2, df2
            τ2 = τ_new
        end
    end
    return x
end

end