@testset "Graph coloring" begin

    function normal_49_states(;
        reverse_constraint = false,
        tests = true,
        time_limit = Inf,
        simplify = true,
    )
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "keep_logs" => true,
            "logging" => [],
            "time_limit" => time_limit,
            "simplify" => simplify,
            "seed" => 1,
        ))
        num_colors = 20

        @variable(m, 1 <= states[1:50] <= num_colors, Int)
        @variable(m, 1 <= max_color <= num_colors, Int)

        washington,
        montana,
        maine,
        north_dakota,
        south_dakota,
        wyoming,
        wisconsin,
        idaho,
        vermont,
        minnesota,
        oregon,
        new_hampshire,
        iowa,
        massachusetts,
        nebraska,
        new_york,
        pennsylvania,
        connecticut,
        rhode_island,
        new_jersey,
        indiana,
        nevada,
        utah,
        california,
        ohio,
        illinois,
        washington_dc,
        delaware,
        west_virginia,
        maryland,
        colorado,
        kentucky,
        kansas,
        virginia,
        missouri,
        arizona,
        oklahoma,
        north_carolina,
        tennessee,
        texas,
        new_mexico,
        alabama,
        mississippi,
        georgia,
        south_carolina,
        arkansas,
        louisiana,
        florida,
        michigan = states

        @constraint(m, washington != oregon)
        @constraint(m, washington != idaho)
        @constraint(m, oregon != idaho)
        @constraint(m, oregon != nevada)
        @constraint(m, oregon != california)
        @constraint(m, california != nevada)
        @constraint(m, california != arizona)
        @constraint(m, nevada != idaho)
        @constraint(m, nevada != utah)
        @constraint(m, nevada != arizona)
        @constraint(m, idaho != montana)
        @constraint(m, idaho != wyoming)
        @constraint(m, idaho != utah)
        @constraint(m, utah != wyoming)
        @constraint(m, utah != colorado)
        @constraint(m, utah != new_mexico)
        @constraint(m, utah != arizona)
        @constraint(m, arizona != colorado)
        @constraint(m, arizona != new_mexico)
        @constraint(m, montana != north_dakota)
        @constraint(m, montana != south_dakota)
        @constraint(m, montana != wyoming)
        @constraint(m, wyoming != south_dakota)
        @constraint(m, wyoming != nebraska)
        @constraint(m, wyoming != colorado)
        @constraint(m, colorado != nebraska)
        @constraint(m, colorado != kansas)
        @constraint(m, colorado != oklahoma)
        @constraint(m, colorado != new_mexico)
        @constraint(m, new_mexico != oklahoma)
        @constraint(m, new_mexico != texas)
        @constraint(m, north_dakota != minnesota)
        @constraint(m, north_dakota != south_dakota)
        @constraint(m, south_dakota != minnesota)
        @constraint(m, south_dakota != iowa)
        @constraint(m, south_dakota != nebraska)
        @constraint(m, nebraska != iowa)
        @constraint(m, nebraska != missouri)
        @constraint(m, nebraska != kansas)
        @constraint(m, kansas != missouri)
        @constraint(m, kansas != oklahoma)
        @constraint(m, oklahoma != arkansas)
        @constraint(m, oklahoma != texas)
        @constraint(m, texas != arkansas)
        @constraint(m, texas != louisiana)
        @constraint(m, minnesota != wisconsin)
        @constraint(m, minnesota != iowa)
        @constraint(m, iowa != wisconsin)
        @constraint(m, iowa != illinois)
        @constraint(m, iowa != missouri)
        @constraint(m, missouri != illinois)
        @constraint(m, missouri != kentucky)
        @constraint(m, missouri != tennessee)
        @constraint(m, missouri != arkansas)
        @constraint(m, arkansas != tennessee)
        @constraint(m, arkansas != mississippi)
        @constraint(m, arkansas != louisiana)
        @constraint(m, louisiana != mississippi)
        @constraint(m, wisconsin != illinois)
        @constraint(m, wisconsin != michigan)
        @constraint(m, illinois != indiana)
        @constraint(m, illinois != kentucky)
        @constraint(m, kentucky != indiana)
        @constraint(m, kentucky != ohio)
        @constraint(m, kentucky != west_virginia)
        @constraint(m, kentucky != virginia)
        @constraint(m, kentucky != tennessee)
        @constraint(m, tennessee != virginia)
        @constraint(m, tennessee != north_carolina)
        @constraint(m, tennessee != georgia)
        @constraint(m, tennessee != alabama)
        @constraint(m, tennessee != mississippi)
        @constraint(m, mississippi != alabama)
        @constraint(m, michigan != indiana)
        @constraint(m, michigan != ohio)
        @constraint(m, indiana != ohio)
        @constraint(m, alabama != georgia)
        @constraint(m, alabama != florida)
        @constraint(m, ohio != pennsylvania)
        @constraint(m, ohio != west_virginia)
        @constraint(m, maine != new_hampshire)
        @constraint(m, new_hampshire != vermont)
        @constraint(m, new_hampshire != massachusetts)
        @constraint(m, vermont != massachusetts)
        @constraint(m, vermont != new_york)
        @constraint(m, massachusetts != new_york)
        @constraint(m, massachusetts != rhode_island)
        @constraint(m, massachusetts != connecticut)
        @constraint(m, connecticut != rhode_island)
        @constraint(m, new_york != connecticut)
        @constraint(m, new_york != pennsylvania)
        @constraint(m, new_york != new_jersey)
        @constraint(m, pennsylvania != new_jersey)
        @constraint(m, pennsylvania != delaware)
        @constraint(m, pennsylvania != maryland)
        @constraint(m, pennsylvania != west_virginia)
        @constraint(m, new_jersey != delaware)
        @constraint(m, maryland != washington_dc)
        @constraint(m, maryland != west_virginia)
        @constraint(m, maryland != virginia)
        @constraint(m, washington_dc != virginia)
        @constraint(m, west_virginia != virginia)
        @constraint(m, virginia != north_carolina)
        @constraint(m, north_carolina != south_carolina)
        @constraint(m, south_carolina != georgia)
        @constraint(m, georgia != florida)

        if reverse_constraint
            @constraint(m, max_color .>= states)
        else
            @constraint(m, states .<= max_color)
        end

        @objective(m, Min, max_color)

        optimize!(m)

        status = JuMP.termination_status(m)

        com = CS.get_inner_model(m)

        CS.save_logs(
            com,
            "graph_color_optimize.json",
            :states => states,
            :max_color => max_color,
        )
        rm("graph_color_optimize.json")

        if tests
            @test status == MOI.OPTIMAL
            @test com.best_sol == 4 == JuMP.objective_value(m) == JuMP.objective_bound(m)
            @test maximum([JuMP.value(var) for var in states]) == 4 == JuMP.value(max_color)
            @test 0 <= MOI.get(m, MOI.SolveTime()) < 5
        end
        return com, m
    end

    @testset "49 US states + DC time limit" begin
        com1, m1 = normal_49_states(; time_limit = 0.001, tests = false, simplify = false)
        com2, m2 = normal_49_states(; simplify = false)
        @test test_string([constraint.indices for constraint in com1.constraints]) == test_string([constraint.indices for constraint in com2.constraints])
        info_1 = com1.info
        info_2 = com2.info
        @test info_1.pre_backtrack_calls == info_2.pre_backtrack_calls
        @test JuMP.termination_status(m1) == MOI.TIME_LIMIT
        # can take longer as preprocessing takes time
        @test 0 <= MOI.get(m1, MOI.SolveTime()) < 0.1
    end

    @testset "49 US states + DC" begin
        com1, _ = normal_49_states(; reverse_constraint = true)
        com2, _ = normal_49_states()
        info_1 = com1.info
        info_2 = com2.info
        @test info_1.pre_backtrack_calls == info_2.pre_backtrack_calls
        @test info_1.backtrack_fixes == info_2.backtrack_fixes
        @test info_1.in_backtrack_calls == info_2.in_backtrack_calls
        @test info_1.backtrack_reverses == info_2.backtrack_reverses
        logs_1 = CS.get_logs(com1)
        logs_2 = CS.get_logs(com2)
        @test CS.sanity_check_log(logs_1[:tree])
        @test CS.same_logs(logs_1[:tree], logs_2[:tree])
    end

    @testset "49 US states + DC without sorting + some states same color" begin
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "keep_logs" => true,
            "backtrack_sorting" => false,
            "logging" => [],
        ))
        num_colors = 8
        @variable(m, 1 <= max_color <= num_colors, Int)

        @variable(m, 1 <= states[1:50] <= num_colors, Int)
        washington,
        montana,
        maine,
        north_dakota,
        south_dakota,
        wyoming,
        wisconsin,
        idaho,
        vermont,
        minnesota,
        oregon,
        new_hampshire,
        iowa,
        massachusetts,
        nebraska,
        new_york,
        pennsylvania,
        connecticut,
        rhode_island,
        new_jersey,
        indiana,
        nevada,
        utah,
        california,
        ohio,
        illinois,
        washington_dc,
        delaware,
        west_virginia,
        maryland,
        colorado,
        kentucky,
        kansas,
        virginia,
        missouri,
        arizona,
        oklahoma,
        north_carolina,
        tennessee,
        texas,
        new_mexico,
        alabama,
        mississippi,
        georgia,
        south_carolina,
        arkansas,
        louisiana,
        florida,
        michigan = states

        @constraint(m, washington != oregon)
        @constraint(m, washington != idaho)
        @constraint(m, oregon != idaho)
        @constraint(m, oregon != nevada)
        @constraint(m, oregon != california)
        @constraint(m, california != nevada)
        @constraint(m, california != arizona)
        @constraint(m, nevada != idaho)
        @constraint(m, nevada != utah)
        @constraint(m, nevada != arizona)
        @constraint(m, idaho != montana)
        @constraint(m, idaho != wyoming)
        @constraint(m, idaho != utah)
        @constraint(m, utah != wyoming)
        @constraint(m, utah != colorado)
        @constraint(m, utah != new_mexico)
        @constraint(m, utah != arizona)
        @constraint(m, arizona != colorado)
        @constraint(m, arizona != new_mexico)
        @constraint(m, montana != north_dakota)
        @constraint(m, montana != south_dakota)
        @constraint(m, montana != wyoming)
        @constraint(m, wyoming != south_dakota)
        @constraint(m, wyoming != nebraska)
        @constraint(m, wyoming != colorado)
        @constraint(m, colorado != nebraska)
        @constraint(m, colorado != kansas)
        @constraint(m, colorado != oklahoma)
        @constraint(m, colorado != new_mexico)
        @constraint(m, new_mexico != oklahoma)
        @constraint(m, new_mexico != texas)
        @constraint(m, north_dakota != minnesota)
        @constraint(m, north_dakota != south_dakota)
        @constraint(m, south_dakota != minnesota)
        @constraint(m, south_dakota != iowa)
        @constraint(m, south_dakota != nebraska)
        @constraint(m, nebraska != iowa)
        @constraint(m, nebraska != missouri)
        @constraint(m, nebraska != kansas)
        @constraint(m, kansas != missouri)
        @constraint(m, kansas != oklahoma)
        @constraint(m, oklahoma != arkansas)
        @constraint(m, oklahoma != texas)
        @constraint(m, texas != arkansas)
        @constraint(m, texas != louisiana)
        @constraint(m, minnesota != wisconsin)
        @constraint(m, minnesota != iowa)
        @constraint(m, iowa != wisconsin)
        @constraint(m, iowa != illinois)
        @constraint(m, iowa != missouri)
        @constraint(m, missouri != illinois)
        @constraint(m, missouri != kentucky)
        @constraint(m, missouri != tennessee)
        @constraint(m, missouri != arkansas)
        @constraint(m, arkansas != tennessee)
        @constraint(m, arkansas != mississippi)
        @constraint(m, arkansas != louisiana)
        @constraint(m, louisiana != mississippi)
        @constraint(m, wisconsin != illinois)
        @constraint(m, wisconsin != michigan)
        @constraint(m, illinois != indiana)
        @constraint(m, illinois != kentucky)
        @constraint(m, kentucky != indiana)
        @constraint(m, kentucky != ohio)
        @constraint(m, kentucky != west_virginia)
        @constraint(m, kentucky != virginia)
        @constraint(m, kentucky != tennessee)
        @constraint(m, tennessee != virginia)
        @constraint(m, tennessee != north_carolina)
        @constraint(m, tennessee != georgia)
        @constraint(m, tennessee != alabama)
        @constraint(m, tennessee != mississippi)
        @constraint(m, mississippi != alabama)
        @constraint(m, michigan != indiana)
        @constraint(m, michigan != ohio)
        @constraint(m, indiana != ohio)
        @constraint(m, alabama != georgia)
        @constraint(m, alabama != florida)
        @constraint(m, ohio != pennsylvania)
        @constraint(m, ohio != west_virginia)
        @constraint(m, maine != new_hampshire)
        @constraint(m, new_hampshire != vermont)
        @constraint(m, new_hampshire != massachusetts)
        @constraint(m, vermont != massachusetts)
        @constraint(m, vermont != new_york)
        @constraint(m, massachusetts != new_york)
        @constraint(m, massachusetts != rhode_island)
        @constraint(m, massachusetts != connecticut)
        @constraint(m, connecticut != rhode_island)
        @constraint(m, new_york != connecticut)
        @constraint(m, new_york != pennsylvania)
        @constraint(m, new_york != new_jersey)
        @constraint(m, pennsylvania != new_jersey)
        @constraint(m, pennsylvania != delaware)
        @constraint(m, pennsylvania != maryland)
        @constraint(m, pennsylvania != west_virginia)
        @constraint(m, new_jersey != delaware)
        @constraint(m, maryland != washington_dc)
        @constraint(m, maryland != west_virginia)
        @constraint(m, maryland != virginia)
        @constraint(m, washington_dc != virginia)
        @constraint(m, west_virginia != virginia)
        @constraint(m, virginia != north_carolina)
        @constraint(m, north_carolina != south_carolina)
        @constraint(m, south_carolina != georgia)
        @constraint(m, georgia != florida)

        # test for AllEqual constraint
        @constraint(m, [california, new_york, florida] in CS.AllEqual())

        @constraint(m, max_color .>= states)

        # test for constant in objective
        @objective(m, Min, max_color + 1.1)

        optimize!(m)

        status = JuMP.termination_status(m)

        com = CS.get_inner_model(m)
        @test com.info.n_constraint_types.equality == 1
        @test com.info.n_constraint_types.inequality == length(states)

        CS.save_logs(com, "graph_color_optimize.json")
        rm("graph_color_optimize.json")

        @test status == MOI.OPTIMAL

        # all values fixed
        @test com.best_sol ≈ 5.1
        @test maximum([JuMP.value(var) for var in states]) == JuMP.value(max_color) == 4
        @test is_solved(com)
    end

    @testset "49 US states + DC only 3 colors" begin
        m = Model(CSJuMPTestOptimizer())
        num_colors = 3

        @variable(m, 1 <= states[1:50] <= num_colors, Int)
        washington,
        montana,
        maine,
        north_dakota,
        south_dakota,
        wyoming,
        wisconsin,
        idaho,
        vermont,
        minnesota,
        oregon,
        new_hampshire,
        iowa,
        massachusetts,
        nebraska,
        new_york,
        pennsylvania,
        connecticut,
        rhode_island,
        new_jersey,
        indiana,
        nevada,
        utah,
        california,
        ohio,
        illinois,
        washington_dc,
        delaware,
        west_virginia,
        maryland,
        colorado,
        kentucky,
        kansas,
        virginia,
        missouri,
        arizona,
        oklahoma,
        north_carolina,
        tennessee,
        texas,
        new_mexico,
        alabama,
        mississippi,
        georgia,
        south_carolina,
        arkansas,
        louisiana,
        florida,
        michigan = states

        @constraint(m, washington != oregon)
        @constraint(m, washington != idaho)
        @constraint(m, oregon != idaho)
        @constraint(m, oregon != nevada)
        @constraint(m, oregon != california)
        @constraint(m, california != nevada)
        @constraint(m, california != arizona)
        @constraint(m, nevada != idaho)
        @constraint(m, nevada != utah)
        @constraint(m, nevada != arizona)
        @constraint(m, idaho != montana)
        @constraint(m, idaho != wyoming)
        @constraint(m, idaho != utah)
        @constraint(m, utah != wyoming)
        @constraint(m, utah != colorado)
        @constraint(m, utah != new_mexico)
        @constraint(m, utah != arizona)
        @constraint(m, arizona != colorado)
        @constraint(m, arizona != new_mexico)
        @constraint(m, montana != north_dakota)
        @constraint(m, montana != south_dakota)
        @constraint(m, montana != wyoming)
        @constraint(m, wyoming != south_dakota)
        @constraint(m, wyoming != nebraska)
        @constraint(m, wyoming != colorado)
        @constraint(m, colorado != nebraska)
        @constraint(m, colorado != kansas)
        @constraint(m, colorado != oklahoma)
        @constraint(m, colorado != new_mexico)
        @constraint(m, new_mexico != oklahoma)
        @constraint(m, new_mexico != texas)
        @constraint(m, north_dakota != minnesota)
        @constraint(m, north_dakota != south_dakota)
        @constraint(m, south_dakota != minnesota)
        @constraint(m, south_dakota != iowa)
        @constraint(m, south_dakota != nebraska)
        @constraint(m, nebraska != iowa)
        @constraint(m, nebraska != missouri)
        @constraint(m, nebraska != kansas)
        @constraint(m, kansas != missouri)
        @constraint(m, kansas != oklahoma)
        @constraint(m, oklahoma != arkansas)
        @constraint(m, oklahoma != texas)
        @constraint(m, texas != arkansas)
        @constraint(m, texas != louisiana)
        @constraint(m, minnesota != wisconsin)
        @constraint(m, minnesota != iowa)
        @constraint(m, iowa != wisconsin)
        @constraint(m, iowa != illinois)
        @constraint(m, iowa != missouri)
        @constraint(m, missouri != illinois)
        @constraint(m, missouri != kentucky)
        @constraint(m, missouri != tennessee)
        @constraint(m, missouri != arkansas)
        @constraint(m, arkansas != tennessee)
        @constraint(m, arkansas != mississippi)
        @constraint(m, arkansas != louisiana)
        @constraint(m, louisiana != mississippi)
        @constraint(m, wisconsin != illinois)
        @constraint(m, wisconsin != michigan)
        @constraint(m, illinois != indiana)
        @constraint(m, illinois != kentucky)
        @constraint(m, kentucky != indiana)
        @constraint(m, kentucky != ohio)
        @constraint(m, kentucky != west_virginia)
        @constraint(m, kentucky != virginia)
        @constraint(m, kentucky != tennessee)
        @constraint(m, tennessee != virginia)
        @constraint(m, tennessee != north_carolina)
        @constraint(m, tennessee != georgia)
        @constraint(m, tennessee != alabama)
        @constraint(m, tennessee != mississippi)
        @constraint(m, mississippi != alabama)
        @constraint(m, michigan != indiana)
        @constraint(m, michigan != ohio)
        @constraint(m, indiana != ohio)
        @constraint(m, alabama != georgia)
        @constraint(m, alabama != florida)
        @constraint(m, ohio != pennsylvania)
        @constraint(m, ohio != west_virginia)
        @constraint(m, maine != new_hampshire)
        @constraint(m, new_hampshire != vermont)
        @constraint(m, new_hampshire != massachusetts)
        @constraint(m, vermont != massachusetts)
        @constraint(m, vermont != new_york)
        @constraint(m, massachusetts != new_york)
        @constraint(m, massachusetts != rhode_island)
        @constraint(m, massachusetts != connecticut)
        @constraint(m, connecticut != rhode_island)
        @constraint(m, new_york != connecticut)
        @constraint(m, new_york != pennsylvania)
        @constraint(m, new_york != new_jersey)
        @constraint(m, pennsylvania != new_jersey)
        @constraint(m, pennsylvania != delaware)
        @constraint(m, pennsylvania != maryland)
        @constraint(m, pennsylvania != west_virginia)
        @constraint(m, new_jersey != delaware)
        @constraint(m, maryland != washington_dc)
        @constraint(m, maryland != west_virginia)
        @constraint(m, maryland != virginia)
        @constraint(m, washington_dc != virginia)
        @constraint(m, west_virginia != virginia)
        @constraint(m, virginia != north_carolina)
        @constraint(m, north_carolina != south_carolina)
        @constraint(m, south_carolina != georgia)
        @constraint(m, georgia != florida)

        optimize!(m)

        @test JuMP.termination_status(m) == MOI.INFEASIBLE
    end

    @testset "Maximization objective" begin
        m = Model(CSJuMPTestOptimizer())
        num_colors = 20

        @variable(m, 1 <= states[1:50] <= num_colors, Int)
        @variable(m, 1 <= max_color <= num_colors, Int)

        washington,
        montana,
        maine,
        north_dakota,
        south_dakota,
        wyoming,
        wisconsin,
        idaho,
        vermont,
        minnesota,
        oregon,
        new_hampshire,
        iowa,
        massachusetts,
        nebraska,
        new_york,
        pennsylvania,
        connecticut,
        rhode_island,
        new_jersey,
        indiana,
        nevada,
        utah,
        california,
        ohio,
        illinois,
        washington_dc,
        delaware,
        west_virginia,
        maryland,
        colorado,
        kentucky,
        kansas,
        virginia,
        missouri,
        arizona,
        oklahoma,
        north_carolina,
        tennessee,
        texas,
        new_mexico,
        alabama,
        mississippi,
        georgia,
        south_carolina,
        arkansas,
        louisiana,
        florida,
        michigan = states

        @constraint(m, washington != oregon)
        @constraint(m, washington != idaho)
        @constraint(m, oregon != idaho)
        @constraint(m, oregon != nevada)
        @constraint(m, oregon != california)
        @constraint(m, california != nevada)
        @constraint(m, california != arizona)
        @constraint(m, nevada != idaho)
        @constraint(m, nevada != utah)
        @constraint(m, nevada != arizona)
        @constraint(m, idaho != montana)
        @constraint(m, idaho != wyoming)
        @constraint(m, idaho != utah)
        @constraint(m, utah != wyoming)
        @constraint(m, utah != colorado)
        @constraint(m, utah != new_mexico)
        @constraint(m, utah != arizona)
        @constraint(m, arizona != colorado)
        @constraint(m, arizona != new_mexico)
        @constraint(m, montana != north_dakota)
        @constraint(m, montana != south_dakota)
        @constraint(m, montana != wyoming)
        @constraint(m, wyoming != south_dakota)
        @constraint(m, wyoming != nebraska)
        @constraint(m, wyoming != colorado)
        @constraint(m, colorado != nebraska)
        @constraint(m, colorado != kansas)
        @constraint(m, colorado != oklahoma)
        @constraint(m, colorado != new_mexico)
        @constraint(m, new_mexico != oklahoma)
        @constraint(m, new_mexico != texas)
        @constraint(m, north_dakota != minnesota)
        @constraint(m, north_dakota != south_dakota)
        @constraint(m, south_dakota != minnesota)
        @constraint(m, south_dakota != iowa)
        @constraint(m, south_dakota != nebraska)
        @constraint(m, nebraska != iowa)
        @constraint(m, nebraska != missouri)
        @constraint(m, nebraska != kansas)
        @constraint(m, kansas != missouri)
        @constraint(m, kansas != oklahoma)
        @constraint(m, oklahoma != arkansas)
        @constraint(m, oklahoma != texas)
        @constraint(m, texas != arkansas)
        @constraint(m, texas != louisiana)
        @constraint(m, minnesota != wisconsin)
        @constraint(m, minnesota != iowa)
        @constraint(m, iowa != wisconsin)
        @constraint(m, iowa != illinois)
        @constraint(m, iowa != missouri)
        @constraint(m, missouri != illinois)
        @constraint(m, missouri != kentucky)
        @constraint(m, missouri != tennessee)
        @constraint(m, missouri != arkansas)
        @constraint(m, arkansas != tennessee)
        @constraint(m, arkansas != mississippi)
        @constraint(m, arkansas != louisiana)
        @constraint(m, louisiana != mississippi)
        @constraint(m, wisconsin != illinois)
        @constraint(m, wisconsin != michigan)
        @constraint(m, illinois != indiana)
        @constraint(m, illinois != kentucky)
        @constraint(m, kentucky != indiana)
        @constraint(m, kentucky != ohio)
        @constraint(m, kentucky != west_virginia)
        @constraint(m, kentucky != virginia)
        @constraint(m, kentucky != tennessee)
        @constraint(m, tennessee != virginia)
        @constraint(m, tennessee != north_carolina)
        @constraint(m, tennessee != georgia)
        @constraint(m, tennessee != alabama)
        @constraint(m, tennessee != mississippi)
        @constraint(m, mississippi != alabama)
        @constraint(m, michigan != indiana)
        @constraint(m, michigan != ohio)
        @constraint(m, indiana != ohio)
        @constraint(m, alabama != georgia)
        @constraint(m, alabama != florida)
        @constraint(m, ohio != pennsylvania)
        @constraint(m, ohio != west_virginia)
        @constraint(m, maine != new_hampshire)
        @constraint(m, new_hampshire != vermont)
        @constraint(m, new_hampshire != massachusetts)
        @constraint(m, vermont != massachusetts)
        @constraint(m, vermont != new_york)
        @constraint(m, massachusetts != new_york)
        @constraint(m, massachusetts != rhode_island)
        @constraint(m, massachusetts != connecticut)
        @constraint(m, connecticut != rhode_island)
        @constraint(m, new_york != connecticut)
        @constraint(m, new_york != pennsylvania)
        @constraint(m, new_york != new_jersey)
        @constraint(m, pennsylvania != new_jersey)
        @constraint(m, pennsylvania != delaware)
        @constraint(m, pennsylvania != maryland)
        @constraint(m, pennsylvania != west_virginia)
        @constraint(m, new_jersey != delaware)
        @constraint(m, maryland != washington_dc)
        @constraint(m, maryland != west_virginia)
        @constraint(m, maryland != virginia)
        @constraint(m, washington_dc != virginia)
        @constraint(m, west_virginia != virginia)
        @constraint(m, virginia != north_carolina)
        @constraint(m, north_carolina != south_carolina)
        @constraint(m, south_carolina != georgia)
        @constraint(m, georgia != florida)

        # test strictly less than
        @constraint(m, max_color .< states+1)

        @objective(m, Max, max_color)

        optimize!(m)

        status = JuMP.termination_status(m)

        com = CS.get_inner_model(m)

        @test status == MOI.OPTIMAL
        @test com.best_sol == 17
        @test minimum([JuMP.value(var) for var in states]) == 17 == JuMP.value(max_color)
    end

    @testset "small graph coloring correctness test" begin
        cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
        m = Model(optimizer_with_attributes(
            CS.Optimizer,
            "logging" => [],
            "lp_optimizer" => cbc_optimizer,
            "keep_logs" => true,
        ))

        n = 10
        @variable(m, 1 <= c[1:n] <= n, Int)
        @variable(m, 1 <= max_color <= n, Int)

        edge_list = [
            (1, 2),
            (1, 5),
            (1, 7),
            (1, 8),
            (2, 4),
            (2, 7),
            (2, 8),
            (2, 9),
            (3, 7),
            (3, 9),
            (3, 10),
            (4, 9),
            (4, 10),
            (5, 10),
            (6, 7),
            (6, 9),
            (7, 8),
            (7, 10),
        ]

        for edge in edge_list
            @constraint(m, c[edge[1]] != c[edge[2]])
        end

        @constraint(m, max_color .>= c)
        @objective(m, Min, max_color)

        optimize!(m)

        status = JuMP.termination_status(m)
        objval = JuMP.objective_value(m)
        @test status == MOI.OPTIMAL
        @test objval ≈ 4
        colors = JuMP.value.(c)
        for edge in edge_list
            @test !(colors[edge[1]] ≈ colors[edge[2]])
        end
    end
