REMORA
Regional Modeling of Oceans Refined Adaptively
Loading...
Searching...
No Matches
REMORA_NCTimeSeries.cpp
Go to the documentation of this file.
1#include "REMORA_Constants.H"
3#include "REMORA_NCFile.H"
4
5#include "AMReX_FillPatchUtil.H"
6#include "AMReX_Interpolater.H"
7#include "AMReX_ParallelDescriptor.H"
8
9#include <algorithm>
10#include <cmath>
11#include <string>
12#include <vector>
13
14#ifdef REMORA_USE_NETCDF
15/**
16 * @param[in ] a_file_names vector of file name(s) to read from
17 * @param[in ] a_field_name name of field to read in
18 * @param[in ] a_time_name name of time variable in NetCDF file
19 * @param[in ] a_domain simulation domain
20 * @param[inout] a_mf_var MultiFab of data to either store into or reference for dimensions
21 * @param[in ] a_is2d Whether the variable we're working with is 2D
22 * @param[in ] a_save_interpolated Whether the interpolated value should be saved internally
23 * @param[in ] a_time_ref remora.time_ref, the reference date of the model clock
24 */
25NCTimeSeries::NCTimeSeries (const amrex::Vector<std::string>& a_file_names, const std::string a_field_name,
26 const std::string a_time_name,
27 const amrex::Box& a_domain,
28 amrex::MultiFab* a_mf_var, bool a_is2d, bool a_save_interpolated,
29 double a_time_ref) {
30 file_names.assign(a_file_names.begin(), a_file_names.end());
35 is2d = a_is2d;
38}
39
41 // open file
42 amrex::Print() << "Loading " << field_name << " from NetCDF file(s)" << std::endl;
43
44 // The time field can have any number of names, depending on the field.
45 // If not specified in input file (time_name.empty()) then set it by default
46 if (time_name.empty())
47 {
48 if (field_name.find("wind") != std::string::npos) {
49 time_name = "wind_time";
50 } else if ((field_name.find("str") != std::string::npos) and (field_name[0] == 's')) {
51 time_name = "sms_time";
52 } else if ((field_name.find("str") != std::string::npos) and (field_name[0] == 'b')) {
53 time_name = "bms_time";
54 } else {
55 time_name = "ocean_time";
56 }
57 }
58
59 amrex::Vector<int> file_is_cycle;
60 amrex::Vector<double> file_cycle_length;
62 for (int ifile = 0; ifile < file_names.size(); ++ifile) {
63 const std::string& file_name = file_names[ifile];
64
65 // The time stamps must count days. Their reference date, if the units
66 // name one, is honored: this is the shift that puts them on the model clock.
68 true, true);
69
70 // `cycle_length` is normally numeric, so use the NCVar utility directly.
71 // By ROMS convention it is in the same units as the time variable.
72 // REMORA currently requires time units in days, so convert to seconds below.
73 bool l_is_cycle = false;
74 double l_cycle_length = 0.0;
76
79
80 if (amrex::ParallelDescriptor::IOProcessor())
81 {
82 auto time_var = ncf.var(time_name);
83
84 l_is_cycle = time_var.has_attr("cycle_length");
85
86 if (l_is_cycle) {
87 std::vector<double> cycle_attr;
88 time_var.get_attr("cycle_length", cycle_attr);
89
91 cycle_attr.size() == 1,
92 "NetCDF time variable cycle_length attribute must be scalar");
93
94 l_cycle_length = cycle_attr[0] * 60.0 * 60.0 * 24.0;
95 }
96
97 if (!ncf.has_var(field_name)) {
98 amrex::Abort("NetCDF time series variable " + field_name +
99 " not found in " + file_name);
100 }
101
102 const int field_rank = ncf.var(field_name).ndim();
103 const int expected_spatial_rank = is2d ? 3 : 4;
104 l_is_spatially_uniform = (field_rank == 1) ? 1 : 0;
105
107 amrex::Abort("Unsupported NetCDF time series rank for variable " +
108 field_name + " in " + file_name + ": rank " +
109 std::to_string(field_rank) + ". Expected rank 1 for " +
110 "uniform time-only data or rank " +
111 std::to_string(expected_spatial_rank) +
112 " for spatial data.");
113 }
114 }
115
116 ncf.close();
117
118 const int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
119 int is_cycle_int = l_is_cycle ? 1 : 0;
120 amrex::ParallelDescriptor::Bcast(&is_cycle_int, 1, ioproc);
121 amrex::ParallelDescriptor::Bcast(&l_cycle_length, 1, ioproc);
122 amrex::ParallelDescriptor::Bcast(&l_is_spatially_uniform, 1, ioproc);
123
124 file_is_cycle.push_back(is_cycle_int);
127
128 // get times and put in array
129 amrex::Vector<NDArray<double>> array_ts(1);
130 ReadNetCDFFile(file_name, {time_name}, array_ts); // filled only on proc 0
131 if (amrex::ParallelDescriptor::IOProcessor())
132 {
133 int ntimes_io = array_ts[0].get_vshape()[0];
134 for (int nt(0); nt < ntimes_io; nt++)
135 {
136 // Onto the model clock, then from days to seconds
137 ocean_times.push_back((*(array_ts[0].get_data() + nt) + offset_days) * 60.0 * 60.0 * 24.0);
138 file_for_time.push_back(ifile);
139 file_itime_offset.push_back(nt);
140 }
141 }
142 }
143
144 bool all_files_cycle_equal = std::all_of(file_is_cycle.begin(), file_is_cycle.end(),
145 [&](const auto& x) { return x == file_is_cycle.front(); });
146 bool all_files_cycle_length_equal = std::all_of(file_cycle_length.begin(), file_cycle_length.end(),
147 [&](const auto& x) { return x == file_cycle_length.front(); });
148
150 amrex::Abort("If one time series file in a set has a cycle, they all must, and cycle lengths must be equal");
151 }
152
153 // Store values to class members
156
157 // Arrays will be padded if time series file gives a cycle
158 int ntimes = (is_cycle) ? ocean_times.size() + 2 : ocean_times.size();
159 // Only do checks on IO processor since ocean_times isn't populated on other ranks yet
160 if (amrex::ParallelDescriptor::IOProcessor()) {
161 AMREX_ASSERT(std::is_sorted(ocean_times.begin(), ocean_times.end()));
162 if (ntimes <= 1) {
163 amrex::Error("Time series data must be given at at least two times");
164 }
165 }
166 int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
167 amrex::ParallelDescriptor::Bcast(&ntimes,1,ioproc);
168 if (!(amrex::ParallelDescriptor::IOProcessor())) {
169 ocean_times.resize(ntimes);
170 file_for_time.resize(ntimes);
172 } else {
173 // If we're in a cycle, hack the lists to close the loop on the IOProc rank
174 if (is_cycle) {
176 // "first time" is now at index 1 because we already added to the front.
178 file_for_time.insert(file_for_time.begin(),file_for_time[file_for_time.size()-1]);
179 file_for_time.insert(file_for_time.end(), file_for_time[1]);
182 }
183 }
184 amrex::ParallelDescriptor::Bcast(ocean_times.data(), ocean_times.size(), ioproc);
185 amrex::ParallelDescriptor::Bcast(file_for_time.data(), file_for_time.size(), ioproc);
186 amrex::ParallelDescriptor::Bcast(file_itime_offset.data(), file_itime_offset.size(), ioproc);
187
188 // Initialize MultiFabs
189 // NetCDF data is always read and temporally interpolated on level 0.
190 mf_before = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
191 mf_after = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
192 mf_interp_lev0 = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
193
194 // dummy initialization
195 i_time_before = -100;
196}
197
198/**
199 * @param time time to interpolate to
200 */
202 amrex::MultiFab* mf_lev,
203 const amrex::Vector<amrex::Geometry>& geom,
204 const amrex::Vector<amrex::IntVect>& ref_ratio) {
205
206 // Wrap into [ocean_times[0], ocean_times[0]+cycle_length], which the padded time
207 // array always covers. The phase is taken relative to the first stored time rather
208 // than to zero because a cycling file is free to carry an absolute time axis (days
209 // since some epoch) alongside its cycle length; fmod(time,cycle_length) would then
210 // land far below every time the file stores.
211 double l_time = time;
212 if (is_cycle) {
213 const double t_lo = ocean_times[0];
214 l_time = t_lo + std::fmod(time - t_lo, cycle_length);
215 if (l_time < t_lo) l_time += cycle_length;
216 }
217 // Figure out time index:
219 int i_time_new = -1;
220 for (int nt=0; nt < ocean_times.size()-1; nt++) {
221 if ((ocean_times[nt] <= l_time) and (ocean_times[nt+1] >= l_time)) {
222 i_time_new = nt;
223 break;
224 }
225 }
226 // Bracketing must succeed: falling through with the dummy index would read the
227 // time series at a negative offset. This is a runtime check, not an assert,
228 // because the failure is a mismatch between the file and the run.
229 if (i_time_new < 0) {
230 amrex::Abort("Time " + std::to_string(time) + " (mapped to " + std::to_string(l_time)
231 + ") is not spanned by the time series for '" + field_name + "', which covers ["
232 + std::to_string(ocean_times[0]) + ", "
233 + std::to_string(ocean_times[ocean_times.size()-1])
234 + "]. Extend the forcing file to cover the run, or give its time variable a"
235 + " cycle_length attribute so it repeats. The file's times were placed on the"
236 + " model clock from the reference date in their units attribute (the mapping"
237 + " is printed when the series is loaded), so also check remora.time_ref and"
238 + " remora.start_time.");
239 }
243 int i_time_after = i_time_before + 1;
244
245 if (i_time_before_old + 1 == i_time_before) {
246 // swap multifabs so we only have to read in one MultiFab
247 std::swap(mf_before, mf_after);
249 } else if (i_time_before_old != i_time_before) {
252 }
253
254 // Both differences are small, so Real holds them even when the times do not.
255 const amrex::Real dt = static_cast<amrex::Real>(time_after - time_before);
256 const amrex::Real time_since_before = static_cast<amrex::Real>(l_time - time_before);
257
258 auto nodality = mf_interp_lev0->ixType();
259
260#ifdef AMREX_USE_OMP
261#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
262#endif
263 for (amrex::MFIter mfi(*mf_interp_lev0,true); mfi.isValid(); ++mfi) {
264 // Adjust box to match ROMS grid
265 amrex::Box bx = mfi.growntilebox(amrex::IntVect(1-nodality[0],1-nodality[1],0));
266
267
268 // Temporal interpolation is done once on level 0.
269 amrex::MultiFab* mf_to_fill = mf_interp_lev0;
270 amrex::Array4<amrex::Real> to_fill = mf_to_fill->array(mfi);
271 amrex::Array4<const amrex::Real> before = mf_before->const_array(mfi);
272 amrex::Array4<const amrex::Real> after = mf_after->const_array(mfi);
273 amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
274 {
275 to_fill(i,j,k) = before(i,j,k) + time_since_before * (after(i,j,k) - before(i,j,k)) / dt;
276 });
277 }
278
279 amrex::MultiFab* mf_to_fill_lev = mf_lev;
280 if (save_interpolated) {
281 if (mf_interpolated_lev.size() <= static_cast<amrex::Long>(lev)) {
282 mf_interpolated_lev.resize(lev+1);
283 }
284 if (!mf_interpolated_lev[lev] ||
285 mf_interpolated_lev[lev]->boxArray() != mf_lev->boxArray() ||
286 mf_interpolated_lev[lev]->nGrowVect() != mf_lev->nGrowVect()) {
287 mf_interpolated_lev[lev] = std::make_unique<amrex::MultiFab>(
288 mf_lev->boxArray(), mf_lev->DistributionMap(), 1, mf_lev->nGrowVect());
289 }
291 }
292
294
295 if (lev == 0) {
296 amrex::MultiFab::Copy(*mf_to_fill_lev, *mf_interp_lev0, 0, 0, 1, mf_to_fill_lev->nGrowVect());
297 return;
298 }
299
300 amrex::PhysBCFunctNoOp null_bc_for_fill;
301 amrex::Vector<amrex::BCRec> null_dom_bcs(1);
302 for (int dir = 0; dir < AMREX_SPACEDIM; ++dir) {
303 null_dom_bcs[0].setLo(dir, amrex::BCType::int_dir);
304 null_dom_bcs[0].setHi(dir, amrex::BCType::int_dir);
305 }
306
307 amrex::Interpolater* mapper = nullptr;
308 const auto& idx_type = mf_to_fill_lev->ixType();
309 if (idx_type == amrex::IndexType(amrex::IntVect(0,0,0))) {
310 mapper = &amrex::cell_cons_interp;
311 } else {
312 mapper = &amrex::face_cons_linear_interp;
313 }
314
315 amrex::InterpFromCoarseLevel(*mf_to_fill_lev, zero, *mf_interp_lev0,
316 0, 0, 1,
317 geom[0], geom[lev],
320 mapper, null_dom_bcs, 0);
321
322 mf_to_fill_lev->FillBoundary(geom[lev].periodicity());
323}
324
325amrex::IntVect
327 const amrex::Vector<amrex::IntVect>& ref_ratio) const {
328 amrex::IntVect rr(1,1,1);
329 for (int l = 0; l < lev; ++l) {
330 rr[0] *= ref_ratio[l][0];
331 rr[1] *= ref_ratio[l][1];
332 rr[2] *= ref_ratio[l][2];
333 }
334 return rr;
335}
336
337const amrex::MultiFab*
340 AMREX_ALWAYS_ASSERT(lev < static_cast<int>(mf_interpolated_lev.size()));
342 // Under subcycling the levels reach this at different times, so a buffer another level
343 // filled is at the wrong time even though it is the right shape.
345 "NCTimeSeries '" + field_name + "' was last interpolated for level "
346 + std::to_string(last_updated_lev) + " but read for level " + std::to_string(lev)
347 + ". Call update_interpolated_to_time for this level first.");
348 return mf_interpolated_lev[lev].get();
349}
350
351/**
352 * @param[inout] mf multifab to store time step data into
353 * @param[in ] itime index of time step to read from file
354 */
355void NCTimeSeries::read_in_at_time (amrex::MultiFab* mf, int itime) {
356 const int ifile = file_for_time[itime];
357 const std::string& file_name = file_names[ifile];
359
360 amrex::Print() << "Reading in " << field_name << " at time index " << itime
361 << " from " << file_name << std::endl;
362
364 ifile < static_cast<int>(file_is_spatially_uniform.size()),
365 "NetCDF time series file layout was not initialized");
366
369 amrex::Vector<RARRAY> array_dat(1);
371
372 amrex::Real uniform_value = 0.0;
373 if (amrex::ParallelDescriptor::IOProcessor()) {
374 const std::vector<MPI_Offset> shape = array_dat[0].get_vshape();
376 shape.size() == 1 && shape[0] == 1,
377 "Uniform NetCDF time series read did not produce one value");
378 uniform_value = *(array_dat[0].get_data());
379 }
380
381 const int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
382 amrex::ParallelDescriptor::Bcast(&uniform_value, 1, ioproc);
383 mf->setVal(uniform_value);
384 return;
385 }
386
387 // This all assumes that we're on level 0 with only one boxes_at_level
388 amrex::FArrayBox NC_fab;
389 amrex::Vector<amrex::FArrayBox*> NC_fabs;
390 amrex::Vector<std::string> NC_names;
391 amrex::Vector<enum NC_Data_Dims_Type> NC_dim_types;
392
393 NC_fabs.push_back(&NC_fab);
394 NC_names.push_back(field_name);
395
396 if (is2d) {
398 } else {
400 }
401
403 NC_fabs, true, itime_offset);
404
405#ifdef _OPENMP
406#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
407#endif
408 {
409 // Don't tile this since we are operating on full FABs in this routine
410 for ( amrex::MFIter mfi(*mf, false); mfi.isValid(); ++mfi )
411 {
412 amrex::FArrayBox &fab = (*mf)[mfi];
413
414 //
415 // FArrayBox to FArrayBox copy does "copy on intersection"
416 // This only works here because we have broadcast the FArrayBox of data from the netcdf file to all ranks
417
419 } // mf
420 } // omp
421}
422#endif // REMORA_USE_NETCDF
constexpr amrex::Real zero
mf_h setVal(geomdata.ProbHi(2))
AMREX_ALWAYS_ASSERT(!NSPeriodic||!EWPeriodic)
void ReadNetCDFFile(const std::string &fname, amrex::Vector< std::string > names, amrex::Vector< NDArray< DType > > &arrays, bool one_time=false, int fill_time=0)
Read in data from netcdf file and save to data arrays.
double ReadNetCDFTimeOffsetDays(const std::string &fname, const std::string &time_name, double time_ref, bool units_required, bool announce)
Offset, in days, that puts a file's time variable onto the model clock.
static PhysBCFunctNoOp null_bc_for_fill
amrex::Vector< std::unique_ptr< amrex::MultiFab > > mf_interpolated_lev
Interpolated data on each requested AMR level if save_interpolated=true.
bool is2d
Whether the field we're reading in is 2d.
void read_in_at_time(amrex::MultiFab *mf, int itime)
Read in data from file at time index itime and fill into mf.
amrex::Vector< int > file_itime_offset
Offset to access a particular time within its file.
double time_before
Time in ocean_times immediately before the last time interpolated to.
NCTimeSeries(const amrex::Vector< std::string > &a_file_names, const std::string a_field_name, const std::string a_time_name, const amrex::Box &a_domain, amrex::MultiFab *a_mf_var, bool a_is2d, bool a_save_interpolated, double a_time_ref)
Constructor.
amrex::MultiFab * mf_interp_lev0
Multifab storing temporally interpolated data on level 0.
bool is_cycle
Whether the time series is a cycle.
amrex::Box domain
Domain.
void Initialize()
Read in time array from file and allocate data arrays.
amrex::Vector< int > file_is_spatially_uniform
Whether each file stores this field as one value per time record.
amrex::MultiFab * mf_before
Multifab to store data at time_before.
amrex::Vector< double > ocean_times
int i_time_before
Time index immediately before the last time interpolated to.
std::string field_name
Field name in netcdf file.
amrex::IntVect cumulative_ref_ratio(int lev, const amrex::Vector< amrex::IntVect > &ref_ratio) const
Build cumulative refinement ratio from level 0 to lev.
amrex::Vector< std::string > file_names
File names to read from.
amrex::MultiFab * mf_after
Multifab to store data at time_after.
double cycle_length
If a cycle, what is the cycle length?
std::string time_name
Field name for time series in netcdf file.
amrex::Vector< int > file_for_time
File index to access a particular time.
const amrex::MultiFab * get_interpolated_mf(int lev) const
Access interpolated data saved for a specific level.
void update_interpolated_to_time(double time, int lev, amrex::MultiFab *mf_lev, const amrex::Vector< amrex::Geometry > &geom, const amrex::Vector< amrex::IntVect > &ref_ratio)
Calculate interpolated values at time, in seconds on the model clock, and fill data for level lev.
amrex::MultiFab * mf_var
double time_after
Time in ocean_times immediately after the last time interpolated to.
static NCFile open(const std::string &name, const int cmode=NC_NOWRITE, MPI_Comm comm=MPI_COMM_WORLD, MPI_Info info=MPI_INFO_NULL)
Open an existing file.
NDArray is the datatype designed to hold any data, including scalars, multidimensional arrays,...