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<amrex::Real> 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 amrex::Real 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 = static_cast<amrex::Real>(cycle_attr[0])
97 * amrex::Real(60.0)
98 * amrex::Real(60.0)
99 * amrex::Real(24.0);
100 }
101
102 if (!ncf.has_var(field_name)) {
103 amrex::Abort("NetCDF time series variable " + field_name +
104 " not found in " + file_name);
105 }
106
107 const int field_rank = ncf.var(field_name).ndim();
108 const int expected_spatial_rank = is2d ? 3 : 4;
109 l_is_spatially_uniform = (field_rank == 1) ? 1 : 0;
110
112 amrex::Abort("Unsupported NetCDF time series rank for variable " +
113 field_name + " in " + file_name + ": rank " +
114 std::to_string(field_rank) + ". Expected rank 1 for " +
115 "uniform time-only data or rank " +
116 std::to_string(expected_spatial_rank) +
117 " for spatial data.");
118 }
119 }
120
121 ncf.close();
122
123 const int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
124 int is_cycle_int = l_is_cycle ? 1 : 0;
125 amrex::ParallelDescriptor::Bcast(&is_cycle_int, 1, ioproc);
126 amrex::ParallelDescriptor::Bcast(&l_cycle_length, 1, ioproc);
127 amrex::ParallelDescriptor::Bcast(&l_is_spatially_uniform, 1, ioproc);
128
129 file_is_cycle.push_back(is_cycle_int);
132
133 // get times and put in array
135 amrex::Vector<RARRAY> array_ts(1);
136 ReadNetCDFFile(file_name, {time_name}, array_ts); // filled only on proc 0
137 if (amrex::ParallelDescriptor::IOProcessor())
138 {
139 int ntimes_io = array_ts[0].get_vshape()[0];
140 for (int nt(0); nt < ntimes_io; nt++)
141 {
142 // Convert ocean time from days to seconds
143 ocean_times.push_back((*(array_ts[0].get_data() + nt)) * amrex::Real(60.0) * amrex::Real(60.0) * amrex::Real(24.0));
144 file_for_time.push_back(ifile);
145 file_itime_offset.push_back(nt);
146 }
147 }
148 }
149
150 bool all_files_cycle_equal = std::all_of(file_is_cycle.begin(), file_is_cycle.end(),
151 [&](const auto& x) { return x == file_is_cycle.front(); });
152 bool all_files_cycle_length_equal = std::all_of(file_cycle_length.begin(), file_cycle_length.end(),
153 [&](const auto& x) { return x == file_cycle_length.front(); });
154
156 amrex::Abort("If one time series file in a set has a cycle, they all must, and cycle lengths must be equal");
157 }
158
159 // Store values to class members
162
163 // Arrays will be padded if time series file gives a cycle
164 int ntimes = (is_cycle) ? ocean_times.size() + 2 : ocean_times.size();
165 // Only do checks on IO processor since ocean_times isn't populated on other ranks yet
166 if (amrex::ParallelDescriptor::IOProcessor()) {
167 AMREX_ASSERT(std::is_sorted(ocean_times.begin(), ocean_times.end()));
168 if (ntimes <= 1) {
169 amrex::Error("Time series data must be given at at least two times");
170 }
171 }
172 int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
173 amrex::ParallelDescriptor::Bcast(&ntimes,1,ioproc);
174 if (!(amrex::ParallelDescriptor::IOProcessor())) {
175 ocean_times.resize(ntimes);
176 file_for_time.resize(ntimes);
178 } else {
179 // If we're in a cycle, hack the lists to close the loop on the IOProc rank
180 if (is_cycle) {
182 // "first time" is now at index 1 because we already added to the front.
184 file_for_time.insert(file_for_time.begin(),file_for_time[file_for_time.size()-1]);
185 file_for_time.insert(file_for_time.end(), file_for_time[1]);
188 }
189 }
190 amrex::ParallelDescriptor::Bcast(ocean_times.data(), ocean_times.size(), ioproc);
191 amrex::ParallelDescriptor::Bcast(file_for_time.data(), file_for_time.size(), ioproc);
192 amrex::ParallelDescriptor::Bcast(file_itime_offset.data(), file_itime_offset.size(), ioproc);
193
194 // Initialize MultiFabs
195 // NetCDF data is always read and temporally interpolated on level 0.
196 mf_before = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
197 mf_after = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
198 mf_interp_lev0 = new amrex::MultiFab(mf_var->boxArray(), mf_var->DistributionMap(), 1, mf_var->nGrowVect());
199
200 // dummy initialization
201 i_time_before = -100;
202}
203
204/**
205 * @param time time to interpolate to
206 */
208 amrex::MultiFab* mf_lev,
209 const amrex::Vector<amrex::Geometry>& geom,
210 const amrex::Vector<amrex::IntVect>& ref_ratio) {
211
212 // Wrap into [ocean_times[0], ocean_times[0]+cycle_length], which the padded time
213 // array always covers. The phase is taken relative to the first stored time rather
214 // than to zero because a cycling file is free to carry an absolute time axis (days
215 // since some epoch) alongside its cycle length; fmod(time,cycle_length) would then
216 // land far below every time the file stores.
217 amrex::Real l_time = time;
218 if (is_cycle) {
219 const amrex::Real t_lo = ocean_times[0];
220 l_time = t_lo + std::fmod(time - t_lo, cycle_length);
221 if (l_time < t_lo) l_time += cycle_length;
222 }
223 // Figure out time index:
225 int i_time_new = -1;
226 for (int nt=0; nt < ocean_times.size()-1; nt++) {
227 if ((ocean_times[nt] <= l_time) and (ocean_times[nt+1] >= l_time)) {
228 i_time_new = nt;
229 break;
230 }
231 }
232 // Bracketing must succeed: falling through with the dummy index would read the
233 // time series at a negative offset. This is a runtime check, not an assert,
234 // because the failure is a mismatch between the file and the run.
235 if (i_time_new < 0) {
236 amrex::Abort("Time " + std::to_string(time) + " (mapped to " + std::to_string(l_time)
237 + ") is not spanned by the time series for '" + field_name + "', which covers ["
238 + std::to_string(ocean_times[0]) + ", "
239 + std::to_string(ocean_times[ocean_times.size()-1])
240 + "]. Extend the forcing file to cover the run, or give its time variable a"
241 + " cycle_length attribute so it repeats.");
242 }
246 int i_time_after = i_time_before + 1;
247
248 if (i_time_before_old + 1 == i_time_before) {
249 // swap multifabs so we only have to read in one MultiFab
250 std::swap(mf_before, mf_after);
252 } else if (i_time_before_old != i_time_before) {
255 }
256
257 amrex::Real dt = time_after - time_before;
258
259 auto nodality = mf_interp_lev0->ixType();
260
261#ifdef AMREX_USE_OMP
262#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
263#endif
264 for (amrex::MFIter mfi(*mf_interp_lev0,true); mfi.isValid(); ++mfi) {
265 // Adjust box to match ROMS grid
266 amrex::Box bx = mfi.growntilebox(amrex::IntVect(1-nodality[0],1-nodality[1],0));
267
268 amrex::Real time_before_copy = time_before;
269
270 // Temporal interpolation is done once on level 0.
271 amrex::MultiFab* mf_to_fill = mf_interp_lev0;
272 amrex::Array4<amrex::Real> to_fill = mf_to_fill->array(mfi);
273 amrex::Array4<const amrex::Real> before = mf_before->const_array(mfi);
274 amrex::Array4<const amrex::Real> after = mf_after->const_array(mfi);
275 amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k)
276 {
277 to_fill(i,j,k) = before(i,j,k) + (l_time - time_before_copy) * (after(i,j,k) - before(i,j,k)) / dt;
278 });
279 }
280
281 amrex::MultiFab* mf_to_fill_lev = mf_lev;
282 if (save_interpolated) {
283 if (mf_interpolated_lev.size() <= static_cast<amrex::Long>(lev)) {
284 mf_interpolated_lev.resize(lev+1);
285 }
286 if (!mf_interpolated_lev[lev] ||
287 mf_interpolated_lev[lev]->boxArray() != mf_lev->boxArray() ||
288 mf_interpolated_lev[lev]->nGrowVect() != mf_lev->nGrowVect()) {
289 mf_interpolated_lev[lev] = std::make_unique<amrex::MultiFab>(
290 mf_lev->boxArray(), mf_lev->DistributionMap(), 1, mf_lev->nGrowVect());
291 }
293 }
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*
344
345/**
346 * @param[inout] mf multifab to store time step data into
347 * @param[in ] itime index of time step to read from file
348 */
349void NCTimeSeries::read_in_at_time (amrex::MultiFab* mf, int itime) {
350 const int ifile = file_for_time[itime];
351 const std::string& file_name = file_names[ifile];
353
354 amrex::Print() << "Reading in " << field_name << " at time index " << itime
355 << " from " << file_name << std::endl;
356
358 ifile < static_cast<int>(file_is_spatially_uniform.size()),
359 "NetCDF time series file layout was not initialized");
360
363 amrex::Vector<RARRAY> array_dat(1);
365
366 amrex::Real uniform_value = 0.0;
367 if (amrex::ParallelDescriptor::IOProcessor()) {
368 const std::vector<MPI_Offset> shape = array_dat[0].get_vshape();
370 shape.size() == 1 && shape[0] == 1,
371 "Uniform NetCDF time series read did not produce one value");
372 uniform_value = *(array_dat[0].get_data());
373 }
374
375 const int ioproc = amrex::ParallelDescriptor::IOProcessorNumber();
376 amrex::ParallelDescriptor::Bcast(&uniform_value, 1, ioproc);
377 mf->setVal(uniform_value);
378 return;
379 }
380
381 // This all assumes that we're on level 0 with only one boxes_at_level
382 amrex::FArrayBox NC_fab;
383 amrex::Vector<amrex::FArrayBox*> NC_fabs;
384 amrex::Vector<std::string> NC_names;
385 amrex::Vector<enum NC_Data_Dims_Type> NC_dim_types;
386
387 NC_fabs.push_back(&NC_fab);
388 NC_names.push_back(field_name);
389
390 if (is2d) {
392 } else {
394 }
395
397 NC_fabs, true, itime_offset);
398
399#ifdef _OPENMP
400#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
401#endif
402 {
403 // Don't tile this since we are operating on full FABs in this routine
404 for ( amrex::MFIter mfi(*mf, false); mfi.isValid(); ++mfi )
405 {
406 amrex::FArrayBox &fab = (*mf)[mfi];
407
408 //
409 // FArrayBox to FArrayBox copy does "copy on intersection"
410 // This only works here because we have broadcast the FArrayBox of data from the netcdf file to all ranks
411
413 } // mf
414 } // omp
415}
416#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 update_interpolated_to_time(amrex::Real 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 and fill data for level lev.
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.
amrex::MultiFab * mf_interp_lev0
Multifab storing temporally interpolated data on level 0.
amrex::Real time_after
Time in ocean_times immediately after the last time interpolated to.
amrex::Real time_before
Time in ocean_times immediately before the last time interpolated to.
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::Real cycle_length
If a cycle, what is the cycle length?
amrex::MultiFab * mf_before
Multifab to store data at time_before.
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.
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.
amrex::Vector< amrex::Real > ocean_times
Time points in netcdf file.
amrex::MultiFab * mf_var
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,...