@testset "ANOVA Tests" begin
    @testset "Multidimensional Array" begin
        @testset "1-way ANOVA" begin
            expected = [AnovaValue( "Total", 4822.4575, 19),
                        AnovaResult(    "A", 4684.9975,  3, 1561.66583, 181.773995, 1.43627554e-12),
                        AnovaFactor("Error",  137.46,   16,    8.59125)]

            @testset "Replicate Vectors" begin
                observations = Array{Vector{Float64}, 1}(undef, 4)
                observations[1] = [ 60.8,  57.0,  65.0, 58.6,  61.7]
                observations[2] = [ 68.7,  67.7,  74.9, 66.3,  69.8]
                observations[3] = [102.6, 102.1, 100.2, 96.5, 100.4]
                observations[4] = [ 87.9,  84.2,  83.1, 85.7,  90.3]

                results = anova(observations)

                @test all(expected .≈ results.effects)
            end

            @testset "Replicate First Dimension" begin
                observations = [60.8 68.7 102.6 87.9;
                                57.0 67.7 102.1 84.2;
                                65.0 74.9 100.2 83.1;
                                58.6 66.3  96.5 85.7;
                                61.7 69.8 100.4 90.3]

                results = anova(observations)

                @test all(expected .≈ results.effects)
            end
        end

        @testset "2-way ANOVA" begin
            observations1 = Array{Vector{Float64}, 2}(undef, 2, 2)
            observations1[1,1] = [16.5, 18.4, 12.7, 14.0, 12.8]
            observations1[1,2] = [14.5, 11.0, 10.8, 14.3, 10.0]
            observations1[2,1] = [39.1, 26.2, 21.3, 35.8, 40.2]
            observations1[2,2] = [32.0, 23.8, 28.8, 25.0, 29.3]

            observations2 = cat(hcat([16.5, 18.4, 12.7, 14.0, 12.8], [39.1, 26.2, 21.3, 35.8, 40.2]),
                                hcat([14.5, 11.0, 10.8, 14.3, 10.0], [32.0, 23.8, 28.8, 25.0, 29.3]), dims = 3)

            @testset "Fixed-effects (Model I)" begin
                expected = [AnovaValue( "Total", 1827.6975, 19),
                            AnovaResult(    "A",   70.3125,  1,   70.3125,  3.0706495,  0.098856175),
                            AnovaResult(    "B", 1386.1125,  1, 1386.1125, 60.533556,   7.9430782e-7),
                            AnovaResult("A × B",    4.9005,  1,    4.9005,  0.21401199, 0.64987001),
                            AnovaFactor("Error",  366.372,  16,   22.89825)]

                @testset "Replicate Vectors" begin
                    results = anova(observations1)
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2)
                    @test all(expected .≈ results.effects)
                end
            end

            @testset "Random-effects (Model II)" begin
                expected = [AnovaValue( "Total", 1827.6975, 19),
                            AnovaResult(    "A",   70.3125,  1,   70.3125,  14.3480257,  0.16431864),
                            AnovaResult(    "B", 1386.1125,  1, 1386.1125, 282.85124,    0.037808553),
                            AnovaResult("A × B",    4.9005,  1,    4.9005,   0.21401199, 0.64987001),
                            AnovaFactor("Error",  366.372,  16,   22.89825)]

                @testset "Replicate Vectors" begin
                    results = anova(observations1, [random, random])
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2, [random, random])
                    @test all(expected .≈ results.effects)
                end
            end

            @testset "Mixed-effects (Model III)" begin
                expected = [AnovaValue( "Total", 1827.6975, 19),
                            AnovaResult(    "A",   70.3125,  1,   70.3125, 14.3480257,  0.16431864),
                            AnovaResult(    "B", 1386.1125,  1, 1386.1125, 60.533556,   7.9430782e-7),
                            AnovaResult("A × B",    4.9005,  1,    4.9005,  0.21401199, 0.64987001),
                            AnovaFactor("Error",  366.372,  16,   22.89825)]

                @testset "Replicate Vectors" begin
                    results = anova(observations1, [random])
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2, [random])
                    @test all(expected .≈ results.effects)
                end
            end
        end

        @testset "2-way ANOVA without replicates" begin
            observations1 = Array{Vector{Float64}, 2}(undef, 3, 4)
            observations1[1,1] = [123]
            observations1[1,2] = [138]
            observations1[1,3] = [110]
            observations1[1,4] = [151]
            observations1[2,1] = [145]
            observations1[2,2] = [165]
            observations1[2,3] = [140]
            observations1[2,4] = [167]
            observations1[3,1] = [156]
            observations1[3,2] = [176]
            observations1[3,3] = [185]
            observations1[3,4] = [175]

            observations2 = [123 138 110 151; 145 165 140 167; 156 176 185 175]

            expected = [AnovaValue(     "Total", 5594.9167,  11),
                        AnovaResult(        "A", 1116.91667,  3,  372.30556,  2.6316513, 0.144561215),
                        AnovaResult(        "B", 3629.1667,   2, 1814.58333, 12.8264284, 0.0068110648),
                        AnovaFactor("Remainder",  848.83333,  6,  141.472222)]

            @testset "Fixed-effects (Model I)" begin
                @testset "Replicate Vectors" begin
                    results = anova(observations1)
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2, hasreplicates = false)
                    @test all(expected .≈ results.effects)
                end
            end

            @testset "Random-effects (Model II)" begin
                @testset "Replicate Vectors" begin
                    results = anova(observations1, [random, random])
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2, [random, random], hasreplicates = false)
                    @test all(expected .≈ results.effects)
                end
            end

            @testset "Mixed-effects (Model III)" begin
                @testset "Replicate Vectors" begin
                    results = anova(observations1, [random])
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2, [random], hasreplicates = false)
                    @test all(expected .≈ results.effects)
                end
            end
        end

        @testset "1-way ANOVA with 1 nested factor" begin
            expected = [AnovaValue( "Total", 71.666667, 11),
                        AnovaResult(    "A", 61.166667,  2, 30.583333, 61.166667,   0.0037032412),
                        AnovaResult(    "B",  1.5,       3,  0.5,       0.33333333, 0.80220227),
                        AnovaFactor("Error",  9.0,       6,  1.5)]

            @testset "Replicate Vectors" begin
                observations = Array{Vector{Float64}, 2}(undef, 2, 3)
                observations[1,1] = [102, 104]
                observations[1,2] = [108, 110]
                observations[1,3] = [104, 106]
                observations[2,1] = [103, 104]
                observations[2,2] = [109, 108]
                observations[2,3] = [105, 107]

                results = anova(observations, [nested])
                @test all(expected .≈ results.effects)
            end

            @testset "Replicate First Dimension" begin
                observations = cat(hcat([102, 104], [103, 104]),
                                   hcat([108, 110], [109, 108]),
                                   hcat([104, 106], [105, 107]), dims = 3)

                results = anova(observations, [nested])
                @test all(expected .≈ results.effects)
            end
        end

        @testset "1-way ANOVA with 2 nested factors" begin
            expected = [AnovaValue( "Total", 101.58753,   47),
                        AnovaResult(    "A",   7.5108875,  2, 3.7554438, 2.00918315, 0.21481422),
                        AnovaResult(    "B",   5.5708812,  3, 1.8569604, 0.99348408, 0.45717102),
                        AnovaResult(    "C",  11.2148375,  6, 1.8691396, 0.87059412, 0.52587983),
                        AnovaFactor("Error",  77.290925,  36, 2.14697014)]

            @testset "Replicate Vectors" begin
                observations = Array{Vector{Float64}, 3}(undef, 2, 2, 3)
                observations[1,1,1] = [3.17, 4.41, 1.81, 1.74]
                observations[2,1,1] = [2.81, 4.98, 2.62, 2.53]
                observations[1,2,1] = [3.0,  3.02, 4.73, 1.77]
                observations[2,2,1] = [4.13, 0.71, 3.18, 3.34]
                observations[1,1,2] = [2.42, 1.28, 1.4,  2.56]
                observations[2,1,2] = [1.26, 1.08, 1.42, 0.85]
                observations[1,2,2] = [0.36, 0.35, 2.64, 3.75]
                observations[2,2,2] = [3.86, 2.53, 3.97, 3.03]
                observations[1,1,3] = [5.24, 2.24, 0.18, 3.06]
                observations[2,1,3] = [4.04, 4.14, 0.33, 4.61]
                observations[1,2,3] = [6.06, 1.61, 2.25, 2.44]
                observations[2,2,3] = [0.02, 3.95, 0.87, 2.0]

                results = anova(observations, [nested, nested])
                @test all(expected .≈ results.effects)
            end

            @testset "Replicate First Dimension" begin
                observations = cat(cat(hcat([3.17, 4.41, 1.81, 1.74], [2.81, 4.98, 2.62, 2.53]),
                                       hcat([3.0,  3.02, 4.73, 1.77], [4.13, 0.71, 3.18, 3.34]), dims = 3),
                                   cat(hcat([2.42, 1.28, 1.4,  2.56], [1.26, 1.08, 1.42, 0.85]),
                                       hcat([0.36, 0.35, 2.64, 3.75], [3.86, 2.53, 3.97, 3.03]), dims = 3),
                                   cat(hcat([5.24, 2.24, 0.18, 3.06], [4.04, 4.14, 0.33, 4.61]),
                                       hcat([6.06, 1.61, 2.25, 2.44], [0.02, 3.95, 0.87, 2.0]), dims = 3), dims = 4)

                results = anova(observations, [nested, nested])
                @test all(expected .≈ results.effects)
            end
        end

        @testset "2-way ANOVA with 1 nested factor, no replicates" begin
            observations1 = Array{Vector{Float64}, 3}(undef, 5, 2, 2)
            observations1[1,1,1] = [16.5]
            observations1[2,1,1] = [18.4]
            observations1[3,1,1] = [12.7]
            observations1[4,1,1] = [14.0]
            observations1[5,1,1] = [12.8]
            observations1[1,2,1] = [39.1]
            observations1[2,2,1] = [26.2]
            observations1[3,2,1] = [21.3]
            observations1[4,2,1] = [35.8]
            observations1[5,2,1] = [40.2]
            observations1[1,1,2] = [14.5]
            observations1[2,1,2] = [11.0]
            observations1[3,1,2] = [10.8]
            observations1[4,1,2] = [14.3]
            observations1[5,1,2] = [10.0]
            observations1[1,2,2] = [32.0]
            observations1[2,2,2] = [23.8]
            observations1[3,2,2] = [28.8]
            observations1[4,2,2] = [25.0]
            observations1[5,2,2] = [29.3]

            observations2 = cat(hcat([16.5, 18.4, 12.7, 14.0, 12.8], [39.1, 26.2, 21.3, 35.8, 40.2]),
                                hcat([14.5, 11.0, 10.8, 14.3, 10.0], [32.0, 23.8, 28.8, 25.0, 29.3]), dims = 3)

            @testset "Fixed-effects (Model I)" begin
               expected = [AnovaValue( "Total", 1827.6975, 19),
                           AnovaResult(    "A",   70.3125,  1,   70.3125,  3.0706495,  0.098856175),
                           AnovaResult(    "B", 1386.1125,  1, 1386.1125, 60.533556,   7.9430782e-7),
                           AnovaResult("A × B",    4.9005,  1,    4.9005,  0.21401199, 0.64987001),
                           AnovaFactor(    "C",  366.372,  16,   22.89825)]

               @testset "Replicate Vectors" begin
                   results = anova(observations1, [nested])
                   @test all(expected .≈ results.effects)
               end

               @testset "Replicate First Dimension" begin
                   results = anova(observations2, [nested], hasreplicates = false)
                   @test all(expected .≈ results.effects)
               end
            end

            @testset "Random-effects (Model II)" begin
               expected = [AnovaValue( "Total", 1827.6975, 19),
                           AnovaResult(    "A",   70.3125,  1,   70.3125,  14.3480257,  0.16431864),
                           AnovaResult(    "B", 1386.1125,  1, 1386.1125, 282.85124,    0.037808553),
                           AnovaResult("A × B",    4.9005,  1,    4.9005,   0.21401199, 0.64987001),
                           AnovaFactor(    "C",  366.372,  16,   22.89825)]

               @testset "Replicate Vectors" begin
                   results = anova(observations1, [nested, random, random])
                   @test all(expected .≈ results.effects)
               end

               @testset "Replicate First Dimension" begin
                   results = anova(observations2, [nested, random, random], hasreplicates = false)
                   @test all(expected .≈ results.effects)
               end
            end

            @testset "Mixed-effects (Model III)" begin
               expected = [AnovaValue( "Total", 1827.6975, 19),
                           AnovaResult(    "A",   70.3125,  1,   70.3125, 14.3480257,  0.16431864),
                           AnovaResult(    "B", 1386.1125,  1, 1386.1125, 60.533556,   7.9430782e-7),
                           AnovaResult("A × B",    4.9005,  1,    4.9005,  0.21401199, 0.64987001),
                           AnovaFactor(    "C",  366.372,  16,   22.89825)]

               @testset "Replicate Vectors" begin
                   results = anova(observations1, [nested, random])
                   @test all(expected .≈ results.effects)
               end

               @testset "Replicate First Dimension" begin
                   results = anova(observations2, [nested, random], hasreplicates = false)
                   @test all(expected .≈ results.effects)
               end
            end
        end

        @testset "3-way ANOVA" begin
            observations1 = Array{Vector{Float64}, 3}(undef, 2, 3, 3)
            observations1[1,1,1] = [1.9, 1.8, 1.6, 1.4]
            observations1[1,1,2] = [2.1, 2.0, 1.8, 2.2]
            observations1[1,1,3] = [1.1, 1.2, 1.0, 1.4]
            observations1[1,2,1] = [2.3, 2.1, 2.0, 2.6]
            observations1[1,2,2] = [2.4, 2.6, 2.7, 2.3]
            observations1[1,2,3] = [2.0, 2.1, 1.9, 2.2]
            observations1[1,3,1] = [2.9, 2.8, 3.4, 3.2]
            observations1[1,3,2] = [3.6, 3.1, 3.4, 3.2]
            observations1[1,3,3] = [2.9, 2.8, 3.0, 3.1]
            observations1[2,1,1] = [1.8, 1.7, 1.4, 1.5]
            observations1[2,1,2] = [2.3, 2.0, 1.9, 1.7]
            observations1[2,1,3] = [1.4, 1.0, 1.3, 1.2]
            observations1[2,2,1] = [2.4, 2.7, 2.4, 2.6]
            observations1[2,2,2] = [2.0, 2.3, 2.1, 2.4]
            observations1[2,2,3] = [2.4, 2.6, 2.3, 2.2]
            observations1[2,3,1] = [3.0, 3.1, 3.0, 2.7]
            observations1[2,3,2] = [3.1, 3.0, 2.8, 3.2]
            observations1[2,3,3] = [3.2, 2.9, 2.8, 2.9]

            observations2 = cat(cat(hcat([1.9, 1.8, 1.6, 1.4], [1.8, 1.7, 1.4, 1.5]),
                                    hcat([2.3, 2.1, 2.0, 2.6], [2.4, 2.7, 2.4, 2.6]),
                                    hcat([2.9, 2.8, 3.4, 3.2], [3.0, 3.1, 3.0, 2.7]), dims = 3),
                                cat(hcat([2.1, 2.0, 1.8, 2.2], [2.3, 2.0, 1.9, 1.7]),
                                    hcat([2.4, 2.6, 2.7, 2.3], [2.0, 2.3, 2.1, 2.4]),
                                    hcat([3.6, 3.1, 3.4, 3.2], [3.1, 3.0, 2.8, 3.2]), dims = 3),
                                cat(hcat([1.1, 1.2, 1.0, 1.4], [1.4, 1.0, 1.3, 1.2]),
                                    hcat([2.0, 2.1, 1.9, 2.2], [2.4, 2.6, 2.3, 2.2]),
                                    hcat([2.9, 2.8, 3.0, 3.1], [3.2, 2.9, 2.8, 2.9]), dims = 3), dims = 4)

            @testset "All Fixed" begin
                expected = [AnovaValue(     "Total", 30.355,       71),
                            AnovaResult(        "A",  1.8175,       2,  0.90875,      24.475062,    2.71459995e-8),
                            AnovaResult(        "B", 24.655833,     2, 12.3279167,   332.02369,     4.5550981e-31),
                            AnovaResult(        "C",  0.008888889,  1,  0.008888889,   0.239401496, 0.62662035),
                            AnovaResult(    "A × B",  1.10166667,   4,  0.27541667,    7.4177057,   7.7516681e-5),
                            AnovaResult(    "A × C",  0.37027778,   2,  0.18513889,    4.9862843,   0.0102990988),
                            AnovaResult(    "B × C",  0.17527778,   2,  0.08763889,    2.3603491,   0.104056404),
                            AnovaResult("A × B × C",  0.220555556,  4,  0.055138889,   1.4850374,   0.21958095),
                            AnovaFactor(    "Error",  2.005,       54,  0.03712963)]

                @testset "Replicate Vectors" begin
                    results = anova(observations1)
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2)
                    @test all(expected .≈ results.effects)
                end
            end

            @testset "All Random" begin
                expected = [AnovaValue(     "Total", 30.355,       71),
                            AnovaResult(        "A",  1.8175,       2,  0.90875,      4.1754946,   0.179725175),
                            AnovaResult(        "B", 24.655833,     2, 12.3279167,   40.036536,    0.0022084069),
                            AnovaResult(        "C",  0.008888889,  1,  0.008888889,  0.021925317, 0.88874495),
                            AnovaResult(    "A × B",  1.10166667,   4,  0.27541667,   4.9949622,   0.074190827),
                            AnovaResult(    "A × C",  0.37027778,   2,  0.18513889,   3.3576826,   0.139349696),
                            AnovaResult(    "B × C",  0.17527778,   2,  0.08763889,   1.58942065,  0.31046402),
                            AnovaResult("A × B × C",  0.220555556,  4,  0.055138889,  1.4850374,   0.21958095),
                            AnovaFactor(    "Error",  2.005,       54,  0.03712963)]

                @testset "Replicate Vectors" begin
                    results = anova(observations1, [random, random, random])
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2, [random, random, random])
                    @test all(expected .≈ results.effects)
                end
            end

            @testset "1 random, 2 fixed" begin
                expected = [AnovaValue(     "Total", 30.355,       71),
                            AnovaResult(        "A",  1.8175,       2,  0.90875,      4.9084771,   0.16924835),
                            AnovaResult(        "B", 24.655833,     2, 12.3279167,   44.760968,    0.001829334),
                            AnovaResult(        "C",  0.008888889,  1,  0.008888889,  0.239401496, 0.62662035),
                            AnovaResult(    "A × B",  1.10166667,   4,  0.27541667,   4.9949622,   0.074190827),
                            AnovaResult(    "A × C",  0.37027778,   2,  0.18513889,   4.9862843,   0.0102990988),
                            AnovaResult(    "B × C",  0.17527778,   2,  0.08763889,   2.3603491,   0.104056404),
                            AnovaResult("A × B × C",  0.220555556,  4,  0.055138889,  1.4850374,   0.21958095),
                            AnovaFactor(    "Error",  2.005,       54,  0.03712963)]

                @testset "Replicate Vectors" begin
                    results = anova(observations1, [random])
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2, [random])
                    @test all(expected .≈ results.effects)
                end
            end

            @testset "2 random, 1 fixed" begin
                expected = [AnovaValue(     "Total", 30.355,       71),
                            AnovaResult(        "A",  1.8175,       2,  0.90875,      4.1754946,    0.179725175),
                            AnovaResult(        "B", 24.655833,     2, 12.3279167,   44.760968,    0.001829334),
                            AnovaResult(        "C",  0.008888889,  1,  0.008888889,  0.032274332, 0.8661604),
                            AnovaResult(    "A × B",  1.10166667,   4,  0.27541667,   4.9949622,   0.074190827),
                            AnovaResult(    "A × C",  0.37027778,   2,  0.18513889,   3.3576826,   0.139349696),
                            AnovaResult(    "B × C",  0.17527778,   2,  0.08763889,   2.3603491,   0.104056404),
                            AnovaResult("A × B × C",  0.220555556,  4,  0.055138889,  1.4850374,   0.21958095),
                            AnovaFactor(    "Error",  2.005,       54,  0.03712963)]

                @testset "Replicate Vectors" begin
                    results = anova(observations1, [random, random])
                    @test all(expected .≈ results.effects)
                end

                @testset "Replicate First Dimension" begin
                    results = anova(observations2, [random, random])
                    @test all(expected .≈ results.effects)
                end
            end
        end
    end

    @testset "Vectors" begin
        observations = [1.9, 1.8, 1.6, 1.4, 2.1, 2.0, 1.8, 2.2, 1.1, 1.2, 1.0, 1.4,
                        2.3, 2.1, 2.0, 2.6, 2.4, 2.6, 2.7, 2.3, 2.0, 2.1, 1.9, 2.2,
                        2.9, 2.8, 3.4, 3.2, 3.6, 3.1, 3.4, 3.2, 2.9, 2.8, 3.0, 3.1,
                        1.8, 1.7, 1.4, 1.5, 2.3, 2.0, 1.9, 1.7, 1.4, 1.0, 1.3, 1.2,
                        2.4, 2.7, 2.4, 2.6, 2.0, 2.3, 2.1, 2.4, 2.4, 2.6, 2.3, 2.2,
                        3.0, 3.1, 3.0, 2.7, 3.1, 3.0, 2.8, 3.2, 3.2, 2.9, 2.8, 2.9]

        factorassignments = [[repeat([1], 36); repeat([2], 36)],
                             repeat([repeat([1], 12); repeat([2], 12); repeat([3], 12)], 2),
                             repeat([repeat([1], 4); repeat([2], 4); repeat([3], 4)], 6)]

        expected = [AnovaValue(     "Total", 30.355,       71),
                    AnovaResult(        "A",  1.8175,       2,  0.90875,      24.475062,    2.71459995e-8),
                    AnovaResult(        "B", 24.655833,     2, 12.3279167,   332.02369,     4.5550981e-31),
                    AnovaResult(        "C",  0.008888889,  1,  0.008888889,   0.239401496, 0.62662035),
                    AnovaResult(    "A × B",  1.10166667,   4,  0.27541667,    7.4177057,   7.7516681e-5),
                    AnovaResult(    "A × C",  0.37027778,   2,  0.18513889,    4.9862843,   0.0102990988),
                    AnovaResult(    "B × C",  0.17527778,   2,  0.08763889,    2.3603491,   0.104056404),
                    AnovaResult("A × B × C",  0.220555556,  4,  0.055138889,   1.4850374,   0.21958095),
                    AnovaFactor(    "Error",  2.005,       54,  0.03712963)]

        @testset "3-way ANOVA ordered" begin
            results = anova(observations, factorassignments)
            @test all(expected .≈ results.effects)
        end

        @testset "3-way ANOVA shuffled" begin
            randorder = [65,  6, 31, 70, 45,  2, 41, 59,  3, 39, 40, 47, 61, 55, 13, 68, 67, 25,
                         60, 66, 44, 33, 42, 52, 56, 15, 49, 43, 69, 10, 51, 58, 19, 22,  8, 37,
                         71, 23, 46, 24, 54, 53, 16, 18, 11, 32, 29, 35, 63, 34, 64, 62, 28, 72,
                         30, 38, 17, 21,  9,  5, 50, 12,  7, 36, 48, 27, 14, 20,  4,  1, 57, 26]
            shuffledobservations = observations[randorder]
            shuffledfactorassignments = [fa[randorder] for fa ∈ factorassignments]

            results = anova(shuffledobservations, shuffledfactorassignments)

            @test all(expected .≈ results.effects)
        end

        @testset "3-way ANOVA tolerates nonconsecutive factor levels" begin
            nonconsecutivefactorassignments = [fa .* 2 for fa ∈ factorassignments]
            results = anova(observations, nonconsecutivefactorassignments)
            @test all(expected .≈ results.effects)
        end

        @testset "3-way ANOVA tolerates noninteger factor levels" begin
            factorassignmentstrings = [repeat(["1"], 36); repeat(["2"], 36)]
            stringsinfactorassignments = [[factorassignmentstrings]; factorassignments[2:3]]
            results = anova(observations, stringsinfactorassignments)
            @test all(expected .≈ results.effects)
        end

        @testset "2-way ANOVA rejects nested factor with exclusive levels" begin
            factorlevels = factorassignments .|> unique .|> sort
            nfactorlevels = length.(factorlevels)
            nlevels = [4; nfactorlevels]
            nestedfactorassignment = (repeat(1:4, Int(length(observations) / 4)) .+
                                      sum([(factorassignments[i] .- 1) .* prod(nlevels[1:i]) for i ∈ 1:length(factorassignments)])) ./ 4 .|> ceil .|> Int
            nestedfactorassignments = [[nestedfactorassignment]; factorassignments[2:3]]

            @test_throws Exception anova(observations, nestedfactorassignments, [nested])
        end
    end

    @testset "DataFrame" begin
        observations = [1.9, 1.8, 1.6, 1.4, 2.1, 2.0, 1.8, 2.2, 1.1, 1.2, 1.0, 1.4,
                        2.3, 2.1, 2.0, 2.6, 2.4, 2.6, 2.7, 2.3, 2.0, 2.1, 1.9, 2.2,
                        2.9, 2.8, 3.4, 3.2, 3.6, 3.1, 3.4, 3.2, 2.9, 2.8, 3.0, 3.1,
                        1.8, 1.7, 1.4, 1.5, 2.3, 2.0, 1.9, 1.7, 1.4, 1.0, 1.3, 1.2,
                        2.4, 2.7, 2.4, 2.6, 2.0, 2.3, 2.1, 2.4, 2.4, 2.6, 2.3, 2.2,
                        3.0, 3.1, 3.0, 2.7, 3.1, 3.0, 2.8, 3.2, 3.2, 2.9, 2.8, 2.9]

        factorassignments = [[repeat([1], 36); repeat([2], 36)],
                             repeat([repeat([1], 12); repeat([2], 12); repeat([3], 12)], 2),
                             repeat([repeat([1], 4); repeat([2], 4); repeat([3], 4)], 6)]

        expected = [AnovaValue(                       "Total", 30.355,       71),
                    AnovaResult(                    "FactorA",  1.8175,       2,  0.90875,      24.475062,    2.71459995e-8),
                    AnovaResult(                    "FactorB", 24.655833,     2, 12.3279167,   332.02369,     4.5550981e-31),
                    AnovaResult(                    "FactorC",  0.008888889,  1,  0.008888889,   0.239401496, 0.62662035),
                    AnovaResult(          "FactorA × FactorB",  1.10166667,   4,  0.27541667,    7.4177057,   7.7516681e-5),
                    AnovaResult(          "FactorA × FactorC",  0.37027778,   2,  0.18513889,    4.9862843,   0.0102990988),
                    AnovaResult(          "FactorB × FactorC",  0.17527778,   2,  0.08763889,    2.3603491,   0.104056404),
                    AnovaResult("FactorA × FactorB × FactorC",  0.220555556,  4,  0.055138889,   1.4850374,   0.21958095),
                    AnovaFactor(                      "Error",  2.005,       54,  0.03712963)]

        @testset "3-way ANOVA shuffled" begin
            df = DataFrame([[observations]; factorassignments], [:observations; :FactorC; :FactorB; :FactorA])
            results = anova(df, :observations, [:FactorC; :FactorB; :FactorA])
            @test all(expected .≈ results.effects)
        end
    end
