diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83eb83c533fb1962623280fc730dbf303eb6796a..d78f731a36797f3806a8a8e73ca3123d350e02a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - added soil map section to GIS docs
 
+- the `Weather` type now stores all weather information for the whole
+  simulation as a "struct-of-arrays" and with a function interface,
+  e.g. `sunshine(weather, date)` (previously, `Weather` was a struct
+  that stored the weather data for one day only, and
+  `AgricultureModel` had a field `weather::Dict{Date,Weather}`, making
+  it a "dict-of-struct" data layout)
+
+- `AgricultureModel` and `Weather` are now defined with `@kwdef`,
+  allowing for keyword arguments in their constructors
+
+- when reading weather data, we now throw an error when there are
+  missing values for the fields `min_temperature`, `max_temperature`,
+  `mean_temperature`, `precipitation`, and
+  `potential_evapotranspiration` (in the future missing values could
+  also be imputed)
+
 ### Deprecated
 
 ### Removed
diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index b19f4024a459ef835feb6e2eed0f636528a1a97e..e57f075b9b722ae78c920bd058b4a40d50413523 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -12,20 +12,20 @@ This is the heart of the model - a struct that holds all data and state
 for one simulation run. It is created by [`initialise`](@ref) and passed
 as input to most model functions.
 """
-mutable struct AgricultureModel{Tcroptype,Tcropstate} <: SimulationModel
-    settings::Dict{String,Any}
+@kwdef mutable struct AgricultureModel{Tcroptype, Tcropstate} <: SimulationModel
+    settings = Dict{String, Any}()
     rng::AbstractRNG
     logger::AbstractLogger
-    dataoutputs::Dict{String,DataOutput}
+    dataoutputs = Dict{String, DataOutput}()
     date::Date
-    landscape::Matrix{Pixel}
-    weather::Dict{Date,Weather}
-    crops::Dict{String,Tcroptype}
-    farmers::Vector{Farmer}
-    farmplots::Vector{FarmPlot{Tcropstate}}
-    animals::Vector{Union{Animal,Nothing}}
-    migrants::Vector{Pair{Animal,AnnualDate}}
-    events::Vector{FarmEvent}
+    landscape = Matrix{Pixel}()
+    weather::Weather
+    crops = Dict{String, Tcroptype}()
+    farmers = Vector{Farmer}()
+    farmplots = Vector{FarmPlot{Tcropstate}}()
+    animals = Vector{Union{Animal, Nothing}}()
+    migrants = Vector{Pair{Animal, AnnualDate}}()
+    events = Vector{FarmEvent}()
 end
 
 """
@@ -36,7 +36,31 @@ Return the total number of agents in a model object.
 function nagents(model::AgricultureModel)
     length(model.animals)+length(model.farmers)+length(model.farmplots)
 end
-    
+
+windspeed(model::AgricultureModel, date::Date) = windspeed(model.weather, date)
+windspeed(model::AgricultureModel) = windspeed(model, model.date)
+
+precipitation(model::AgricultureModel, date::Date) = precipitation(model.weather, date)
+precipitation(model::AgricultureModel) = precipitation(model, model.date)
+
+sunshine(model::AgricultureModel, date::Date) = sunshine(model.weather, date)
+sunshine(model::AgricultureModel) = sunshine(model, model.date)
+
+humidity(model::AgricultureModel, date::Date) = humidity(model.weather, date)
+humidity(model::AgricultureModel) = humidity(model, model.date)
+
+meantemp(model::AgricultureModel, date::Date) = meantemp(model.weather, date)
+meantemp(model::AgricultureModel) = meantemp(model, model.date)
+
+maxtemp(model::AgricultureModel, date::Date) = maxtemp(model.weather, date)
+maxtemp(model::AgricultureModel) = maxtemp(model, model.date)
+
+mintemp(model::AgricultureModel, date::Date) = mintemp(model.weather, date)
+mintemp(model::AgricultureModel) = mintemp(model, model.date)
+
+evapotranspiration(model::AgricultureModel, date::Date) = evapotranspiration(model.weather, date)
+evapotranspiration(model::AgricultureModel) = evapotranspiration(model, model.date)
+
 """
     stepagent!(agent, model)
 
@@ -117,22 +141,14 @@ function initmodel(settings::Dict{String, Any})
                               settings["core.enddate"])
         crops, Tcroptype, Tcropstate = initcropmodel(settings["crop.cropmodel"],
                                                      settings["crop.cropdirectory"])
-        farmers = Vector{Farmer}()
-        farmplots = Vector{FarmPlot{Tcropstate}}()
-        model = AgricultureModel{Tcroptype,Tcropstate}(
+        model = AgricultureModel{Tcroptype, Tcropstate}(;
             settings,
-            StableRNG(settings["core.seed"]),
+            rng = StableRNG(settings["core.seed"]),
             logger,
-            Dict{String,DataOutput}(),
-            settings["core.startdate"],
+            date = settings["core.startdate"],
             landscape,
             weather,
-            crops,
-            farmers,
-            farmplots,
-            Vector{Union{Animal,Nothing}}(),
-            Vector{Pair{Animal, Date}}(),
-            Vector{FarmEvent}()
+            crops
         )
         saveinputfiles(model)
 
diff --git a/src/world/weather.jl b/src/world/weather.jl
index 055d2e0115629438ee8477c5d03fa0a2448bcc42..8883a8d978034c9213f4d1e799206c1b3d443c47 100644
--- a/src/world/weather.jl
+++ b/src/world/weather.jl
@@ -7,18 +7,63 @@
 """
     Weather
 
