diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 868bef11..81fdde18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,8 +23,7 @@ jobs: strategy: fail-fast: false matrix: - version: - - '1.9' + version: - '1' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index 53b00169..4e4db555 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Omniscape" uuid = "a38d70fc-99f5-11e9-1e3d-cbca093024c3" -authors = ["Vincent A. Landau "] -version = "0.6.1" +authors = ["Vincent A. Landau"] +version = "0.6.2" [deps] ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" @@ -18,5 +18,5 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" ArchGDAL = "0.10" Circuitscape = "^5.13.1" ProgressMeter = "1.3" -StatsBase = "0.33" -julia = "1.9" +StatsBase = "0.34" +julia = "1.10" diff --git a/docker/Dockerfile b/docker/Dockerfile index 70ab48fa..fe81088b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,9 +6,9 @@ RUN apt-get update && \ curl # Install julia -RUN curl -L https://julialang-s3.julialang.org/bin/linux/x64/1.9/julia-1.9.3-linux-x86_64.tar.gz > /tmp/julia.tar.gz +RUN curl -L https://julialang-s3.julialang.org/bin/linux/x64/1.10/julia-1.10.0-linux-x86_64.tar.gz > /tmp/julia.tar.gz RUN tar -zxvf /tmp/julia.tar.gz -RUN ln -s /julia-1.9.3/bin/julia /usr/bin/julia +RUN ln -s /julia-1.10.0/bin/julia /usr/bin/julia # Install Omniscape RUN julia -e 'using Pkg; Pkg.add(["Omniscape", "Test", "BenchmarkTools"])' diff --git a/src/consts.jl b/src/consts.jl index 2c1991d6..7b2d4cc1 100644 --- a/src/consts.jl +++ b/src/consts.jl @@ -11,6 +11,12 @@ const SINGLE = ["single", "Single", "SINGLE"] const SOLVERS = ["cg+amg", "cholmod"] +const N_CONDITIONS_VALUES = ["1", "2"] + +const COMPARE_TO_FUTURE_VALUES = ["none", "1", "2", "both"] + +const COMPARISONS = ["equal", "within"] + const SUPPORTED_ARGS = ["resistance_file", "resistance_is_conductance", "source_file", "project_name", "parallelize", "parallel_batch_size", "block_size", "radius", "buffer", "source_threshold", diff --git a/src/errors_warnings.jl b/src/errors_warnings.jl index 1beb8210..662172a4 100644 --- a/src/errors_warnings.jl +++ b/src/errors_warnings.jl @@ -22,6 +22,7 @@ function missing_args_error(missing_args) @error "The following arguments are missing from your .ini (or config Dictionary) with no defaults: $(join(map(string, missing_args), " "))" end + function check_raster_alignment(raster1, raster2, name1, name2, allow_different_projections) sizes_not_equal = size(raster1[1]) != size(raster2[1]) projections_not_equal = (raster1[2] != raster2[2]) || (raster1[3] != raster2[3]) @@ -101,3 +102,42 @@ function check_unsupported_args(cfg) $(join(map(string, bad_args), " "))" end end + +function check_arg_values( + cfg::Dict{String, String}, + reclass_table::Union{Nothing, MissingArray{T, 2} where T <: Number}, + condition1::Union{Nothing, MissingArray{T, 2} where T <: Number}, + condition2::Union{Nothing, MissingArray{T, 2} where T <: Number} + ) + # Case when reclass_table is specified but reclass is false + if (cfg["reclassify_resistance"] ∉ TRUELIST) && (reclass_table !== nothing || cfg["reclass_table"] != "") + @warn "You provided a reclass_table, but did not set reclassify_resistance to true. Resistance will not be reclassified." + end + + if (condition1 !== nothing || condition2 !== nothing) && (cfg["conditional"] ∉ TRUELIST) + @error "You provided condition rasters but conditional was not set to true in your config." + return true + end + + if cfg["compare_to_future"] ∉ COMPARE_TO_FUTURE_VALUES + @error "compare_to_future must be one of $(COMPARE_TO_FUTURE_VALUES). Got $(cfg["compare_to_future"])." + return true + end + + if cfg["comparison1"] ∉ COMPARISONS + @error "comparison1 must be one of $(COMPARISONS). Got $(cfg["comparison1"])." + return true + end + + if cfg["comparison2"] ∉ COMPARISONS + @error "comparison2 must be one of $(COMPARISONS). Got $(cfg["comparison2"])." + return true + end + + if cfg["n_conditions"] ∉ N_CONDITIONS_VALUES + @error "n_conditions must be one of $(N_CONDITIONS_VALUES). Got $(cfg["n_conditions"])." + return true + end + return false +end + diff --git a/src/main.jl b/src/main.jl index 87dbeb9e..4e6c7c08 100644 --- a/src/main.jl +++ b/src/main.jl @@ -95,16 +95,16 @@ key. function run_omniscape( cfg::Dict{String, String}, resistance::MissingArray{T, 2} where T <: Number; - reclass_table::MissingArray{T, 2} where T <: Number = MissingArray{Float64, 2}(undef, 1, 2), + reclass_table::Union{Nothing, MissingArray{T, 2} where T <: Number} = nothing, source_strength::MissingArray{T, 2} where T <: Number = source_from_resistance(resistance, cfg, reclass_table), - condition1::MissingArray{T, 2} where T <: Number = MissingArray{Float64, 2}(undef, 1, 1), - condition2::MissingArray{T, 2} where T <: Number = MissingArray{Float64, 2}(undef, 1, 1), - condition1_future::MissingArray{T, 2} where T <: Number = condition1, - condition2_future::MissingArray{T, 2} where T <: Number = condition2, - wkt::String = "", - geotransform::Array{Float64, 1} = [0., 1., 0., 0., 0., -1.0], - write_outputs::Bool = false) - + condition1::Union{Nothing, MissingArray{T, 2} where T <: Number} = nothing, + condition2::Union{Nothing, MissingArray{T, 2} where T <: Number} = nothing, + condition1_future::Union{Nothing, MissingArray{T, 2} where T <: Number} = nothing, + condition2_future::Union{Nothing, MissingArray{T, 2} where T <: Number} = nothing, + wkt::Union{String, Nothing} = nothing, + geotransform::Union{Array{Float64, 1}, Nothing} = nothing, + write_outputs::Bool = false + ) start_time = time() n_threads = nthreads() cfg_user = cfg @@ -116,6 +116,9 @@ function run_omniscape( cfg = init_cfg() update_cfg!(cfg, cfg_user) + # Check for bad values passed to options + check_arg_values(cfg, reclass_table, condition1, condition2) && return + ## Parse commonly called integer arguments int_arguments = Dict{String, Int64}() @@ -140,6 +143,19 @@ function run_omniscape( project_name = cfg["project_name"] file_format = os_flags.write_as_tif ? "tif" : "asc" + # Warn user if we need to write to a different directory if write_outputs + ## create new directory if project_name already exists + if isdir(string(project_name)) + initial_project_name = deepcopy(project_name) + dir_suffix = 1 + while isdir(string(project_name, "_$(dir_suffix)")) + dir_suffix += 1 + end + isdir(project_name) && (project_name = string(project_name, "_$(dir_suffix)")) + @warn("Your specified project directory, $(initial_project_name), already exists. Writing outputs to $(project_name).") + end + mkpath(project_name) + ## Set number of BLAS threads to 1 when parallel processing if os_flags.parallelize && nthreads() != 1 BLAS.set_num_threads(1) @@ -174,7 +190,7 @@ function run_omniscape( condition2_future = condition2 end - condition_layers = ConditionLayers(condition1, condition1_future, + condition_layers = ConditionLayers{precision}(condition1, condition1_future, condition2, condition2_future) ## Setup Circuitscape configuration @@ -326,16 +342,6 @@ function run_omniscape( normalized_cum_currmap[isnan.(normalized_cum_currmap)] .= 0 end - if write_outputs - ## create new directory if project_name already exists - dir_suffix = 1 - while isdir(string(project_name, "_$(dir_suffix)")) - dir_suffix+=1 - end - isdir(project_name) && (project_name = string(project_name, "_$(dir_suffix)")) - mkpath(project_name) - end - ## Overwrite no data if os_flags.mask_nodata if os_flags.calc_normalized_current @@ -440,11 +446,15 @@ function run_omniscape(path::String) if os_flags.reclassify reclass_table = read_reclass_table("$(cfg["reclass_table"])", precision) else - reclass_table = MissingArray{precision, 2}(undef, 1, 2) + reclass_table = nothing end ## Load source strengths if !os_flags.source_from_resistance + if cfg["source_file"] == "" + @error("You did not provide a source raster file path. Set source_from_resistance to true in your config if you want to generate source strength from the resistance layer.") + return + end sources_raster = read_raster("$(cfg["source_file"])", precision) source_strength = sources_raster[1] @@ -475,7 +485,7 @@ function run_omniscape(path::String) "resistance_file", "condition1_file", allow_different_projections) && return - # get rid of unneedecheck_rasterd raster to save memory + # get rid of unneeded raster to save memory condition1_raster = nothing if compare_to_future == "1" || compare_to_future == "both" @@ -490,7 +500,10 @@ function run_omniscape(path::String) # get rid of unneeded raster to save memory condition1_future_raster = nothing else - condition1_future = MissingArray{precision, 2}(undef, 1, 1) + if cfg["condition1_future_file"] != "" + @error("You provided a future condition raster (condition1_future_file), but did not specify compare_to_future in your INI. compare_to_future must be \"1\" or \"both\" in order to compare to future conditions.") + end + condition1_future = nothing end if n_conditions == 2 @@ -517,16 +530,25 @@ function run_omniscape(path::String) # get rid of unneeded raster to save memory condition2_future_raster = nothing else - condition2_future = MissingArray{precision, 2}(undef, 1, 1) + if cfg["condition2_future_file"] != "" + @error("You provided a future condition raster (condition2_future_file), but did not specify compare_to_future in your INI. compare_to_future must be \"2\" or \"both\" in order to compare to future conditions.") + end + condition2_future = nothing end else - condition2 = MissingArray{precision, 2}(undef, 1, 1) + if cfg["condition2_file"] != "" + @error("You provided a condition2_file, but n_conditions was not set to 2 in your INI file. Set n_conditions to 2 if you wish to use a second condition for calculating conditional connectivity.") + end + condition2 = nothing condition2_future = condition2 end else - condition1 = MissingArray{precision, 2}(undef, 1, 1) - condition2 = MissingArray{precision, 2}(undef, 1, 1) + if (cfg["condition2_file"] != "") || (cfg["condition1_file"] != "") + @error("conditional is set to false in your INI but you provided file paths to condition files. Please set conditional to true if you wish to compute conditional connectivity.") + end + condition1 = nothing + condition2 = nothing condition1_future = condition1 condition2_future = condition2 end diff --git a/src/structs.jl b/src/structs.jl index aeb22270..d3b903e3 100644 --- a/src/structs.jl +++ b/src/structs.jl @@ -38,9 +38,9 @@ end const MissingArray{T, N} = Array{Union{Missing, T}, N} -struct ConditionLayers{T, N} - condition1_present::MissingArray{T, N} - condition1_future::MissingArray{T, N} - condition2_present::MissingArray{T, N} - condition2_future::MissingArray{T, N} +struct ConditionLayers{T} + condition1_present::Union{MissingArray{T, 2}, Nothing} + condition1_future::Union{MissingArray{T, 2}, Nothing} + condition2_present::Union{MissingArray{T, 2}, Nothing} + condition2_future::Union{MissingArray{T, 2}, Nothing} end diff --git a/src/utils.jl b/src/utils.jl index ea06ae73..7e11569e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -159,7 +159,6 @@ function get_source( (source_subset[coalesce.(source_subset .> 0, false)] * target.amps) / source_sum if conditional - xlower_buffered = Int64(max(target.x_coord - radius - buffer, 1)) xupper_buffered = Int64(min(target.x_coord + radius + buffer, ncols)) ylower_buffered = Int64(max(target.y_coord - radius - buffer, 1)) @@ -219,7 +218,7 @@ function source_target_match!( value1 = median(skipmissing(condition1_future[ylower:yupper, xlower:xupper])) source_subset[coalesce.(((con1_present_subset .- value1) .> condition1_upper) .| ((con1_present_subset .- value1) .< condition1_lower), false)] .= 0 - elseif comparison1 == "equals" + elseif comparison1 == "equal" value1 = mode(skipmissing(condition1_future[ylower:yupper, xlower:xupper])) source_subset[coalesce.(con1_present_subset .!= value1, false)] .= 0 end @@ -231,7 +230,7 @@ function source_target_match!( value2 = median(skipmissing(condition2_future[ylower:yupper, xlower:xupper])) source_subset[coalesce.(((con2_present_subset .- value2) .> condition2_upper) .| ((con2_present_subset .- value2) .< condition2_lower), false)] .= 0 - elseif comparison2 == "equals" + elseif comparison2 == "equal" value2 = mode(skipmissing(condition2_future[ylower:yupper, xlower:xupper])) source_subset[coalesce.(con2_present_subset .!= value2, false)] .= 0 end @@ -529,7 +528,7 @@ end # Calculate the source layer using resistance surface and arguments from cfg function source_from_resistance(resistance::MissingArray{T, 2} where T <: Number, cfg::Dict{String, String}, - reclass_table::MissingArray{T, 2} where T <: Number) + reclass_table::Union{MissingArray{T, 2} where T <: Number, Nothing}) full_cfg = init_cfg() update_cfg!(full_cfg, cfg) r_cutoff = parse(Float64, full_cfg["r_cutoff"]) diff --git a/test/input/bad_compare_to_future.ini b/test/input/bad_compare_to_future.ini new file mode 100644 index 00000000..c2a909df --- /dev/null +++ b/test/input/bad_compare_to_future.ini @@ -0,0 +1,31 @@ +[Input files] +resistance_file = input/resistance.asc +resistance_is_conductance = true +source_file = input/source.asc + +[Options] +block_size = 3 +radius = 5 +buffer = 0 +source_threshold = 0.2 +project_name = test6 +correct_artifacts = true +source_from_resistance = false +r_cutoff = 0.0 + +[Conditional connectivity options] +conditional = true +n_conditions = 1 +compare_to_future = foo + +condition1_file = input/temperature.asc +comparison1 = within +condition1_lower = -0.5 +condition1_upper = 0.5 + +[Output options] +write_raw_currmap = false +calc_normalized_current = false +calc_flow_potential = false + +parallelize = true diff --git a/test/input/bad_comparison1.ini b/test/input/bad_comparison1.ini new file mode 100644 index 00000000..7f93f99c --- /dev/null +++ b/test/input/bad_comparison1.ini @@ -0,0 +1,31 @@ +[Input files] +resistance_file = input/resistance.asc +resistance_is_conductance = true +source_file = input/source.asc + +[Options] +block_size = 3 +radius = 5 +buffer = 0 +source_threshold = 0.2 +project_name = test6 +correct_artifacts = true +source_from_resistance = false +r_cutoff = 0.0 + +[Conditional connectivity options] +conditional = true +n_conditions = 1 +compare_to_future = none + +condition1_file = input/temperature.asc +comparison1 = foo +condition1_lower = -0.5 +condition1_upper = 0.5 + +[Output options] +write_raw_currmap = false +calc_normalized_current = false +calc_flow_potential = false + +parallelize = true diff --git a/test/input/bad_comparison2.ini b/test/input/bad_comparison2.ini new file mode 100644 index 00000000..905d5872 --- /dev/null +++ b/test/input/bad_comparison2.ini @@ -0,0 +1,36 @@ +[Input files] +resistance_file = input/resistance.asc +resistance_is_conductance = true +source_file = input/source.asc + +[Options] +block_size = 3 +radius = 5 +buffer = 0 +source_threshold = 0.2 +project_name = test6 +correct_artifacts = true +source_from_resistance = false +r_cutoff = 0.0 + +[Conditional connectivity options] +conditional = true +n_conditions = 2 +compare_to_future = none + +condition1_file = input/temperature.asc +comparison1 = within +condition1_lower = -0.5 +condition1_upper = 0.5 + +condition2_file = input/temperature.asc +comparison2 = bar +condition2_lower = -0.5 +condition2_upper = 0.5 + +[Output options] +write_raw_currmap = false +calc_normalized_current = false +calc_flow_potential = false + +parallelize = true diff --git a/test/input/bad_n_conditions.ini b/test/input/bad_n_conditions.ini new file mode 100644 index 00000000..fe082a1e --- /dev/null +++ b/test/input/bad_n_conditions.ini @@ -0,0 +1,31 @@ +[Input files] +resistance_file = input/resistance.asc +resistance_is_conductance = true +source_file = input/source.asc + +[Options] +block_size = 3 +radius = 5 +buffer = 0 +source_threshold = 0.2 +project_name = test6 +correct_artifacts = true +source_from_resistance = false +r_cutoff = 0.0 + +[Conditional connectivity options] +conditional = true +n_conditions = 3 +compare_to_future = none + +condition1_file = input/temperature.asc +comparison1 = within +condition1_lower = -0.5 +condition1_upper = 0.5 + +[Output options] +write_raw_currmap = false +calc_normalized_current = false +calc_flow_potential = false + +parallelize = true diff --git a/test/input/config5.ini b/test/input/config5.ini index fb2a828a..3978fc3f 100644 --- a/test/input/config5.ini +++ b/test/input/config5.ini @@ -1,6 +1,7 @@ [Input files] resistance_file = input/resistance.asc source_file = input/source.asc +reclass_table = file/path.asc [Options] block_size = 3 @@ -18,10 +19,10 @@ n_conditions = 2 compare_to_future = none condition1_file = input/landcover.asc -comparison1 = equals +comparison1 = equal condition2_file = input/landcover.asc -comparison2 = equals +comparison2 = equal [Output options] write_raw_currmap = false diff --git a/test/input/no_source_provided.ini b/test/input/no_source_provided.ini new file mode 100644 index 00000000..95244094 --- /dev/null +++ b/test/input/no_source_provided.ini @@ -0,0 +1,21 @@ +[Input files] +resistance_file = input/resistance.asc + +[Options] +block_size = 2 +radius = 5 +buffer = 2 +source_threshold = 0.2 +project_name = test1 +calc_flow_potential = true +correct_artifacts = true +source_from_resistance = false +mask_nodata = true + +write_raw_currmap = true +calc_normalized_current = true + +parallelize = true +parallel_batch_size = 10 + +solver = wrong_solver_name \ No newline at end of file diff --git a/test/output_verify/test5/config.ini b/test/output_verify/test5/config.ini index 2bdc1356..914eea37 100644 --- a/test/output_verify/test5/config.ini +++ b/test/output_verify/test5/config.ini @@ -18,10 +18,10 @@ n_conditions = 2 compare_to_future = none condition1_file = input/landcover.asc -comparison1 = equals +comparison1 = equal condition2_file = input/landcover.asc -comparison2 = equals +comparison2 = equal [Output options] write_raw_currmap = true diff --git a/test/runtests.jl b/test/runtests.jl index 21d99bda..f011dbaa 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -32,8 +32,8 @@ source_subset = convert(Array{Union{Float64, Missing}, 2}, source_subset) condition_layers = Omniscape.ConditionLayers( convert(Array{Union{Float64, Missing}, 2}, con1pres), convert(Array{Union{Float64, Missing}, 2}, con1fut), - Array{Union{Float64, Missing}, 2}(undef, 1, 1), - Array{Union{Float64, Missing}, 2}(undef, 1, 1) + nothing, + nothing ) Omniscape.source_target_match!(source_subset, @@ -83,9 +83,13 @@ block_sources = source_strength[Int(targets[1,2] - int_arguments["block_radius"] # Test error throws @info "Testing error throws" +@test run_omniscape("input/no_source_provided.ini") === nothing @test run_omniscape("input/config7.ini") === nothing @test run_omniscape("input/bad_config.ini") === nothing - +@test run_omniscape("input/bad_n_conditions.ini") === nothing +@test run_omniscape("input/bad_compare_to_future.ini") === nothing +@test run_omniscape("input/bad_comparison1.ini") === nothing +@test run_omniscape("input/bad_comparison2.ini") === nothing end @testset "run_omnsicape()" begin