### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe. ### ### This file holds various utility types and functions, especially for dealing with ### units and dates. ### ### UNITS AND DIMENSIONS ## Import and define units and dimensions import Unitful: cm, m, km, ha, mg, g, kg, Length, Area, Mass const m² = m^2 const km² = km^2 # enable division with different length/area unit types Base.:(/)(x::S,y::T) where {S<:Length, T<:Length} = (upreferred(x)/m) / (upreferred(y)/m) Base.:(/)(x::S,y::T) where {S<:Area, T<:Area} = (upreferred(x)/m²) / (upreferred(y)/m²) Base.:(/)(x::S,y::T) where {S<:Mass, T<:Mass} = (upreferred(x)/g) / (upreferred(y)/g) ## RECURRING DATES """ AnnualDate A type to handle recurring dates (e.g. migration, harvest). Stores a month and a day, and can be compared against normal dates. To save typing, a Tuple{Int64,Int64} is automatically converted to an AnnualDate, allowing this syntax: `nestingend::AnnualDate = (August, 15)`. """ mutable struct AnnualDate month::Int64 day::Int64 # inner constructor to make sure inputs are in range AnnualDate(month::Int64, day::Int64) = if !(0 < month <= 12) Base.error("AnnualDate: month $month is out of range.") #TODO replace with exception elseif !(0 < day <= 31) # not strictly accurate (AnnualDate(February, 30) is possible), but good enough Base.error("AnnualDate: day $day is out of range.") #TODO replace with exception else new(month, day) end end # (automatically) convert integer tuples to AnnualDate. # Allows writing `(August, 2)` instead of `AnnualDate(August, 2)` where an AnnualDate is expected. AnnualDate(ad::Tuple{Int64,Int64}) = AnnualDate(ad...) Base.convert(::Type{AnnualDate}, ad::Tuple{Int64,Int64}) = AnnualDate(ad) Base.convert(::Type{Tuple{Int64,Int64}}, ad::AnnualDate) = (ad.month, ad.day) # allow creating AnnualDates from a string of the format "8 August" AnnualDate(ad::String) = AnnualDate(Date(ad, dateformat"d U")) Base.convert(::Type{AnnualDate}, ad::String) = AnnualDate(ad) Base.tryparse(::Type{AnnualDate}, ad::String) = AnnualDate(ad) # Interface with Dates AnnualDate(date::Date) = AnnualDate(month(date), day(date)) Base.convert(::Type{AnnualDate}, ad::Date) = AnnualDate(ad) Dates.month(ad::AnnualDate) = ad.month Dates.day(ad::AnnualDate) = ad.day Dates.monthday(ad::AnnualDate) = (ad.month, ad.day) Date(year::Int64, ad::AnnualDate) = Date(year, ad.month, ad.day) # Comparison between AnnualDates and with Dates Base.:(==)(ad1::AnnualDate, ad2::AnnualDate) = (month(ad1) == month(ad2) && day(ad1) == day(ad2)) Base.:(==)(ad::AnnualDate, d::Date) = (month(d) == month(ad) && day(d) == day(ad)) Base.:(==)(d::Date, ad::AnnualDate) = (ad == d) Base.:(<)(ad1::AnnualDate, ad2::AnnualDate) = (month(ad1) < month(ad2) || (month(ad1) == month(ad2) && day(ad1) < day(ad2))) Base.:(<)(ad::AnnualDate, d::Date) = ad < AnnualDate(d) Base.:(<)(d::Date, ad::AnnualDate) = AnnualDate(d) < ad # Addition and subtraction of date periods Base.:(+)(ad::AnnualDate, time::DatePeriod) = AnnualDate(Date(2022, ad) + time) Base.:(-)(ad::AnnualDate, time::DatePeriod) = AnnualDate(Date(2022, ad) - time) Base.:(-)(ad1::AnnualDate, ad2::AnnualDate) = ad1 > ad2 ? Date(2022, ad1) - Date(2022, ad2) : Date(2022, ad1) - Date(2021, ad2) # Taking ranges Base.:(:)(start::AnnualDate, stop::AnnualDate) = if start < stop # normal case, e.g. Easter:Christmas AnnualDate.(Date(2022, start):Date(2022, stop)) else # handle wrap-around, e.g. Christmas:Easter #XXX ignores leap years AnnualDate.(Date(2021, start):Date(2022, stop)) end # Instantiate a recurring date for a given year """ thisyear(annualdate, model) nextyear(annualdate, model) lastyear(annualdate, model) Convert an AnnualDate to a Date, using the current/next/previous year of the simulation run. """ thisyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date), ad) nextyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)+1, ad) lastyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)-1, ad) ### RANDOM NUMBERS """ randn(vector) Return a random element from the given vector, following a (mostly) normal distribution based on index values (i.e. elements in the middle of the vector will be returned most frequently). """ function Base.randn(v::AbstractVector, rng=Random.default_rng()) r = bounds(randn(rng) + 4, min = 1, max=7) # normal distribution with mean 4, values of [1,7] step = 7 / length(v) i = Int(round(r / step)) v[i] end """ @randn(vector) Return a normally-distributed random number or element from the sample, using the model RNG. This is a utility wrapper that can only be used a context where the `model` object is available. """ macro randn(v) :($(esc(:randn))($(esc(v)), $(esc(:model)).rng)) end """ @rand(args...) Return a random number or element from the sample, using the model RNG. This is a utility wrapper that can only be used a context where the `model` object is available. """ macro rand(args...) :($(esc(:rand))($(esc(:model)).rng, $(map(esc, args)...))) end """ @shuffle!(collection) Shuffle the given collection in place, using the model RNG. This is a utility wrapper that can only be used a context where the `model` object is available. """ macro shuffle!(collection) :($(esc(:shuffle!))($(esc(:model)).rng, $(esc(collection)))) end """ @chance(odds) Return true if a random number is less than the odds (0.0 <= `odds` <= 1.0), using the model RNG. This is a utility wrapper that can only be used a context where the `model` object is available. """ macro chance(odds) :($(esc(:rand))($(esc(:model)).rng) < $(esc(odds))) end ### MISCELLANEOUS """ bounds(x; max=Inf, min=0) A utility function to make sure that a number is within a given set of bounds. Returns `max`/`min` if `x` is greater/less than this. """ function bounds(x::Number; max::Number=Inf, min::Number=0) if unit(x) != NoUnits max = max*unit(x) min = min*unit(x) end x > max ? max : x < min ? min : x end """ cycle!(vector, n=1) Move the first element of the vector to the end, repeat n times. """ function cycle!(v::AbstractVector, n::Int64=1) for i in 1:n push!(v, v[1]) deleteat!(v, 1) end v end """ @areaof(npixels) Calculate the area of a given number of landscape pixels, knowing the resolution of the world map (requires the `model` object to be available). """ macro areaof(npixels) :($(esc(npixels)) * $(esc(:model)).settings["world.mapresolution"]^2) end