-A single weather datum, combining the observations from one day.
-"""
-struct Weather
-    windspeed::Union{Missing,Float64}
-    precipitation::Union{Missing,Float64}
-    sunshine::Union{Missing,Float64}
-    cloudcover::Union{Missing,Float64}
-    humidity::Union{Missing,Float64}
-    meantemp::Union{Missing,Float64}
-    maxtemp::Union{Missing,Float64}
-    mintemp::Union{Missing,Float64}
-    evapotranspiration::Union{Missing,Float64}
+Holds the weather information for the whole simulation period.
+"""
+@kwdef struct Weather
+    firstdate::Date
+    lastdate::Date
+    # TODO: types for some of these fields can be just `Vector{Float64}` after
+    #       fixing missing measurement values (replacing with values
+    #       approximated from previous/later measurements)
+    windspeed::Vector{Union{Missing,Float64}}
+    precipitation::Vector{Float64}
+    sunshine::Vector{Union{Missing,Float64}}
+    cloudcover::Vector{Union{Missing,Float64}}
+    humidity::Vector{Union{Missing,Float64}}
+    meantemp::Vector{Float64}
+    maxtemp::Vector{Float64}
+    mintemp::Vector{Float64}
+    evapotranspiration::Vector{Float64}
+end
+
+function Base.length(weather::Weather)
+    return Dates.value(weather.lastdate - weather.firstdate) + 1
+end
+
+function daynumber(weather::Weather, date::Date)
+    @assert weather.firstdate <= date <= weather.lastdate
+    return Dates.value(date - weather.firstdate) + 1
+end
+
+function findspans(predicate_fn, arr::AbstractVector)
+    spans = Tuple{Int, Int}[]
+    startidx = nothing
+    for i = firstindex(arr):lastindex(arr)
+        if predicate_fn(arr[i])
+            if startidx === nothing
+                startidx = i
+            end
+        elseif startidx !== nothing
+            push!(spans, (startidx, i - 1))
+            startidx = nothing
+        end
+    end
+    if startidx !== nothing
+        push!(spans, (startidx, lastindex(arr)))
+    end
+    return spans
+end
+
+function fix_missing_weatherdata!(df::DataFrame)
+    colnames_to_fix = ["min_temperature", "max_temperature", "mean_temperature",
+                       "precipitation", "potential_evapotranspiration"]
+    for colname in colnames_to_fix
+        spans = findspans(ismissing, getproperty(df, colname))
+        # TODO: fix missing values and warn here instead of error
+        if ! isempty(spans)
+            error("Column $colname has missing values: $spans")
+        end
+    end
 end
 
 """
@@ -33,97 +78,103 @@ mapped to dates.
 """
 function initweather(weatherfile::String, startdate::Date, enddate::Date)
     @debug "Initialising weather"