end


using Knockoffs
using Test
using LinearAlgebra
using Random
using StatsBase
using Statistics

@testset "fixed equi knockoffs" begin
    Random.seed!(2021)

    # simulate matrix and normalize columns
    n = 3000
    p = 1000
    X = randn(n, p)
    zscore!(X, mean(X, dims=1), std(X, dims=1)) # center/scale Xj to mean 0 var 1
    normalize_col!(X) # normalize Xj to unit length

    # equi-correlated knockoff
    @time knockoff = fixed_knockoffs(X, :equi)
    X = knockoff.X
    X̃ = knockoff.X̃
    s = knockoff.s
    Σ = knockoff.Σ
    Σinv = knockoff.Σinv

    @test all(X' * X .≈ Σ)
    @test all(isapprox.(X̃' * X̃, Σ, atol=5e-2)) # numerical accuracy not good?
    @test all(s .≥ 0)
    @test all(1 .≥ s)
    # λ = eigvals(2Σ - Diagonal(s))
    # for λi in λ
    #     @test λi ≥ 0 || λi ≈ 0
    # end
    # @test all(isapprox.(Ũ' * X, 0, atol=1e-10))
    for i in 1:p, j in 1:p
        if i == j
            @test isapprox(dot(X[:, i], X̃[:, i]), Σ[i, i] - s[i])
            @test isapprox(dot(X[:, i], X̃[:, i]), 1 - s[i], atol=5e-2) # numerical accuracy not good?
        else
            @test dot(X[:, i], X̃[:, j]) ≈ dot(X[:, i], X[:, j])
        end
    end
end

@testset "fixed SDP knockoffs" begin
    Random.seed!(2021)

    # simulate matrix and normalize columns
    n = 1000
    p = 100
    X = randn(n, p)
    zscore!(X, mean(X, dims=1), std(X, dims=1)) # center/scale Xj to mean 0 var 1
    normalize_col!(X) # normalize Xj to unit length

    # SDP knockoff
    @time knockoff = fixed_knockoffs(X, :sdp)
    X = knockoff.X
    X̃ = knockoff.X̃
    s = knockoff.s
    Σ = knockoff.Σ
    Σinv = knockoff.Σinv

    @test all(X' * X .≈ Σ)
    @test all(isapprox.(X̃' * X̃, Σ, atol=5e-1)) # numerical accuracy not good?
    @test all(s .≥ 0)
    @test all(1 .≥ s)
    # λ = eigvals(2Σ - Diagonal(s))
    # for λi in λ
    #     @test λi ≥ 0 || λi ≈ 0
    # end
    # @test all(isapprox.(Ũ' * X, 0, atol=1e-10))
    for i in 1:p, j in 1:p
        if i == j
            @test isapprox(dot(X[:, i], X̃[:, i]), Σ[i, i] - s[i], atol=1e-8)
            @test isapprox(dot(X[:, i], X̃[:, i]), 1 - s[i], atol=1e-8)
        else
            @test dot(X[:, i], X̃[:, j]) ≈ dot(X[:, i], X[:, j])
        end
    end
end

@testset "Knockoff data structure" begin
    Random.seed!(2021)

    # simulate matrix and normalize columns
    n = 1000
    p = 100
    X = randn(n, p)
    zscore!(X, mean(X, dims=1), std(X, dims=1)) # center/scale Xj to mean 0 var 1
    normalize_col!(X) # normalize Xj to unit length

    # construct knockoff struct and the real [A Ã]
    @time A = fixed_knockoffs(X, :sdp)
    Atrue = [A.X A.X̃]

    # array operations
    @test size(Atrue) == size(A)
    @test eltype(Atrue) == eltype(A)
    @test getindex(Atrue, 127) == getindex(A, 127)
    @test getindex(Atrue, 2, 19) == getindex(A, 2, 19)
    @test getindex(Atrue, 900, 110) == getindex(A, 900, 110)
    @test all(@view(Atrue[:, 1]) .== @view(A[:, 1]))
    @test all(@view(Atrue[1:2:end, 1:5:end]) .== @view(A[1:2:end, 1:5:end]))

    # matrix-vector multiplication
    b = randn(2p)
    ctrue, c = zeros(n), zeros(n)
    mul!(ctrue, Atrue, b)
    mul!(c, A, b)
    @test all(ctrue .≈ c)

    # matrix-matrix multiplication
    B = randn(2p, n)
    Ctrue, C = zeros(n, n), zeros(n, n)
    mul!(Ctrue, Atrue, B)
    mul!(C, A, B)
    @test all(Ctrue .≈ C)
end

@testset "model X Guassian Knockoffs" begin
    Random.seed!(2021)

    # simulate matrix (but manually normalizing columns leads to rank deficiency for some reason)
    n = 300
    p = 600
    X = randn(n, p)
    # zscore!(X, mean(X, dims=1), std(X, dims=1))
    # normalize_col!(X)
    # @show rank(X)

    # generate knockoff
    @time knockoff = modelX_gaussian_knockoffs(X, :sdp, zeros(p));
    X = knockoff.X
    X̃ = knockoff.X̃
    s = knockoff.s
    Σ = knockoff.Σ
    Σinv = knockoff.Σinv

    # test properties
    @test all(X' * X .≈ Σ)
    @test all(isapprox.(X̃' * X̃, Σ, atol=0.5)) # numerical accuracy not good?
    @test all(s .≥ 0)
    @test all(1 .≥ s)
    for i in 1:p, j in 1:p
        if i == j
            @test isapprox(dot(X[:, i], X̃[:, i]), Σ[i, i] - s[i], atol=1.0) # numerical accuracy not good?
        else
            @test isapprox(dot(X[:, i], X̃[:, j]), dot(X[:, i], X[:, j]), atol=1.0) # numerical accuracy not good?
        end
    end
end

@testset "threshold functions" begin
    w = [0.1, 1.9, 1.3, 1.8, 0.8, -0.7, -0.1]
    @test threshold(w, 0.2) == 0.8
    @test threshold(w, 0.2, :knockoff_plus) == Inf

    w = [0.27, 0.76, 0.21, 0.1, -0.38, -0.01]
    @test threshold(w, 0.4) == 0.1
    @test threshold(w, 0.5, :knockoff_plus) == 0.1

    w = [0.74, -0.65, -0.83, -0.27, -0.19, 0.4]
    @test threshold(w, 0.25) == Inf
    @test threshold(w, 0.25, :knockoff_plus) == Inf
end

@testset "coefficient_diff" begin
    # knockoffs are at the end (e.g. [XX̃])
    β = [1.0, 0.2, -0.3, 0.8, -0.1, 0.5]
    w = coefficient_diff(β, :concatenated)
    @test length(w) == 3
    @test w[1] ≈ 0.2
    @test w[2] ≈ 0.1
    @test w[3] ≈ -0.2

    # knockoffs are interleaved (e.g. [x₁x̃₁x₂x̃₂...])
    β = [1.0, 0.2, -0.3, 0.8, -0.1, 0.5]
    w = coefficient_diff(β, :interleaved)
    @test length(w) == 3
    @test w[1] ≈ 0.8
    @test w[2] ≈ -0.5
    @test w[3] ≈ -0.4

    # multivariate: knockoffs are at the end (e.g. [XX̃])
    B = [[1.0, 2.0, -0.3, 0.8, -0.1, 0.5] [-0.4, 0.6, 1.8, -0.3, 1.4, 0.3]]
    w = coefficient_diff(B, :concatenated)
    @test length(w) == 3
    @test w[1] ≈ 0.3
    @test w[2] ≈ 1.1
    @test w[3] ≈ 1.3

    # multivariate: knockoffs are interleaved (e.g. [x₁x̃₁x₂x̃₂...])
    B = [[1.0, 2.0, -0.3, 0.8, -0.1, 0.5] [-0.4, 0.6, 1.8, -0.3, 1.4, 0.3]]
    w = coefficient_diff(B, :interleaved)
    @test length(w) == 3
    @test w[1] ≈ -1.2
    @test w[2] ≈ 1.0
    @test w[3] ≈ 0.7

    # knockoffs and original are randomly swapped
    β = [1.0, 0.2, -0.3, 0.8, -0.1, 0.5]
    original = [1, 4, 6]
    knockoff = [2, 3, 5]
    w = coefficient_diff(β, original, knockoff)
    @test length(w) == 3
    @test w[1] ≈ 0.8
    @test w[2] ≈ 0.5
    @test w[3] ≈ 0.4

    # group knockoffs
    β = [1.0, 0.2, -0.3, 0.8, -0.1, 0.5, 0.5, -0.1]
    groups = [1, 1, 1, 1, 2, 2, 2, 2]
    original = [1, 4, 6, 7]
    knockoff = [2, 3, 5, 8]
    w = coefficient_diff(β, groups, original, knockoff)
    @test length(w) == 2
    @test w[1] ≈ 1.8 - 0.5
    @test w[2] ≈ 1.0 - 0.2
end


using SpecialFunctions
using LsqFit

import Distributions: TDist, cdf

import Base: show, <<, push!, count, isempty, merge, merge!, zero, zeros, sum

if VERSION < v"0.7.0-beta.85"
    import Base: mean, var
else
    import Statistics: mean, var
end

export MCObservable, ScalarObservable, VectorObservable
export mean, var, stderror, confidence_interval
export p_value
export show, dump_plot
export merge, merge!

export confidence_interval

abstract type MCObservable end
abstract type ScalarObservable <: MCObservable end
abstract type VectorObservable <: MCObservable end

function p_value(X::MCObservable, y::Real; verbose::Bool=false)
    MX = mean(X)
    mid = 0.5(MX+y)
    er = max(stderror(X), abs(mid)*sqrt(eps()), sqrt(nextfloat(0.0)))
    t = (MX-y)/er
    d = TDist(count(X)-1)
    if verbose
        @show MX, y, er, t
    end
    return 2cdf(d,-abs(t))
end

function p_value(X::MCObservable, Y::MCObservable)
    NX = count(X)
    NY = count(Y)
    MX = mean(X)
    MY = mean(Y)
    mid = 0.5(MX+MY)
    N = 1.0/(1.0/NX + 1.0/NY)
    V = ( (NX-1)*var(X) + (NY-1)*var(Y) )/(NX+NY-2)
    se = max(sqrt(V/N), abs(mid)*sqrt(eps()), sqrt(nextfloat(0.0)))
    t = (MX-MY)/se
    d = TDist(NX+NY-2)
    return 2cdf(d,-abs(t))
end

isempty(obs::MCObservable) = count(obs) == 0

zero(::Type{Obs}) where (Obs<:MCObservable) = Obs()
zero(obs::MCObservable) = zero(typeof(obs))
function zeros(::Type{Obs}, dim...) where (Obs<:MCObservable)
    reshape(Obs[Obs() for i in 1:prod(dim)], dim)
end

function show(io::IO, obs::MCObservable)
    if !isempty(obs)
        print(io, mean(obs), " +/- ", stderror(obs))
    else
        print(io, "No Entries")
    end
end

function dump_plot(io::IO, obs::MCObservable; put_following_space::Bool=false, newline::Bool=false)
    print(io, mean(obs), " ", stderror(obs))
    if put_following_space
        print(io, " ")
    end
    if newline
        println(io)
    end
end

const confidence_rate_1sigma = 0.5erf(0.5sqrt(2.0))

include("util.jl")
include("observableset.jl")
include("parsesigma.jl")
include("tiny.jl")
include("tinyvector.jl")
include("simple.jl")
include("simplevector.jl")
include("binning.jl")
include("binningvector.jl")
include("jackknife.jl")
include("jackknifevector.jl")

## these three definitions are needed to resolve ambiguousness with Base.<<(Any,Integer)
<<(obs::MCObservable, x::Int32) = push!(obs,x)
<<(obs::MCObservable, x::Int64) = push!(obs,x)
<<(obs::MCObservable, x::Integer) = push!(obs,x)

<<(obs::MCObservable, x) = push!(obs,x)


# MIT license
# Copyright (c) Microsoft Corporation. All rights reserved.
# See LICENSE in the project root for full license information.

# only returns real roots
function quadraticroots(a::T, b::T, c::T) where {T<:Real}
    temp = b^2 - 4 * a * c

    if temp < zero(T)
        return nothing # no real roots so no ray cylinder intersection
    end

    radical = sqrt(temp)
    if b >= zero(T)
        radicalterm = -b - radical
        x1 = radicalterm / (2a)
        x2 = (2c) / radicalterm
    else
        radicalterm = -b + radical
        x1 = (2c) / radicalterm
        x2 = radicalterm / (2a)
    end

    return SVector{2,T}(x1, x2)
end

replprint(a) = show(IOContext(stdout), "text/plain", a)

@inline samepoint(pt1, pt2) = isapprox(pt1, pt2, atol = 1e-10)

password() = String(collect(rand(('1':'9'..., 'A':'Z'..., 'a':'z'...)) for i in 1:14))

# 1 if val>= 0 and -1 otherwise. Unlike Math.Sign which returns 0 if val==0

@inline sign(val::T) where {T<:Real} = val >= zero(T) ? 1 : -1

@inline function NaNsafeatan(x::T, y::T)::T where {T<:Real}
    if y == zero(T)
        if x == zero(T)
            return zero(T)
        elseif x > zero(T)
            return T(π / 2)
        else
            return -T(π / 2)
        end
    else
        return atan(x, y)
    end
end

@inline function NaNsafeasin(x::T)::T where {T<:Real}
    if x == zero(T)
        return zero(T)
    elseif x == one(T)
        return T(π / 2)
    elseif x == -one(T)
        return -T(π / 2)
    else
        return asin(x)
    end
end

@inline function NaNsafeacos(x::T)::T where {T<:Real}
    if x == zero(T)
        return T(π / 2)
    elseif x == one(T)
        return zero(T)
    elseif x == -one(T)
        return T(π)
    else
        return acos(x)
    end
end


# NOTE: s₃ = image(+, s₁, s₂) satisfies indices(s₃) == indices(s₁) ∪ indices(s₂)

function +̄ end
function -̄ end
function *̄ end
function ^̄ end

# fallback methods

image(::typeof(-), s₁::VectorSpace, s₂::VectorSpace) = image(+, s₁, s₂)
image(::typeof(-̄), s₁::VectorSpace, s₂::VectorSpace) = image(+̄, s₁, s₂)

# Parameter space

image(::typeof(+), ::ParameterSpace, ::ParameterSpace) = ParameterSpace()
image(::typeof(+̄), ::ParameterSpace, ::ParameterSpace) = ParameterSpace()

# Sequence spaces

for f ∈ (:+, :+̄, :*, :*̄)
    @eval image(::typeof($f), s₁::TensorSpace{<:NTuple{N,BaseSpace}}, s₂::TensorSpace{<:NTuple{N,BaseSpace}}) where {N} =
        TensorSpace(map((s₁ᵢ, s₂ᵢ) -> image($f, s₁ᵢ, s₂ᵢ), spaces(s₁), spaces(s₂)))
end

function image(::typeof(^), s::SequenceSpace, n::Int)
    n < 0 && return throw(DomainError(n, "^ is only defined for positive integers"))
    n == 0 && return s
    n == 1 && return s
    n == 2 && return image(*, s, s)
    # power by squaring
    t = trailing_zeros(n) + 1
    n >>= t
    while (t -= 1) > 0
        s = image(*, s, s)
    end
    new_s = s
    while n > 0
        t = trailing_zeros(n) + 1
        n >>= t
        while (t -= 1) ≥ 0
            s = image(*, s, s)
        end
        new_s = image(*, new_s, s)
    end
    return new_s
end

function image(::typeof(^̄), s::SequenceSpace, n::Int)
    n < 0 && return throw(DomainError(n, "^̄ is only defined for positive integers"))
    n == 0 && return s
    n == 1 && return s
    n == 2 && return image(*̄, s, s)
    # power by squaring
    t = trailing_zeros(n) + 1
    n >>= t
    while (t -= 1) > 0
        s = image(*̄, s, s)
    end
    new_s = s
    while n > 0
        t = trailing_zeros(n) + 1
        n >>= t
        while (t -= 1) ≥ 0
            s = image(*̄, s, s)
        end
        new_s = image(*̄, new_s, s)
    end
    return new_s
end

# Taylor

image(::typeof(+), s₁::Taylor, s₂::Taylor) = union(s₁, s₂)
image(::typeof(+̄), s₁::Taylor, s₂::Taylor) = intersect(s₁, s₂)

image(::typeof(*), s₁::Taylor, s₂::Taylor) = Taylor(order(s₁) + order(s₂))
image(::typeof(*̄), s₁::Taylor, s₂::Taylor) = intersect(s₁, s₂)

# Fourier

image(::typeof(+), s₁::Fourier, s₂::Fourier) = union(s₁, s₂)
image(::typeof(+̄), s₁::Fourier, s₂::Fourier) = intersect(s₁, s₂)

function image(::typeof(*), s₁::Fourier{T}, s₂::Fourier{S}) where {T,S}
    frequency(s₁) == frequency(s₂) || return throw(DomainError)
    R = promote_type(T, S)
    return Fourier(order(s₁) + order(s₂), convert(R, frequency(s₁)))
end
image(::typeof(*̄), s₁::Fourier, s₂::Fourier) = intersect(s₁, s₂)

# Chebyshev

image(::typeof(+), s₁::Chebyshev, s₂::Chebyshev) = union(s₁, s₂)
image(::typeof(+̄), s₁::Chebyshev, s₂::Chebyshev) = intersect(s₁, s₂)

image(::typeof(*), s₁::Chebyshev, s₂::Chebyshev) = Chebyshev(order(s₁) + order(s₂))
image(::typeof(*̄), s₁::Chebyshev, s₂::Chebyshev) = intersect(s₁, s₂)

# Cartesian spaces

for f ∈ (:+, :+̄)
    @eval begin
        function image(::typeof($f), s₁::CartesianPower, s₂::CartesianPower)
            nb_cartesian_product(s₁) == nb_cartesian_product(s₂) || return throw(DimensionMismatch)
            return CartesianPower(image($f, space(s₁), space(s₂)), nb_cartesian_product(s₁))
        end
        function image(::typeof($f), s₁::CartesianProduct, s₂::CartesianProduct)
            nb_cartesian_product(s₁) == nb_cartesian_product(s₂) || return throw(DimensionMismatch)
            return CartesianProduct(map((s₁ᵢ, s₂ᵢ) -> image($f, s₁ᵢ, s₂ᵢ), spaces(s₁), spaces(s₂)))
        end
        function image(::typeof($f), s₁::CartesianPower, s₂::CartesianProduct)
            nb_cartesian_product(s₁) == nb_cartesian_product(s₂) || return throw(DimensionMismatch)
            return CartesianProduct(map(s₂ᵢ -> image($f, space(s₁), s₂ᵢ), spaces(s₂)))
        end
        function image(::typeof($f), s₁::CartesianProduct, s₂::CartesianPower)
            nb_cartesian_product(s₁) == nb_cartesian_product(s₂) || return throw(DimensionMismatch)
            return CartesianProduct(map(s₁ᵢ -> image($f, s₁ᵢ, space(s₂)), spaces(s₁)))
        end
    end
end


module SuperFastGraphs

using LightGraphs
using DataStructures

export sampleDistance
export iFub
export ccSample
export triangleCountingDegree
export triangleForward
export prunedBFS
export preProcess
export topKcc

function sampleDistance(g::AbstractGraph, precision::Int64 )
	numV = nv(g)
	distApp = 0
	k = convert(Int64, floor(log(numV) * precision)) # precision = 100 per precisione allo 0.1
	k = k < numV ? k : numV # il sottoinsieme preso puo essere al massimo grande quanto quello originale
	vertexSample = randKVector(vertices(g), k)

	for v in vertexSample
		distApp = distApp + sum(gdistances(g, v))
	end
	agvApp = distApp / (k * (numV - 1))
	return agvApp
end

# Given a vector the function returns a random subvector with k diffent elements
function randKVector(v::UnitRange{Int64}, k::Int64)
	kv = zeros(Int64, length(v))
	for i = 1:k
		elem = v[rand(1:end)]
		while kv[elem] != 0
			elem = v[rand(1:end)]
		end
		kv[elem] = elem
	end
	return filter(x -> x != 0, kv)
end

function iFub(g::AbstractGraph)
	return iFub(g, findmax(degree_centrality(g, normalize=false))[2])
end

# u : starting vertex
function iFub(g::AbstractGraph, u::Int64)
		dist = gdistances(g, u)
		lb = eccentricity(g, u)
	# println(u)
		ub = 2 * lb
		i = lb
		nodeCounter = 1
		while (ub > lb)
				biu = lb
				for v in vertices(g)
						if (dist[v] == i)
								e = eccentricity(g, v)
				# println(nodeCounter, "  " , v)
				# nodeCounter = nodeCounter + 1
								if (e > biu)
										biu = e
								end
								if (biu == ub)
										break
								end
						end
				end
				if (biu > 2 * (i - 1))
						ub = biu
						lb = biu
				else
						lb = biu
						ub = 2 * (i - 1)
				end
				i = i - 1
		end
		return ub
end

# k : sample size
# u : vertex for cc
function ccSample(g::AbstractGraph, k::Int64, u::Int64)
	ccSample = 0
	tempCC = 0
	vertexSample = randKVector(vertices(g), k)
	n = nv(g)
	dists = gdistances(g, u)
	for i in 1:length(vertexSample)
		tempCC = tempCC + (dists[vertexSample[i]] * n) / (k * (n - 1))
	end
	ccSample = 1 / tempCC
	return ccSample
end

function triangleForward(g::AbstractGraph)
	listT = Array{Array{Int}}(nv(g))
	for i in 1:length(listT)
		listT[i] = []
	end
	sortedV = sort!(collect(1:nv(g)), by = x -> -degree(g, x))
	nTriangles = 0
	for s in sortedV
		vNeighbors = neighbors(g, s)
		for t in vNeighbors
			if degree(g, s) < degree(g, t) || (degree(g, s) == degree(g, t) && s < t)
				nTriangles = nTriangles + length(intersect(listT[s], listT[t]))
			else
				push!(listT[t], s)
			end
		end
	end
	return nTriangles
end

function triangleCountingDegree(g::AbstractGraph)
	t = zeros(Int64, nv(g))
	dv, du, dw = 0, 0, 0
	for v in vertices(g)
		vNeighbors = neighbors(g, v)
		dv = degree(g, v)
		for u in vNeighbors
			du = degree(g, u)
			if (dv < du || ((dv == du && v < u)))
				for w in vNeighbors
					w >= u && break
					dw = degree(g, w)
					if ((dv < dw || ((dv == dw && v < w))) && has_edge(g, u, w))
						t[v] = t[v] + 1
						t[u] = t[u] + 1
						t[w] = t[w] + 1
					end
				end
			end
		end
	end
	# println(t)
	# println(triangleNumber)
	return t
end

function prunedBFS(g::AbstractGraph, v::Int64, x::Float64, preProcessData)
	q = Queue(Int64)
	visited = zeros(Int64, nv(g)) #0: not visited, 1:visited, 2:completely explored
	enqueue!(q, v)
	f = Int64[]
	push!(f, v)
	depth = 0
	timeToDepthIncrease = 1
	elementsToDepthIncrease = 0
	nodeAtDepth = Int64[]
	fdValue = 0
	nodeExplored = 0
	ccV = 1
	#gamma_d1 = degree(g, v)
	while !in(v, preProcessData[1][ccV]) #trovo la componente connessa di v
		ccV = ccV + 1
	end
	# println("---VERTICE ", v,": ")
	root  = true
	while length(q)>0
		e = dequeue!(q)
		push!(nodeAtDepth, e)
		visited[e] = 2
		neighborhood = neighbors(g, e)
		elementsToDepthIncrease = elementsToDepthIncrease + length(neighborhood)
		if (!is_directed(g) && root == true)
			elementsToDepthIncrease = elementsToDepthIncrease - 1
			root = false
		end
		timeToDepthIncrease = timeToDepthIncrease - 1
		if timeToDepthIncrease == 0
			n = length(vertices(g))
			fdValue = fdValue + (length(nodeAtDepth)*depth)

			nodeExplored = nodeExplored + length(nodeAtDepth)

			gamma_d1 = sum(degree(g, nodeAtDepth)) - length(nodeAtDepth)

			fdTildeValueAlpha = fdValue - gamma_d1 + (depth+2)*(preProcessData[2][ccV] - nodeExplored)
			fdTildeValueOmega = fdValue - gamma_d1 + (depth+2)*(preProcessData[3][ccV] - nodeExplored)
			# println("R(V): ", preProcessData[2][ccV])
			a = ((preProcessData[2][ccV]-1)^2) / fdTildeValueAlpha
			b = ((preProcessData[3][ccV]-1)^2) / fdTildeValueOmega
			bound = max( a, b ) / (n-1)
			#println("fdValue: ",fdValue)
			#println("n_d: ", nodeExplored)
			#println("gamma_d1: ",gamma_d1)
			#println("depth: ", depth)
			#println("bound: ", bound)
			#println("alpha: ", preProcessData[2][ccV]," omega: ",preProcessData[3][ccV] )
			#println("fdtilde: ",fdTildeValueAlpha, " ", fdTildeValueOmega)
			if (x > bound )
				#println("v: ", v, " bound: ", bound, " NON TOPK")
				return 0 #il nodo non è tra i topk
			end
			timeToDepthIncrease = elementsToDepthIncrease
			elementsToDepthIncrease = 0
			depth = depth + 1
			nodeAtDepth = Int64[]
			gamma_d1 = 0
		end
		for neighbor in neighborhood
			if visited[neighbor] == 0
				visited[neighbor] = 1
				#gamma_d1 = gamma_d1 + degree(g, neighbor)
				#print(gamma_d1, " ")
				enqueue!(q, neighbor)
				push!(f,neighbor)
			end
		end
	end

	#println("RITORNO VALORE->")
	#println("fvalue: ", fdValue)
	#println("rv: ", preProcessData[1][ccV])
	d = dijkstra_shortest_paths(g,v).dists
	δ = filter(x->x != typemax(x), d)
	# println("PreProcess[1][", ccV,"]: ", preProcessData[1][ccV])
	#return ((length(preProcessData[1][ccV]) - 1 )^2)/((nv(g)-1)*sum(δ))
	return ((length(d) - 1 )^2)/((nv(g)-1)*sum(δ))
end

function preProcess(g::AbstractGraph) #preprocess for compute upper boud of closeness centrality
	if (is_directed(g) && is_strongly_connected(g))
		return ([collect(vertices(g))], [nv(g)], [nv(g)])
	elseif (!is_directed(g) && is_connected(g))
		return ([collect(vertices(g))], [nv(g)], [nv(g)])
	elseif (!is_directed(g) && !is_connected(g))
		ccg = connected_components(g)
		lenccg = Int64[]
		for cc in ccg
			push!(lenccg, length(cc))
		end
		return (ccg, lenccg, lenccg)
	elseif (is_directed(g) && !is_strongly_connected(g) && is_weakly_connected(g))
		ccg = strongly_connected_components(g)
		condensationGraph = condensation(g)
		ccDAGTopologicalSorted = topological_sort_by_dfs(condensationGraph)
		ccgTopologicalSorted = Array{Array{Int64}}(length(ccg))

		alpha = zeros(Int64, length(ccDAGTopologicalSorted))
		omega = zeros(Int64, length(ccDAGTopologicalSorted))
		c = ccg[ccDAGTopologicalSorted[end]]
		omega[end] = length(c)
		alpha[end] = length(c)
		maxAlpha = alpha[end]
		ccgTopologicalSorted[end] = c
		for i in length(ccg)-1:-1:1 #dynamic programming
			c = ccg[ccDAGTopologicalSorted[i]]
			# println(i,": ",c," -> ", length(c))
			ccgTopologicalSorted[i] = c
			omega[i] = length(c)+sum(omega)
			alpha[i] = maxAlpha + length(c)
			if (alpha[i] > maxAlpha)
				maxAlpha = alpha[i]
			end
		end
		return (ccgTopologicalSorted, alpha, omega)
	end
end

function topKcc(g::AbstractGraph, k::Int64)
	preprocessData = preProcess(g)
	# println("Dati PreProcess: ", preprocessData)
	xk = 0.0
	topK = PriorityQueue{Int64,Float64}()
	for v in vertices(g)
		if (degree(g, v) != 0)
			t_xk = prunedBFS(g, v, xk, preprocessData)
			t_xk = prunedBFS(g, v, 0.0, preprocessData)
			# println(v," --- analizzato --->", t_xk)
			if t_xk != 0
				if (length(topK) < k)
					enqueue!(topK, v, t_xk)
					# println(topK)
					# println(v," --- trovato top k --->", t_xk)
					if length(topK) == k
						xk = peek(topK)[2]
						# println("valore di xk piu piccolo con coda piena ", xk)
					end
				elseif (length(topK) == k && peek(topK)[2] <= t_xk)
					dequeue!(topK)
					enqueue!(topK, v, t_xk)
					xk = peek(topK)[2]
					# println("valore di xk piu piccolo con dequeue ", xk)
					# println(topK)
					# println(v," --- trovato top k --->", t_xk)
				end
			end
		end
	end
	return priorityQueueToList(topK)
end

function priorityQueueToList(q::PriorityQueue)
	list = []
	for i in 1:length(q)
		push!(list,  peek(q))
		dequeue!(q)
	end
	return list
end

end # module


module DiscreteModelPortionsTests

using Gridap.Arrays
using Gridap.ReferenceFEs
using Gridap.Geometry

domain = (0,2,0,2,0,2)
partition = (10,10,10)
oldmodel = CartesianDiscreteModel(domain,partition)

const R = 0.7

function is_in(coords)
  n = length(coords)
  x = (1/n)*sum(coords)
  d = x[1]^2 + x[2]^2 - R^2
  d < 0
end

oldgrid = get_grid(oldmodel)
oldcell_to_coods = get_cell_coordinates(oldgrid)
oldcell_to_mask = lazy_map(is_in,oldcell_to_coods)

model = DiscreteModelPortion(oldmodel,oldcell_to_mask)
test_discrete_model(model)

#using Gridap.Visualization
#
#writevtk(model,"model")

end # module


## This example shows an application of `DelaySSA.jl` for a bursty model with delay

## A Bursty model with delay is described as
# ab^n/(1+b)^(n+1): 0 -> n P, which triggers n P to degrade after delay time τ

using DelaySSAToolkit
using Catalyst, DiffEqJump
begin # construct reaction network
    @parameters a b t
    @variables X(t)
    burst_sup = 30
    rxs = [Reaction(a*b^i/(1+b)^(i+1),nothing,[X],nothing,[i]) for i in 1:burst_sup]
    rxs = vcat(rxs)
    @named rs = ReactionSystem(rxs,t,[X],[a,b])
end

# convert the ReactionSystem to a JumpSystem
jumpsys = convert(JumpSystem, rs, combinatoric_ratelaws=false)

u0 = [0]
de_chan0 = [[]]
tf = 200.
tspan = (0,tf)
ps = [0.0282, 3.46]
dprob = DiscreteProblem(jumpsys,u0,tspan,ps)
τ = 130.

delay_trigger_affect! = []
for i in 1:burst_sup
    push!(delay_trigger_affect!, function (integrator, rng)
    append!(integrator.de_chan[1], fill(τ, i))
    end)
end
delay_trigger_affect!
delay_trigger = Dict([Pair(i, delay_trigger_affect![i]) for i in 1:burst_sup])
delay_complete = Dict(1=>[1=>-1])
delay_interrupt = Dict()
alg = DelayRejection()
# alg = DelayMNRM()
# alg = DelayDirect()
# alg = DelayDirectCR()
delayjumpset = DelayJumpSet(delay_trigger, delay_complete, delay_interrupt)
jprob = DelayJumpProblem(jumpsys, dprob, alg, delayjumpset, de_chan0, save_positions=(false, false))
seed = 2
sol = solve(jprob, SSAStepper(), seed = seed, saveat = 100,  save_delay_channel = true)
sol = solve(jprob, SSAStepper(), seed = seed, save_delay_channel = true)

using Plots, DiffEqBase
ensprob = EnsembleProblem(jprob)
@time ens = solve(ensprob, SSAStepper(), EnsembleSerial(), trajectories=10^5)

# Check with the exact probability distribution
using TaylorSeries
function taylor_coefficients(NT::Int,at_x,gen::Function)
    Q = zeros(NT)
    taylor_gen = taylor_expand(u->gen(u),at_x,order=NT)
    for j in 1:NT
        Q[j] = taylor_gen[j-1]
    end
    return Q
end
function delay_bursty(params,NT::Int)
    a, b, τ, t = params
    gen1(u) = exp(a*b*min(τ,t)*u/(1-b*u))
    taylor_coefficients(NT,-1,gen1)
end

using DifferentialEquations.EnsembleAnalysis
theme(:vibrant)
sol_end = componentwise_vectors_timepoint(ens, tf)
histogram(sol_end,bins=0:1:60,normalize=:pdf, label = "Delay SSA",fillalpha = 0.6, linecolor = :orange)
sol_exact = delay_bursty([ps;130;200], 61)
fig = plot!(0:60,sol_exact, linewidth = 3, label = "Exact solution", fmt=:svg, xlabel = "# of products", ylabel = "Probability")
# savefig(fig, "docs/src/assets/bursty.svg")


using WAV, DataFrames, CSV, PlotlyJS, DSP, PyPlot
include("spectrograms.jl")

function plot_classes(classes, frequencies)
    plot(bar(;x=classes, y=frequencies))
end

function create_audio_metadata(labels)
    audiodata = DataFrame(sampleRate=Float32[], channels=Int8[], samples=Int32[], length=Float32[], class=String[])
    looked = 0
    n = length(labels)
    for (id, lbl) in labels
        println("Analyzing file $id.wav")
        data, srate = wavread("train/Train/$id.wav")
        channels = size(data,2)
        samples = size(data,1)
        len = samples / srate
        row = [srate channels samples len lbl]
        push!(audiodata, row)
        looked += 1
        println("$(convert(Int8, round(looked / n * 100)))% Analyzed")
    end
    return audiodata
end

# Read class data
df = CSV.read("train/train.csv", header=true)
labels = gen_label_dict("train/train.csv")

# Plot class distribution
classdata = by(df, :Class, nrow)
# plot_classes(classdata[:,1], classdata[:,2])

# audio_metadata = create_audio_metadata(labels)
audio_metadata = CSV.read("metadata.csv", header=true)
# describe(audio_metadata, stats=:all)
# CSV.write("metadata.csv", audio_metadata)

# Plot a single wav file
# data, fs = wavread("train/0.wav")
# trace1 = scatter(;x=1:size(y,1), y=y[:,1], mode="lines")
# trace2 = scatter(;x=1:size(y,1), y=y[:,2], mode="lines")
# plot([trace1, trace2])
spectr = create_spectrogram("train/Train/0.wav")
imshow(spectr)
# imshow(log10.(power(spec)), extent=[first(t), last(t),
#              fs*first(f), fs*last(f)], aspect="auto")


function keyword_check{T}(ta::TimeArray{T,1}, prices::Bool, log_transform::Bool)
    if prices == false && log_transform == false # simple returns
        return ta
    elseif prices == false && log_transform == true # log returns
        return basecall(ta,expm1)
    elseif prices == true && log_transform == false # regular prices
        return percentchange(ta)
    elseif prices == true && log_transform == true # log prices
        return percentchange(basecall(ta,exp))
    else
        error("invalid keyword argument")
    end
end


using Catalyst, Test, OrdinaryDiffEq, LinearAlgebra

rn = @reaction_network begin
    (a1,a2), C <--> 0
    (k1,k2), A + B <--> C
    b1, 0 <-- B
end a1 a2 k1 k2 b1

rn2 = @reaction_network begin
    a1, C --> 0
    a2, 0 --> C
    k1, A + B --> C
    k2, C --> A + B
    b1, B --> 0
end a1 a2 k1 k2 b1

ps = ones(5)
u0 = [10.0,20.0,0.0]
tspan = (0,100.0)
oprob1 = ODEProblem(rn, u0, tspan, ps)
oprob2 = ODEProblem(rn2, u0, tspan, ps)
sol1 = solve(oprob1, Tsit5())
sol2 = solve(oprob2, Tsit5())

tv = range(tspan[1], tspan[2], length=100)
@test norm(sol1 - sol2, Inf) ≈ 0


"""
````
function simplexgrid(coord::Array{Tc,2},
                     cellnodes::Array{Ti,2},
                     cellregions::Array{Ti,1},
                     bfacenodes::Array{Ti,2},
                     bfaceregions::Array{Ti,1}
                     ) where {Tc,Ti}
````

    Create simplex grid from five arrays.
"""
function simplexgrid(coord::Array{Tc,2},
                     cellnodes::Array{Ti,2},
                     cellregions::Array{Ti,1},
                     bfacenodes::Array{Ti,2},
                     bfaceregions::Array{Ti,1}
                     ) where {Tc,Ti}
    @assert size(coord,2)>0
    dim=size(coord,1)
    if dim==1
        eltype=Edge1D
        btype=Vertex0D
        csys=Cartesian1D
    elseif dim==2
        eltype=Triangle2D
        btype=Edge1D
        csys=Cartesian2D
    elseif dim==3
        eltype=Tetrahedron3D
        btype=Triangle2D
        csys=Cartesian3D
    end

    grid=ExtendableGrid{Tc,Ti}()
    grid[Coordinates]=coord
    grid[CellNodes]=cellnodes
    grid[CellRegions]=cellregions
    grid[CellGeometries]=VectorOfConstants(eltype,length(cellregions))
    grid[BFaceNodes]=bfacenodes
    grid[BFaceRegions]=bfaceregions
    grid[BFaceGeometries]=VectorOfConstants(btype,length(bfaceregions))
    grid[CoordinateSystem]=csys
    return grid
end

"""
````
function simplexgrid(coord::Array{Tc,2},
                     cellnodes::Array{Ti,2},
                     cellregions::Array{Ti,1},
                     bfacenodes::Array{Ti,2},
                     bfaceregions::Array{Ti,1}
                     bedgenodes::Array{Ti,2},
                     bedgeregions::Array{Ti,1}
                     ) where {Tc,Ti}
````

    Create simplex grid from coordinates, cell-nodes-adjancency, cell-region-numbers,
    boundary-face-nodes adjacency, boundary-face-region-numbers, boundary-edge-nodes, and
    boundary-edge-region-numbers arrays.
"""
function simplexgrid(coord::Array{Tc,2},
                     cellnodes::Array{Ti,2},
                     cellregions::Array{Ti,1},
                     bfacenodes::Array{Ti,2},
                     bfaceregions::Array{Ti,1},
                     bedgenodes::Array{Ti,2},
                     bedgeregions::Array{Ti,1}
                     ) where {Tc,Ti}
    grid = simplexgrid(coord, cellnodes, cellregions, bfacenodes, bfaceregions)
    grid[BEdgeNodes]   = bedgenodes
    grid[BEdgeRegions] = bedgeregions
    return grid
end

simplexgrid(C,CN,CR,BFN,BFR)=simplexgrid(collect(C),collect(CN),collect(CR),collect(BFN), collect(BFR))

##########################################################
abstract type XCoordinates <: AbstractGridFloatArray1D end
abstract type YCoordinates <: AbstractGridFloatArray1D end
abstract type ZCoordinates <: AbstractGridFloatArray1D end

"""
$(TYPEDSIGNATURES)

Constructor for 1D grid.

Construct 1D grid from an array of node cordinates.
It creates two boundary regions with index 1 at the left end and
index 2 at the right end.

Primal grid holding unknowns: marked by `o`, dual
grid marking control volumes: marked by `|`.

```@raw html
 o-----o-----o-----o-----o-----o-----o-----o-----o
 |--|-----|-----|-----|-----|-----|-----|-----|--|
```
"""
function simplexgrid(_X::AbstractVector)
    X=collect_or_assign(_X)
    #    is_monotone(X) || error("X not monotone")
    coord=reshape(X,1,length(X))
    cellnodes=zeros(Int32,2,length(X)-1)
    cellregions=zeros(Int32,length(X)-1)
    for i=1:length(X)-1
        cellnodes[1,i]=i
        cellnodes[2,i]=i+1
        cellregions[i]=1
    end
    bfacenodes=Array{Int32}(undef,1,2)
    bfaceregions=zeros(Int32,2)
    bfacenodes[1,1]=1
    bfacenodes[1,2]=length(X)
    bfaceregions[1]=1
    bfaceregions[2]=2
    grid=simplexgrid(coord,
                     cellnodes,
                     cellregions,
                     bfacenodes,
                     bfaceregions)
    grid[XCoordinates]=X
    grid
end

##########################################################
"""
$(TYPEDSIGNATURES)

Constructor for 2D grid
from coordinate arrays.
Boundary region numbers count counterclockwise:

| location  |  number |
| --------- | ------- |
| south     |       1 |
| east      |       2 |
| north     |       3 |
| west      |       4 |

"""
function  simplexgrid(_X::AbstractVector, _Y::AbstractVector)
    X=collect_or_assign(_X)
    Y=collect_or_assign(_Y)
    is_monotone(X) || error("X not monotone")
    is_monotone(Y) || error("Y not monotone")

    Tc=promote_type(eltype(X),eltype(Y))

    function leq(x, x1, x2)
        if (x>x1)
            return false
        end
        if (x>x2)
            return false
        end
        return true
    end

    function geq(x, x1, x2)
        if (x<x1)
            return false
        end
        if (x<x2)
            return false
        end
        return true
    end

    nx=length(X)
    ny=length(Y)

    hmin=X[2]-X[1]
    for i=1:nx-1
        h=X[i+1]-X[i]
        if h <hmin
            hmin=h
        end
    end
    for i=1:ny-1
        h=Y[i+1]-Y[i]
        if h <hmin
            hmin=h
        end
    end

    @assert(hmin>0.0)
    eps=1.0e-5*hmin

    x1=X[1]+eps
    xn=X[nx]-eps
    y1=Y[1]+eps
    yn=Y[ny]-eps

    function  check_insert_bface(ibface,coord,n1,n2)
        if (geq(x1,coord[1,n1],coord[1,n2]))
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
	    bfaceregions[ibface]=4
        elseif (leq(xn,coord[1,n1],coord[1,n2]))
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
	    bfaceregions[ibface]=2
        elseif (geq(y1,coord[2,n1],coord[2,n2]))
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
	    bfaceregions[ibface]=1
        elseif (leq(yn,coord[2,n1],coord[2,n2]))
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
	    bfaceregions[ibface]=3
        end
        ibface
    end

    num_nodes=nx*ny
    num_cells=2*(nx-1)*(ny-1)
    num_bfacenodes=2*(nx-1)+2*(ny-1)

    coord=zeros(Tc,2,num_nodes)
    cellnodes=zeros(Int32,3,num_cells)
    cellregions=zeros(Int32,num_cells)
    bfacenodes=Array{Int32}(undef,2,num_bfacenodes)
    bfaceregions=zeros(Int32,num_bfacenodes)

    ipoint=0
    for iy=1:ny
        for ix=1:nx
            ipoint=ipoint+1
            coord[1,ipoint]=X[ix]
            coord[2,ipoint]=Y[iy]
        end
    end
    @assert(ipoint==num_nodes)

    icell=0
    for iy=1:ny-1
        for ix=1:nx-1
	    ip=ix+(iy-1)*nx
	    p00 = ip
	    p10 = ip+1
	    p01 = ip  +nx
	    p11 = ip+1+nx

            icell=icell+1
            cellnodes[1,icell]=p00
            cellnodes[2,icell]=p10
            cellnodes[3,icell]=p11
            cellregions[icell]=1

            icell=icell+1
            cellnodes[1,icell]=p11
            cellnodes[2,icell]=p01
            cellnodes[3,icell]=p00
            cellregions[icell]=1
        end
    end
    @assert(icell==num_cells)

    #lazy way to  create boundary grid

    ibface=0
    for icell=1:num_cells
        n1=cellnodes[1,icell]
	n2=cellnodes[2,icell]
	n3=cellnodes[3,icell]
        ibface=check_insert_bface(ibface,coord,n1,n2)
	ibface=check_insert_bface(ibface,coord,n1,n3)
	ibface=check_insert_bface(ibface,coord,n2,n3)
    end
    @assert(ibface==num_bfacenodes)

    grid=simplexgrid(coord,
                     cellnodes,
                     cellregions,
                     bfacenodes,
                     bfaceregions)

    grid[XCoordinates]=X
    grid[YCoordinates]=Y
    grid
end

##########################################################
"""
$(TYPEDSIGNATURES)

Constructor for 3D grid
from coordinate arrays.
Boundary region numbers:

| location  |  number |
| --------- | ------- |
| south     |       1 |
| east      |       2 |
| north     |       3 |
| west      |       4 |
| bottom    |       5 |
| top       |       6 |

"""
function  simplexgrid(_X::AbstractVector,_Y::AbstractVector,_Z::AbstractVector)
    X=collect_or_assign(_X)
    Y=collect_or_assign(_Y)
    Z=collect_or_assign(_Z)

    is_monotone(X) || error("X not monotone")
    is_monotone(Y) || error("Y not monotone")
    is_monotone(Z) || error("Z not monotone")

    Tc=promote_type(eltype(X),eltype(Y),eltype(Z))

    leq(x, x1, x2, x3)=x≤x1 && x≤x2 && x≤x3
    geq(x, x1, x2, x3)=x≥x1 && x≥x2 && x≥x3

    nx=length(X)
    ny=length(Y)
    nz=length(Z)

    hmin=X[2]-X[1]
    for i=1:nx-1
        h=X[i+1]-X[i]
        if h <hmin
            hmin=h
        end
    end
    for i=1:ny-1
        h=Y[i+1]-Y[i]
        if h <hmin
            hmin=h
        end
    end

    for i=1:nz-1
        h=Z[i+1]-Z[i]
        if h <hmin
            hmin=h
        end
    end

    @assert(hmin>0.0)
    eps=1.0e-5*hmin

    x1=X[1]+eps
    xn=X[end]-eps

    y1=Y[1]+eps
    yn=Y[end]-eps

    z1=Z[1]+eps
    zn=Z[end]-eps

    function  check_insert_bface(ibface,coord,bfacenodes,n1,n2,n3)
        if geq(x1,coord[1,n1],coord[1,n2],coord[1,n3])
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
            bfacenodes[3,ibface]=n3
	    bfaceregions[ibface]=4
        elseif leq(xn,coord[1,n1],coord[1,n2],coord[1,n3])
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
            bfacenodes[3,ibface]=n3
	    bfaceregions[ibface]=2
        elseif geq(y1,coord[2,n1],coord[2,n2],coord[2,n3])
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
            bfacenodes[3,ibface]=n3
	    bfaceregions[ibface]=1
        elseif leq(yn,coord[2,n1],coord[2,n2],coord[2,n3])
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
            bfacenodes[3,ibface]=n3
	    bfaceregions[ibface]=3
        elseif geq(z1,coord[3,n1],coord[3,n2],coord[3,n3])
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
            bfacenodes[3,ibface]=n3
	    bfaceregions[ibface]=5
        elseif leq(zn,coord[3,n1],coord[3,n2],coord[3,n3])
            ibface=ibface+1
            bfacenodes[1,ibface]=n1
            bfacenodes[2,ibface]=n2
            bfacenodes[3,ibface]=n3
	    bfaceregions[ibface]=6
        end
        ibface
    end

    num_nodes=nx*ny*nz
    num_cells=6*(nx-1)*(ny-1)*(nz-1)
    num_bfacenodes=4*(nx-1)*(ny-1)+4*(nx-1)*(nz-1)+4*(ny-1)*(nz-1)

    Ti=Int32
    coord=zeros(Tc,3,num_nodes)
    cellnodes=zeros(Ti,4,num_cells)
    cellregions=zeros(Ti,num_cells)
    bfacenodes=zeros(Ti,3,num_bfacenodes)
    bfaceregions=zeros(Ti,num_bfacenodes)

    ipoint=0
    for iz=1:nz
        for iy=1:ny
            for ix=1:nx
                ipoint=ipoint+1
                coord[1,ipoint]=X[ix]
                coord[2,ipoint]=Y[iy]
                coord[3,ipoint]=Z[iz]
            end
        end
    end

    @assert(ipoint==num_nodes)

    icell=0
    nxy=nx*ny
    for iz=1:nz-1
        for iy=1:ny-1
            for ix=1:nx-1

	        ip=ix+(iy-1)*nx+(iz-1)*nxy;

	        p000 = ip ;
	        p100 = ip+1;
	        p010 = ip  +nx;
	        p110 = ip+1+nx;
	        p001 = ip     +nxy;
	        p101 = ip+1   +nxy;
	        p011 = ip  +nx+nxy;
	        p111 = ip+1+nx+nxy;

                icell=icell+1; cellregions[icell]=1; @. cellnodes[:,icell]=(p000,p100,p110,p111)
                icell=icell+1; cellregions[icell]=1; @. cellnodes[:,icell]=(p000,p100,p101,p111)
                icell=icell+1; cellregions[icell]=1; @. cellnodes[:,icell]=(p000,p010,p011,p111)
                icell=icell+1; cellregions[icell]=1; @. cellnodes[:,icell]=(p000,p010,p110,p111)
                icell=icell+1; cellregions[icell]=1; @. cellnodes[:,icell]=(p000,p001,p101,p111)
                icell=icell+1; cellregions[icell]=1; @. cellnodes[:,icell]=(p000,p001,p011,p111)
            end
        end
    end
    @assert(icell==num_cells)
    #lazy way to  create boundary grid
    # lazy but easy... bad for partitioning !!!

    ibface=0
    for icell=1:num_cells
        n1=cellnodes[1,icell]
	n2=cellnodes[2,icell]
	n3=cellnodes[3,icell]
	n4=cellnodes[4,icell]
        ibface=check_insert_bface(ibface,coord,bfacenodes,n1,n2,n3)
	ibface=check_insert_bface(ibface,coord,bfacenodes,n1,n2,n4)
	ibface=check_insert_bface(ibface,coord,bfacenodes,n1,n3,n4)
	ibface=check_insert_bface(ibface,coord,bfacenodes,n2,n3,n4)
    end
    @assert(ibface==num_bfacenodes)

    grid=simplexgrid(coord,
                     cellnodes,
                     cellregions,
                     bfacenodes,
                     bfaceregions)

    grid[XCoordinates]=X
    grid[YCoordinates]=Y
    grid[ZCoordinates]=Z
    grid
end

"""
$(TYPEDSIGNATURES)
Create tensor product of 2D  grid and 1D coordinate arrays.

Cellregions and outer facet regions are taken over from 2D grid.
Top an bottom facet region given in ibot and itop, by default these
are detected from the largest cell region number of the 2D grid.
"""
function simplexgrid(grid2::ExtendableGrid, coordZ; ibot=0, itop=0)

    coord2=grid2[Coordinates]
    nnodes2=size(coord2,2)
    nnodesZ=length(coordZ)
    nnodes3=nnodes2*nnodesZ
    coord3=zeros(3,nnodes3)

    cells2=grid2[CellNodes]
    ncells2=size(cells2,2)
    ncells3=ncells2*3*(nnodesZ-1)
    cells3=zeros(Int, 4,ncells3)

    cellregions2=grid2[CellRegions]
    cellregions3=zeros(Int, ncells3)

    bfaces2=grid2[BFaceNodes]
    nbfaces2=size(bfaces2,2)
    nbfaces3=2*ncells2+2*nbfaces2*(nnodesZ-1)
    bfaces3=zeros(Int,3,nbfaces3)

    bfaceregions2=grid2[BFaceRegions]
    bfaceregions3=zeros(Int,nbfaces3)

    # handle defaults of top and bottom bc numbers
    nbfr=maximum(bfaceregions2)
    if ibot==0
        nbfr=nbfr+1
        ibot=nbfr
    end

    if itop==0
        nbfr=nbfr+1
        itop=nbfr
    end

    # 3D coordinates
    i3=1
    for iZ=1:nnodesZ
        for i2=1:nnodes2
            coord3[1,i3]=coord2[1,i2]
            coord3[2,i3]=coord2[2,i2]
            coord3[3,i3]=coordZ[iZ]
            i3+=1
        end
    end

    # 3D cells
    i3=1
    ishift=0
    for iZ=1:nnodesZ-1
        for i2=1:ncells2
            n1 = cells2[1,i2]+ishift
            n2 = cells2[2,i2]+ishift
            n3 = cells2[3,i2]+ishift

            n1>n2 ? (n1,n2) = (n2,n1) : nothing
            n2>n3 ? (n2,n3) = (n3,n2) : nothing
            n1>n2 ? (n1,n2) = (n2,n1) : nothing

            cells3[:,i3  ].=(n1,n2,        n3,        n3+nnodes2)
            cells3[:,i3+1].=(n1,n2,        n2+nnodes2,n3+nnodes2)
            cells3[:,i3+2].=(n1,n1+nnodes2,n2+nnodes2,n3+nnodes2)

            cellregions3[i3]   = cellregions2[i2]
            cellregions3[i3+1] = cellregions2[i2]
            cellregions3[i3+2] = cellregions2[i2]

            i3+=3
        end
        ishift+=nnodes2
    end

    #xy parallel  facets
    i3=1
    for iZ ∈ [1,nnodesZ]
        ishift=(iZ-1)*nnodes2
        for i2=1:ncells2
            @views bfaces3[:,i3].=cells2[:,i2].+ishift
            bfaceregions3[i3] = iZ==1 ? ibot : itop
            i3+=1
        end
    end

    # facets vertical to the x-y plane
    ishift=0
    for iZ=1:nnodesZ-1
        for i2=1:nbfaces2
            n1=bfaces2[1,i2]+ishift
            n2=bfaces2[2,i2]+ishift
            n1>n2 ? (n1,n2)=(n2,n1) : nothing
            @views bfaces3[:,i3  ].=(n1,n2,        n2+nnodes2)
            @views bfaces3[:,i3+1].=(n1,n1+nnodes2,n2+nnodes2)
            bfaceregions3[i3]   = bfaceregions2[i2]
            bfaceregions3[i3+1] = bfaceregions2[i2]
            i3+=2
        end
        ishift+=nnodes2
    end
    simplexgrid(coord3,cells3,cellregions3,bfaces3,bfaceregions3)
end

######################################################
"""
$(TYPEDSIGNATURES)

Read grid from file. Currently for pdelib sg format only
"""
function simplexgrid(file::String;format="")
    (fbase,fext)=splitext(file)
    if format==""
        format=fext[2:end]
    end
    @assert format=="sg"

    tks=TokenStream(file)
    expecttoken(tks,"SimplexGrid")
    version=parse(Float64,gettoken(tks))
    version20=false;

    if (version==2.0)
        version20=true;
    elseif (version==2.1)
        version20=false;
    else
        error("Read grid: wrong format version: $(version)")
    end

    dim::Int32=0
    coord=Array{Float64,2}(undef,0,0)
    cells=Array{Int32,2}(undef,0,0)
    regions=Array{Int32,1}(undef,0)
    faces=Array{Int32,2}(undef,0,0)
    bregions=Array{Int32,1}(undef,0)
    while(true)
        if (trytoken(tks,"DIMENSION"))
            dim=parse(Int32,gettoken(tks));
        elseif (trytoken(tks,"NODES"))
            nnodes=parse(Int32,gettoken(tks));
            embdim=parse(Int32,gettoken(tks));
            if(dim!=embdim)
                error("Dimension error (DIMENSION $(dim)) in section NODES")
            end
            coord=Array{Float64,2}(undef,dim,nnodes)
            for inode=1:nnodes
                for idim=1:embdim
                    coord[idim,inode]=parse(Float64,gettoken(tks))
                end
            end
        elseif (trytoken(tks,"CELLS"))
            ncells=parse(Int32,gettoken(tks));
            cells=Array{Int32,2}(undef,dim+1,ncells)
            regions=Array{Int32,1}(undef,ncells)
            for icell=1:ncells
                for inode=1:dim+1
                    cells[inode,icell]=parse(Int32,gettoken(tks));
                end
                regions[icell]=parse(Int32,gettoken(tks));
	        if version20
		    for j=1:dim+1
		        gettoken(tks);  # skip file format garbage
                    end
                end
            end
        elseif (trytoken(tks,"FACES"))
            nfaces=parse(Int32,gettoken(tks));
            faces=Array{Int32,2}(undef,dim,nfaces)
            bregions=Array{Int32,1}(undef,nfaces)
            for iface=1:nfaces
                for inode=1:dim
                    faces[inode,iface]=parse(Int32,gettoken(tks));
                end
                bregions[iface]=parse(Int32,gettoken(tks));
	        if (version20)
		    for j=1:dim+2
		        gettoken(tks); #skip file format garbage
                    end
                end
            end
        else
            expecttoken(tks,"END")
            break
        end
    end
    simplexgrid(coord,cells,regions,faces,bregions);
end

"""
$(TYPEDSIGNATURES)

Write grid to file. Currently for pdelib sg format only
"""
function Base.write(fname::String, g::ExtendableGrid; format="")
    (fbase,fext)=splitext(fname)
    if format==""
        format=fext[2:end]
    end
    @assert format=="sg"

    dim_g=dim_grid(g)
    dim_s=dim_space(g)
    nn=num_nodes(g)
    nc=num_cells(g)
    nbf=num_bfaces(g)
    coord=g[Coordinates]
    cellnodes=g[CellNodes]
    bfacenodes=g[BFaceNodes]
    cellregions=g[CellRegions]
    bfaceregions=g[BFaceRegions]

    # TODO: replace @sprintf by someting non-allocating
    open(fname, "w") do file
        write(file,@sprintf("SimplexGrid"))
        write(file,@sprintf(" "))
        write(file,@sprintf("2.1\n"))
        write(file,@sprintf("#created by ExtendableGrids.jl (c) J.Fuhrmann et al\n"))
        write(file,@sprintf("#mailto:{fuhrmann|streckenbach}@wias-berlin.de\n"))
        write(file,@sprintf("#%s\n",Dates.format(Dates.now(),"yyyy-mm-ddTHH-mm-SS")))

        write(file,@sprintf("DIMENSION\n%d\n",dim_g))
        write(file,@sprintf("NODES\n%d %d\n",nn,dim_s))

        for inode=1:nn
            for idim=1:dim_s
	        write(file,@sprintf("%.20e ",coord[idim,inode]))
                write(file,@sprintf("\n"))
            end
        end

        write(file,@sprintf("CELLS\n%d\n",nc))
        for icell=1:nc
            for inode=1:dim_g+1
	        write(file,@sprintf("%d ", cellnodes[inode,icell]))
            end
            write(file,@sprintf("%d\n",cellregions[icell]))
        end

        write(file,@sprintf("FACES\n%d\n",nbf))
        for ibface=1:nbf
            for inode=1:dim_g
	        write(file,@sprintf("%d ", bfacenodes[inode,ibface]))
            end
            write(file,@sprintf("%d\n",bfaceregions[ibface]))
        end
        write(file,@sprintf("END\n"))
        flush(file)
        flush(file)
    end
    nothing
end

"""
$(TYPEDSIGNATURES)

Merge two grids along their common boundary facets.

- g1: First grid to be merged
- g2: Second grid to be merged
- breg:  Interior boundary region number of merged facets. If zero (default value), no extra facets are generated.
- tol:  Distance below which two points are seen as identical. Default: 1.0e-10

"""
function glue(g1,g2; breg=0, tol=1.0e-10)

    dim=dim_space(g1)

    bfn1=g1[BFaceNodes]
    bfn2=g2[BFaceNodes]

    nbf1=size(bfn1,2)
    nbf2=size(bfn2,2)

    coord1=g1[Coordinates]
    coord2=g2[Coordinates]

    nn1=size(coord1,2)
    nn2=size(coord2,2)

    # numbers of faces in grid1 which match nodes in grid2
    matching_faces=zeros(Int,nbf2)
    n_matching_faces=0

    # numbers of nodes in grid1 which match nodes in grid2
    matching_nodes=zeros(Int,nn2)
    n_matching_nodes=0

    # Add matching pair to index of matching pairs
    function add_match!(match_list, nmatch, i1,i2)
        @assert match_list[i2]==0 || match_list[i2]==i1
        if match_list[i2]==0
            match_list[i2]=i1;
            nmatch+=1
        end
        nmatch
    end

    # Add two point indices to list of matching points
    add_matching_points(ip1,ip2) = n_matching_nodes = add_match!(matching_nodes,n_matching_nodes,ip1,ip2)

    # Add two face indices to list of matching faces
    add_matching_faces(if1,if2) = n_matching_faces = add_match!(matching_faces,n_matching_faces,if1,if2)

    ## Check if two points match (distance < tol)
    points_match(ip1, ip2) = @views norm(coord1[:,ip1]-coord2[:,ip2])<tol

    # Check if points in faces match, if so, add them to point matching index
    # Check if faces match, if so, add them to face matching index
    function match_faces(if1,if2)
        nmatch=0
        for ip1=1:dim
            for ip2=1:dim
                xip1=bfn1[ip1,if1]
                xip2=bfn2[ip2,if2]
                if points_match(xip1,xip2)
                    add_matching_points(xip1,xip2)
                    nmatch+=1
                end
            end
        end

      if nmatch==dim
          add_matching_faces(if1,if2);
      end
    end

    # Run over all pairs of boundary faces and try to match them
    for ibf1=1:nbf1
        for ibf2=1:nbf2
            match_faces(ibf1,ibf2)
        end
    end
    @info "glue: matches found: nodes $(n_matching_nodes) bfaces: $(n_matching_faces)"

    creg1=g1[CellRegions]
    creg2=g2[CellRegions]

    breg1=g1[BFaceRegions]
    breg2=g2[BFaceRegions]

    cn1=g1[CellNodes]
    cn2=g2[CellNodes]

    nc1=size(cn1,2)
    nc2=size(cn2,2)

    # transposed list of matching faces
    mf1=zeros(Int,nbf1)
    for if2=1:nbf2
        if matching_faces[if2]!=0
            mf1[matching_faces[if2]]=if2
        end
    end

    mfac = breg == 0 ? 2 : 1

    coordx=zeros(dim,nn1+nn2-n_matching_nodes)
    cnx=zeros(Int, dim+1,nc1+nc2)
    cregx=zeros(Int,nc1+nc2)
    bfnx=zeros(Int,dim,nbf1+nbf2-mfac*n_matching_faces)
    bregx=zeros(Int,nbf1+nbf2-mfac*n_matching_faces)

    #   copy all data from g1 into new fields
    for ix=1:nn1
        @views coordx[:,ix].=coord1[:,ix]
    end

    for ix=1:nc1
        cregx[ix]=creg1[ix]
        @views cnx[:,ix].=cn1[:,ix]
    end

    ibfx=1
    for ibf1=1:nbf1
        if mf1[ibf1]!=0 && breg == 0
            continue
        end
        if mf1[ibf1]!=0
            bregx[ibfx]=breg
        else
            bregx[ibfx]=breg1[ibf1]
        end
        @views bfnx[:,ibfx].=bfn1[:,ibf1]
        ibfx+=1
    end

    # re-calculate matching nodes with new global numbers
    # add missing coordinates
    ix=nn1+1
    for in2=1:nn2
        if matching_nodes[in2]!=0
            continue
        end
        matching_nodes[in2]=ix
        @views coordx[:,ix].=coord2[:,in2]
        ix=ix+1
    end

    # copy missing data from g2 into arrays.
    ix=nc1+1
    for ic2=1:nc2
        cregx[ix]=creg2[ic2]
        for id=1:dim+1
            cnx[id,ix]=matching_nodes[cn2[id,ic2]]
        end
        ix=ix+1
    end

    for ibf2=1:nbf2
        if matching_faces[ibf2]!=0
            continue
        end
        bregx[ibfx]=breg2[ibf2]
        for id=1:dim
            bfnx[id,ibfx]=matching_nodes[bfn2[id,ibf2]]
        end
        ibfx+=1
    end
    @assert ibfx == nbf1+nbf2-mfac*n_matching_faces+1
    simplexgrid(coordx,cnx,cregx,bfnx,bregx)
end


"""
    RosslerLorenzUnidir{T, 6} <: ContinuousSystemModel{T, N}

Create a 6D Rössler-Lorenz model consisting of two independent 3D subsystems:
one Rössler system and one Lorenz system (from [^1]). They are coupled such that the
second component (`x₂`) of the Rössler system unidirectionally forces the
second component (`y₂`) of the Lorenz system.

The parameter `c_xy` controls the coupling strength. The implementation here also
allows for tuning the parameters of each subsystem by introducing the constants
`a₁`, `a₂`, `a₃`, `b₁`, `b₂`, `b₃`. Default values for these parameters are
as in [^1].

## Equations of motion

The dynamics is generated by the following vector field

```math
\\begin{aligned}
\\dot x_1 &= a_1(x_2 + x_3) \\\\
\\dot x_2 &= a_2(x_1 + 0.2x_2) \\\\
\\dot x_3 &= a_2(0.2 + x_3(x_1 - a_3)) \\\\
\\dot y_1 &= b_1(y_2 - y_1) \\\\
\\dot y_2 &= y_1(b_2 - y_3) - y_2 +c_{xy}(x_2)^2 \\\\
\\dot y_3 &= y_1 y_2 - b_3y_3
\\end{aligned}
```

with the coupling constant ``c_{xy} \\geq 0``.

## Fields

- **`ui::SVector{6, T}`**: The initial condition.
- **`dt::T`**: The time step.
- **`c_xy::T`**: The coupling strength between the systems.
- **`a₁::T`**: The parameter `a₁` controlling the Rössler subsystem.
- **`a₂::T`**: The parameter `a₂` controlling the Rössler subsystem.
- **`a₃::T`**: The parameter `a₃` controlling the Rössler subsystem.
- **`b₁::T`**: The parameter `b₁` controlling the Lorenz subsystem.
- **`b₂::T`**: The parameter `b₂` controlling the Lorenz subsystem.
- **`b₃::T`**: The parameter `b₃` controlling the Lorenz subsystem.
- **`observational_noise_level`**: The magnitude of observational noise to add after sampling
    orbits of the system (given as percentage of empirical standard deviation). If
    `observational_noise_level = 30`, then noise equivalent to 0.3 times the empirical standard
    deviation will be added to each variable of the system (taking the standard deviation for
    that variable only).

## Implements

- [`rand(::Type{RosslerLorenzUnidir})`](@ref). This method creates an instance of `RosslerLorenzUnidir`
    with randomised parameters.

    ## References

[^1]:
    Krakovská, Anna, et al. "Comparison of six methods for the detection of causality in a
    bivariate time series." Physical Review E 97.4 (2018):042207.
    [https://journals.aps.org/pre/abstract/10.1103/PhysRevE.97.042207](https://journals.aps.org/pre/abstract/10.1103/PhysRevE.97.042207)

"""
struct RosslerLorenzUnidir{T, N} <: ContinuousSystemModel{T, N}
    ui::SVector{N, T}
    dt::T
    c_xy::T
    a₁::T
    a₂::T
    a₃::T
    b₁::T
    b₂::T
    b₃::T
    observational_noise_level::T # how much observational noise to add?
end

function RosslerLorenzUnidir(; ui::AbstractVector{T} = rand(6), c_xy = 1.0,
        a₁ = 6, a₂ = 0.2, a₃ = 5.7, b₁ = 10, b₂ = 28, b₃ = 8/3,
        observational_noise_level = 0.5,
        dt = 0.05) where T <: AbstractFloat

    if length(ui) != 6
        error("Number of elements in initial condition must match 6")
    end
    ps = T.([dt, c_xy, a₁, a₂, a₃, b₁, b₂, b₃, observational_noise_level])
    RosslerLorenzUnidir(SVector{6, T}(ui), ps...)
end

@inline @inbounds function eom_RosslerLorenzUnidir(u, p::RosslerLorenzUnidir, t)

    x1, x2, x3, y1, y2, y3 = u[1], u[2], u[3], u[4], u[5], u[6]

    dx1 = -p.a₁*(x2 + x3)
    dx2 = p.a₁*(x1 + p.a₂*x2)
    dx3 = p.a₁*(p.a₂ + x3*(x1 - p.a₃))
    dy1 = p.b₁*(-y1 + y2)
    dy2 = p.b₂*y1 - y2 - y1*y3 + p.c_xy*(x2^2)
    dy3 = y1*y2 - p.b₃*y3

    return SVector{6}(dx1, dx2, dx3, dy1, dy2, dy3)
end

function ContinuousDynamicalSystem(x::RosslerLorenzUnidir{T, N}) where {T, N}
    ContinuousDynamicalSystem(eom_RosslerLorenzUnidir, x.ui, x)
end

function trajectory(x::RosslerLorenzUnidir, npts::Int; sample_dt::Int = 1, Ttr = 1000,
        alg = SimpleDiffEq.SimpleATsit5())

    sys = ContinuousDynamicalSystem(x)
    T = npts*x.dt*sample_dt

    o = trajectory(sys, T, dt = x.dt, Ttr = Ttr*x.dt, alg = alg)[1:sample_dt:end-1, :]

    percent_noise = x.observational_noise_level
    if x.observational_noise_level > 0
        o = add_observational_noise!(o, percent_noise)
    end

    return o
end

"""
    rand(::Type{RosslerLorenzUnidir};
        dt::PT = 0.05,
        ui::Union{PT, AbstractVector{PT} = rand(6),
        a₁::PT = UncertainValue([Uniform(5.7, 5.95), Uniform(5.7, 5.95)], [70, 30])
        a₂::PT = Uniform(0.18, 0.22),
        a₃::PT = Uniform(5.5, 5.9),
        b₁::PT = Uniform(9.5, 10.5),
        b₂::PT = Uniform(27, 29),
        b₃::PT = Uniform(7.5/3, 8.5/3),
        observational_noise_level::PT = 20,
        n_maxtries::Int = 500) where {PT <: Union{Number, Distribution, AbstractUncertainValue}}

Generate `RosslerLorenzUnidir` model with randomised parameters, time step, initial condition and
observational noise level.

!!! note

    Any of the parameters be scalar values, uncertain values specified
    by either distributions, or more complex uncertain values defined as in the
    [UncertainData](https://github.com/kahaaga/UncertainData.jl) package.

## Keyword arguments

Let `const PT = Union{Number, Distribution, AbstractUncertainValue}`. Then

- **`dt::PT = 0.05`**: The time step.
- **`ui::Union{PT, AbstractVector{PT} = rand(6))`**: The initial condition.
- **`a₁::PT = UncertainValue([Uniform(5.7, 5.95), Uniform(5.7, 5.95)], [70, 30])`**: The parameter `a₁`.
- **`a₂::PT = Uniform(0.18, 0.22)`**: The parameter `a₂`.
- **`a₃::PT = Uniform(5.5, 5.9)`**: The parameter `a₃`.
- **`b₁::PT = Uniform(9.5, 10.5)`**: The parameter `b₁`.
- **`b₂::PT = Uniform(27, 29)`**: The parameter `b₂`.
- **`b₃::PT = Uniform(7.5/3, 8.5/3)`**: The parameter `b₃`.
- **`observational_noise_level`** = 20: The magnitude of observational noise to add after sampling
    orbits of the system (given as percentage of empirical standard deviation). If
    `observational_noise_level = 30`, then noise equivalent to 0.3 times the empirical standard
    deviation will be added to each variable of the system (taking the standard deviation for
    that variable only).
- **`n_maxtries`**: The number of draws to try until the quest of finding a system yielding
    a good orbit comes to an end.

## Examples

We'll use some handy constructs from the `UncertainData` package to
sample the parameter space and create [`RosslerLorenzUnidir`](@ref) models
with randomised parameters. This model is defined by

- its time step `dt` (which we'll keep fixed),
- its initial condition `ui` (we'll sample it randomly according to the default,
    which is to draw each marginal point from a uniform distribution on `[0, 1]`)
- its parameters `c_xy`, `a₁`, `a₂`, `a₃`, `b₁`, `b₂`, `b₃`, We'll
    randomise the four first parameters, keeping the remaining parameters fixed.

```julia
using CausalityTools, UncertainData, Distributions

# Sample `a₁` from the intervals [5.7, 5.95] ∪ [6.05, 6.3], with a 70 %
# sampling probability first subinterval and a 30 % sampling probability
# of sampling from the second interval. We'll make sampling probabilities
# within each interval uniform. This information is
# represented by a population of uncertain values, which is provided to
# the `UncertainValue` constructor as a vector of uncertain values along
# with their probability weights.
a₁ = UncertainValue([Uniform(5.7, 5.95), Uniform(5.7, 5.95)], [70, 30])

# Sample `a₂` and `a₃` uniformly from [0.18, 0.22] and [5.5, 5.9]
a₂ = Uniform(0.18, 0.22)
a₃ = Uniform(5.5, 5.9)

# Let the coupling strength be drawn from a normal distribution `N(μ, σ)`
# with `μ = 1.0`, `σ = 0.5`. Truncate at 0 and 2, to ensure valid coupling
# strengts are drawn.

# Create 100 different models with parameters randomised as above.
n = 100
models = Vector{RosslerLorenzUnidir}(undef, n)

for i = 1:n
    models[n] = rand(RosslerLorenzUnidir, c_xy = 0.05, dt = 0.05,
        a₁ = a₁, a₂ = a₂, a₃ = a₃,
        b₁ = 10, b₂ = 28, b₃ = 8.5/3)
end
```
"""
function rand(::Type{RosslerLorenzUnidir};
        c_xy = 0.5,
        a₁ = UncertainValue([Uniform(5.7, 5.95), Uniform(5.7, 5.95)], [70, 30]),
        a₂ = Uniform(0.18, 0.22),
        a₃ = Uniform(5.5, 5.9),
        b₁ = Uniform(9.5, 10.5),
        b₂ = Uniform(27, 29),
        b₃ = Uniform(7.5/3, 8.5/3),
        ui = Uniform(0, 1),
        observational_noise_level = 20,
        dt = 0.05,
        n_maxtries::Int = 500)

    a₁ = UncertainValue(a₁)
    a₂ = UncertainValue(a₂)
    a₃ = UncertainValue(a₃)
    b₁ = UncertainValue(b₁)
    b₂ = UncertainValue(b₂)
    b₃ = UncertainValue(b₃)

    c_xy = UncertainValue(c_xy)
    dt = UncertainValue(dt)
    observational_noise_level = UncertainValue(observational_noise_level)

    n_tries = 0

    npts = 1000

    while n_tries <= n_maxtries
        # Randomly sample parameters. If not uncertain, simple use the parameters.
        sa₁ = rand(a₁)
        sa₂ = rand(a₂)
        sa₃ = rand(a₃)
        sb₁ = rand(b₁)
        sb₂ = rand(b₂)
        sb₃ = rand(b₃)
        sdt = rand(dt)
        sc_xy = rand(c_xy)
        sobservational_noise_level = rand(observational_noise_level)
        c_xy = rand(c_xy)
        sdt = rand(dt)

        if ui isa Vector{<:AbstractUncertainValue}
            rui = rand.(ui)
        elseif ui isa Distribution
            rui = rand(ui, 6)
        elseif ui isa Number
            rui = repeat([ui], 6)
        elseif ui isa Vector && length(ui) == 6
            rui = ui
        else
            error("Initial condition `ui` not specified correctly.")
        end

        # Without noise while finding good orbit.
        sys = RosslerLorenzUnidir(ui = rui, c_xy = sc_xy,
            a₁ = sa₁, a₂ = sa₂, a₃ = sa₃,
            b₁ = sb₁, b₂ = sb₂, b₃ = sb₃, dt = sdt,
            observational_noise_level = 0.0)

        # Try to sample a trajectory
        pts = trajectory(sys, npts, sample_dt = 1, Ttr = 1000)

        M = Matrix(pts)

        if all(isfinite.(M)) && all(M .< 1e10) && count(M .≈ 0) < npts*0.1 && count(abs.(M) .< 1e-10) < npts*0.1 &&
            (count(abs.(M) .< 1e-12) < npts*0.1)
            return RosslerLorenzUnidir(ui = rui,
                c_xy = sc_xy,
                a₁ = sa₁, a₂ = sa₂, a₃ = sa₃,
                b₁ = sb₁, b₂ = sb₂, b₃ = sb₃, dt = sdt,
                observational_noise_level = sobservational_noise_level)
        end
        println("no attractor found. trying with new initial condition and parameters")
        n_tries += 1
    end
    println("could not find attractor!")
end


type IIDRandomSequence{S<:ValueSupport,T<:UnivariateDistribution} <: AbstractIDSequence{S}
  d::T
  length::Int64
end
function IIDRandomSequence{T<:UnivariateDistribution}(d::T,length::Int64)
    S = supertype(T).parameters[2]
    return IIDRandomSequence{S,T}(d,length)
end

length(X::IIDRandomSequence) = X.length
distributions(X::IIDRandomSequence) = fill(X.d,length(X))

function insupport(X::IIDRandomSequence,x::AbstractVector)
  length(x)==length(X) && all(insupport(X.d,x))
end

_rand!{T<:Real}(X::IIDRandomSequence, x::AbstractVector{T}) = rand!(X.d,x)

function _logpdf{T<:Real}(X::IIDRandomSequence, x::AbstractVector{T})
  return sum(x_i->logpdf(X.d,x_i),x)
end

mean(X::IIDRandomSequence) = mean(X.d)*ones(length(X))
var(X::IIDRandomSequence) = var(X.d)*ones(length(X))
cov(X::IIDRandomSequence) = Diagonal(var(X))
entropy(X::IIDRandomSequence) = entropy(X.d)*length(X)

function mgf(X::IIDRandomSequence, t::AbstractVector)
  prod([mgf(X.d,t_i) for t_i in t])
end
function cf(X::IIDRandomSequence, t::AbstractVector)
  prod([cf(X.d,t_i) for t_i in t])
end


@testset "test_synthesis" begin
P = tf(1.,[1.,1])
C = tf([1.,1],[1.,0])
L = P*C
Lsys = ss(L)

B = [1]
A = [1,1]
R = [1,1]
S = [1]
T = [1]

@test isapprox(minreal(feedback(P,C),1e-5), tf([1,0],[1,2,1]), rtol = 1e-5)
@test isapprox(numpoly(minreal(feedback(L),1e-5))[1].a, numpoly(tf(1,[1,1]))[1].a)# This test is ugly, but numerical stability is poor for minreal
@test feedback2dof(B,A,R,S,T) == tf(B.*T, conv(A,R) + [0;0;conv(B,S)])
@test feedback2dof(P,R,S,T) == tf(B.*T, conv(A,R) + [0;0;conv(B,S)])
@test isapprox(pole(minreal(tf(feedback(Lsys)),1e-5)) , pole(minreal(feedback(L),1e-5)), atol=1e-5)

Pint = tf(1,[1,1])
Cint = tf([1,1],[1,0])
Lint = P*C

@test isapprox(minreal(feedback(Pint,Cint),1e-5), tf([1,0],[1,2,1]), rtol = 1e-5) # TODO consider keeping minreal of Int system Int
@test isapprox(numpoly(minreal(feedback(Lint),1e-5))[1].a, numpoly(tf(1,[1,1]))[1].a)# This test is ugly, but numerical stability is poor for minreal
@test isapprox(pole(minreal(tf(feedback(Lsys)),1e-5)) , pole(minreal(feedback(L),1e-5)), atol=1e-5)

@test_throws ErrorException feedback(ss(1),ss(1))
@test_throws ErrorException feedback(ss([1 0; 0 1], ones(2,2), ones(1,2),0))
end


mutable struct Gaussian_Pyramid
    dims::Array{Tuple{Int64,Int64},1}
    imgs::Array{Array{Float64,2},1}
    kerns::Array{Tuple{KernelFactors.ReshapedOneD,KernelFactors.ReshapedOneD},1}
end

function Gaussian_Pyramid(in_sz::Tuple)
    Gaussian_Pyramid([in_sz],[zeros(Float64,in_sz)],
        Array{Tuple{KernelFactors.ReshapedOneD,KernelFactors.ReshapedOneD},1}())
end

function Gaussian_Pyramid(in_sz::Tuple,out_sz::Tuple)
    mygauss=Gaussian_Pyramid(in_sz)

    if (in_sz[1] * in_sz[2]) > (out_sz[1] * out_sz[2])

        println("Downsampling pyramid")

        new_sz = (div(in_sz[1],2),div(in_sz[2],2))
        #push!(mygauss.dims,new_sz)
        #push!(mygauss.imgs,zeros(Float64,new_sz))
        #push!(mygauss.kerns,make_gaussian_kernel(in_sz,new_sz))
        old_sz = in_sz

        while ((new_sz[1] > out_sz[1])&&(new_sz[2] > out_sz[2]))

            push!(mygauss.dims,new_sz)
            push!(mygauss.imgs,zeros(Float64,new_sz))
            push!(mygauss.kerns,make_gaussian_kernel(old_sz,new_sz))

            old_sz = new_sz
            new_sz = (div(new_sz[1],2),div(new_sz[2],2))

        end
        if mygauss.dims[end] != out_sz
            push!(mygauss.dims,out_sz)
            push!(mygauss.imgs,zeros(Float64,out_sz))
            push!(mygauss.kerns,make_gaussian_kernel(new_sz,out_sz))
        end

    else
        println("Upsampling pyramid")

        new_sz = (in_sz[1]*2,in_sz[2]*2)
        old_sz = in_sz

        #push!(mygauss.dims,new_sz)
        #push!(mygauss.imgs,zeros(Float64,new_sz))
        #push!(mygauss.kerns,make_gaussian_kernel(in_sz,new_sz))

        while ((new_sz[1] <= out_sz[1]) && (new_sz[2] <= out_sz[2]))

            push!(mygauss.dims,new_sz)
            push!(mygauss.imgs,zeros(Float64,new_sz))
            push!(mygauss.kerns,make_gaussian_kernel(old_sz,new_sz))
            old_sz = new_sz
            new_sz = (new_sz[1]*2,new_sz[2]*2)
        end
        if mygauss.dims[end] != out_sz
            push!(mygauss.dims,out_sz)
            push!(mygauss.imgs,zeros(Float64,out_sz))
            push!(mygauss.kerns,make_gaussian_kernel(new_sz,out_sz))
        end
    end
    mygauss
end

function lowpass_filter_resize(gauss::Gaussian_Pyramid)

    for k=1:length(gauss.kerns)
       lowpass_filter_resize!(gauss.imgs[k],gauss.imgs[k+1],gauss.kerns[k])
    end
end

function lowpass_filter_resize(gauss::Gaussian_Pyramid,input::AbstractArray{Float64,2},output::AbstractArray{Float64,2})

   gauss.imgs[1][:] = input
    lowpass_filter_resize(gauss)
    output[:] = gauss.imgs[end]

    nothing
end