diff --git a/Project.toml b/Project.toml index 19374e62b64673b888a974e5cb0da792ea861607..9add9d69ea9cc595f6d292dc10d0940ed6d55998 100644 --- a/Project.toml +++ b/Project.toml @@ -14,3 +14,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" + +[compat] +julia = ">= 1.9" +Agents = ">= 5.6" \ No newline at end of file diff --git a/src/Persephone.jl b/src/Persephone.jl index 3d86d2dd5cb70ddcc31abb5ab3c99b4bb1704bf2..8b56527bbdff1802d6b7b47f4ef4711c14bb3f0b 100644 --- a/src/Persephone.jl +++ b/src/Persephone.jl @@ -52,8 +52,10 @@ export @distanceto, @distancetoedge, @countanimals, + @rand, #functions simulate, + simulate!, initialise, stepsimulation!, createevent!, diff --git a/src/core/input.jl b/src/core/input.jl index 814d72d166d2ef8f8b4056559de80d988f312640..2f5687e31a4075d478d8f5ed5bc1d1ec1dafdc16 100644 --- a/src/core/input.jl +++ b/src/core/input.jl @@ -53,7 +53,8 @@ function getsettings(configfile::String, seed::Union{Int64,Nothing}=nothing) # pre-process certain parameters if !isnothing(seed) settings["core"]["seed"] = seed - elseif settings["core"]["seed"] == 0 + end + if settings["core"]["seed"] == 0 settings["core"]["seed"] = abs(rand(RandomDevice(), Int32)) end defaultoutdir = defaults["core"]["outdir"] @@ -74,7 +75,7 @@ Certain software parameters can be set via the commandline. """ function parsecommandline() versionstring = """ - Persephone $(@project_version), commit $(read(`git rev-parse HEAD`, String)[1:8]) + Persephone $(pkgversion(Persephone)) © 2022-2023 Daniel Vedder, Lea Kolb (MIT license) https://git.idiv.de/xo30xoqa/persephone """ @@ -96,6 +97,7 @@ function parsecommandline() arg_type = String required = false end + #XXX this changes the global RNG?! (https://github.com/carlobaldassi/ArgParse.jl/issues/121) args = parse_args(s) for a in keys(args) (args[a] == nothing) && delete!(args, a) diff --git a/src/core/simulation.jl b/src/core/simulation.jl index d47c372b05092f85c2c84d09ddafb2b9c3606b55..ca13ff26b1180773f06880981fa1460225bb6d4a 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -72,14 +72,23 @@ function finalise!(model::AgentBasedModel) end """ - simulate(config=PARAMFILE, seed=nothing) + simulate!(model) -Carry out a complete simulation run, optionally specifying a configuration file -and a seed for the RNG. +Carry out a complete simulation run using a pre-initialised model object. """ -function simulate(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing) - model = initialise(config, seed) +function simulate!(model::AgentBasedModel) runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1 step!(model, dummystep, stepsimulation!, runtime) finalise!(model) end + +""" + simulate(config=PARAMFILE, seed=nothing) + +Initialise a model object and carry out a complete simulation run, optionally +specifying a configuration file and a seed for the RNG. +""" +function simulate(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing) + model = initialise(config, seed) + simulate!(model) +end diff --git a/src/crop/crops.jl b/src/crop/crops.jl index 84a78f50baf1d95b3f0d20cdc73dfbed0435f2b7..a16dae30d1ace24e3ea398ca4887c20d34aec7e7 100644 --- a/src/crop/crops.jl +++ b/src/crop/crops.jl @@ -52,7 +52,7 @@ function initfields!(model::AgentBasedModel) model.landscape[x,y].fieldid = objectid push!(model[objectid].pixels, (x,y)) else - fp = add_agent!(FarmPlot, model, [(x,y)], fallow, 0.0, 0.0) + fp = add_agent!((x,y), FarmPlot, model, [(x,y)], fallow, 0.0, 0.0) model.landscape[x,y].fieldid = fp.id convertid[rawid] = fp.id n += 1 diff --git a/src/nature/nature.jl b/src/nature/nature.jl index 8010f135f4099dcdd6285f9dc58eb05efba5c23a..ffee8c28fce880b27d26a535c431d4573e9813e7 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -372,7 +372,9 @@ Return a random number or element from the sample, using the model RNG. This is a utility wrapper that can only be used nested within `@phase` or `@habitat`. """ macro rand(args...) - :(rand($(esc(:model)).rng, $(map(esc, args)...))) + :($(esc(:rand))($(esc(:model)).rng, $(map(esc, args)...))) end +##TODO @chance macro: @chance(0.5) => rand(model.rng) < 0.5 + ##TODO add movement macros diff --git a/src/nature/species/wolpertinger.jl b/src/nature/species/wolpertinger.jl index af1f1b6efc3c9ef04390ee907b5bb4122a7104f8..e51a4752f5264d3706f252c9aa831999a11e2ae7 100644 --- a/src/nature/species/wolpertinger.jl +++ b/src/nature/species/wolpertinger.jl @@ -24,7 +24,7 @@ of a deer. and occasionally reproduce by spontaneous parthogenesis... """ @phase lifephase begin - direction = Tuple(rand([-1,1], 2)) + direction = Tuple(@rand([-1,1], 2)) for i in 1:@rand(1:@trait(maxspeed)) walk!(animal, direction, model; ifempty=false) end diff --git a/test/io_tests.jl b/test/io_tests.jl index a9dd780e24fc9bff05ea7c3c26d3a76a9a37af27..5884d9d7b90e130cecf13e5f5a8d8589d3d175b0 100644 --- a/test/io_tests.jl +++ b/test/io_tests.jl @@ -4,9 +4,11 @@ ### @testset "Model configuration" begin - properties = Dict{Symbol,Any}(:settings=>TESTSETTINGS) + settings = Ps.getsettings(TESTPARAMETERS) + properties = Dict{Symbol,Any}(:settings=>settings) space = GridSpace((10,10), periodic=false) model = AgentBasedModel(Animal, space, properties=properties, warn=false) + @test @param(core.configfile) == TESTPARAMETERS @test @param(core.startdate) == Date(2022, 2, 1) @test @param(nature.targetspecies) == ["Wolpertinger", "Wyvern"] diff --git a/test/landscape_tests.jl b/test/landscape_tests.jl index b02ee20006d10a7ece354da82b9f35579ba8d424..ab8dac4adf0cf74db863a9bf6e276ea2b4ccf6a3 100644 --- a/test/landscape_tests.jl +++ b/test/landscape_tests.jl @@ -30,7 +30,8 @@ function smalltestlandscape(agenttype::Type=Animal) :events=>Vector{FarmEvent}(), :dataoutputs=>Vector{DataOutput}(), :settings=>TESTSETTINGS) - return AgentBasedModel(agenttype, space, properties=properties, warn=false) + return AgentBasedModel(agenttype, space, properties=properties, + rng=StableRNG(TESTSETTINGS["core"]["seed"]), warn=false) end @testset "Landscape initialisation" begin diff --git a/test/nature_tests.jl b/test/nature_tests.jl index 661ea5791a8ba54834579013127637b1113d90a3..c0e4158f3863390e6a8baebd6a77634dcfbffb96 100644 --- a/test/nature_tests.jl +++ b/test/nature_tests.jl @@ -68,15 +68,15 @@ end @testset "Species macros" begin # create a model landscape and a test species - #TODO test @rand model = smalltestlandscape(Union{Animal,Farmer,FarmPlot}) + @species Mermaid begin ageofmaturity = 2 pesticidemortality = 1.0 @initialise(@habitat(@landcover() == Persephone.water), pairs=true) @phase life begin @debug "$(Persephone.animalid(animal)) is swimming happily in its pond." - @respond Persephone.pesticide @kill(@trait(pesticidemortality)) + @respond Persephone.pesticide @kill(@trait(pesticidemortality), "poisoning") @respond Persephone.harvest @setphase(drought) @debug "Animal: $animal" if @trait(sex) == Persephone.female && @countanimals() < 3 && @@ -123,12 +123,16 @@ end @test Ps.countanimals(pond, model) == 3 createevent!(model, [pond], Ps.pesticide) @test_logs((:debug, "Mermaid 1 is swimming happily in its pond."), - (:debug, "Mermaid 1 has died."), + (:debug, "Mermaid 1 has died from poisoning."), (:debug, "Mermaid 2 is swimming happily in its pond."), - (:debug, "Mermaid 2 has died."), + (:debug, "Mermaid 2 has died from poisoning."), (:debug, "Mermaid 3 is swimming happily in its pond."), - (:debug, "Mermaid 3 has died."), + (:debug, "Mermaid 3 has died from poisoning."), min_level=Logging.Debug, match_mode=:any, stepsimulation!(model)) @test Ps.countanimals(pond, model) == 0 + + # test @rand (this is done more easily outside of @species) + @test typeof(@rand()) == Float64 + @test @rand([true, true]) end diff --git a/test/runtests.jl b/test/runtests.jl index 5d463187b1e646c1100034b1d33d7492975ca073..69bd24d0b5db209f08bde7d6f181acecd0c41d41 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,12 +6,13 @@ using Pkg Pkg.activate("..") +using Agents +using Dates +using Logging using Persephone -using Test using Random -using Logging -using Dates -using Agents +using StableRNGs +using Test const Ps = Persephone diff --git a/test/simulation_tests.jl b/test/simulation_tests.jl index d4a2d0b50aad367e757617cce7ad8a86057ac968..1be977ac4458f133ff929d87e85f8a3d8e1e5648 100644 --- a/test/simulation_tests.jl +++ b/test/simulation_tests.jl @@ -15,19 +15,24 @@ end @testset "Model simulation" begin + # The primary reason for this testset is to make sure that a complete + # simulation will run through without errors. Thus, there are few tests. + # Additionally, it makes sure that no part of the code uses the global + # RNG, as this would compromise reproducibility. If one of the `rand()` + # tests fail, that requirement has been violated somewhere. + Random.seed!(1) + rand1 = rand() + Random.seed!(1) + model = initialise(TESTPARAMETERS, 218) + #XXX upstream problem with ArgParse (https://github.com/carlobaldassi/ArgParse.jl/issues/121) + @test_broken rand() == rand1 Random.seed!(1) - @test isapprox(rand(), 0.07337, atol=0.0001) - model = simulate(TESTPARAMETERS, 218) @test @param(core.seed) == 218 + @test_logs((:info, "Simulating day 2022-02-01."), + (:info, "Simulated 59 days."), + min_level=Logging.Debug, match_mode=:any, + simulate!(model)) @test model.date == Date(2022,4,1) - nwol = sum((typeof(a)==Animal && a.traits["name"]=="Wolpertinger") for a in allagents(model)) - nwyv = sum((typeof(a)==Animal && a.traits["name"]=="Wyvern") for a in allagents(model)) - #FIXME these still fail - although it might not be that clever to rely on random model outcomes - @test nwol == 32 - @test nwyv == 9 - # To retain reproducibility, the model code must never use the global RNG. - # If this next test fails, that requirement has probably been violated somewhere. - #FIXME it does fail... Might it be called by some dependency? - @test isapprox(rand(), 0.34924, atol=0.0001) + @test rand() == rand1 end