-    data = CSV.File(weatherfile, missingstring="NA", dateformat="yyyy-mm-dd",
-                    types=[Date, Float64, Float64, Float64, Float64, Float64,
-                           Float64, Float64, Float64, Float64])
-    weather = Dict{Date,Weather}()
-    for row in data
-        if row.date >= startdate && row.date <= enddate
-            weather[row.date] = Weather(row.mean_windspeed, row.precipitation,
-                                        row.sunshine_hours, row.mean_cloud_cover,
-                                        row.mean_humidity, row.mean_temperature,
-                                        row.max_temperature, row.min_temperature,
-                                        row.potential_evapotranspiration)
-        end
+    if startdate > enddate
+        error("Startdate is after enddate: $startdate > $enddate.")
+    end
+    Tfloat = Union{Missing, Float64}
+    df = CSV.read(weatherfile, DataFrame;
+                  missingstring="NA", dateformat="yyyy-mm-dd",
+                  types=[Date, Tfloat, Tfloat, Tfloat, Tfloat, Tfloat,
+                         Tfloat, Tfloat, Tfloat, Tfloat])
+    needed_cols = [
+        "date", "mean_windspeed", "precipitation", "sunshine_hours",
+        "mean_cloud_cover", "mean_humidity", "mean_temperature",
+        "max_temperature", "min_temperature", "potential_evapotranspiration"
+    ]
+    if ! issubset(needed_cols, names(df))
+        error("Missing columns in weather data file: $(setdiff(needed_cols, names(df)))")
     end
-    if length(weather) <= Dates.value(enddate-startdate)
-        @warn ("There are missing days in the weather input file:"
-               * " expected $(Dates.value(enddate-startdate) + 1), got $(length(weather)).")
+    filter!(r -> r.date >= startdate && r.date <= enddate, df)
+    sort!(df, :date)
+    if startdate:enddate != df.date
+        error("There are missing days (rows) in the weather data file."
+              * " expected $(Dates.value(enddate - startdate) + 1), got $(nrow(df)))."
+              * " Missing dates are $(setdiff(startdate:enddate, df.date)).")
     end
-    weather
+    fix_missing_weatherdata!(df)
+    return Weather(; firstdate = minimum(df.date),
+                   lastdate = maximum(df.date),
+                   windspeed = df.mean_windspeed,
+                   precipitation = df.precipitation,
+                   sunshine = df.sunshine_hours,
+                   cloudcover = df.mean_cloud_cover,
+                   humidity = df.mean_humidity,
+                   meantemp = df.mean_temperature,
+                   maxtemp = df.max_temperature,
+                   mintemp = df.min_temperature,
+                   evapotranspiration = df.potential_evapotranspiration)
 end
 
 """
-    windspeed(model)
+    windspeed(weather, date)
 
-Return today's average windspeed in m/s.
+Return the average windspeed in m/s on `date`.
 """
-function windspeed(model::SimulationModel)
-    model.weather[model.date].windspeed
-end
+windspeed(weather::Weather, date::Date) =
+    weather.windspeed[daynumber(weather, date)]
 
 """
-    precipitation(model)
+    precipitation(weather, date)
 
-Return today's total precipitation in mm.
+Return the total precipitation in mm on `date`.
 """
-function precipitation(model::SimulationModel)
-    model.weather[model.date].precipitation
-end
+precipitation(weather::Weather, date::Date) =
+    weather.precipitation[daynumber(weather, date)]
 
 """
-    sunshine(model)
+    sunshine(weather, date)
 
-Return today's sunshine duration in hours.
+Return the sunshine duration in hours on `date`.
 """
-function sunshine(model::SimulationModel)
-    model.weather[model.date].sunshine
-end
+sunshine(weather::Weather, date::Date) =
+    weather.sunshine[daynumber(weather, date)]
 
 """
-    humidity(model)
+    humidity(weather, date)
 
 Return today's average vapour pressure in %.
 """
-function humidity(model::SimulationModel)
-    model.weather[model.date].humidity
-end
+humidity(weather::Weather, date::Date) =
+    weather.humidity[daynumber(weather, date)]
 
 """
-    meantemp(model)
+    meantemp(weather, date)
 
-Return today's mean temperature in °C.
+Return the mean temperature in °C on `date`.
 """
-function meantemp(model::SimulationModel)
-    model.weather[model.date].meantemp
-end
+meantemp(weather::Weather, date::Date) =
+    weather.meantemp[daynumber(weather, date)]
 
 """
-    maxtemp(model)
+    maxtemp(weather, date)
 
-Return today's maximum temperature in °C.
+Return the maximum temperature in °C on `date`.
 """
