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 */
24NCTimeSeries::NCTimeSeries (const amrex::Vector<std::string>& a_file_names, const std::string a_field_name,
25 const std::string a_time_name,
26 const amrex::Box& a_domain,
27 amrex::MultiFab* a_mf_var, bool a_is2d, bool a_save_interpolated) {
28 file_names.assign(a_file_names.begin(), a_file_names.end());
33 is2d = a_is2d;
35}
36
38 // open file
39 amrex::Print() << "Loading " << field_name << " from NetCDF file(s)" << std::endl;
40
41 // The time field can have any number of names, depending on the field.
42 // If not specified in input file (time_name.empty()) then set it by default
43 if (time_name.empty())
44 {
45 if (field_name.find("wind") != std::string::npos) {
46 time_name = "wind_time";
47 } else if ((field_name.find("str") != std::string::npos) and (field_name[0] == 's')) {
48 time_name = "sms_time";
49 } else if ((field_name.find("str") != std::string::npos) and (field_name[0] == 'b')) {
50 time_name = "bms_time";
51 } else {
52 time_name = "ocean_time";
53 }
54 }
55
56 amrex::Vector<int> file_is_cycle;
57 amrex::Vector<double> file_cycle_length;
59 for (int ifile = 0; ifile < file_names.size(); ++ifile) {
60 const std::string& file_name = file_names[ifile];
61
62 // Check units of time stamps; should be days
63 std::string unit_str = ReadNetCDFVarAttrStr(file_name, time_name, "units"); // works on proc 0
64 if (amrex::ParallelDescriptor::IOProcessor())
65 {
66 if (unit_str.find("days") == std::string::npos) {
67 amrex::Print() << "Units of ocean_time given as: " << unit_str << std::endl;
68 amrex::Abort("Units must be in days.");
69 }
70 }
71
72 // `cycle_length` is normally numeric, so use the NCVar utility directly.
73 // By ROMS convention it is in the same units as the time variable.
74 // REMORA currently requires time units in days, so convert to seconds below.
75 bool l_is_cycle = false;
76 double l_cycle_length = 0.0;
78
81
82 if (amrex::ParallelDescriptor::IOProcessor())
83 {
84 auto time_var = ncf.var(time_name);
85
86 l_is_cycle = time_var.has_attr("cycle_length");
87
88 if (l_is_cycle) {
89 std::vector<double> cycle_attr;
90 time_var.get_attr("cycle_length", cycle_attr);
91
93 cycle_attr.size() == 1,
94 "NetCDF time variable cycle_length attribute must be scalar");
95
96 l_cycle_length = cycle_attr[0] * 60.0 * 60.0 * 24.0;
97 }
98
99 if (!ncf.has_var(field_name)) {
100 amrex::Abort("NetCDF time series variable " + field_name +
101 " not found in " + file_name);
102 }
103
104 const int field_rank = ncf.var(field_name).ndim();
105 const int expected_spatial_rank = is2d ? 3 : 4;
106 l_is_spatially_uniform = (field_rank == 1) ? 1 : 0;
107
109 amrex::Abort("Unsupported NetCDF time series rank for variable " +
110 field_name + " in " + file_name + ": rank " +
111 std::to_string(field_rank) + ". Expected rank 1 for " +
112 "uniform time-only data or rank " +
113 std::to_string(expected_spatial_rank) +
114 " for spatial data.");
115 }
116 }
117
118 ncf.close();
119
120 const int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
121 int is_cycle_int = l_is_cycle ? 1 : 0;
122 amrex::ParallelDescriptor::Bcast(&is_cycle_int, 1, ioproc);
123 amrex::ParallelDescriptor::Bcast(&l_cycle_length, 1, ioproc);
124 amrex::ParallelDescriptor::Bcast(&l_is_spatially_uniform, 1, ioproc);
125
126 file_is_cycle.push_back(is_cycle_int);
129
130 // get times and put in array
131 amrex::Vector<NDArray<double>> array_ts(1);
132 ReadNetCDFFile(file_name, {time_name}, array_ts); // filled only on proc 0
133 if (amrex::ParallelDescriptor::IOProcessor())
134 {
135 int ntimes_io = array_ts[0].get_vshape()[0];
136 for (int nt(0); nt < ntimes_io; nt++)
137 {
138 // Convert ocean time from days to seconds
139 ocean_times.push_back((*(array_ts[0].get_data() + nt)) * 60.0 * 60.0 * 24.0);
140 file_for_time.push_back(ifile);
141 file_itime_offset.push_back(nt);
142 }
143 }
144 }
145
146 bool all_files_cycle_equal = std::all_of(file_is_cycle.begin(), file_is_cycle.end(),
147 [&](const auto& x) { return x == file_is_cycle.front(); });
148 bool all_files_cycle_length_equal = std::all_of(file_cycle_length.begin(), file_cycle_length.end(),
149 [&](const auto& x) { return x == file_cycle_length.front(); });
150
152 amrex::Abort("If one time series file in a set has a cycle, they all must, and cycle lengths must be equal");
153 }
154
155 // Store values to class members
158
159 // Arrays will be padded if time series file gives a cycle
160 int ntimes = (is_cycle) ? ocean_times.size() + 2 : ocean_times.size();
161 // Only do checks on IO processor since ocean_times isn't populated on other ranks yet
162 if (amrex::ParallelDescriptor::IOProcessor()) {
163 AMREX_ASSERT(std::is_sorted(ocean_times.begin(), ocean_times.end()));
164 if (ntimes <= 1) {
165 amrex::Error("Time series data must be given at at least two times");
166 }
167 }
168 int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
169 amrex::ParallelDescriptor::Bcast(&ntimes,1,ioproc);
170 if (!(amrex::ParallelDescriptor::IOProcessor())) {
171 ocean_times.resize(ntimes);
172 file_for_time.resize(ntimes);
174 } else {
175 // If we're in a cycle, hack the lists to close the loop on the IOProc rank
176 if (is_cycle) {
178 // "first time" is now at index 1 because we already added to the front.
180 file_for_time.insert(file_for_time.begin(),file_for_time[file_for_time.size()-1]);
181 file_for_time.insert(file_for_time.end(), file_for_time[1]);
184 }
185 }
186 amrex::ParallelDescriptor::Bcast(ocean_times.data(), ocean_times.size(), ioproc);
187 amrex::ParallelDescriptor::Bcast(file_for_time.data(), file_for_time.size(), ioproc);
188 amrex::ParallelDescriptor::Bcast(file_itime_offset.data(), file_itime_offset.size(), ioproc);
189
190 // Initialize MultiFabs
191 // NetCDF data is always read and temporally interpolated on level 0.
192 mf_before = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
193 mf_after = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
194 mf_interp_lev0 = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
195
196 // dummy initialization
197 i_time_before = -100;
198}
199
200/**
201 * @param time time to interpolate to
202 */
204 amrex::MultiFab* mf_lev,
205 const amrex::Vector<amrex::Geometry>& geom,
206 const amrex::Vector<amrex::IntVect>& ref_ratio) {
207
208 // Wrap into [ocean_times[0], ocean_times[0]+cycle_length], which the padded time
209 // array always covers. The phase is taken relative to the first stored time rather
210 // than to zero because a cycling file is free to carry an absolute time axis (days
211 // since some epoch) alongside its cycle length; fmod(time,cycle_length) would then
212 // land far below every time the file stores.
213 double l_time = time;
214 if (is_cycle) {
215 const double t_lo = ocean_times[0];
216 l_time = t_lo + std::fmod(time - t_lo, cycle_length);
217 if (l_time < t_lo) l_time += cycle_length;
218 }
219 // Figure out time index:
221 int i_time_new = -1;
222 for (int nt=0; nt < ocean_times.size()-1; nt++) {
223 if ((ocean_times[nt] <= l_time) and (ocean_times[nt+1] >= l_time)) {
224 i_time_new = nt;
225 break;
226 }
227 }
228 // Bracketing must succeed: falling through with the dummy index would read the
229 // time series at a negative offset. This is a runtime check, not an assert,
230 // because the failure is a mismatch between the file and the run.
231 if (i_time_new < 0) {
232 amrex::Abort("Time " + std::to_string(time) + " (mapped to " + std::to_string(l_time)
233 + ") is not spanned by the time series for '" + field_name + "', which covers ["
234 + std::to_string(ocean_times[0]) + ", "
235 + std::to_string(ocean_times[ocean_times.size()-1])
236 + "]. Extend the forcing file to cover the run, or give its time variable a"
237 + " cycle_length attribute so it repeats.");
238 }
242 int i_time_after = i_time_before + 1;
243
244 if (i_time_before_old + 1 == i_time_before) {
245 // swap multifabs so we only have to read in one MultiFab
246 std::swap(mf_before, mf_after);
248 } else if (i_time_before_old != i_time_before) {
251 }
252
253 // Both differences are small, so Real holds them even when the times do not.
254 const amrex::Real dt = static_cast<amrex::Real>(time_after - time_before);
255 const amrex::Real time_since_before = static_cast<amrex::Real>(l_time - time_before);
256
257 auto nodality = mf_interp_lev0->ixType();
258
259#ifdef AMREX_USE_OMP
260#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
261#endif
262 for (amrex::MFIter mfi(*mf_interp_lev0,true); mfi.isValid(); ++mfi) {
263 // Adjust box to match ROMS grid
264 amrex::Box bx = mfi.growntilebox(amrex::IntVect(1-nodality[0],1-nodality[1],0));
265
266
267 // Temporal interpolation is done once on level 0.
268 amrex::MultiFab* mf_to_fill = mf_interp_lev0;
269 amrex::Array4<amrex::Real> to_fill = mf_to_fill->array(mfi);
270 amrex::Array4<const amrex::Real> before = mf_before->const_array(mfi);
271 amrex::Array4<const amrex::Real> after = mf_after->const_array(mfi);
272 amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
273 {
274 to_fill(i,j,k) = before(i,j,k) + time_since_before * (after(i,j,k) - before(i,j,k)) / dt;
275 });
276 }
277
278 amrex::MultiFab* mf_to_fill_lev = mf_lev;
279 if (save_interpolated) {
280 if (mf_interpolated_lev.size() <= static_cast<amrex::Long>(lev)) {
281 mf_interpolated_lev.resize(lev+1);
282 }
283 if (!mf_interpolated_lev[lev] ||
284 mf_interpolated_lev[lev]->boxArray() != mf_lev->boxArray() ||
285 mf_interpolated_lev[lev]->nGrowVect() != mf_lev->nGrowVect()) {
286 mf_interpolated_lev[lev] = std::make_unique<amrex::MultiFab>(
287 mf_lev->boxArray(), mf_lev->DistributionMap(), 1, mf_lev->nGrowVect());
288 }
290 }
291
293
294 if (lev == 0) {
295 amrex::MultiFab::Copy(*mf_to_fill_lev, *mf_interp_lev0, 0, 0, 1, mf_to_fill_lev->nGrowVect());
296 return;
297 }
298
299 amrex::PhysBCFunctNoOp null_bc_for_fill;
300 amrex::Vector<amrex::BCRec> null_dom_bcs(1);
301 for (int dir = 0; dir < AMREX_SPACEDIM; ++dir) {
302 null_dom_bcs[0].setLo(dir, amrex::BCType::int_dir);
303 null_dom_bcs[0].setHi(dir, amrex::BCType::int_dir);
304 }
305
306 amrex::Interpolater* mapper = nullptr;
307 const auto& idx_type = mf_to_fill_lev->ixType();
308 if (idx_type == amrex::IndexType(amrex::IntVect(0,0,0))) {
309 mapper = &amrex::cell_cons_interp;
310 } else {
311 mapper = &amrex::face_cons_linear_interp;
312 }
313
314 amrex::InterpFromCoarseLevel(*mf_to_fill_lev, zero, *mf_interp_lev0,
315 0, 0, 1,
316 geom[0], geom[lev],
319 mapper, null_dom_bcs, 0);
320
321 mf_to_fill_lev->FillBoundary(geom[lev].periodicity());
322}
323
324amrex::IntVect
326 const amrex::Vector<amrex::IntVect>& ref_ratio) const {
327 amrex::IntVect rr(1,1,1);
328 for (int l = 0; l < lev; ++l) {
329 rr[0] *= ref_ratio[l][0];
330 rr[1] *= ref_ratio[l][1];
331 rr[2] *= ref_ratio[l][2];
332 }
333 return rr;
334}
335
336const amrex::MultiFab*
339 AMREX_ALWAYS_ASSERT(lev < static_cast<int>(mf_interpolated_lev.size()));
341 // Under subcycling the levels reach this at different times, so a buffer another level
342 // filled is at the wrong time even though it is the right shape.
344 "NCTimeSeries '" + field_name + "' was last interpolated for level "
345 + std::to_string(last_updated_lev) + " but read for level " + std::to_string(lev)
346 + ". Call update_interpolated_to_time for this level first.");
347 return mf_interpolated_lev[lev].get();
348}
349
350/**
351 * @param[inout] mf multifab to store time step data into
352 * @param[in ] itime index of time step to read from file
353 */
354void NCTimeSeries::read_in_at_time (amrex::MultiFab* mf, int itime) {
355 const int ifile = file_for_time[itime];
356 const std::string& file_name = file_names[ifile];
358
359 amrex::Print() << "Reading in " << field_name << " at time index " << itime
360 << " from " << file_name << std::endl;
361
363 ifile < static_cast<int>(file_is_spatially_uniform.size()),
364 "NetCDF time series file layout was not initialized");
365
368 amrex::Vector<RARRAY> array_dat(1);
370
371 amrex::Real uniform_value = 0.0;
372 if (amrex::ParallelDescriptor::IOProcessor()) {
373 const std::vector<MPI_Offset> shape = array_dat[0].get_vshape();
375 shape.size() == 1 && shape[0] == 1,
376 "Uniform NetCDF time series read did not produce one value");
377 uniform_value = *(array_dat[0].get_data());
378 }
379
380 const int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
381 amrex::ParallelDescriptor::Bcast(&uniform_value, 1, ioproc);
382 mf->setVal(uniform_value);
383 return;
384 }
385
386 // This all assumes that we're on level 0 with only one boxes_at_level
387 amrex::FArrayBox NC_fab;
388 amrex::Vector<amrex::FArrayBox*> NC_fabs;
389 amrex::Vector<std::string> NC_names;
390 amrex::Vector<enum NC_Data_Dims_Type> NC_dim_types;
391
392 NC_fabs.push_back(&NC_fab);
393 NC_names.push_back(field_name);
394
395 if (is2d) {
397 } else {
399 }
400
402 NC_fabs, true, itime_offset);
403
404#ifdef _OPENMP
405#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
406#endif
407 {
408 // Don't tile this since we are operating on full FABs in this routine
409 for ( amrex::MFIter mfi(*mf, false); mfi.isValid(); ++mfi )
410 {
411 amrex::FArrayBox &fab = (*mf)[mfi];
412
413 //
414 // FArrayBox to FArrayBox copy does "copy on intersection"
415 // This only works here because we have broadcast the FArrayBox of data from the netcdf file to all ranks
416
418 } // mf
419 } // omp
420}
421#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.
std::string ReadNetCDFVarAttrStr(const std::string &fname, const std::string &var_name, const std::string &attr_name)
Helper function for reading a single variable attribute.
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.
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.
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)
Constructor.
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,...