end


# Testing out LowRankModels in order to test

TrueA = repmat(randn(5,100),20,1); 			# Create a 100x100 matrix of rank 5, then add some noise

A = TrueA + 0.2 * randn(100,100);

u1,s1,v1 = svd(A)		# find svd of matrix and take the top 5 values. u1 * diagm(s1_trunkated) * v1'
						# should be close to A

SVDError = maximum(abs(u1 * diagm([s1[1:5],zeros(95)]) * v1' - TrueA))

using LowRankModels
m,n,k = 100,100,100
losses = fill(quadratic(),n)
rx = onesparse() # each row is assigned to exactly one cluster
ry = zeroreg() # no regularization on the cluster centroids
glrm = GLRM(A,losses,rx,ry,k)

X,Y,ch = fit!(glrm)


function main()

    io = open("xenon.txt", "w");
    io_p = open("xenon_pressure.txt", "w");

    B1 = 103701.61e-8
    B2 = 31228.61e-8
    C1 = 12.75e-3*(1e-6)^2
    C2 = 0.561e-3*(1e-6)^2

    p0 = 1
    k0(λ) = 2π/λ
    a = 14.9e-6
    T = 293
    T0 = 273
    λ0 = 800e-9
    function δ(λ, T=T)
        return T0 / T * (B1 * λ^2/(λ^2 - C1) + B2 * λ^2 / (λ^2 - C2))
    end

    function n(λ, p, u, T=T)
        return sqrt(1 + δ(λ, T)*p/p0 - (u / k0(λ) / a)^2 )
    end

    e11 = 2.405
    e12 = 5.52
    e13 = 8.6573
    e23 = 10.1735
    e33 = 11.6198
    e43 = 13.0152

    ps = range(0, 4, length=1000)
    λs = range(400e-9, 1000e-9, length=1000)

    for p in ps
        res13 = (n(λ0, p, e11) - n(λ0/3, p, e13)) * 10^4
        res23 = (n(λ0, p, e11) - n(λ0/3, p, e23)) * 10^4
        res33 = (n(λ0, p, e11) - n(λ0/3, p, e33)) * 10^4
        write(io_p, "$p \t $res13 \t $res23 \t $res33 \n")
    end

    p = 0.96
    for x in λs
        i = x * 10^9
        res11 = (n(x, p, e11) - n(x/3, p, e11)) * 10^4
        res12 = (n(x, p, e11) - n(x/3, p, e12)) * 10^4
        res13 = (n(x, p, e11) - n(x/3, p, e13)) * 10^4
        res23 = (n(x, p, e11) - n(x/3, p, e23)) * 10^4
        res33 = (n(x, p, e11) - n(x/3, p, e33)) * 10^4
        write(io, "$i \t $res11 \t $res12 \t $res13 \t $res23 \t $res33 \n")
    end

    close(io)
    close(io_p)
end
main()


# Returns the gradient of x, computed by finited differences
#
# Except for the first and last element, all gradients are computed by central
# finite differences
function fdgrad(x::AbstractVector{T}, dt::Real) where T
    n = length(x)
    GType = typeof((zero(T) - zero(T)) / dt)
    g = Array{GType}(undef, n)
    if n == 1
        g[1] = zero(GType)
    else
        g[1] = (x[2] - x[1]) / dt
        for i = 2:(n-1)
            g[i] = (x[i+1] - x[i-1]) / 2dt
        end
        g[n] = (x[n] - x[n-1]) / dt
    end
    return g
end


#=
---------------------------------------------------------------------
This function computes the normal vectors and Jacobian of each FACE.

The strategy follows Equation 12.33 in F.X. Giraldo's Introduction to Element-based
Galerkin Methods using Tensor-Product Bases: Analysis, Algorithms, and Applications.

Written by F.X. Giraldo on 5/2019
           Department of Applied Mathematics
           Naval Postgraduate School
           Monterey; CA 93943-5216
---------------------------------------------------------------------
=#

include("map_deriv.jl")

function compute_normals(face,intma,coord,Nface,Np,Nq,ψ,dψ)

    #global arrays
    normals=zeros(DFloat,2,Nq,Nface)
    jac_face=zeros(DFloat,Nq,Nface)

    #local arrays
    x=zeros(DFloat,Np,Np)
    y=zeros(DFloat,Np,Np)

    #loop thru the sides
    for f=1:Nface

        #Store Left & Right Elements
        ilocl=face[1,f]
        ilocr=face[2,f]
        iel=face[3,f]
        ier=face[4,f]

        #Store Element Variables
        for j=1:Np, i=1:Np
            I=intma[i,j,iel]
            x[i,j]=coord[1,I]
            y[i,j]=coord[2,I]
        end #i, j

        #Construct Mapping Derivatives: dx/dksi; dx/deta; dy/dksi
        #dy/deta
        (x_ξ,x_η)=map_deriv(ψ,dψ,x,Np,Nq,DFloat)
        (y_ξ,y_η)=map_deriv(ψ,dψ,y,Np,Nq,DFloat)

        #Compute Normals
        for l=1:Nq
            if (ilocl == 1)
                #Side 1: eta=-1
                i=l
                j=1
                normals[1,l,f]=+y_ξ[i,j]
                normals[2,l,f]=-x_ξ[i,j]
                jac_face[l,f]=sqrt( x_ξ[i,j]^2 + y_ξ[i,j]^2 )
            elseif (ilocl == 2)
                #Side 2: ksi=+1
                i=Nq
                j=l
                normals[1,l,f]=+y_η[i,j]
                normals[2,l,f]=-x_η[i,j]
                jac_face[l,f]=sqrt( x_η[i,j]^2 + y_η[i,j]^2 )
            elseif (ilocl == 3)
                #Side 3: eta=+1
                i=Nq+1-l
                j=Nq
                normals[1,l,f]=-y_ξ[i,j]
                normals[2,l,f]=+x_ξ[i,j]
                jac_face[l,f]=sqrt( x_ξ[i,j]^2 + y_ξ[i,j]^2 )
            elseif (ilocl == 4)
                #Side 4: ksi=-1
                i=1
                j=Nq+1-l
                normals[1,l,f]=-y_η[i,j]
                normals[2,l,f]=+x_η[i,j]
                jac_face[l,f]=sqrt( x_η[i,j]^2 + y_η[i,j]^2 )
            end
        end #l

        #Normalize Normals
        for l=1:Nq
            rnx=sqrt( normals[1,l,f]^2 + normals[2,l,f]^2 )
            normals[1,l,f]=normals[1,l,f]/rnx
            normals[2,l,f]=normals[2,l,f]/rnx
        end #l

    end #s

    return(normals,jac_face)
end


"Geometry utils for the SDD ecosystem."
module SDDGeometry

import Base: show

include("utils.jl")

export
    dot,
    perp

include("abstractcurves.jl")
include("lines.jl")
include("circles.jl")

export
    AbstractCurve2D,
    AbstractLinearCircularCurve,
    AbstractLinearCurve,
    AbstractLine,
    AbstractRay,
    AbstractLineSegment,
    AbstractCircularCurve,
    AbstractCircle,
    AbstractArc,
    Line,
    Ray,
    LineSegment,
    basepoint,
    basepointC,
    basepointR2,
    lineangle,
    normal,
    normalC,
    normalR2,
    initialpoint,
    finalpoint,
    initialpointC,
    finalpointC,
    initialpointR2,
    finalpointR2,
    linepoints,
    linecomplexes,
    isinside,
    isoutside,
    Circle,
    Arc,
    #CLine,
    center,
    centerC,
    centerR2,
    centerx,
    centery,
    radius,
    radius2,
    circlepoints,
    circlecomplexes

#include("intersections.jl")
#include("lengths.jl")
#include("metrics.jl")
#include("stereographicprojections.jl")

include("mobiustransformations.jl")

export
    AbstractMobiusTransformation,
    MobiusTransformation,
    LinearTransformation,
    Translation,
    AffineTransformation,
    InversionReflection,
    #a, b, c, d,
    inverse,
    derivative,
    fixedpoints,
    kind,
    tr,
    det,
    compose,
    ∘,
    maptozerooneinf,
    mapfromzerooneinf

#include("inversions.jl")
#export

end # module


# Superoperator Tests
using Schrodinger
using Test, LinearAlgebra
using Schrodinger: apply_process, operator_to_choi, trace_norm,
    gate_fidelity_choi, kraus_to_natural, natural_to_kraus, natural_to_choi,
    choi_to_natural, kraus_to_choi, choi_to_kraus, choi_to_chi, chi_to_choi

println("Testing Superoperator Functions...")

@testset "Representation Changes" begin
d = 4
for i = 1:20
    U = rand_unitary(d)
    U2 = qeye(d) ⊗ U
    @test operator_to_choi(U) ≈ sum(U2*(ket((i,i),(d,d))*ket((j,j),(d,d))')*U2' for i=0:d-1,j=0:d-1)
end
end


type StochasticDeployment <: DeploymentModel
    m::JuMP.Model
    x::Vector{JuMP.Variable}
end
deployment(m::StochasticDeployment) = [round(Int,x) for x in JuMP.getvalue(m.x)]

function StochasticDeployment(p::DeploymentProblem; nperiods=params.nperiods, tol=params.δ,
    solver=GurobiSolver(OutputFlag=0))

    nperiods = min(length(p.train), nperiods)
    demand = p.demand[p.train,:]
    I = 1:p.nlocations
    J = 1:p.nregions
    T = 1:nperiods

    m = JuMP.Model(solver=solver)
    JuMP.@variable(m, x[1:p.nlocations] >= 0, Int)
    JuMP.@variable(m, y[1:p.nlocations,1:p.nregions,1:nperiods] >= 0, Int)
    JuMP.@variable(m, z[1:p.nregions,1:nperiods] >= 0, Int)

    JuMP.@objective(m, Min, sum(z[j,t] for j=J, t=T) + tol*sum(y[i,j,t] for i=I, j=J, t=T))

    JuMP.@constraint(m, sum(x[i] for i=I) <= p.nambulances)

    for j in J # coverage over all regions
        JuMP.@constraint(m, sum(x[i] for i in filter(i->p.coverage[j,i], I)) >= 1)
    end

    # flow constraints at each station
    for i in I, t in T
        JuMP.@expression(m, outflow, sum(y[i,j,t] for j in filter(j->p.coverage[j,i], J)))
        JuMP.@constraint(m, x[i] >= outflow)
    end

    # shortfall from satisfying demand/calls
    for j in J, t in T
        JuMP.@expression(m, inflow, sum(y[i,j,t] for i in filter(i->p.coverage[j,i], I)))
        JuMP.@constraint(m, z[j,t] >= demand[t,j] - inflow)
    end

    StochasticDeployment(m, x)
end

solve(model::StochasticDeployment) = JuMP.solve(model.m)
solve(model::StochasticDeployment, p::DeploymentProblem) = JuMP.solve(model.m)


#
# @testset "cholesky_U_deravitive" begin
#     for n = 1:10
#         for k = 1:100
#             A = randn(MMatrix{n,n,Float64,n^2})
#             A = Symmetric(MMatrix(A * A'))  # needs to be PD
#             Ȧ = randn(MMatrix{n,n,Float64,n^2})
#             Ȧ = Symmetric(MMatrix(Ȧ + Ȧ'))  # doesn't need to be PD
#             U, U⁻¹, U̇ = cholesky_U_deravitive(A, Ȧ)
#             Ȧ_formula = U̇' * U + U' * U̇
#             @test sum(abs.(Ȧ_formula - Ȧ)) < (1.0e-10 * n^2)
#         end
#     end
# end


function parseKillerJSON(json_sums)
    sums = []
    for s in json_sums
        indices = Tuple[]
        for ind in s["indices"]
            push!(indices, tuple(ind...))
        end

        if haskey(s, "color")
            push!(sums, (result=s["result"], indices=indices, color=s["color"]))
        else
            push!(sums, (result=s["result"], indices=indices, color="white"))
        end
    end
    return sums
end

@testset "Killer Sudoku" begin

@testset "Killer Sudoku from wikipedia" begin
    com = CS.init()

    grid = zeros(Int, (9,9))

    com_grid = Array{CS.Variable, 2}(undef, 9, 9)
    for (ind,val) in enumerate(grid)
        com_grid[ind] = add_var!(com, 1, 9)
    end

    sums = parseKillerJSON(JSON.parsefile("data/killer_wikipedia"))

    for s in sums
        add_constraint!(com, sum([com_grid[CartesianIndex(ind)] for ind in s.indices]) == s.result)
    end

    add_sudoku_constr!(com, com_grid)

    @test solve!(com) == :Solved
    @test fulfills_sudoku_constr(com_grid)
    for s in sums
        @test s.result == sum([value(com_grid[CartesianIndex(i)]) for i in s.indices])
    end
end

@testset "Killer Sudoku niallsudoku_5500 with coefficients" begin
    com = CS.init()

    grid = zeros(Int, (9,9))

    com_grid = Array{CS.Variable, 2}(undef, 9, 9)
    for (ind,val) in enumerate(grid)
        com_grid[ind] = add_var!(com, 1, 9)
    end

    sums = parseKillerJSON(JSON.parsefile("data/killer_niallsudoku_5500"))

    # the upper left sum constraint is x+y+z = 10 and the solution is 2+1+7
    # here I change it to 5*x+7*y+z = 24
    add_constraint!(com, 5*com_grid[CartesianIndex(1,1)]+7*com_grid[CartesianIndex(2,1)]+com_grid[CartesianIndex(2,2)] == 24)

    for s in sums[2:end]
        add_constraint!(com, sum([com_grid[CartesianIndex(ind)] for ind in s.indices]) == s.result)
    end

    add_sudoku_constr!(com, com_grid)

    @test solve!(com) == :Solved
    @test fulfills_sudoku_constr(com_grid)
    @test 5*value(com_grid[CartesianIndex(1,1)])+7*value(com_grid[CartesianIndex(2,1)])+value(com_grid[CartesianIndex(2,2)]) == 24
    for s in sums[2:end]
        @test s.result == sum([value(com_grid[CartesianIndex(i)]) for i in s.indices])
    end
end

@testset "Killer Sudoku niallsudoku_5500 with negative coefficients and -9 to -1" begin
    com = CS.init()

    grid = zeros(Int, (9,9))

    com_grid = Array{CS.Variable, 2}(undef, 9, 9)
    for (ind,val) in enumerate(grid)
        com_grid[ind] = add_var!(com, -9, -1)
    end

    sums = parseKillerJSON(JSON.parsefile("data/killer_niallsudoku_5500"))

    # the upper left sum constraint is x+y+z = 10 and the solution is -2 + -1 + -7
    # here I change it to 5*x-7*y-z = 4
    add_constraint!(com, 5*com_grid[CartesianIndex(1,1)]-7*com_grid[CartesianIndex(2,1)]-com_grid[CartesianIndex(2,2)] == 4)

    for s in sums[2:end]
        add_constraint!(com, sum([com_grid[CartesianIndex(ind)] for ind in s.indices]) == -s.result)
    end

    add_sudoku_constr!(com, com_grid)

    @test solve!(com) == :Solved
    @test fulfills_sudoku_constr(com_grid)
    @test 5*value(com_grid[CartesianIndex(1,1)])-7*value(com_grid[CartesianIndex(2,1)])-value(com_grid[CartesianIndex(2,2)]) == 4
    for s in sums[2:end]
        @test -s.result == sum([value(com_grid[CartesianIndex(i)]) for i in s.indices])
    end
end

end


function  kerker_mix(res, mixOpts::kerkerMixOptions)
#The Kerker mixing, following the parameter setup of elliptic
#preconditioner.
#
#Lin Lin
#Last revision: 5/6/2012

return kerker_mix(res, mixOpts.KerkerB, mixOpts.kmult,
                       mixOpts.YukawaK, mixOpts.epsil0)

end

function  kerker_mix(res, KerkerB::Float64, kmult, YukawaK::Float64, epsil0::Float64)
#The Kerker mixing, following the parameter setup of elliptic
#preconditioner.
#
#Lin Lin
#Last revision: 5/6/2012

resfft = fft(res);

resfft = ( epsil0 / (4*pi) * (kmult .+ YukawaK^2) )./
  ( KerkerB .+ epsil0 / (4*pi) * (kmult .+ YukawaK^2) ) .* resfft;

resprec = real(ifft(resfft));
resprec = resprec .- mean(resprec);

return resprec

end


export ⋅, absdot, magnitude², magnitude, normalize,
×, coordinate_system_from, permute, face_forward

# General functions for VectorLikes.

@inline ⋅(u::VectorLike, v::VectorLike) = sum(u.coordinates .* v.coordinates)

@inline absdot(u::VectorLike, v::VectorLike) = abs(u ⋅ v)

@inline magnitude²(v::VectorLike) = v ⋅ v

@inline magnitude(v::VectorLike) = √(magnitude²(v))

@inline normalize(v::VectorLike) = v / magnitude(v)

@inline Base.min(a::Vect{N,T}, b::Vect{N,U}) where {N,T,U} = promote_type(typeof(a), typeof(b))(min.(a.coordinates, b.coordinates)...)

@inline Base.max(a::Vect{N,T}, b::Vect{N,U}) where {N,T,U} = promote_type(typeof(a), typeof(b))(max.(a.coordinates, b.coordinates)...)

# Vector3-specific functions.

@inline ×(a::VectorLike{3,T}, b::VectorLike{3,U}) where {T,U} =
        Vector3{promote_type(T,U)}(
            a.y * b.z - a.z * b.y,
            a.z * b.x - a.x * b.z,
            a.x * b.y - a.y * b.x)

@inline face_forward(u::VectorLike{3,T}, v::VectorLike{3,U}) where {T,U} =
        u ⋅ v < zero(promote_type(T,U)) ? -u : u

@inline function coordinate_system_from(v₁::Vector3{T}) where T
    if abs(v₁.x) > abs(v₁.y)
        v₂ = typeof(v₁)(-v₁.z, 0.0, v₁.x) / √(v₁.x * v₁.x + v₁.z * v₁.z)
    else
        v₂ = typeof(v₂)(0.0, v₁.z, -v₁.y) / √(v₁.y * v₁.y + v₁.z * v₁.z)
    end
    v₁, v₂, v₁ × v₂
end

@inline permute(a::Vector3{T}, (x, y, z)) where T = typeof(a)(a[x], a[y], a[z])


" Temperature correction parameters"
abstract type AbstractTemperatureCorrection{T} end

# Temperature mixins
@mix @columns struct Tbase{T}
    # Field       | Default | Unit | Prior            | Limits              | Description
    reftemp::T    | 300.0  | K | Gamma(310.0, 1.0)   | [273.0, 325.0]     | _ | "Reference temperature for all rate parameters"
    arrtemp::T    | 2000.0 | K | Gamma(2000.0, 1.0)  | [200.0, 4000.0]    | _ | "Arrhenius temperature"
end

@mix @columns struct Tlow{T}
    tbelow::T     | -30.0   | K | Gamma(280.0, 1.0)   | [0.0, -40.0]       | _ | "Lower boundary of tolerance range"
    arrlower::T   | 20000.0 | K | Gamma(20000.0, 1.0) | [2000.0, 40000.0]  | _ | "Arrhenius temperature for lower boundary"
end

@mix @columns struct Tup{T}
    tabove::T     | 5.0     | K | Gamma(315.0, 1.0)   | [0.0, 20]          | _ | "Upper boundary of tolerance range"
    arrupper::T   | 70000.0 | K | Gamma(70000, 1.0)   | [7000.0, 140000.0] | _ | "Arrhenius temperature for upper boundary"
end

# Temperature types
" Simple temperature correction parameters "
@Tbase struct TempCorr{T} <: AbstractTemperatureCorrection{T} end
" Temperature correction with lower boudn parameters"
@Tbase @Tlow struct TempCorrLower{T} <: AbstractTemperatureCorrection{T} end
" Temperature correction with lower and upper bound parameters"
@Tbase @Tlow @Tup struct TempCorrLowerUpper{T} <: AbstractTemperatureCorrection{T} end

tempcorr(tc::Nothing, t) = 1.0
tempcorr(tc::TempCorr, t) = tempcorr(t |> K, tc.reftemp, tc.arrtemp)
tempcorr(tc::TempCorrLower, t) =
    tempcorr(t |> K, tc.reftemp, tc.arrtemp, tc.tbelow + tc.reftemp, tc.arrlower)
tempcorr(tc::TempCorrLowerUpper, t) =
    tempcorr(t |> K, tc.reftemp, tc.arrtemp, tc.tbelow + tc.reftemp, tc.arrlower, tc.tabove + tc.reftemp, tc.arrupper)

"""
    tempcorr(T, T1, A, [L, AL,] [H, AH])
DEB tempcorr function. Uses lower and uppper bounds if they are supplied.
Temperatures all in Kelvins.
"""
tempcorr(t, tref, a) = exp(a/tref - a/t)
tempcorr(t, tref, a, l, al) =
    exp(a/tref - a/t) * (1.0 + exp(al/tref - al/l)) / (1.0 + exp(al/t - al/l))
tempcorr(t, tref, a, l, al, h, ah) =
    exp(a/tref - a/t) * (1.0 + exp(al/tref - al/l) + exp(ah/h - ah/tref)) /
    (1.0 + exp(al/t - al/l) + exp(ah/h - ah/t))

@columns @flattenable struct ParentTardieu{Δ,Al,T,A} <: AbstractTemperatureCorrection{T}
    ΔH_A::Δ | true  | 63.5  | kJ*mol^-1 | Gamma(310.0, 1.0)   | [55.0, 65.0]   | _ | "The enthalpy of activation of the reaction. determines the curvature at low temperature"
    α::Al   | true  | 3.5   | _         | Gamma(310.0, 1.0)   | [1.0, 10.0]    | _ | "The ratio ΔH_D / ΔH_A"
    t0::T   | true  | 300.0 | K         | Gamma(310.0, 1.0)   | [273.0, 325.0] | _ | "Reference temperature"
    A::A    | false | 0.0   | K^-1      | Gamma(310.0, 1.0)   | [0.0, 1.0]     | _ | "Trait scaling coefficient"
end

ParentTardieu(ΔH_A, α, t0, A) = begin
    ParentTardieu(ΔH_A, α, t0)
end
ParentTardieu(ΔH_A, α, t0) = begin
    A = 1 / parent_tardieua_unscaled(ΔH_A, α, t0, t0)
    ParentTardieu{typeof.((ΔH_A, α, t0, A))...}(ΔH_A, α, t0, A)
end

parent_tardieua_unscaled(ΔH_A, α, t0, t) = begin
    ex = exp(-ΔH_A / (R * t))
    t * ex / (1 + ex^(α * (1 - (t / t0))))
end

tempcorr(f::ParentTardieu, t) = f.A * parent_tardieua_unscaled(f.ΔH_A, f.α, f.t0, t)


"""
    IRLS(d,operators,parameters;<keyword arguments>)

Non-quadratic regularization with Iteratively Reweighted Least Squares (IRLS).

# Arguments
- `Niter_external=3`
- 'Niter_internal=10'
- `mu=0`

"""
function IRLS(d,operators,parameters;Niter_external=3,Niter_internal=10,mu=0)

	cost = Float64[]
	weights = ones(Float64,size(d))
	parameters[end][:w] = weights
	m = []
	for iter_external = 1 : Niter_external
		v,cost1 = ConjugateGradients(d,operators,parameters,Niter=Niter_internal,mu=mu)
		append!(cost,cost1)
		m = v .* weights
		weights = abs.(m./maximum(abs.(m[:])))
		parameters[end][:w] = weights
	end

	return m, cost

end


export StraightLineHomotopy, StraightLineHomotopyCache, gamma

"""
    StraightLineHomotopy(G, F; gamma=exp(i * 2π*rand()))

Construct the homotopy ``H(x, t) = γtG(x) + (1-t)F(x)``.
"""
struct StraightLineHomotopy{S<:AbstractSystem,T<:AbstractSystem} <: AbstractHomotopy
    start::S
    target::T
    gamma::Complex{Float64}

end
function StraightLineHomotopy(start::AbstractSystem, target::AbstractSystem; gamma=randomish_gamma())
    StraightLineHomotopy(start, target, gamma)
end
(H::StraightLineHomotopy)(x, t, c=cache(H, x, t)) = evaluate(H, x, t, c)

"""
    StraightLineHomotopyCache

An simple cache for `StartTargetHomotopy`s consisting of the caches for the
start and target system as well as a `Vector` and a `Matrix`.
"""
struct StraightLineHomotopyCache{SC, TC, T1, T2} <: AbstractHomotopyCache
    start::SC
    target::TC

    u::Vector{T1} # for evaluation
    U::Matrix{T2} # for Jacobian
end

function cache(H::StraightLineHomotopy, x, t)
    start_cache = cache(H.start, x)
    target_cache = cache(H.target, x)
    u = complex.(float.(evaluate(H.start, x, start_cache)))
    U = complex.(float.(jacobian(H.start, x, start_cache)))
    StraightLineHomotopyCache(start_cache, target_cache, u, U)
end

Base.size(H::StraightLineHomotopy) = size(H.start)

"""
    gamma(H::StraightLineHomotopy)

Obtain the gamma used in the StraightLineHomotopy.
"""
gamma(H::StraightLineHomotopy) = H.gamma

"""
    γ(H)

Obtain the gamma used in the StraightLineHomotopy.
"""
γ(H::StraightLineHomotopy) = gamma(H)

function evaluate!(u, H::StraightLineHomotopy, x, t, c::StraightLineHomotopyCache)
    @inbounds evaluate!(c.u, H.start, x, c.start)
    @inbounds evaluate!(u, H.target, x, c.target)

    @inbounds for i in eachindex(u)
        u[i] = (γ(H) * t) * c.u[i] + (1.0 - t) * u[i]
    end

    u
end

function dt!(u, H::StraightLineHomotopy, x, t, c::StraightLineHomotopyCache)
    @inbounds evaluate!(c.u, H.start, x, c.start)
    @inbounds evaluate!(u, H.target, x, c.target)

    @inbounds for i in eachindex(u)
        u[i] = γ(H) * c.u[i] - u[i]
    end
    u
end

function jacobian!(U, H::StraightLineHomotopy, x, t, c::StraightLineHomotopyCache)
    @inbounds jacobian!(c.U, H.start, x, c.start)
    @inbounds jacobian!(U, H.target, x, c.target)

    @inbounds for i in eachindex(U)
        U[i] = (γ(H) * t) * c.U[i] + (1.0 - t) * U[i]
    end

    U
end

function evaluate_and_jacobian!(u, U, H::StraightLineHomotopy, x, t, c::StraightLineHomotopyCache)
    @inbounds evaluate_and_jacobian!(c.u, c.U, H.start, x, c.start)
    @inbounds evaluate_and_jacobian!(u, U, H.target, x, c.target)

    @inbounds for i in eachindex(u)
        u[i] = (γ(H) * t) * c.u[i] + (1.0 - t) * u[i]
    end
    @inbounds for i in eachindex(U)
        U[i] = (γ(H) * t) * c.U[i] + (1.0 - t) * U[i]
    end

    nothing
end

function jacobian_and_dt!(U, u, H::StraightLineHomotopy, x, t, c::StraightLineHomotopyCache)
    @inbounds evaluate_and_jacobian!(c.u, c.U, H.start, x, c.start)
    @inbounds evaluate_and_jacobian!(u, U, H.target, x, c.target)

    @inbounds for i in eachindex(U)
        U[i] = (γ(H) * t) * c.U[i] + (1.0 - t) * U[i]
    end
    @inbounds for i in eachindex(u)
        u[i] = γ(H) * c.u[i] - u[i]
    end

    nothing
end

function evaluate(H::StraightLineHomotopy, x, t, c::StraightLineHomotopyCache)
    M, N = size(H)
    evaluate!(similar(c.u, M), H, x, t, c)
end
function jacobian(H::StraightLineHomotopy, x, t, c::StraightLineHomotopyCache)
    M, N = size(H)
    jacobian!(similar(c.U, M, N), H, x, t, c)
end
function dt(H::StraightLineHomotopy, x, t, c::StraightLineHomotopyCache)
    M, N = size(H)
    dt!(similar(c.u, M), H, x, t, c)
end


@testset "cell" begin
	# 1D crystals
	cell = UnitCell(1, :lamellar, 10.0)
	@test cell.volume ≈ 10.0

	# 2D crystals
	cell = UnitCell(2, :square, 10.0)
	@test cell.volume ≈ 100.0

	cell = UnitCell(2, :rectangular, 10.0, 5.0)
	@test cell.volume ≈ 50.0

	cell = UnitCell(2, :hexagonal, 10.0)
	@test cell.volume ≈ 86.60254037844385

	cell = UnitCell(2, :oblique, 10.0, 5.0, pi/4)
	@test cell.volume ≈ 35.35533905932737

	# 3D crystals
	cell = UnitCell(3, :cubic, 10.0)
	@test cell.volume ≈ 1000.0

	cell = cell = UnitCell(3, :tetragonal, 10.0, 5.0)
	@test cell.volume ≈ 500.0

	cell = UnitCell(3, :orthorhombic, 10.0, 5.0, 2.0)
	@test cell.volume ≈ 100.0

	cell = UnitCell(3, :hexagonal, 10.0, 5.0)
	@test cell.volume ≈ 433.01270189221924

	cell = UnitCell(3, :monoclinic, 10.0, 5.0, 2.0, pi/4)
	@test cell.volume ≈ 70.71067811865474

	cell = UnitCell(3, :triclinic, 10.0, 5.0, 2.0, pi/2, pi/2, pi/2)
	@test cell.volume ≈ 100.0
end


#=
Popis
  Funkcia vypocita pre kazdy vrchol hodnotu jeho eigenvector centrality.

Parametre
  g : datova struktura grafu :: GenericGraph
  dist_key : nazov atributu, v ktorom je ulozena vlastnost vrchola :: String

Navratova hodnota
  eigenvector : slovnik obsahujuci mapovanie vrcholov a k nim prisluchajucich hodnot
           eigenvector centrality (index_vrchola => hodnota_eigenvector_centrality) :: Dict{Int64, Float64}()

Poznamky
  Eigen vector a eigen hodnoty su pocitane pomocou externeho procesu. Vzhladom na problemy implementacie
  je vysledok vypoctu nedeterministicky. Graf nesmie obsahovat paralelne hrany.
=#

function centrality_eigenvector(g::GenericGraph; dist_key::AbstractString="weight")

  if GraphAnalysis.contains_parallel_edges(g)
      println("Graf obsahuje paralelne hrany!")
      return false
  end

  matrix = sparse(weight_matrix(g,map(x->x.attributes[dist_key],edges(g))))
  matrix = matrix+matrix'

  (d, v) = eigs(matrix, nev=1, which=:LR, maxiter=50)

  largest = vec(v)
  x = vecnorm(largest)
  map!(v -> real(v), largest)
  norm = sign(sum(largest))*x

  eigenvector = DataStructures.OrderedDict(zip([v.index for v=vertices(g)],map(float,largest/norm)))

  return eigenvector

end


import Random: AbstractRNG, rand, Sampler

export GeneralTermSampler,
    large_terms,
    BoundedTermSampler

function S∞(z)
    sq = z^6 + 2z^5 - 5z^4 + 4z^3 - z^2 - 2z + 1
    num = z^3 - z^2 - z + 1 - √sq
    den = 2z^2 * (1 - z)
    num / den
end

p₁(x) = x^2 / (1 - x) / S∞(x)
p₂(x) = p₁(x) + x^2

const ρ = 0.509308127024237357194177485
const ρ² = ρ * ρ
const p₁ρ = (1 - ρ²) / 2
const p₂ρ = p₁ρ + ρ²

struct GeneralTermSampler <: Sampler{Term}
    x::Float64
    p₁::Float64
    p₂::Float64
end

GeneralTermSampler(x) = GeneralTermSampler(x, p₁(x), p₂(x))

const large_terms = GeneralTermSampler(ρ, p₁ρ, p₂ρ)

function rand_index(rng::AbstractRNG, s::GeneralTermSampler)
    result = 1

    while rand(rng) < s.x
        result += 1
    end

    return result
end

function rand(rng::AbstractRNG, s::GeneralTermSampler)
    p = rand(rng)

    if p < s.p₁
        return Var(rand_index(rng, s))
    elseif p < s.p₂
        return Abs(rand(rng, s))
    else
        App(rand(rng, s), rand(rng, s))
    end
end

struct BoundedTermSampler <: Sampler{Term}
    lower::Int
    upper::Int
end

function rand_ceiled(rng::AbstractRNG, s::BoundedTermSampler)::Union{Term, Nothing}
    p = rand(rng)

    if p < p₁ρ
        i = rand_index(rng, large_terms)
        return i < s.upper ? Var(i) : nothing
    elseif p < p₂ρ
        mbt = rand_ceiled(rng, s)
        if mbt !== nothing && length(mbt) + 2 ≤ s.upper
            return Abs(mbt)
        else
            return nothing
        end
    else
        mbt1, mbt2 = rand_ceiled(rng, s), rand_ceiled(rng, s)
        if mbt1 !== nothing && mbt2 !== nothing && length(mbt1) + length(mbt2) + 2 ≤ s.upper
            return App(mbt1, mbt2)
        else
            return nothing
        end
    end
end

function rand(rng::AbstractRNG, s::BoundedTermSampler)
    candidate = rand_ceiled(rng, s)

    while candidate == nothing || length(candidate) < s.lower
        candidate = rand_ceiled(rng, s)
    end

    return candidate
end


# # Bouncing ball

# ## Model

# We model the bouncing ball as a hybrid automaton with one location and a self-loop.
# See for example [^LG09] pp. 79-83.

module bouncing_ball #jl

using ReachabilityAnalysis, ModelingToolkit

function _bouncing_ball()

    var = @variables x v

    ## "falling" mode with invariant x >= 0
    invariant = HalfSpace(x ≥ 0, var)
    flow = @system(z' = [0.0 1.0; 0.0 0.0] * z + [0.0, -9.81], z ∈ invariant)

    ## guard x == 0 && v ≤ 0
    guard = HPolyhedron([0 ≤ x, x ≤ 0, v ≤ 0], var)

    ## reset map v⁺ := -cv
    assignment = ConstrainedLinearMap([1.0 0.0; 0.0 -0.75], guard)

    ## initial-value problem
    return HybridSystem(flow, assignment)
end

function model(X0)
    H = _bouncing_ball()
    return @ivp(H, z(0) ∈ X0)
end

end #jl

# ## References

# [^LG09]: Le Guernic, Colas. Reachability analysis of hybrid systems with linear continuous dynamics. Diss. 2009.


@testset "Check identifiability of `ODESystem` object" begin
    @parameters a01 a21 a12
    @variables t x0(t) x1(t) y1(t) [output = true]
    D = Differential(t)

    eqs = [D(x0) ~ -(a01 + a21) * x0 + a12 * x1, D(x1) ~ a21 * x0 - a12 * x1, y1 ~ x0]

    de = ODESystem(eqs, t, name=:Test)
    correct = Dict{Any,Any}(
        "nonidentifiable" => [a12, a21, substitute(x1, t=>0), a01],
        "locally_not_globally" => Num[],
        "globally" => [substitute(x0, t=>0)]
    )
    output = identifiability_ode(de)
    # have to use string here, otherwise fails on Ubutnu
    # TODO: figure out test on Ubuntu
    @test isequal(Set(correct["nonidentifiable"]), Set(output["nonidentifiable"]))
    @test isequal(Set(correct["locally_not_globally"]), Set(output["locally_not_globally"]))
    @test isequal(Set(correct["globally"]), Set(output["globally"]))

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

    @parameters a01 a21 a12
    @variables t x0(t) x1(t) y1(t) [output = true]
    D = Differential(t)
    using SpecialFunctions

    eqs = [
        D(x0) ~ -(a01 + a21) * SpecialFunctions.erfc(x0) + a12 * x1,
        D(x1) ~ a21 * x0 - a12 * x1,
        y1 ~ x0
    ]

    de = ODESystem(eqs, t, name=:Test)

    @test_throws ArgumentError identifiability_ode(de)
    # ----------
    # @parameters a b c
    # @variables t x1(t) x2(t) y(t) [output = true]
    # D = Differential(t)

    # eqs = [D(x1) ~ -a * x1 + x2 * b / (x1 + b / (c^2 - x2)), D(x2) ~ x2 * c^2 + x1, y ~ x2]
    # de = ODESystem(eqs, t, name=:Test)
    # correct = Dict(a => :globally, b => :globally, c => :locally)
    # to_check = [a, b, c]
    # @test isequal(correct, assess_identifiability(de, to_check))
end


if basename(pwd()) == "aoc"
    cd("2015/14")
end

function loadreindeer(filename::AbstractString)
    map(eachmatch(r"(.*) can fly (\d+) km/s for (\d+) seconds, but then must rest for (\d+) seconds\.", readchomp(filename))) do m
        m[1], (parse(Int, m[i]) for i in 2:4)...
    end
end

function position(reindeer, time::Integer)
    _, speed, on, off = reindeer
    fulls, final = divrem(time, on + off)
    (fulls * on + min(final, on)) * speed
end
position(time::Integer) = Base.Fix2(position, time)

part1(filename::AbstractString, n) = part1(loadreindeer(filename), n)
part1(reindeer, n = 2503) = maximum(position(n), reindeer)

part2(filename::AbstractString, n = 2503) = part2(loadreindeer(filename), n)

function part2(reindeer, n = 2503)
    scores = Dict(r[1] => 0 for r in reindeer)
    for i in 1:n
        top = maximum(position(i), reindeer)
        for r in reindeer
            if position(r, i) == top
                scores[r[1]] += 1
            end
        end
    end
    maximum(values(scores))
end


@doc raw"""
    SpecialEuclidean(n)

Special Euclidean group $\mathrm{SE}(n)$, the group of rigid motions.

``\mathrm{SE}(n)`` is the semidirect product of the [`TranslationGroup`](@ref) on $ℝ^n$ and
[`SpecialOrthogonal(n)`](@ref)

````math
\mathrm{SE}(n) ≐ \mathrm{T}(n) ⋊_θ \mathrm{SO}(n),
````

where $θ$ is the canonical action of $\mathrm{SO}(n)$ on $\mathrm{T}(n)$ by vector rotation.

This constructor is equivalent to calling

```julia
Tn = TranslationGroup(n)
SOn = SpecialOrthogonal(n)
SemidirectProductGroup(Tn, SOn, RotationAction(Tn, SOn))
```

Points on $\mathrm{SE}(n)$ may be represented as points on the underlying product manifold
$\mathrm{T}(n) × \mathrm{SO}(n)$. For group-specific functions, they may also be
represented as affine matrices with size `(n + 1, n + 1)` (see [`affine_matrix`](@ref)), for
which the group operation is [`MultiplicationOperation`](@ref).
"""
const SpecialEuclidean{N} = SemidirectProductGroup{
    ℝ,
    TranslationGroup{Tuple{N},ℝ},
    SpecialOrthogonal{N},
    RotationAction{TranslationGroup{Tuple{N},ℝ},SpecialOrthogonal{N},LeftAction},
}

function SpecialEuclidean(n)
    Tn = TranslationGroup(n)
    SOn = SpecialOrthogonal(n)
    A = RotationAction(Tn, SOn)
    return SemidirectProductGroup(Tn, SOn, A)
end

Base.show(io::IO, ::SpecialEuclidean{n}) where {n} = print(io, "SpecialEuclidean($(n))")

Base.@propagate_inbounds function submanifold_component(
    ::SpecialEuclidean{n},
    p::AbstractMatrix,
    ::Val{1},
) where {n}
    return view(p, 1:n, n + 1)
end
Base.@propagate_inbounds function submanifold_component(
    ::SpecialEuclidean{n},
    p::AbstractMatrix,
    ::Val{2},
) where {n}
    return view(p, 1:n, 1:n)
end

function submanifold_components(G::SpecialEuclidean{n}, p::AbstractMatrix) where {n}
    @assert size(p) == (n + 1, n + 1)
    @inbounds t = submanifold_component(G, p, Val(1))
    @inbounds R = submanifold_component(G, p, Val(2))
    return (t, R)
end

Base.@propagate_inbounds function _padpoint!(
    ::SpecialEuclidean{n},
    q::AbstractMatrix,
) where {n}
    for i in 1:n
        q[n + 1, i] = 0
    end
    q[n + 1, n + 1] = 1
    return q
end

Base.@propagate_inbounds function _padvector!(
    ::SpecialEuclidean{n},
    X::AbstractMatrix,
) where {n}
    for i in 1:(n + 1)
        X[n + 1, i] = 0
    end
    return X
end

@doc raw"""
    adjoint_action(::SpecialEuclidean{3}, p, fX::TFVector{<:Any,VeeOrthogonalBasis{ℝ}})

Adjoint action of the [`SpecialEuclidean`](@ref) group on the vector with coefficients `fX`
tangent at point `p`.

The formula for the coefficients reads ``t×(R⋅ω) + R⋅r`` for the translation part and
``R⋅ω`` for the rotation part, where `t` is the translation part of `p`, `R` is the rotation
matrix part of `p`, `r` is the translation part of `fX` and `ω` is the rotation part of `fX`,
``×`` is the cross product and ``⋅`` is the matrix product.
"""
function adjoint_action(::SpecialEuclidean{3}, p, fX::TFVector{<:Any,VeeOrthogonalBasis{ℝ}})
    t = p.parts[1]
    R = p.parts[2]
    r = fX.data[SA[1, 2, 3]]
    ω = fX.data[SA[4, 5, 6]]
    Rω = R * ω
    return TFVector([cross(t, Rω) + R * r; Rω], fX.basis)
end

@doc raw"""
    affine_matrix(G::SpecialEuclidean, p) -> AbstractMatrix

Represent the point $p ∈ \mathrm{SE}(n)$ as an affine matrix.
For $p = (t, R) ∈ \mathrm{SE}(n)$, where $t ∈ \mathrm{T}(n), R ∈ \mathrm{SO}(n)$, the
affine representation is the $n + 1 × n + 1$ matrix

````math
\begin{pmatrix}
R & t \\
0^\mathrm{T} & 1
\end{pmatrix}.
````

This function embeds $\mathrm{SE}(n)$ in the general linear group $\mathrm{GL}(n+1)$.
It is an isometric embedding and group homomorphism [^RicoMartinez1988].

See also [`screw_matrix`](@ref) for matrix representations of the Lie algebra.

[^RicoMartinez1988]:
    > Rico Martinez, J. M., “Representations of the Euclidean group and its applications
    > to the kinematics of spatial chains,” PhD Thesis, University of Florida, 1988.
"""
function affine_matrix(G::SpecialEuclidean{n}, p) where {n}
    pis = submanifold_components(G, p)
    pmat = allocate_result(G, affine_matrix, pis...)
    map(copyto!, submanifold_components(G, pmat), pis)
    @inbounds _padpoint!(G, pmat)
    return pmat
end
affine_matrix(::SpecialEuclidean{n}, p::AbstractMatrix) where {n} = p
function affine_matrix(::GT, ::Identity{GT}) where {n,GT<:SpecialEuclidean{n}}
    s = maybesize(Size(n, n))
    s isa Size && return SDiagonal{n,Float64}(I)
    return Diagonal{Float64}(I, n)
end

@doc raw"""
    screw_matrix(G::SpecialEuclidean, X) -> AbstractMatrix

Represent the Lie algebra element $X ∈ 𝔰𝔢(n) = T_e \mathrm{SE}(n)$ as a screw matrix.
For $X = (b, Ω) ∈ 𝔰𝔢(n)$, where $Ω ∈ 𝔰𝔬(n) = T_e \mathrm{SO}(n)$, the screw representation is
the $n + 1 × n + 1$ matrix

````math
\begin{pmatrix}
Ω & b \\
0^\mathrm{T} & 0
\end{pmatrix}.
````

This function embeds $𝔰𝔢(n)$ in the general linear Lie algebra $𝔤𝔩(n+1)$ but it's not
a homomorphic embedding (see [`SpecialEuclideanInGeneralLinear`](@ref) for a homomorphic one).

See also [`affine_matrix`](@ref) for matrix representations of the Lie group.
"""
function screw_matrix(G::SpecialEuclidean{n}, X) where {n}
    Xis = submanifold_components(G, X)
    Xmat = allocate_result(G, screw_matrix, Xis...)
    map(copyto!, submanifold_components(G, Xmat), Xis)
    @inbounds _padvector!(G, Xmat)
    return Xmat
end
screw_matrix(::SpecialEuclidean{n}, X::AbstractMatrix) where {n} = X

function allocate_result(G::SpecialEuclidean{n}, f::typeof(affine_matrix), p...) where {n}
    return allocate(p[1], Size(n + 1, n + 1))
end
function allocate_result(G::SpecialEuclidean{n}, f::typeof(screw_matrix), X...) where {n}
    return allocate(X[1], Size(n + 1, n + 1))
end

compose(::SpecialEuclidean, p::AbstractMatrix, q::AbstractMatrix) = p * q

function compose!(
    ::SpecialEuclidean,
    x::AbstractMatrix,
    p::AbstractMatrix,
    q::AbstractMatrix,
)
    return mul!(x, p, q)
end

@doc raw"""
    group_exp(G::SpecialEuclidean{n}, X)

Compute the group exponential of $X = (b, Ω) ∈ 𝔰𝔢(n)$, where $b ∈ 𝔱(n)$ and $Ω ∈ 𝔰𝔬(n)$:

````math
\exp X = (t, R),
````

where $t ∈ \mathrm{T}(n)$ and $R = \exp Ω$ is the group exponential on $\mathrm{SO}(n)$.

In the [`screw_matrix`](@ref) representation, the group exponential is the matrix
exponential (see [`group_exp`](@ref)).
"""
group_exp(::SpecialEuclidean, ::Any)

@doc raw"""
    group_exp(G::SpecialEuclidean{2}, X)

Compute the group exponential of $X = (b, Ω) ∈ 𝔰𝔢(2)$, where $b ∈ 𝔱(2)$ and $Ω ∈ 𝔰𝔬(2)$:

````math
\exp X = (t, R) = (U(θ) b, \exp Ω),
````

where $t ∈ \mathrm{T}(2)$, $R = \exp Ω$ is the group exponential on $\mathrm{SO}(2)$,

````math
U(θ) = \frac{\sin θ}{θ} I_2 + \frac{1 - \cos θ}{θ^2} Ω,
````

and $θ = \frac{1}{\sqrt{2}} \lVert Ω \rVert_e$
(see [`norm`](@ref norm(M::Rotations, p, X))) is the angle of the rotation.
"""
group_exp(::SpecialEuclidean{2}, ::Any)

@doc raw"""
    group_exp(G::SpecialEuclidean{3}, X)

Compute the group exponential of $X = (b, Ω) ∈ 𝔰𝔢(3)$, where $b ∈ 𝔱(3)$ and $Ω ∈ 𝔰𝔬(3)$:

````math
\exp X = (t, R) = (U(θ) b, \exp Ω),
````

where $t ∈ \mathrm{T}(3)$, $R = \exp Ω$ is the group exponential on $\mathrm{SO}(3)$,

````math
U(θ) = I_3 + \frac{1 - \cos θ}{θ^2} Ω + \frac{θ - \sin θ}{θ^3} Ω^2,
````

and $θ = \frac{1}{\sqrt{2}} \lVert Ω \rVert_e$
(see [`norm`](@ref norm(M::Rotations, p, X))) is the angle of the rotation.
"""
group_exp(::SpecialEuclidean{3}, ::Any)

function group_exp!(G::SpecialEuclidean, q, X)
    Xmat = screw_matrix(G, X)
    qmat = exp(Xmat)
    map(copyto!, submanifold_components(G, q), submanifold_components(G, qmat))
    _padpoint!(G, q)
    return q
end
function group_exp!(G::SpecialEuclidean{2}, q, X)
    SO2 = submanifold(G, 2)
    b, Ω = submanifold_components(G, X)
    t, R = submanifold_components(G, q)
    @assert size(R) == (2, 2)
    @assert size(t) == (2,)
    @assert size(b) == (2,)

    θ = vee(SO2, Identity(SO2, Ω), Ω)[1]
    sinθ, cosθ = sincos(θ)
    if θ ≈ 0
        α = 1 - θ^2 / 6
        β = θ / 2
    else
        α = sinθ / θ
        β = (1 - cosθ) / θ
    end

    @inbounds begin
        R[1] = cosθ
        R[2] = sinθ
        R[3] = -sinθ
        R[4] = cosθ
        t[1] = α * b[1] - β * b[2]
        t[2] = α * b[2] + β * b[1]
        _padpoint!(G, q)
    end
    return q
end
function group_exp!(G::SpecialEuclidean{3}, q, X)
    SO3 = submanifold(G, 2)
    b, Ω = submanifold_components(G, X)
    t, R = submanifold_components(G, q)
    @assert size(R) == (3, 3)
    @assert size(t) == (3,)

    θ = norm(SO3, Identity(SO3, Ω), Ω) / sqrt(2)
    θ² = θ^2
    if θ ≈ 0
        α = 1 - θ² / 6
        β = θ / 2
        γ = 1 / 6 - θ² / 120
    else
        sinθ, cosθ = sincos(θ)
        α = sinθ / θ
        β = (1 - cosθ) / θ²
        γ = (1 - α) / θ²
    end

    Ω² = Ω^2
    Jₗ = I + β .* Ω .+ γ .* Ω²
    R .= I + α .* Ω .+ β .* Ω²
    copyto!(t, Jₗ * b)
    @inbounds _padpoint!(G, q)
    return q
end

@doc raw"""
    group_log(G::SpecialEuclidean{n}, p) where {n}

Compute the group logarithm of $p = (t, R) ∈ \mathrm{SE}(n)$, where $t ∈ \mathrm{T}(n)$
and $R ∈ \mathrm{SO}(n)$:

````math
\log p = (b, Ω),
````

where $b ∈ 𝔱(n)$ and $Ω = \log R ∈ 𝔰𝔬(n)$ is the group logarithm on $\mathrm{SO}(n)$.

In the [`affine_matrix`](@ref) representation, the group logarithm is the matrix logarithm
(see [`group_log`](@ref)):
"""
group_log(::SpecialEuclidean, ::Any)

@doc raw"""
    group_log(G::SpecialEuclidean{2}, p)

Compute the group logarithm of $p = (t, R) ∈ \mathrm{SE}(2)$, where $t ∈ \mathrm{T}(2)$
and $R ∈ \mathrm{SO}(2)$:

````math
\log p = (b, Ω) = (U(θ)^{-1} t, \log R),
````

where $b ∈ 𝔱(2)$, $Ω = \log R ∈ 𝔰𝔬(2)$ is the group logarithm on $\mathrm{SO}(2)$,

````math
U(θ) = \frac{\sin θ}{θ} I_2 + \frac{1 - \cos θ}{θ^2} Ω,
````

and $θ = \frac{1}{\sqrt{2}} \lVert Ω \rVert_e$
(see [`norm`](@ref norm(M::Rotations, p, X))) is the angle of the rotation.
"""
group_log(::SpecialEuclidean{2}, ::Any)

@doc raw"""
    group_log(G::SpecialEuclidean{3}, p)

Compute the group logarithm of $p = (t, R) ∈ \mathrm{SE}(3)$, where $t ∈ \mathrm{T}(3)$
and $R ∈ \mathrm{SO}(3)$:

````math
\log p = (b, Ω) = (U(θ)^{-1} t, \log R),
````

where $b ∈ 𝔱(3)$, $Ω = \log R ∈ 𝔰𝔬(3)$ is the group logarithm on $\mathrm{SO}(3)$,

````math
U(θ) = I_3 + \frac{1 - \cos θ}{θ^2} Ω + \frac{θ - \sin θ}{θ^3} Ω^2,
````

and $θ = \frac{1}{\sqrt{2}} \lVert Ω \rVert_e$
(see [`norm`](@ref norm(M::Rotations, p, X))) is the angle of the rotation.
"""
group_log(::SpecialEuclidean{3}, ::Any)

function group_log!(G::SpecialEuclidean, X, q)
    qmat = affine_matrix(G, q)
    Xmat = real(log_safe(qmat))
    map(copyto!, submanifold_components(G, X), submanifold_components(G, Xmat))
    _padvector!(G, X)
    return X
end
function group_log!(G::SpecialEuclidean{2}, X, q)
    SO2 = submanifold(G, 2)
    b, Ω = submanifold_components(G, X)
    t, R = submanifold_components(G, q)
    @assert size(b) == (2,)

    group_log!(SO2, Ω, R)
    @inbounds θ = Ω[2]
    β = θ / 2
    α = θ ≈ 0 ? 1 - β^2 / 3 : β * cot(β)

    @inbounds begin
        b[1] = α * t[1] + β * t[2]
        b[2] = α * t[2] - β * t[1]
        _padvector!(G, X)
    end
    return X
end
function group_log!(G::SpecialEuclidean{3}, X, q)
    b, Ω = submanifold_components(G, X)
    t, R = submanifold_components(G, q)
    @assert size(Ω) == (3, 3)
    @assert size(b) == (3,)

    trR = tr(R)
    cosθ = (trR - 1) / 2
    θ = acos(clamp(cosθ, -1, 1))
    θ² = θ^2
    if θ ≈ 0
        α = 1 / 2 + θ² / 12
        β = 1 / 12 + θ² / 720
    else
        sinθ = sin(θ)
        α = θ / sinθ / 2
        β = 1 / θ² - (1 + cosθ) / 2 / θ / sinθ
    end

    Ω .= (R .- transpose(R)) .* α
    Jₗ⁻¹ = I - Ω ./ 2 .+ β .* Ω^2
    mul!(b, Jₗ⁻¹, t)
    @inbounds _padvector!(G, X)
    return X
end

"""
    lie_bracket(G::SpecialEuclidean, X::ProductRepr, Y::ProductRepr)
    lie_bracket(G::SpecialEuclidean, X::AbstractMatrix, Y::AbstractMatrix)

Calculate the Lie bracket between elements `X` and `Y` of the special Euclidean Lie
algebra. For the matrix representation (which can be obtained using [`screw_matrix`](@ref))
the formula is ``[X, Y] = XY-YX``, while in the [`ProductRepr`](@ref) representation the
formula reads ``[X, Y] = [(t_1, R_1), (t_2, R_2)] = (R_1 t_2 - R_2 t_1, R_1 R_2 - R_2 R_1)``.
"""
function lie_bracket(G::SpecialEuclidean, X::ProductRepr, Y::ProductRepr)
    nX, hX = submanifold_components(G, X)
    nY, hY = submanifold_components(G, Y)
    return ProductRepr(hX * nY - hY * nX, lie_bracket(G.manifold.manifolds[2], hX, hY))
end
function lie_bracket(::SpecialEuclidean, X::AbstractMatrix, Y::AbstractMatrix)
    return X * Y - Y * X
end

function lie_bracket!(G::SpecialEuclidean, Z, X, Y)
    nX, hX = submanifold_components(G, X)
    nY, hY = submanifold_components(G, Y)
    nZ, hZ = submanifold_components(G, Z)
    lie_bracket!(G.manifold.manifolds[2], hZ, hX, hY)
    nZ .= hX * nY .- hY * nX
    @inbounds _padvector!(G, Z)
    return Z
end

"""
    translate_diff(G::SpecialEuclidean, p, q, X, ::RightAction)

Differential of the right action of the [`SpecialEuclidean`](@ref) group on itself.
The formula for the rotation part is the differential of the right rotation action, while
the formula for the translation part reads
````math
R_q⋅X_R⋅t_p + X_t
````
where ``R_q`` is the rotation part of `q`, ``X_R`` is the rotation part of `X`, ``t_p``
is the translation part of `p` and ``X_t`` is the translation part of `X`.
"""
translate_diff(G::SpecialEuclidean, p, q, X, ::RightAction)

function translate_diff!(G::SpecialEuclidean, Y, p, q, X, ::RightAction)
    np, hp = submanifold_components(G, p)
    nq, hq = submanifold_components(G, q)
    nX, hX = submanifold_components(G, X)
    nY, hY = submanifold_components(G, Y)
    hY .= hp' * hX * hp
    copyto!(nY, hq * (hX * np) + nX)
    @inbounds _padvector!(G, Y)
    return Y
end
function translate_diff!(G::SpecialEuclidean, Y, ::Identity, q, X, ::RightAction)
    copyto!(G, Y, X)
    @inbounds _padvector!(G, Y)
    return Y
end
function translate_diff!(G::SpecialEuclidean, Y, p, ::Identity, X, ::RightAction)
    np, hp = submanifold_components(G, p)
    nX, hX = submanifold_components(G, X)
    nY, hY = submanifold_components(G, Y)
    hY .= hp' * hX * hp
    copyto!(nY, hX * np + nX)
    @inbounds _padvector!(G, Y)
end
function translate_diff!(G::SpecialEuclidean, Y, ::Identity, ::Identity, X, ::RightAction)
    copyto!(G, Y, X)
    @inbounds _padvector!(G, Y)
    return Y
end

@doc raw"""
    SpecialEuclideanInGeneralLinear

An explicit isometric and homomorphic embedding of $\mathrm{SE}(n)$ in $\mathrm{GL}(n+1)$
and $𝔰𝔢(n)$ in $𝔤𝔩(n+1)$.
Note that this is *not* a transparently isometric embedding.

# Constructor

    SpecialEuclideanInGeneralLinear(n)
"""
const SpecialEuclideanInGeneralLinear =
    EmbeddedManifold{ℝ,<:SpecialEuclidean,<:GeneralLinear}

function SpecialEuclideanInGeneralLinear(n)
    return EmbeddedManifold(SpecialEuclidean(n), GeneralLinear(n + 1))
end

"""
    embed(M::SpecialEuclideanInGeneralLinear, p)

Embed the point `p` on [`SpecialEuclidean`](@ref) in the [`GeneralLinear`](@ref) group.
The embedding is calculated using [`affine_matrix`](@ref).
"""
function embed(M::SpecialEuclideanInGeneralLinear, p)
    G = M.manifold
    return affine_matrix(G, p)
end
"""
    embed(M::SpecialEuclideanInGeneralLinear, p, X)

Embed the tangent vector X at point `p` on [`SpecialEuclidean`](@ref) in the
[`GeneralLinear`](@ref) group. Point `p` can use any representation valid for
`SpecialEuclidean`. The embedding is similar from the one defined by [`screw_matrix`](@ref)
but the translation part is multiplied by inverse of the rotation part.
"""
function embed(M::SpecialEuclideanInGeneralLinear, p, X)
    G = M.manifold
    np, hp = submanifold_components(G, p)
    nX, hX = submanifold_components(G, X)
    Y = allocate_result(G, screw_matrix, nX, hX)
    nY, hY = submanifold_components(G, Y)
    copyto!(hY, hX)
    copyto!(nY, hp' * nX)
    @inbounds _padvector!(G, Y)
    return Y
end

function embed!(M::SpecialEuclideanInGeneralLinear, q, p)
    return copyto!(q, embed(M, p))
end
function embed!(M::SpecialEuclideanInGeneralLinear, Y, p, X)
    return copyto!(Y, embed(M, p, X))
end

"""
    project(M::SpecialEuclideanInGeneralLinear, p)

Project point `p` in [`GeneralLinear`](@ref) to the [`SpecialEuclidean`](@ref) group.
This is performed by extracting the rotation and translation part as in [`affine_matrix`](@ref).
"""
function project(M::SpecialEuclideanInGeneralLinear, p)
    G = M.manifold
    np, hp = submanifold_components(G, p)
    return ProductRepr(np, hp)
end
"""
    project(M::SpecialEuclideanInGeneralLinear, p, X)

Project tangent vector `X` at point `p` in [`GeneralLinear`](@ref) to the
[`SpecialEuclidean`](@ref) Lie algebra.
This reverses the transformation performed by [`embed`](@ref embed(M::SpecialEuclideanInGeneralLinear, p, X))
"""
function project(M::SpecialEuclideanInGeneralLinear, p, X)
    G = M.manifold
    np, hp = submanifold_components(G, p)
    nX, hX = submanifold_components(G, X)
    return ProductRepr(hp * nX, hX)
end

function project!(M::SpecialEuclideanInGeneralLinear, q, p)
    return copyto!(q, project(M, p))
end
function project!(M::SpecialEuclideanInGeneralLinear, Y, p, X)
    return copyto!(Y, project(M, p, X))
end


function test_cgls()
  cgls_tol = 1.0e-5;

  for npower = 1 : 4
    (b, A, D, HY, HZ, Acond, rnorm) = test(40, 40, 4, npower, 0);  # No regularization.

    (x, stats) = cgls(A, b);
    resid = norm(A' * (A*x - b)) / norm(b)
    @printf("CGLS: Relative residual: %8.1e\n", resid);
    @test(resid <= cgls_tol);
    @test(stats.solved);

    λ = 1.0e-3;
    (x, stats) = cgls(A, b, λ=λ);
    resid = norm(A' * (A*x - b) + λ * x) / norm(b)
    @printf("CGLS: Relative residual: %8.1e\n", resid);
    @test(resid <= cgls_tol);
    @test(stats.solved);
  end

  # Test with preconditioning.
  A = rand(10, 6); b = rand(10);
  M = InverseLBFGSOperator(10, 4);
  for _ = 1 : 6
    s = rand(10);
    y = rand(10);
    push!(M, s, y);
  end

  (x, stats) = cgls(A, b, M=M);
  resid = norm(A' * M * (A * x - b)) / sqrt(dot(b, M * b));
  @printf("CGLS: Preconditioned residual: %8.1e\n", resid);
  @test resid <= cgls_tol;

  # test trust-region constraint
  (x, stats) = cgls(A, b)

  radius = 0.75 * norm(x)
  (x, stats) = cgls(A, b, radius=radius)
  @test(stats.solved);
  @test(abs(radius - norm(x)) <= cgls_tol * radius)

  opA = LinearOperator(A)
  (xop, statsop) = cgls(opA, b, radius=radius)
  @test(abs(radius - norm(xop)) <= cgls_tol * radius)

  # Code coverage.
  (b, A, D, HY, HZ, Acond, rnorm) = test(40, 40, 4, 3, 0);
  (x, stats) = cgls(Matrix(A), b);
  (x, stats) = cgls(sparse(Matrix(A)), b);
  show(stats);

  # Test b == 0
  (x, stats) = cgls(A, zeros(size(A,1)))
  @test x == zeros(size(A,1))
  @test stats.status == "x = 0 is a zero-residual solution"

  # Test integer values
  A = [I; rand(1:10, 2, 3)]
  b = A * ones(Int, 3)
  (x, stats) = cgls(A, b)
  @test stats.solved
end

test_cgls()


"""
    Jaro()

Creates the Jaro metric

The Jaro distance is defined as

``1 - (m / |s1| + m / |s2| + (m - t) / m) / 3``

where ``m`` is the number of matching characters and
``t`` is half the number of transpositions.
"""
struct Jaro <: SemiMetric end
isnormalized(::Jaro) = true

## http://alias-i.com/lingpipe/docs/api/com/aliasi/spell/JaroWinklerDistance.html
## accepts any iterator, including AbstractString
function evaluate(dist::Jaro, s1, s2, max_dist = nothing)
    (ismissing(s1) | ismissing(s2)) && return missing
    s1, s2 = reorder(s1, s2)
    len1, len2 = length(s1), length(s2)
    # if both are empty, m = 0 so should be 1.0 according to wikipedia.
    # Add this line so that not the case
    len2 == 0 && return 0.0
    maxdist = max(0, div(len2, 2) - 1)
    flag = fill(false, len2)
    prevstate1 = firstindex(s1)
    i1_match = fill(prevstate1, len1)
    #  m counts number matching characters
    m = 0
    i1 = 1
    i2 = 1
    x1 = iterate(s1)
    x2 = iterate(s2)
    while x1 !== nothing
        ch1, state1 = x1
        if i2 <= i1 - maxdist - 1
            ch2, state2 = x2
            i2 += 1
            x2 = iterate(s2, state2)
        end
        i2curr = i2
        x2curr = x2
        while x2curr !== nothing
            i2curr > i1 + maxdist && break
            ch2, state2 = x2curr
            if (ch1 == ch2) && !flag[i2curr]
                m += 1
                flag[i2curr] = true
                i1_match[m] = prevstate1
                break
            end
            x2curr = iterate(s2, state2)
            i2curr += 1
        end
        x1 = iterate(s1, state1)
        i1 += 1
        prevstate1 = state1
    end
    m == 0 && return 1.0
    # t counts number of transpositions
    t = 0
    i1 = 0
    i2 = 0
    for ch2 in s2
        i2 += 1
        if flag[i2]
            i1 += 1
            t += ch2 != iterate(s1, i1_match[i1])[1]
        end
    end
    return 1.0 - (m / len1 + m / len2 + (m - t/2) / m) / 3.0
end

"""
    Levenshtein()

Creates the Levenshtein metric

The Levenshtein distance is the minimum number of operations (consisting of insertions, deletions,
substitutions of a single character) required to change one string into the other.
"""
struct Levenshtein <: Metric end

## Source: http://blog.softwx.net/2014/12/optimizing-levenshtein-algorithm-in-c.html
# Return max_dist +1 if distance higher than max_dist
# This makes it possible to differentiate distance equalt to max_dist vs strictly higher
# This is important for find_all
## accepts any iterator, including AbstractString
function evaluate(dist::Levenshtein, s1, s2, max_dist = nothing)
    (ismissing(s1) | ismissing(s2)) && return missing
    s1, s2 = reorder(s1, s2)
    len1, len2 = length(s1), length(s2)
    max_dist !== nothing && len2 - len1 > max_dist && return max_dist + 1
    # prefix common to both strings can be ignored
    k, x1, x2start = common_prefix(s1, s2)
    x1 == nothing && return len2 - k
    # distance initialized to first row of matrix
    # => distance between "" and s2[1:i}
    v = collect(1:(len2-k))
    current = 0
    i1 = 1
    while x1 !== nothing
        ch1, state1 = x1
        left = i1 - 1
        current = i1 - 1
        min_dist = i1 - 2
        i2 = 1
        x2 = x2start
        while x2 !== nothing
            ch2, state2 = x2
            #  update
            above, current, left = current, left, v[i2]
            if ch1 != ch2
                current = min(current + 1, above + 1, left + 1)
            end
            min_dist = min(min_dist, left)
            v[i2] = current
            x2 = iterate(s2, state2)
            i2 += 1
        end
        max_dist !== nothing && min_dist > max_dist && return max_dist + 1
        x1 = iterate(s1, state1)
        i1 += 1
    end
    max_dist !== nothing && current > max_dist && return max_dist + 1
    return current
end

"""
    DamerauLevenshtein()

Creates the DamerauLevenshtein metric

The DamerauLevenshtein distance is the minimum number of operations (consisting of insertions,
deletions or substitutions of a single character, or transposition of two adjacent characters)
required to change one string into the other.
"""
struct DamerauLevenshtein <: SemiMetric end

## http://blog.softwx.net/2015/01/optimizing-damerau-levenshtein_15.html
## accepts any iterator, including AbstractString
function evaluate(dist::DamerauLevenshtein, s1, s2, max_dist = nothing)
    (ismissing(s1) | ismissing(s2)) && return missing
    s1, s2 = reorder(s1, s2)
    len1, len2 = length(s1), length(s2)
    max_dist !== nothing && len2 - len1 > max_dist && return max_dist + 1
    # prefix common to both strings can be ignored
    k, x1, x2start = common_prefix(s1, s2)
    (x1 == nothing) && return len2 - k
    v = collect(1:(len2-k))
    w = similar(v)
    if max_dist !== nothing
        offset = 1 + max_dist - (len2 - len1)
        i2_start = 1
        i2_end = max_dist
    end
    i1 = 1
    current = i1
    prevch1, = x1
    while x1 !== nothing
        ch1, state1 = x1
        left = (i1 - 1)
        current = i1
        nextTransCost = 0
        prevch2, = x2start
        if max_dist !== nothing
            i2_start += (i1 > offset) ? 1 : 0
            i2_end = min(i2_end + 1, len2)
        end
        x2 = x2start
        i2 = 1
        while x2 !== nothing
            ch2, state2 = x2
            if max_dist == nothing || (i2_start <= i2 <= i2_end)
                above = current
                thisTransCost = nextTransCost
                nextTransCost = w[i2]
                # cost of diagonal (substitution)
                w[i2] = current = left
                # left now equals current cost (which will be diagonal at next iteration)
                left = v[i2]
                if ch1 != ch2
                    # insertion
                    if left < current
                        current = left
                    end
                    # deletion
                    if above < current
                        current = above
                    end
                    current += 1
                    if (i1 != 1) & (i2 != 1) & (ch1 == prevch2) & (prevch1 == ch2)
                        thisTransCost += 1
                        if thisTransCost < current
                            current = thisTransCost
                        end
                    end
                end
                v[i2] = current
            end
            x2 = iterate(s2, state2)
            i2 += 1
            prevch2 = ch2
        end
        max_dist !== nothing && v[i1 + len2 - len1] > max_dist && return max_dist + 1
        x1 = iterate(s1, state1)
        i1 += 1
        prevch1 = ch1
    end
    max_dist !== nothing && current > max_dist && return max_dist + 1
    return current
end

"""
    RatcliffObershelp()

Creates the RatcliffObershelp metric

The distance between two strings is defined as one minus  the number of matching characters
divided by the total number of characters in the two strings. Matching characters are those
in the longest common subsequence plus, recursively, matching characters in the unmatched
region on either side of the longest common subsequence.
"""
struct RatcliffObershelp <: SemiMetric end

isnormalized(::RatcliffObershelp) = true

function evaluate(dist::RatcliffObershelp, s1, s2, max_dist = nothing)
    (ismissing(s1) | ismissing(s2)) && return missing
    n_matched = sum(last.(matching_blocks(s1, s2)))
    len1, len2 = length(s1), length(s2)
    len1 + len2 == 0 ? 0. : 1.0 - 2 *  n_matched / (len1 + len2)
end

function matching_blocks(s1, s2)
    matching_blocks!(Set{Tuple{Int, Int, Int}}(), s1, s2, length(s1), length(s2), 1, 1)
end

function matching_blocks!(x::Set{Tuple{Int, Int, Int}}, s1, s2,
    len1::Integer, len2::Integer, start1::Integer, start2::Integer)
    a = longest_common_pattern(s1, s2, len1 , len2)
    # exit if there is no common substring
    a[3] == 0 && return x
    # add the info of the common to the existing set
    push!(x, (a[1] + start1 - 1, a[2] + start2 - 1, a[3]))
    # add the longest common substring that happens before
    s1before = _take(s1, a[1] - 1)
    s2before = _take(s2, a[2] - 1)
    matching_blocks!(x, s1before, s2before, a[1] - 1, a[2] - 1, start1, start2)
    # add the longest common substring that happens after
    s1after = _drop(s1, a[1] + a[3] - 1)
    s2after = _drop(s2, a[2] + a[3] - 1)
    matching_blocks!(x, s1after, s2after, len1 - (a[1] + a[3]) + 1,
        len2 - (a[2] + a[3]) + 1, start1 + a[1] + a[3] - 1, start2 + a[2] + a[3] - 1)
    return x
end

function longest_common_pattern(s1, s2, len1::Integer, len2::Integer)
    if len1 > len2
        start2, start1, len = longest_common_pattern(s2, s1, len2, len1)
    else
        start1, start2, len = 0, 0, 0
        p = zeros(Int, len2)
        i1 = 0
        for ch1 in s1
            i1 += 1
            i2 = 0
            oldp = 0
            for ch2 in s2
                i2 += 1
                newp = 0
                if ch1 == ch2
                    newp = oldp > 0 ? oldp : i2
                    currentlength = (i2 - newp + 1)
                    if currentlength > len
                        start1, start2, len = i1 - currentlength + 1, newp, currentlength
                    end
                end
                p[i2], oldp = newp, p[i2]
            end
        end
    end
    return start1, start2, len
end