-function maxtemp(model::SimulationModel)
-    model.weather[model.date].maxtemp
-end
+maxtemp(weather::Weather, date::Date) =
+    weather.maxtemp[daynumber(weather, date)]
 
 """
-    mintemp(model)
+    mintemp(weather, date)
 
-Return today's minimum temperature in °C.
+Return the minimum temperature in °C on `date`.
 """
-function mintemp(model::SimulationModel)
-    model.weather[model.date].mintemp
-end
+mintemp(weather::Weather, date::Date) =
+    weather.mintemp[daynumber(weather, date)]
 
 """
-    evapotranspiration(model)
+    evapotranspiration(weather, date)
 
-Return today's potential evapotranspiration (ETo), based on
-the
+Return today's potential evapotranspiration (ETo) on `date`.
 """
-function evapotranspiration(model::SimulationModel)
-    model.weather[model.date].evapotranspiration
-end
-
-#TODO add functions for evapotranspiration
+evapotranspiration(weather::Weather, date::Date) =
+    weather.evapotranspiration[daynumber(weather, date)]
diff --git a/test/io_tests.jl b/test/io_tests.jl
index 79d9fa103bb28e9dfb896220ef27680e325a8303..8aa70f38b4b61d5e7cd0e4a9df739541dced9e70 100644
--- a/test/io_tests.jl
+++ b/test/io_tests.jl
@@ -9,8 +9,8 @@
     @test @param(core.startdate) == Date(2022, 2, 1)
     @test @param(core.loglevel) == "warn"
     @test @param(nature.targetspecies) == ["Wolpertinger", "Wyvern"]
-    @param(core.enddate) = Date(2022,1,3)
-    @test @param(core.enddate) == Date(2022,1,3)
+    @param(core.enddate) = Date(2022,2,3)
+    @test @param(core.enddate) == Date(2022,2,3)
 end
 
 @testset "Output functions" begin
diff --git a/test/landscape_tests.jl b/test/landscape_tests.jl
index beba14f22ef5bcb8a29e754aae8f058fa66c8ccd..a500e526b5b3758a05639ecb0daba703d8e6d8e7 100644
--- a/test/landscape_tests.jl
+++ b/test/landscape_tests.jl
@@ -67,7 +67,7 @@ end
 @testset "Weather initialisation" begin
     # these tests are specific to the Jena weather file
     model = inittestmodel()
-    @test length(keys(model.weather)) == 59
+    @test length(model.weather) == 59
     @test ismissing(Ps.windspeed(model))
     @test Ps.precipitation(model) == 1.3
     @test ismissing(Ps.sunshine(model))
diff --git a/test/runtests.jl b/test/runtests.jl
index f4401eb979260e76796ca17e722146e7c8ceea80..f30aca6495bf1d9c79367aa4b7b68143a6f80a59 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -45,22 +45,14 @@ function inittestmodel(smallmap=true)
                              TESTSETTINGS["core.enddate"])
     # TODO: support other crop models besides ALMaSS
     crops = Ps.ALMaSS.readcropparameters(TESTSETTINGS["crop.cropdirectory"])
-    farmers = Vector{Farmer}()
-    farmplots = Vector{Ps.FarmPlot{Ps.ALMaSS.CropState}}()
-    model = AgricultureModel{Ps.ALMaSS.CropType,Ps.ALMaSS.CropState}(
-        TESTSETTINGS,
-        StableRNG(TESTSETTINGS["core.seed"]),
-        global_logger(),
-        Dict{String,DataOutput}(),
-        TESTSETTINGS["core.startdate"],
+    model = AgricultureModel{Ps.ALMaSS.CropType,Ps.ALMaSS.CropState}(;
+        settings = copy(TESTSETTINGS),
+        rng = StableRNG(TESTSETTINGS["core.seed"]),
+        logger = global_logger(),
+        date = TESTSETTINGS["core.startdate"],
         landscape,
         weather,
-        crops,
-        farmers,
-        farmplots,
-        Vector{Union{Animal,Nothing}}(),
-        Vector{Pair{Animal, Date}}(),
-        Vector{FarmEvent}()
+        crops
     )
     model
 end