-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathio.jl
More file actions
1395 lines (1260 loc) · 46.9 KB
/
Copy pathio.jl
File metadata and controls
1395 lines (1260 loc) · 46.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Turn "a.aa.aaa" into (:a, :aa, :aaa)"""
symbols(s::AbstractString) = Tuple(Symbol(x) for x in split(s, '.'))
"Get a nested field using a tuple of Symbols"
param(obj, fields::Tuple{Vararg{Symbol}}) = foldl(getproperty, fields; init = obj)
param(obj, fields::AbstractString) = param(obj, symbols(fields))
"Extract a netCDF variable at a given time"
function get_at(
ds::CFDataset,
var::InputEntry,
metadata,
times::AbstractVector{<:TimeType},
t::TimeType,
dt::Float64,
)
# this behaves like a backward fill interpolation
i = findfirst(>=(t), times)
t < first(times) && throw(DomainError("time $t before dataset begin $(first(times))"))
i === nothing && throw(DomainError("time $t after dataset end $(last(times))"))
return get_at(ds, var, metadata, i, dt)
end
function get_at(ds::CFDataset, var::InputEntry, metadata, i::Int, dt::Float64)
data = read_standardized(ds, variable_name(var), (x = :, y = :, time = i))
apply_affine_transform!(data, var)
data_transformed = apply_unit_and_type_transform!(data, metadata; dt_val = dt)
return data_transformed
end
function get_param_res(model)
return Dict(
"atmosphere_water__precipitation_volume_flux" =>
model.routing.river_flow.boundary_conditions.reservoir.boundary_conditions.precipitation,
"land_surface_water__potential_evaporation_volume_flux" =>
model.routing.river_flow.boundary_conditions.reservoir.boundary_conditions.evaporation,
)
end
const mover_params = (
"atmosphere_water__precipitation_volume_flux",
"land_surface_water__potential_evaporation_volume_flux",
)
function routing_with_reservoirs(model)
(; config) = model
return config.model.reservoir__flag
end
function routing_with_reservoirs(model::AbstractModel{<:SedimentModel})
return false
end
"""
load_fixed_forcing!(model)
Get fixed netCDF forcing input."
"""
function load_fixed_forcing!(model)
(; reader, domain, clock) = model
(; forcing_parameters) = reader
dt = tosecond(clock.dt)
do_reservoirs = routing_with_reservoirs(model)
reverse_indices = domain.land.network.reverse_indices
if do_reservoirs
sel_reservoirs = domain.reservoir.network.indices_coverage
param_res = get_param_res(model)
end
for (par, ncvar) in forcing_parameters
if variable_name(ncvar) === nothing
val = only(ncvar.value) * only(ncvar.scale) + only(ncvar.offset)
param, metadata = get_field_in_model(model, par)
val = apply_unit_and_type_transform!(val, metadata; dt_val = dt)
param .= val
# set fixed precipitation and evaporation over the reservoirs and put these into
# the reservoir structs and set the precipitation and evaporation to 0 in the
# land model
if par in mover_params
if do_reservoirs
for (i, sel_reservoir) in enumerate(sel_reservoirs)
param[reverse_indices[sel_reservoir]] .= 0
param_res[par][i] = val
end
end
end
end
end
return nothing
end
"""
update_forcing!(model::Model)
Get dynamic netCDF input for the given time. Wflow expects `right` labeling of the forcing
time interval, e.g. daily precipitation at 01-02-2000 00:00:00 is the accumulated total
precipitation between 01-01-2000 00:00:00 and 01-02-2000 00:00:00.
"""
function update_forcing!(model)
(; clock, reader, domain, land) = model
(; dataset, dataset_times, forcing_parameters) = reader
dt = tosecond(clock.dt)
do_reservoirs = routing_with_reservoirs(model)
if do_reservoirs
sel_reservoirs = domain.reservoir.network.indices_coverage
param_res = get_param_res(model)
end
# load from netCDF into the model according to the mapping
for (par, ncvar) in forcing_parameters
# no need to update fixed values
variable_name(ncvar) === nothing && continue
time = convert(eltype(dataset_times), clock.time)
metadata = get_metadata(par, land)
if !metadata.allow_dynamic_input
error("Tried to update '$par' forcing from input, which is not allowed.")
end
data = get_at(dataset, ncvar, metadata, dataset_times, time, dt)
# calculate the mean precipitation and evaporation over reservoirs and put these
# into the reservoir structs and set the precipitation and evaporation to 0 in the
# land model
if par in mover_params
if do_reservoirs
for (i, sel_reservoir) in enumerate(sel_reservoirs)
avg = mean(data[sel_reservoir])
data[sel_reservoir] .= 0
param_res[par][i] = avg
end
end
end
sel = active_indices(domain, par)
# missing data for observed reservoir outflow is allowed at reservoir location(s)
if par == "reservoir_water__outgoing_observed_volume_flow_rate"
data_sel = nomissing(data[sel], MISSING_VALUE)
else
data_sel = data[sel]
end
if any(ismissing, data_sel)
msg = "Forcing data at $time has missing values on active model cells for $(variable_name(ncvar))"
throw(ArgumentError(msg))
end
param, _ = get_field_in_model(model, par; check_allow_dynamic_input = true)
param .= data_sel
end
return nothing
end
"""
monthday_passed(curr, avail)
Given two monthday tuples such as (12, 31) and (12, 15), return true if the first argument
falls after or on the same day as the second argument, assuming the same year. The tuples
generally come from `Dates.monthday`.
# Examples
```julia-repl
julia> monthday_passed((12, 31), (12, 15))
true
```
"""
monthday_passed(curr, avail) = (curr[1] >= avail[1]) && (curr[2] >= avail[2])
"Get dynamic and cyclic netCDF input"
function load_dynamic_input!(model)
update_forcing!(model)
if do_cyclic(model.config)
update_cyclic!(model)
end
return nothing
end
"Get cyclic netCDF input for the given time"
function update_cyclic!(model)
(; clock, reader, domain, land) = model
(; cyclic_dataset, cyclic_times, cyclic_parameters) = reader
dt = tosecond(clock.dt)
# pick up the data that is valid for the past model time step
month_day = monthday(clock.time - clock.dt)
is_first_timestep = clock.iteration == 1
for (par, ncvar) in cyclic_parameters
metadata = get_metadata(par, land)
if !metadata.allow_dynamic_input
error("Tried to update '$name' from cyclic input, which is not allowed.")
end
if is_first_timestep || (month_day in cyclic_times[par])
# time for an update of the cyclic forcing
i = findlast(t -> monthday_passed(month_day, t), cyclic_times[par])
isnothing(i) &&
error("Could not find applicable cyclic timestep for $month_day")
# load from netCDF into the model according to the mapping
data = get_at(cyclic_dataset, ncvar, metadata, i, dt)
sel = active_indices(domain, par)
# missing data for observed reservoir outflow is allowed at reservoir
# location(s)
if par == "reservoir_water__outgoing_observed_volume_flow_rate"
data_sel = nomissing(data[sel], MISSING_VALUE)
else
data_sel = data[sel]
end
if any(ismissing, data_sel)
msg = "Cyclic data at month $(month_day[1]) and day $(month_day[2]) has missing values on active model cells for $(variable_name(ncvar))"
throw(ArgumentError(msg))
end
param, _ = get_field_in_model(model, par; check_allow_dynamic_input = true)
param .= data_sel
end
end
return nothing
end
"""
NC_HANDLES::Dict{String, NCDataset{Nothing}}
For each netCDF file that will be opened for writing, store an entry in this Dict from the
absolute path of the file to the NCDataset. This allows us to close the NCDataset if we try
to create them twice in the same session, and thus providing a workaround for this issue:
https://github.com/Alexander-Barth/NCDatasets.jl/issues/106
Note that using this will prevent automatic garbage collection and thus closure of the
NCDataset.
"""
const NC_HANDLES = Dict{String, NCDataset{Nothing}}()
"Safely create a netCDF file, even if it has already been opened for creation"
function create_tracked_netcdf(path)
abs_path = abspath(path)
# close existing NCDataset if it exists
if haskey(NC_HANDLES, abs_path)
# fine if it was already closed
close(NC_HANDLES[abs_path])
end
# create directory if needed
mkpath(dirname(path))
ds = NCDataset(path, "c")
NC_HANDLES[abs_path] = ds
return ds
end
"prepare an output dataset for scalar data"
function setup_scalar_netcdf(
path,
dataset,
modelmap,
calendar,
time_units,
extra_dim,
config,
indices;
float_type = Float32,
)
(; land) = modelmap
ds = create_tracked_netcdf(path)
defDim(ds, "time", Inf) # unlimited
defVar(
ds,
"time",
Float64,
("time",);
attrib = ["units" => time_units, "calendar" => convert(String, calendar)],
)
set_extradim_netcdf(ds, extra_dim)
for scalar_variable in config.output.netcdf_scalar.variable
(; map, _location_dim, location, parameter, name) = scalar_variable
v, metadata = get_field_in_model(modelmap, parameter)
unit_str = to_string(metadata.unit; BMI_standard = true)
# Delft-FEWS requires the attribute :cf_role = "timeseries_id" when a netCDF file
# contains more than one location list
if _location_dim ∉ keys(ds.dim)
locations =
isnothing(map) ? [location] :
string.(locations_map(dataset, map, config, indices))
defVar(
ds,
_location_dim,
locations,
(_location_dim,);
attrib = ["cf_role" => "timeseries_id"],
)
end
base_dims = (_location_dim, "time")
type = eltype(v)
dims = if type <: AbstractFloat
base_dims
elseif type <: SVector
if haskey(netcdfvars, extra_dim.name)
base_dims
else
(base_dims[1], extra_dim.name, base_dims[2])
end
else
error("Unsupported output type: ", type)
end
defVar(
ds,
name,
float_type,
dims;
attrib = ["_FillValue" => float_type(NaN), "units" => unit_str],
)
end
return ds
end
"set extra dimension in output netCDF file"
function set_extradim_netcdf(
ds,
extra_dim::@NamedTuple{
name::String,
value::Vector{T},
} where {T <: Union{String, Float64}},
)
# the axis attribute `Z` is required to import this type of 3D data by Delft-FEWS the
# values of this dimension `extra_dim.value` should be of type Float64
if extra_dim.name == "layer"
attributes =
["long_name" => "layer_index", "standard_name" => "layer_index", "axis" => "Z"]
end
defVar(ds, extra_dim.name, extra_dim.value, (extra_dim.name,); attrib = attributes)
return nothing
end
set_extradim_netcdf(ds, extra_dim::@NamedTuple{}) = nothing
"prepare an output dataset for grid data"
function setup_grid_netcdf(
path,
ncx,
ncy,
parameters,
calendar,
time_units,
extra_dim,
cell_length_in_meter;
float_type = Float32,
deflatelevel = 0,
)
base_dims, attrib_x, attrib_y = if cell_length_in_meter
("x", "y", "time"),
("x coordinate of projection", "projection_x_coordinate", "m"),
("y coordinate of projection", "projection_y_coordinate", "m")
else
("lon", "lat", "time"),
("longitude", "longitude", "degrees_east"),
("latitude", "latitude", "degrees_north")
end
ds = create_tracked_netcdf(path)
defDim(ds, "time", Inf) # unlimited
defVar(
ds,
base_dims[1],
ncx,
(base_dims[1],);
attrib = [
"long_name" => attrib_x[1],
"standard_name" => attrib_x[2],
"axis" => "X",
"units" => attrib_x[3],
],
)
defVar(
ds,
base_dims[2],
ncy,
(base_dims[2],);
attrib = [
"long_name" => attrib_y[1],
"standard_name" => attrib_y[2],
"axis" => "X",
"units" => attrib_y[3],
],
)
set_extradim_netcdf(ds, extra_dim)
defVar(
ds,
"time",
Float64,
("time",);
attrib = ["units" => time_units, "calendar" => convert(String, calendar)],
deflatelevel,
)
for (name, output_data) in parameters
(; vector, unit) = output_data
unit_str = to_string(unit; BMI_standard = true)
type = eltype(vector)
dims = if type <: AbstractFloat
base_dims
elseif type <: SVector
# for SVectors an additional dimension (`extra_dim`) is required
(base_dims[1], base_dims[2], extra_dim.name, base_dims[3])
else
error("Unsupported output type: ", type)
end
defVar(
ds,
name,
float_type,
dims;
attrib = ["_FillValue" => float_type(NaN), "units" => unit_str],
)
end
return ds
end
"Add a new time to the unlimited time dimension, and return the index"
function add_time(ds, time)
i = length(ds["time"]) + 1
ds["time"][i] = time
return i
end
struct NCReader{T}
dataset::CFDataset
dataset_times::Vector{T}
cyclic_dataset::Union{NCDataset, Nothing}
cyclic_times::Dict{String, Vector{Tuple{Int, Int}}}
forcing_parameters::InputEntries
cyclic_parameters::InputEntries
end
@with_kw struct OutputData{T}
par::String
vector::AbstractVector{T}
unit::Unit
end
@with_kw struct NCWriter{
D <: Union{NCDataset, Nothing},
R <: Union{Nothing, Dict{NetCDFScalarVariable, <:Function}},
}
# Path to the NetCDF file
output_path::Union{String, Nothing} = nothing
# NetCDF dataset
output_dataset::D = nothing
# mapping of netCDF variable names to model parameters
output_map::Dict{String, OutputData} = Dict()
# The reducer associated with the output variables
reducer::R = nothing
end
@with_kw struct CSVWriter
# Path to the CSV file
output_path::Union{String, Nothing} = nothing
# File handle to CSV file
output_io::IO = IOBuffer()
# Mapping of CSV variable names to model parameters
output_map::Dict{String, OutputData} = Dict()
# The reducer associated with the output variables
reducer::OrderedDict{CSVColumn, Function} = Dict()
end
@with_kw struct Writer{DG, DS, DE, R}
# Writer for transient grid output (no reducer)
grid_writer::NCWriter{DG, Nothing}
# Writer for transient scalar output (with reducer)
scalar_writer::NCWriter{DS, Dict{NetCDFScalarVariable, R}}
# Writer for for simulation end state output (no reducer)
endstate_writer::NCWriter{DE, Nothing}
# Writer for CSV output
csv_writer::CSVWriter
# Name and values for extra dimension (to store SVectors)
extra_dim::Union{NamedTuple}
end
function NCReader(config)
path_forcing = config.input.path_forcing
abspath_forcing = input_path(config, path_forcing)
cyclic_path = input_path(config, config.input.path_static)
@info "Cyclic parameters are provided by `$cyclic_path`."
# absolute paths are not supported, see Glob.jl#2
# the path separator in a glob pattern is always /
if isabspath(path_forcing)
parts = splitpath(path_forcing)
# use the root/drive as the dir, to support * in directory names as well
glob_dir = parts[1]
glob_path = join(parts[2:end], '/')
else
tomldir = dirname(config)
glob_dir = normpath(tomldir, config.dir_input)
glob_path = replace(path_forcing, '\\' => '/')
end
@info "Forcing parameters are provided by `$abspath_forcing`."
dynamic_paths = glob(glob_path, glob_dir) # expand "data/forcing-year-*.nc"
if isempty(dynamic_paths)
error("No files found with name '$glob_path' in '$glob_dir'")
end
dataset = NCDataset(dynamic_paths; aggdim = "time", deferopen = false)
if haskey(dataset["time"].attrib, "_FillValue")
@warn "Time dimension contains `_FillValue` attribute, this is not in line with CF conventions."
nctimes = dataset["time"][:]
times_dropped = collect(skipmissing(nctimes))
# check if length has changed (missing in time dimension are not allowed), and throw
# an error if the lengths are different
if length(times_dropped) != length(nctimes)
error("Time dimension in `$abspath_forcing` contains missing values")
else
nctimes = times_dropped
nctimes_type = eltype(nctimes)
end
else
nctimes = dataset["time"][:]
nctimes_type = eltype(nctimes)
end
land_type = config.model.type == ModelType.sediment ? SoilLossModel : LandHydrologySBM
for (par, var) in config.input.forcing
ncname = variable_name(var)
variable_info(var)
metadata = get_metadata(par, land_type)
unit_str = isnothing(metadata) ? "<unit not found>" : string(metadata.unit)
@info "Set `$par [$unit_str]` using netCDF variable `$ncname` as forcing parameter."
end
# create map from internal location to netCDF variable name for cyclic parameters and
# store cyclic times for each internal location (duplicate cyclic times are possible
# this way, however it seems not worth to keep track of unique cyclic times for now
# (memory usage))
if do_cyclic(config)
cyclic_dataset = NCDataset(cyclic_path)
cyclic_times = Dict{String, Vector{Tuple{Int, Int}}}()
for (par, var) in config.input.cyclic
ncname = variable_name(var)
i = findfirst(x -> startswith(x, "time"), dimnames(cyclic_dataset[ncname]))
dimname = dimnames(cyclic_dataset[ncname])[i]
cyclic_nc_times = collect(cyclic_dataset[dimname])
cyclic_times[par] = timecycles(cyclic_nc_times)
variable_info(var)
@info "Set `$par [$(get_metadata(par).unit)]` using netCDF variable `$ncname` as cyclic parameter, with `$(length(cyclic_nc_times))` timesteps."
end
else
cyclic_dataset = nothing
cyclic_times = Dict{String, Vector{Tuple{Int, Int}}}()
end
return NCReader{nctimes_type}(
dataset,
nctimes,
cyclic_dataset,
cyclic_times,
config.input.forcing,
config.input.cyclic,
)
end
"Get a Vector of all unique location ids from a 2D map"
function locations_map(ds, mapname, config, indices)
map_2d = ncread(
ds,
config,
mapname,
Writer;
sel = indices,
metadata = ParameterMetadata(; type = Int, allow_missing = true),
)
ids = unique(skipmissing(map_2d))
return ids
end
"Get a Vector{String} of all columns names for the CSV header, except the first, time"
function csv_header(cols, dataset, config, indices)
out = String[]
for col in cols
(; header, map) = col
if !isnothing(map)
ids = locations_map(dataset, map, config, indices)
hvec = [string(header, '_', id) for id in ids]
append!(out, hvec)
else
push!(out, header)
end
end
return out
end
"""
Flatten a nested dictionary, keeping track of the full address of the keys.
Useful for converting TOML of the format:
[a.b]
field_of_b = "name_in_output"
[a.d.c]
field_of_c = "other_name_in_output"
to a non-nested format:
Dict(
symbols"a.b.field_of_b" => "name_in_output,
symbols"a.d.c.field_of_c" => "other_name_in_output,
)
"""
function flat!(d, path, el::AbstractDict)
for (k, v) in pairs(el)
flat!(d, string(path, '.', k), v)
end
return nothing
end
function flat!(d, path, el)
k = path
d[k] = el
return nothing
end
"""
ncnames(dict)
Create a flat mapping from internal parameter locations to netCDF variable names.
Ignores top level values in the Dict. This function is used to convert a TOML such as:
```toml
[output]
path = "path/to/file.nc"
[output.land]
canopystorage = "my_canopystorage"
[output.routing.river_flow]
q = "my_q"
```
To a dictionary of the flattened parameter locations and netCDF names. The top level
values are ignored since the output path is not a netCDF name.
```julia
Dict(
(:land, :canopystorage) => "my_canopystorage,
(:routing, :river_flow, :q) => "my_q,
)
```
"""
function ncnames(dict)
ncnames_dict = Dict{String, String}()
for (k, v) in dict
flat!(ncnames_dict, k, v)
end
return ncnames_dict
end
"""
out_map(output_names_dict, modelmap)
Create a Dict that maps parameter output names to arrays in the Model.
"""
function out_map(output_names_dict, modelmap)
output_map = Dict{String, Any}()
for (par, output_name) in output_names_dict
vector, metadata = get_field_in_model(modelmap, par)
if isnothing(metadata)
@warn "No metadata was found for $par, so the output will be expressed in standard SI units ($(join(STANDARD_UNITS, ", "))) and might fail."
metadata = ParameterMetadata()
end
output_map[output_name] = OutputData(; par, vector, metadata.unit)
end
return output_map
end
function get_reducer_func(col, domain, args...)
(; parameter) = col
if occursin("reservoir", parameter)
reducer_func = reducer(
col,
domain.reservoir.network.reverse_indices,
domain.land.network.indices,
args...,
)
elseif occursin("river", parameter)
reducer_func = reducer(
col,
domain.river.network.reverse_indices,
domain.land.network.indices,
args...,
)
elseif occursin("drain", parameter)
reducer_func = reducer(
col,
domain.drain.network.reverse_indices,
domain.land.network.indices,
args...,
)
else
reducer_func = reducer(
col,
domain.land.network.reverse_indices,
domain.land.network.indices,
args...,
)
end
end
function Writer(
config::Config,
modelmap::NamedTuple,
domain,
nc_static;
extra_dim::NamedTuple = NamedTuple(),
)
x_coords = read_x_axis(nc_static)
y_coords = read_y_axis(nc_static)
# create an output netCDF that will hold all timesteps of selected parameters for grid
# data
grid_writer = if do_netcdf_grid(config)
output_path_grid = output_path(config, config.output.netcdf_grid.path)
# create a flat mapping from internal parameter locations to netCDF variable names
output_ncnames = ncnames(config.output.netcdf_grid.variables)
# fill the output_map by mapping parameter netCDF names to arrays
output_map = out_map(output_ncnames, modelmap)
deflatelevel = config.output.netcdf_grid.compressionlevel
output_dataset = setup_grid_netcdf(
output_path_grid,
x_coords,
y_coords,
output_map,
config.time.calendar,
config.time.time_units,
extra_dim,
config.model.cell_length_in_meter__flag;
deflatelevel,
)
@info "Created an output netCDF file `$output_path_grid` for grid data, using compression level `$deflatelevel`."
NCWriter(; output_path = output_path_grid, output_dataset, output_map)
else
NCWriter()
end
# create a separate state output netCDF that will hold the last timestep of all states
# but only if config.state.path_output has been set
endstate_writer = if !isnothing(config.state.path_output)
output_path_endstate = output_path(config, config.state.path_output)
output_ncnames = check_states(config)
output_map = out_map(output_ncnames, modelmap)
output_dataset = setup_grid_netcdf(
output_path_endstate,
x_coords,
y_coords,
output_map,
config.time.calendar,
config.time.time_units,
extra_dim,
config.model.cell_length_in_meter__flag;
float_type = Float64,
)
@info "Created a state output netCDF file `$output_path_endstate`."
NCWriter(; output_path = output_path_endstate, output_dataset, output_map)
else
NCWriter()
end
# create an output netCDF that will hold all timesteps of selected parameters for scalar
# data, but only if config.netcdf.path and config.netcdf.variable have been set.
scalar_writer = if do_netcdf_scalar(config)
output_path_scalar = output_path(config, config.output.netcdf_scalar.path)
# get netCDF info for scalar data (variable name, locationset (dim) and
# location ids)
indices = domain.land.network.indices
output_dataset = setup_scalar_netcdf(
output_path_scalar,
nc_static,
modelmap,
config.time.calendar,
config.time.time_units,
extra_dim,
config,
indices,
)
output_ncnames =
(var.parameter => var.name for var in config.output.netcdf_scalar.variable)
output_map = out_map(output_ncnames, modelmap)
reducer = Dict(
var => get_reducer_func(var, domain, x_coords, y_coords, config, nc_static) for var in config.output.netcdf_scalar.variable
)
@info "Created an output netCDF file `$output_path_scalar` for scalar data."
NCWriter(; output_path = output_path_scalar, output_dataset, output_map, reducer)
else
NCWriter(; reducer = Dict{NetCDFScalarVariable, Function}())
end
csv_writer = if do_csv(config)
# open CSV file and write header
output_path_csv = output_path(config, config.output.csv.path)
# create directory if needed
mkpath(dirname(output_path_csv))
output_io = open(output_path_csv, "w")
# Add header
print(output_io, "time,")
indices = domain.land.network.indices
header = csv_header(config.output.csv.column, nc_static, config, indices)
println(output_io, join(header, ','))
flush(output_io)
output_csvnames =
(col.parameter => col.header for col in config.output.csv.column)
output_map = out_map(output_csvnames, modelmap)
reducer = OrderedDict(
col => get_reducer_func(col, domain, x_coords, y_coords, config, nc_static) for col in config.output.csv.column
)
@info "Created an output CSV file `$output_path_csv` for scalar data."
CSVWriter(; output_path = output_path_csv, output_io, output_map, reducer)
else
CSVWriter()
end
return Writer(; grid_writer, scalar_writer, endstate_writer, csv_writer, extra_dim)
end
"Write a new timestep with scalar data to a netCDF file"
function write_netcdf_timestep(model::AbstractModel, writer::NCWriter{<:NCDataset, <:Dict})
(; clock) = model
(; output_dataset, output_map, reducer) = writer
(; extra_dim) = model.writer
dt_val = tosecond(clock.dt)
time_index = add_time(output_dataset, clock.time)
for (var, reducer_) in reducer
(; name, layer) = var
(; vector, unit) = output_map[name]
elemtype = eltype(vector)
# could be a value, or a vector in case of map
if elemtype <: AbstractFloat
v = from_SI(reducer_(vector), unit; dt_val)
output_dataset[name][:, time_index] .= v
elseif elemtype <: SVector
# check if an extra dimension and index is specified in the TOML file
if haskey(output_dataset, extra_dim.name)
v = from_SI(reducer_(getindex.(A, layer)), unit; dt_val)
output_dataset[name][:, time_index] .= v
else
nlayer = length(first(A))
for i in 1:nlayer
v = from_SI(reducer_(getindex.(A, layer)), unit; dt_val)
output_dataset[name][:, i, time_index] .= v
end
end
else
error("Unsupported output type: ", elemtype)
end
end
return nothing
end
"Write a new timestep with grid data to a netCDF file"
function write_netcdf_timestep(model::AbstractModel, writer::NCWriter{<:NCDataset, Nothing})
(; output_dataset, output_map) = writer
(; clock, domain) = model
time_index = add_time(output_dataset, clock.time)
dt = tosecond(clock.dt)
buffer = zeros(Union{Float64, Missing}, domain.land.network.modelsize)
for (key, val) in output_map
(; par, vector, unit) = val
sel = active_indices(domain, par)
# write the active cells vector to the 2d buffer matrix
elemtype = eltype(vector)
if elemtype <: AbstractFloat
# ensure no other information is written
fill!(buffer, missing)
# cut off possible boundary conditions/ ghost points with [1:length(sel)]
buffer[sel] .= collect(vector)[1:length(sel)]
from_SI!(buffer, unit; dt_val = dt)
output_dataset[key][:, :, time_index] = buffer
elseif elemtype <: SVector
nlayer = length(first(vector))
for i in 1:nlayer
# ensure no other information is written
fill!(buffer, missing)
buffer[sel] .= getindex.(vector, i)
from_SI!(buffer, unit)
output_dataset[key][:, :, i, time_index] = buffer
end
else
error("Unsupported output type: ", elemtype)
end
end
return model
end
# don't do anything for no dataset, used if no output netCDF is needed
write_netcdf_timestep(model::AbstractModel, writer::NCWriter{Nothing}) = nothing
"Write model output"
function write_output(model)
(; writer) = model
(; grid_writer, scalar_writer, csv_writer) = writer
write_csv_row(model, csv_writer)
write_netcdf_timestep(model, grid_writer)
write_netcdf_timestep(model, scalar_writer)
return model
end
"""
timecycles(times)
Given a vector of times, return a tuple of (month, day) for each time entry, to use as a
cyclic time series that repeats every year. By using `monthday` rather than `dayofyear`,
leap year offsets are avoided.
It can generate such a series from either TimeTypes given that the year is constant, or
it will interpret integers as either months or days of year if possible.
"""
function timecycles(times)
if eltype(times) <: TimeType
# all timestamps are from the same year
year1 = year(first(times))
if !all(==(year1), year.(times))
error("unsupported cyclic timeseries")
end
# sub-daily time steps are not allowed
min_tstep = Second(minimum(diff(times)))
if min_tstep < Second(Day(1))
error("unsupported cyclic timeseries")
else
# returns a (month, day) tuple for each date
return monthday.(times)
end
elseif eltype(times) <: Integer
if length(times) == 12
months = Date(2000, 1, 1):Month(1):Date(2000, 12, 31)
return monthday.(months)
elseif length(times) == 365
days = Date(2001, 1, 1):Day(1):Date(2001, 12, 31)
return monthday.(days)
elseif length(times) == 366
days = Date(2000, 1, 1):Day(1):Date(2000, 12, 31)
return monthday.(days)
else
error("unsupported cyclic timeseries")
end
else
error("unsupported cyclic timeseries")
end
end
"Close input and output datasets that are opened on model initialization"
function close_files(model; delete_output::Bool = false)
(; reader, writer, config) = model
(; grid_writer, scalar_writer, endstate_writer, csv_writer) = writer
# Input files
close(reader.dataset)
if do_cyclic(config)
close(reader.cyclic_dataset)
end
# Output NetCDF files
for writer_ in (grid_writer, scalar_writer, endstate_writer)
(; output_dataset, output_path) = writer_
isnothing(output_dataset) && continue
close(output_dataset)
delete_output && rm(output_path)
end
# Output CSV file
close(csv_writer.output_io) # can be an IOBuffer
delete_output && rm(csv_writer.output_path)
return nothing
end
const function_map = Dict{ReducerType.T, Function}(
ReducerType.maximum => maximum,
ReducerType.minimum => minimum,
ReducerType.mean => mean,
ReducerType.median => median,
ReducerType.first => first,
ReducerType.last => last,
ReducerType.only => only,
ReducerType.sum => sum,
)
reducer_func(::Nothing) = only
reducer_func(reducer_type::ReducerType.T) = function_map[reducer_type]
"Get a reducer function based on output settings for scalar data defined in a dictionary"
function reducer(col, rev_inds, indices, x_nc, y_nc, config, dataset)::Function
(; parameter, map, reducer, index, coordinate) = col
fileformat = col isa CSVColumn ? "CSV" : "NetCDF"
f = reducer_func(reducer)
if !isnothing(map)