Skip to content

Commit

Permalink
Merge pull request #153 from Circuitscape/arg_checks
Browse files Browse the repository at this point in the history
Add argument checks, allow Nothing type for optional args
  • Loading branch information
vlandau authored Jan 31, 2024
2 parents b7093fb + 2d58a4e commit 3ce902a
Show file tree
Hide file tree
Showing 16 changed files with 272 additions and 51 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ jobs:
strategy:
fail-fast: false
matrix:
version:
- '1.9'
version:
- '1'
os:
- ubuntu-latest
Expand Down
8 changes: 4 additions & 4 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Omniscape"
uuid = "a38d70fc-99f5-11e9-1e3d-cbca093024c3"
authors = ["Vincent A. Landau <vincent@vibrantplanet.net>"]
version = "0.6.1"
authors = ["Vincent A. Landau"]
version = "0.6.2"

[deps]
ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"
Expand All @@ -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"
4 changes: 2 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"])'
Expand Down
6 changes: 6 additions & 0 deletions src/consts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions src/errors_warnings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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

76 changes: 49 additions & 27 deletions src/main.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}()

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/structs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 3 additions & 4 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"])
Expand Down
31 changes: 31 additions & 0 deletions test/input/bad_compare_to_future.ini
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions test/input/bad_comparison1.ini
Original file line number Diff line number Diff line change
@@ -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
Loading

2 comments on commit 3ce902a

@vlandau
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/99923

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.6.2 -m "<description of version>" 3ce902a9346e8594bdc4cd388fa2684fcd4eaf98
git push origin v0.6.2

Please sign in to comment.