diff --git a/src/nature/macros.jl b/src/nature/macros.jl
index 67a1abd60e01f55cad661f66427abf6b97b278da..c4be6866b518fc122e2f0d0f743306dfa681a059 100644
--- a/src/nature/macros.jl
+++ b/src/nature/macros.jl
@@ -77,9 +77,6 @@ Set the parameters that are used to initialise this species' population.
 For parameter options, see [`PopInitParams`](@ref).
 """
 macro populate(species, params)
-    #TODO I think I can make this simpler by using parametric methods
-    # (see https://docs.julialang.org/en/v1/manual/methods/#Parametric-Methods,
-    # https://stackoverflow.com/questions/42283820/julia-difference-between-type-and-datatype)
     println("Will register $(species).")
     quote
         # convert the macro body to a dict
@@ -87,7 +84,9 @@ macro populate(species, params)
             $(esc(params))
             return Base.@locals
         end
-        registerpopulationparams($(esc(species)), fun())
+        # construct a parametric method that returns a parameterised PopInitParams object
+        # when called with the species Type
+        $(esc(:populationparameters))(s::Type{$(esc(species))}) = PopInitParams(; fun()...)
     end
 end
 
diff --git a/src/nature/populations.jl b/src/nature/populations.jl
index 63b49bc3053db4c5c71eb9810ea3cff0e815971e..75a80c010656266b64b699fa391aca2140cba0fe 100644
--- a/src/nature/populations.jl
+++ b/src/nature/populations.jl
@@ -41,29 +41,7 @@ using [`@populate`](@ref).
     asexual::Bool = false
 end
 
-"""
-A closure to store the parameters needed to initialise each species' population.
-Users register a new set of parameters using [`@populate`](@ref).
-"""
-#TODO I think I can make this simpler by using parametric methods
-# (see https://docs.julialang.org/en/v1/manual/methods/#Parametric-Methods,
-# https://stackoverflow.com/questions/42283820/julia-difference-between-type-and-datatype)
-let populationparams = Dict{String,Tuple{Type,PopInitParams}}()
-
-    global function getpopulationparams(species::String)
-        populationparams[species]
-    end
-
-    global function registerpopulationparams(species::Type{T}, initparams::Dict{Symbol,Any}) where T <: Animal
-        # convert the dict to an InitParam struct and save it
-        println("Registering $(species).")
-        #XXX why is this necessary?
-        spstrings = split(string(species), ".") # strip out the module name, if necessary
-        speciesstring = length(spstrings) == 1 ? spstrings[1] : spstrings[2]
-        populationparams[speciesstring] = (species, PopInitParams(; initparams...))
-        #populationparams[species] = NamedTuple(k => v for (k,v) in initparams)
-    end
-end
+populationparameters(t::Type) = @error("No population parameters defined for $(t), use @populate.")
 
 """
     initpopulation!(species, model)
@@ -72,11 +50,21 @@ Initialise the population of the given species, based on the parameters stored
 in [`PopInitParams`](@ref). Define these using [`@populate`](@ref).
 """
 function initpopulation!(species::String, model::SimulationModel)
-    s, p = getpopulationparams(species) # species constructor and parameters
+    # get the species Type from its namestring
+    speciestype = nothing
+    for name in names(Persefone, all=true)
+        string(name) == species && (speciestype = getfield(Persefone, name))
+    end
+    isnothing(speciestype) && @error "Species $species is not defined."
+    #XXX can we get rid of the Persefone.<species> in the speciestype?
+    @debug "Initialising population of $species/$speciestype."
+    # get the PopInitParams and check for validity
+    p = populationparameters(speciestype)
     (p.popsize <= 0 && p.popdensity <= 0) && #XXX not sure what this would do
         @warn("initpopulation() called with popsize and popdensity both <= 0")
     (p.popsize > 0 && p.popdensity > 0) && #XXX not sure what this would do
         @warn("initpopulation() called with popsize and popdensity both > 0")
+    # create as many individuals as necessary in the landscape
     n = 0
     lastn = 0
     width, height = size(model.landscape)
@@ -86,8 +74,8 @@ function initpopulation!(species::String, model::SimulationModel)
                 if p.habitat((x,y), model) &&
                     (p.popdensity <= 0 || @chance(1/p.popdensity)) #XXX what if pd==0?
                     if p.pairs
-                        a1 = s(length(model.animals)+1, male, (-1, -1), (x,y), p.phase)
-                        a2 = s(length(model.animals)+1, female, (-1, -1), (x,y), p.phase)
+                        a1 = speciestype(length(model.animals)+1, male, (-1, -1), (x,y), p.phase)
+                        a2 = speciestype(length(model.animals)+1, female, (-1, -1), (x,y), p.phase)
                         create!(a1, model)
                         create!(a2, model)
                         push!(model.animals, a1)
@@ -95,7 +83,7 @@ function initpopulation!(species::String, model::SimulationModel)
                         n += 2
                     else
                         sex = p.asexual ? hermaphrodite : @rand([male, female])
-                        a = s(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase)
+                        a = speciestype(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase)
                         create!(a, model)
                         push!(model.animals, a)
                         n += 1
diff --git a/src/world/landscape.jl b/src/world/landscape.jl
index 50d93ad7b4360bf38b14c394e6b8bec5f3eddab2..9beb16daef112dd4b9aabaadb275678bc34bcda4 100644
--- a/src/world/landscape.jl
+++ b/src/world/landscape.jl
@@ -21,6 +21,7 @@ mutable struct Pixel
     landcover::LandCover
     fieldid::Union{Missing,Int64}
     events::Vector{EventType}
+    #TODO add list of animal IDs
 end
 
 """