REMORA
Regional Modeling of Oceans Refined Adaptively
Loading...
Searching...
No Matches
REMORA_Coupling.cpp
Go to the documentation of this file.
1#include <REMORA.H>
2
3#include <AMReX_BCRec.H>
4#include <AMReX_Box.H>
5#include <AMReX_FillPatchUtil.H>
6#include <AMReX_Geometry.H>
7#include <AMReX_Interpolater.H>
8#include <AMReX_MFIter.H>
9#include <AMReX_MultiFabUtil.H>
10#include <AMReX_iMultiFab.H>
11#include <AMReX_Print.H>
12#include <AMReX_Reduce.H>
13
14#include <limits>
15
16using namespace amrex;
17
18/*
19 Coupling reference context (implementation-side):
20
21 1) Legacy state-passing contract:
22 Warner et al. (2010), COAWST, Fig. 5 / Block B.
23 REMORA receives atmospheric states and computes surface fluxes internally
24 (bulk-physics/COARE-style path).
25
26 2) Future direct flux-passing roadmap:
27 COAWST's ATM2OCN_FLUXES pathway (documented in COAWST manuals/workshops
28 and exercised in Zambon et al., 2014) motivates direct flux exchange
29 (tau_x, tau_y, heat/moisture) instead of state-only exchange.
30 COAWST code anchors:
31 - Master/mct_roms_wrf.h
32 - ROMS/Nonlinear/atm2ocn_flux.F
33 - ROMS/Nonlinear/bulk_flux.F
34*/
35
36namespace {
37constexpr int SSTIndex = 0;
38
39struct WetExtrema
40{
41 amrex::Real min_value;
42 amrex::Real max_value;
43 amrex::Long wet_cells;
44};
45
46// Min/max of one component over cells where mask > 0.5 (valid region only).
47// Land cells hold zeros or whatever the atmosphere sent over land, so an
48// unmasked min/max says nothing about what the ocean actually receives.
49// Collective.
50WetExtrema
51WetMinMax (const amrex::MultiFab& mf, int comp, const amrex::MultiFab& mask)
52{
53 using namespace amrex;
54 constexpr Real lo_sentinel = std::numeric_limits<Real>::max();
55 constexpr Real hi_sentinel = -std::numeric_limits<Real>::max();
56
59 using Tuple = typename decltype(data)::Type;
60 for (MFIter mfi(mf); mfi.isValid(); ++mfi) {
61 Box bx = mfi.validbox();
62 bx.makeSlab(2, 0);
63 const auto f = mf.const_array(mfi, comp);
64 const auto m = mask.const_array(mfi);
65 ops.eval(bx, data, [=] AMREX_GPU_DEVICE (int i, int j, int k) -> Tuple
66 {
67 const bool wet = m(i,j,k) > Real(0.5);
68 return { wet ? f(i,j,k) : lo_sentinel,
69 wet ? f(i,j,k) : hi_sentinel,
70 wet ? Long(1) : Long(0) };
71 });
72 }
73 auto r = data.value(ops);
74 WetExtrema e{get<0>(r), get<1>(r), get<2>(r)};
75 ParallelDescriptor::ReduceRealMin(e.min_value);
76 ParallelDescriptor::ReduceRealMax(e.max_value);
77 ParallelDescriptor::ReduceLongSum(e.wet_cells);
78 return e;
79}
80
81// Count the wet cells of a 2D field outside [lo, hi], NaN included, and name one:
82// the smallest flat (i,j) index, the same cell under any decomposition. The test is
83// !(lo <= v <= hi) because every comparison with NaN is false. Collective.
84struct WetOutOfRange
85{
86 amrex::Long count;
87 amrex::Long nan_count;
88 amrex::IntVect example;
89 amrex::Real example_value;
90};
91
92WetOutOfRange
93WetCellsOutOfRange (const amrex::MultiFab& mf, const amrex::MultiFab& mask,
94 amrex::Real lo_bound, amrex::Real hi_bound)
95{
96 using namespace amrex;
97 const Box domain = mf.boxArray().minimalBox();
98 const Long nx = domain.length(0);
99 constexpr Long no_cell = std::numeric_limits<Long>::max();
100
103 using Tuple = typename decltype(data)::Type;
104 for (MFIter mfi(mf); mfi.isValid(); ++mfi) {
105 const auto f = mf.const_array(mfi);
106 const auto m = mask.const_array(mfi);
107 const auto dlo = lbound(domain);
108 ops.eval(mfi.validbox(), data, [=] AMREX_GPU_DEVICE (int i, int j, int k) -> Tuple
109 {
110 const Real v = f(i,j,k);
111 const bool wet = m(i,j,k) > Real(0.5);
112 const bool bad = wet && !(v >= lo_bound && v <= hi_bound);
113 const bool nan = wet && (v != v);
114 const Long flat = Long(j - dlo.y) * nx + Long(i - dlo.x);
115 return { bad ? Long(1) : Long(0), nan ? Long(1) : Long(0), bad ? flat : no_cell };
116 });
117 }
118 auto r = data.value(ops);
119 WetOutOfRange out{get<0>(r), get<1>(r), IntVect(0), std::numeric_limits<Real>::max()};
120 Long first = get<2>(r);
121 ParallelDescriptor::ReduceLongSum(out.count);
122 ParallelDescriptor::ReduceLongSum(out.nan_count);
123 ParallelDescriptor::ReduceLongMin(first);
124 if (out.count == 0) { return out; }
125
126 out.example = IntVect(domain.smallEnd(0) + static_cast<int>(first % nx),
127 domain.smallEnd(1) + static_cast<int>(first / nx),
128 domain.smallEnd(2));
129 const auto here = get_cell_data(mf, out.example); // empty off the owning rank
130 if (!here.empty()) { out.example_value = here[0]; }
131 ParallelDescriptor::ReduceRealMin(out.example_value);
132 return out;
133}
134
135// -------------------------------------------------------------------------
136// NEW: Conservative Sparse Matrix Remap Engine (Reverse: OCN -> ATM)
137// -------------------------------------------------------------------------
138
139// Source index region each destination box's stencils actually reference.
140//
141// The entries in index_mf are *source* indices, and for non-conformal grids they
142// routinely fall far outside the destination box's own index range, so staging the
143// source on `dst.boxArray()` plus a fixed ghost halo leaves those reads outside
144// the valid region - zero at best, out of bounds at worst. Mirror of the same
145// helper on the ERF side of the coupling.
146//
147// The result becomes a BoxArray, which must be globally consistent, hence the
148// reduction.
149amrex::BoxArray
150StagedSourceBoxArray (const amrex::MultiFab& src,
151 const amrex::MultiFab& dst,
152 const amrex::iMultiFab& index_mf,
154{
155 using namespace amrex;
156
157 const Box src_domain = src.boxArray().minimalBox();
158 const int nboxes = static_cast<int>(dst.boxArray().size());
159 constexpr int int_big = std::numeric_limits<int>::max();
160
161 Vector<int> lo(2 * nboxes, int_big);
162 Vector<int> hi(2 * nboxes, -int_big);
163
164 for (MFIter mfi(dst); mfi.isValid(); ++mfi) {
165 const int b = mfi.index();
166 const Box bx = mfi.validbox();
167 auto const& idx = index_mf.const_array(mfi);
168
171 using ReduceTuple = typename decltype(reduce_data)::Type;
172
174 [=] AMREX_GPU_DEVICE (int i, int j, int k) -> ReduceTuple
175 {
177 for (int m = 0; m < max_stencil_size; ++m) {
178 const int si = idx(i, j, k, m * 3);
179 const int sj = idx(i, j, k, m * 3 + 1);
180 if (si < 0 || sj < 0) { continue; } // -1 marks an unused slot
181 i_min = amrex::min(i_min, si); i_max = amrex::max(i_max, si);
182 j_min = amrex::min(j_min, sj); j_max = amrex::max(j_max, sj);
183 }
184 return {i_min, j_min, i_max, j_max};
185 });
186
187 auto const& hv = reduce_data.value(reduce_op);
188 lo[2*b ] = amrex::get<0>(hv); lo[2*b+1] = amrex::get<1>(hv);
189 hi[2*b ] = amrex::get<2>(hv); hi[2*b+1] = amrex::get<3>(hv);
190 }
191
192 ParallelDescriptor::ReduceIntMin(lo.dataPtr(), static_cast<int>(lo.size()));
193 ParallelDescriptor::ReduceIntMax(hi.dataPtr(), static_cast<int>(hi.size()));
194
195 // The staged boxes must carry the source's index type from the start:
196 // intersecting a cell-centered box with a staggered one trips
197 // Box::operator&='s sameType assertion.
198 const IndexType src_ixtype = src.boxArray().ixType();
199
201 for (int b = 0; b < nboxes; ++b) {
202 Box need;
203 if (lo[2*b] > hi[2*b] || lo[2*b+1] > hi[2*b+1]) {
204 // No stencil anywhere in this destination box. A BoxArray cannot hold
205 // an empty box, so stage a single cell; nothing reads it.
206 need = Box(src_domain.smallEnd(), src_domain.smallEnd(), src_ixtype);
207 } else {
208 need = Box(IntVect(lo[2*b], lo[2*b+1], src_domain.smallEnd(2)),
209 IntVect(hi[2*b], hi[2*b+1], src_domain.bigEnd(2)),
210 src_ixtype);
211 need &= src_domain;
212 }
213 bl.push_back(need);
214 }
215 return BoxArray(std::move(bl));
216}
217
218// fallback_val fills destination cells that no source cell overlaps.
219//
220// Zero is the wrong default for a physical field: the receiving model cannot tell
221// "no data here" from "the ocean says zero", and for an intensive quantity zero is
222// not merely inaccurate, it drives the receiver's flux formulas outside their valid
223// domain. This mirrors the rationale on the atmosphere->ocean twin at
224// ERF/Source/Coupling/ERF_to_REMORA.cpp:122.
225//
226// The ocean->atmosphere SST lane deliberately leaves this at zero and relies on the
227// per-cell coverage flag instead: where REMORA does not cover an ERF cell, ERF keeps
228// its own wrflowinp SST rather than consuming a value invented here. That is a
229// withholding contract, not a value, so no climatological constant belongs at that
230// call site. The parameter exists because the mask blend below needs it and because
231// other lanes have a meaningful value to pass.
232void
233ApplyConservativeRemap (const amrex::MultiFab& src,
234 amrex::MultiFab& dst,
235 const amrex::MultiFab& weight_mf,
236 const amrex::iMultiFab& index_mf,
238 const amrex::MultiFab* dst_mask = nullptr,
239 const amrex::iMultiFab* dst_land_mask = nullptr,
240 amrex::Real fallback_val = amrex::Real(0.0))
241{
242 using namespace amrex;
243
244 // 1. Data Routing: Route REMORA SST data onto the ERF atmospheric layout,
245 // staging exactly the source region the stencils reference.
247 dst.DistributionMap(), src.nComp(), 0);
248 src_on_dst.setVal(0.0);
249 src_on_dst.ParallelCopy(src);
250
251 dst.setVal(0.0);
252
253 // 2. Stencil Application: Execute the local sparse dot product
254 for (MFIter mfi(dst, TilingIfNotGPU()); mfi.isValid(); ++mfi) {
255 Box bx = mfi.tilebox();
256
257 auto const& w_arr = weight_mf.const_array(mfi);
258 auto const& idx_arr = index_mf.const_array(mfi);
259 auto const& src_arr = src_on_dst.const_array(mfi);
260 auto dst_arr = dst.array(mfi);
261 const bool has_mask = (dst_mask != nullptr);
262 auto const& mask_arr = has_mask ? dst_mask->const_array(mfi) : Array4<const Real>{};
263 // ERF land mask: 0 = water, anything non-zero is land. Destination cells
264 // over land are zeroed out (wet/dry masking only, no vector rotation).
265 const bool has_land_mask = (dst_land_mask != nullptr);
266 auto const& land_arr = has_land_mask ? dst_land_mask->const_array(mfi) : Array4<const int>{};
267
268 ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) {
269 // No stencil entry at all means no source cell overlapped this
270 // destination cell. Hand back the fallback rather than an accumulated
271 // zero, and do not apply the masks to it: a masked-out cell still gets
272 // evaluated by the receiving model's flux formulas before the mask is
273 // applied, so it too must hold an admissible value. Mirrors
274 // ERF/Source/Coupling/ERF_to_REMORA.cpp:174.
275 if (idx_arr(i, j, k, 0) < 0) {
276 dst_arr(i, j, k) = fallback_val;
277 return;
278 }
279
280 Real sum = 0.0;
281 for (int m = 0; m < max_stencil_size; ++m) {
282 Real w = w_arr(i, j, k, m);
283 if (w > 0.0) {
284 int src_i = idx_arr(i, j, k, m * 3);
285 int src_j = idx_arr(i, j, k, m * 3 + 1);
286 int src_k = idx_arr(i, j, k, m * 3 + 2);
287
288 sum += w * src_arr(src_i, src_j, src_k);
289 }
290 }
291 // Blend toward the fallback rather than multiplying by the mask.
292 // Multiplying drives partially masked cells toward exactly zero, which
293 // is right for a flux lane (fallback_val = 0, so this reduces to
294 // sum *= mask and is bit-identical) but destructive for an intensive
295 // state lane such as SST, where it scales a temperature toward 0 K.
296 // Mirrors ERF/Source/Coupling/ERF_to_REMORA.cpp:202.
297 if (has_mask) {
298 const Real mask = mask_arr(i, j, k);
299 sum = mask * sum + (Real(1.0) - mask) * fallback_val;
300 }
301 // Test against zero, not against 1: ERF stamps lmask = 2 for cells
302 // under ImmersedForcing buildings (ERF/Source/ERF_MakeNewArrays.cpp:890),
303 // so an == 1 test reads every building as water and hands it a remapped
304 // SST. The driver's atmosphere->ocean side already uses the tolerant
305 // form (ERFRemoraMultiBlockContainer.cpp:1767), so == 1 also made the
306 // two directions disagree about the same cell within one timestep.
307 if (has_land_mask && land_arr(i, j, k) != 0) { sum = Real(0.0); }
308 dst_arr(i, j, k) = sum;
309 });
310 }
311}
312}
313
314amrex::Real
315REMORA::EvolveOneStep (amrex::Real /*time*/, amrex::Real /*dt_request*/)
316{
317 Real cur_time = t_new[0];
318 const int step = istep[0];
319
321 return zero;
322 }
323
324 ComputeDt();
325
326 int lev = 0;
327 int iteration = 1;
328 // Must match Evolve's choice: the subcycle-only paths are gated on do_substep, not on
329 // which driver ran, and timeStepML does not register the mass flux they read.
330 if (max_level == 0 || do_substep) {
332 } else {
334 }
335
336 cur_time += dt[0];
337
339
341
342 return dt[0];
343}
344
345void
354
355void
357{
358 // "constant" is any non-computed type: it makes setup_step pass
359 // vec_longwave_down to bulk_fluxes instead of leaving lw_ptr null. The two
360 // flags below are what ReadParameters would have derived for that type.
364}
365
366void
368 std::array<bool, AtmosState::NumTypes>& deck_configured) const
369{
370 // AtmosState and BulkFlux agree on lanes 0-8 today, but they are separate
371 // enums of separate lengths (BulkFlux carries EminusP, which has no driver
372 // lane). Map explicitly, so inserting into either breaks the build here
373 // rather than silently transposing the answer.
374 static constexpr std::array<int, AtmosState::NumTypes> lane_to_bulk_flux {{
375 BulkFlux::Uwind, // AtmosState::Uwind
376 BulkFlux::Vwind, // AtmosState::Vwind
377 BulkFlux::Pair, // AtmosState::Pair
378 BulkFlux::Qair, // AtmosState::Qair
379 BulkFlux::Tair, // AtmosState::Tair
380 BulkFlux::Cloud, // AtmosState::Cloud
381 BulkFlux::Rain, // AtmosState::Rain
382 BulkFlux::SWrad, // AtmosState::SWrad
383 BulkFlux::LWrad // AtmosState::LWrad
384 }};
385
386 deck_configured.fill(false);
387 for (int lane = 0; lane < AtmosState::NumTypes; ++lane) {
388 const int idx = lane_to_bulk_flux[lane];
391 }
392}
393
394void
399
400void
402 amrex::DistributionMapping& dm) const
403{
405 !vec_srflx.empty() && vec_srflx[0] != nullptr,
406 "REMORA::GetAtmosToOceanRhoLayout requires post-InitData rho-point forcing storage.");
407 ba = vec_srflx[0]->boxArray();
408 dm = vec_srflx[0]->DistributionMap();
409}
410
411void
413 amrex::DistributionMapping& dm) const
414{
416 !vec_sustr.empty() && vec_sustr[0] != nullptr,
417 "REMORA::GetAtmosToOceanUFaceLayout requires post-InitData u-face forcing storage.");
418 ba = vec_sustr[0]->boxArray();
419 dm = vec_sustr[0]->DistributionMap();
420}
421
422void
424 amrex::DistributionMapping& dm) const
425{
427 !vec_svstr.empty() && vec_svstr[0] != nullptr,
428 "REMORA::GetAtmosToOceanVFaceLayout requires post-InitData v-face forcing storage.");
429 ba = vec_svstr[0]->boxArray();
430 dm = vec_svstr[0]->DistributionMap();
431}
432
433void
435 const amrex::MultiFab*& y_psi) const
436{
437 if (vec_xp.empty() || vec_yp.empty() ||
438 vec_xp[0] == nullptr || vec_yp[0] == nullptr) {
439 x_psi = nullptr;
440 y_psi = nullptr;
441 return;
442 }
443 x_psi = vec_xp[0].get();
444 y_psi = vec_yp[0].get();
445}
446
447void
449 const amrex::MultiFab*& lat_psi) const
450{
451 if (vec_lonp.empty() || vec_latp.empty() ||
452 vec_lonp[0] == nullptr || vec_latp[0] == nullptr) {
453 lon_psi = nullptr;
454 lat_psi = nullptr;
455 return;
456 }
457 lon_psi = vec_lonp[0].get();
458 lat_psi = vec_latp[0].get();
459}
460
461void
462REMORA::GetLandSeaMasks (const amrex::MultiFab*& mskr,
463 const amrex::MultiFab*& msku,
464 const amrex::MultiFab*& mskv) const
465{
466 mskr = (!vec_mskr.empty() && vec_mskr[0]) ? vec_mskr[0].get() : nullptr;
467 msku = (!vec_msku.empty() && vec_msku[0]) ? vec_msku[0].get() : nullptr;
468 mskv = (!vec_mskv.empty() && vec_mskv[0]) ? vec_mskv[0].get() : nullptr;
469}
470
471/*
472 * \brief Extracts SST from the 3D conservative state for the atmospheric driver.
473 *
474 * Reads Temp_comp at the top water-column cell (k_sfc), converts from
475 * Celsius to Kelvin, and conservatively remaps the result into state[SSTIndex].
476 */
477void
479 Real /*time*/,
480 const amrex::MultiFab* weight_o2a_mf,
481 const amrex::iMultiFab* index_o2a_mf,
483 const amrex::iMultiFab* dst_land_mask)
484{
485 if (state.empty() || state[SSTIndex] == nullptr) { return; }
486 const int lev = 0;
487
488 // REMORA stores temperature in Celsius. Surface is at k=N (top of water column).
489 const int k_sfc = cons_new[lev]->boxArray().minimalBox().bigEnd(2);
490
491 // Build a temp MultiFab on REMORA's ba2d (k=0) derived from cons_new's BoxArray.
492 BoxList bl2d = cons_new[lev]->boxArray().boxList();
493 for (auto& b : bl2d) { b.setRange(2, 0); }
494 BoxArray ba2d(std::move(bl2d));
495 MultiFab tmp(ba2d, cons_new[lev]->DistributionMap(), 1, 0);
496
497 for (MFIter mfi(*cons_new[lev]); mfi.isValid(); ++mfi) {
498 auto const& c = cons_new[lev]->const_array(mfi);
499 auto t = tmp.array(mfi);
500 Box bx = makeSlab(mfi.validbox(), 2, k_sfc);
501 ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int) {
502 // Write to k=0 in tmp (ba2d range); convert Celsius → Kelvin.
503 t(i, j, 0) = c(i, j, k_sfc, Temp_comp) + Real(273.15);
504 });
505 }
506
507 // Surface temperature sanity check on REMORA's own wet cells, in the SST that
508 // is about to be sent to the atmosphere. Checked every exchange (2D, cheap);
509 // warned the first time and again only when it gets worse. remora.v >= 1 also
510 // prints the wet min/max every exchange. verbose and the reduced counts are
511 // rank-uniform, so every rank takes the same branches.
512 if (!vec_mskr.empty() && vec_mskr[lev] != nullptr) {
513 MultiFab wet(ba2d, cons_new[lev]->DistributionMap(), 1, 0);
514 wet.setVal(zero);
515 wet.ParallelCopy(*vec_mskr[lev], 0, 0, 1);
516
517 constexpr Real t_lo = Real(273.15 - 2.5); // below seawater freezing
518 constexpr Real t_hi = Real(273.15 + 40.0);
519 const WetExtrema e = WetMinMax(tmp, 0, wet);
520 const WetOutOfRange bad = WetCellsOutOfRange(tmp, wet, t_lo, t_hi);
521 if (verbose >= 1) {
522 amrex::Print() << "REMORA surface temperature over " << e.wet_cells
523 << " wet cells: min/max = " << e.min_value << " / "
524 << e.max_value << " K\n";
525 }
526 const bool worse = e.min_value < m_warned_surface_temp_min ||
527 e.max_value > m_warned_surface_temp_max ||
528 bad.nan_count > m_warned_surface_temp_nan;
529 if (bad.count > 0 && worse) {
533 amrex::Print() << "WARNING: REMORA surface temperature outside [" << t_lo << ", "
534 << t_hi << "] K on " << bad.count << " of " << e.wet_cells
535 << " wet cells (" << bad.nan_count << " NaN; min/max " << e.min_value
536 << " / " << e.max_value << " K; e.g. REMORA (" << bad.example[0]
537 << "," << bad.example[1] << ") = " << bad.example_value
538 << " K). Repeated only if it gets worse.\n";
539 }
540 }
541
542 MultiFab& dst = *state[SSTIndex];
543
544 if (weight_o2a_mf != nullptr && index_o2a_mf != nullptr) {
545 // Execute sparse conservative remap from REMORA SST to ERF layout
547 nullptr, dst_land_mask);
548 } else {
549 // Fallback for un-stenciled or synthetic runs
550 dst.setVal(zero);
551 dst.ParallelCopy(tmp, 0, 0, 1);
552 }
553}
554
555/*
556 * \brief Receives atmospheric states from the driver and applies unit conversions.
557 */
558void
560{
564 if (finest_level < 0) { return; }
565
566 // Wind (m/s) — no unit conversion
567 if (vec_uwind[0] != nullptr) {
568 if (states.size() > AtmosState::Uwind && states[AtmosState::Uwind] != nullptr) {
569 vec_uwind[0]->ParallelCopy(*states[AtmosState::Uwind], 0, 0, 1);
570 vec_uwind[0]->FillBoundary(geom[0].periodicity());
572 }
573 }
574 if (vec_vwind[0] != nullptr) {
575 if (states.size() > AtmosState::Vwind && states[AtmosState::Vwind] != nullptr) {
576 vec_vwind[0]->ParallelCopy(*states[AtmosState::Vwind], 0, 0, 1);
577 vec_vwind[0]->FillBoundary(geom[0].periodicity());
579 }
580 }
581
582 // Atmospheric pressure: Pa → mb (REMORA bulk flux expects mb)
583 if (vec_Pair[0] != nullptr) {
584 if (states.size() > AtmosState::Pair && states[AtmosState::Pair] != nullptr) {
585 vec_Pair[0]->ParallelCopy(*states[AtmosState::Pair], 0, 0, 1);
586 vec_Pair[0]->mult(Real(0.01), 0, 1);
587 vec_Pair[0]->FillBoundary(geom[0].periodicity());
589 }
590 }
591
592 // Specific humidity (kg/kg) — no conversion
593 if (vec_qair[0] != nullptr) {
594 if (states.size() > AtmosState::Qair && states[AtmosState::Qair] != nullptr) {
595 vec_qair[0]->ParallelCopy(*states[AtmosState::Qair], 0, 0, 1);
596 vec_qair[0]->FillBoundary(geom[0].periodicity());
598 }
599 }
600
601 // Air temperature: K → °C (REMORA stores/uses Celsius internally)
602 if (vec_Tair[0] != nullptr) {
603 if (states.size() > AtmosState::Tair && states[AtmosState::Tair] != nullptr) {
604 vec_Tair[0]->ParallelCopy(*states[AtmosState::Tair], 0, 0, 1);
605 vec_Tair[0]->plus(Real(-273.15), 0, 1);
606 vec_Tair[0]->FillBoundary(geom[0].periodicity());
608 }
609 }
610
611 // Cloud fraction [0-1], rain, SW/LW radiation — no unit conversion
612 if (vec_cloud[0] != nullptr) {
613 if (states.size() > AtmosState::Cloud && states[AtmosState::Cloud] != nullptr) {
614 vec_cloud[0]->ParallelCopy(*states[AtmosState::Cloud], 0, 0, 1);
615 vec_cloud[0]->FillBoundary(geom[0].periodicity());
617 }
618 }
619 if (vec_rain[0] != nullptr) {
620 if (states.size() > AtmosState::Rain && states[AtmosState::Rain] != nullptr) {
621 vec_rain[0]->ParallelCopy(*states[AtmosState::Rain], 0, 0, 1);
622 vec_rain[0]->FillBoundary(geom[0].periodicity());
624 }
625 }
626 if (vec_srflx[0] != nullptr) {
627 if (states.size() > AtmosState::SWrad && states[AtmosState::SWrad] != nullptr) {
628 vec_srflx[0]->ParallelCopy(*states[AtmosState::SWrad], 0, 0, 1);
629 vec_srflx[0]->FillBoundary(geom[0].periodicity());
631 }
632 }
633 if (vec_longwave_down[0] != nullptr) {
634 if (states.size() > AtmosState::LWrad && states[AtmosState::LWrad] != nullptr) {
635 vec_longwave_down[0]->ParallelCopy(*states[AtmosState::LWrad], 0, 0, 1);
636 vec_longwave_down[0]->FillBoundary(geom[0].periodicity());
638 }
639 }
640
641}
642
643void
645{
649 if (finest_level < 0) { return; }
650
651 if (states.size() <= AtmosFluxes::Evap ||
652 states[AtmosFluxes::TauX] == nullptr ||
653 states[AtmosFluxes::TauY] == nullptr ||
654 states[AtmosFluxes::SHflux] == nullptr ||
655 states[AtmosFluxes::LHflux] == nullptr ||
656 states[AtmosFluxes::SWrad] == nullptr ||
657 states[AtmosFluxes::LWrad] == nullptr ||
658 states[AtmosFluxes::Rain] == nullptr ||
659 states[AtmosFluxes::Evap] == nullptr ||
660 vec_sustr[0] == nullptr || vec_svstr[0] == nullptr ||
661 vec_stflux[0] == nullptr || vec_mskr[0] == nullptr ||
662 vec_msku[0] == nullptr || vec_mskv[0] == nullptr ||
663 vec_srflx[0] == nullptr || vec_lrflx[0] == nullptr ||
664 vec_lhflx[0] == nullptr || vec_shflx[0] == nullptr ||
665 vec_rain[0] == nullptr || vec_evap[0] == nullptr) {
666 return;
667 }
668
669 const Real Hscale2 = one / (solverChoice.rho0 * Cp);
670 const Real rho0 = solverChoice.rho0;
671
672 MultiFab tau_x_tmp(vec_sustr[0]->boxArray(), vec_sustr[0]->DistributionMap(), 1,
673 vec_sustr[0]->nGrowVect());
674 MultiFab tau_y_tmp(vec_svstr[0]->boxArray(), vec_svstr[0]->DistributionMap(), 1,
675 vec_svstr[0]->nGrowVect());
676 MultiFab shflux_tmp(vec_shflx[0]->boxArray(), vec_shflx[0]->DistributionMap(), 1,
677 vec_shflx[0]->nGrowVect());
678 MultiFab lhflux_tmp(vec_lhflx[0]->boxArray(), vec_lhflx[0]->DistributionMap(), 1,
679 vec_lhflx[0]->nGrowVect());
680 MultiFab lwflux_tmp(vec_lrflx[0]->boxArray(), vec_lrflx[0]->DistributionMap(), 1,
681 vec_lrflx[0]->nGrowVect());
682
683 tau_x_tmp.setVal(zero);
684 tau_x_tmp.ParallelCopy(*states[AtmosFluxes::TauX], 0, 0, 1);
685 tau_x_tmp.FillBoundary(geom[0].periodicity());
686
687 tau_y_tmp.setVal(zero);
688 tau_y_tmp.ParallelCopy(*states[AtmosFluxes::TauY], 0, 0, 1);
689 tau_y_tmp.FillBoundary(geom[0].periodicity());
690
691 shflux_tmp.setVal(zero);
692 shflux_tmp.ParallelCopy(*states[AtmosFluxes::SHflux], 0, 0, 1);
693 shflux_tmp.FillBoundary(geom[0].periodicity());
694
695 lhflux_tmp.setVal(zero);
696 lhflux_tmp.ParallelCopy(*states[AtmosFluxes::LHflux], 0, 0, 1);
697 lhflux_tmp.FillBoundary(geom[0].periodicity());
698
699 lwflux_tmp.setVal(zero);
700 lwflux_tmp.ParallelCopy(*states[AtmosFluxes::LWrad], 0, 0, 1);
701 lwflux_tmp.FillBoundary(geom[0].periodicity());
702
703 vec_srflx[0]->setVal(zero);
704 vec_srflx[0]->ParallelCopy(*states[AtmosFluxes::SWrad], 0, 0, 1);
705 vec_srflx[0]->FillBoundary(geom[0].periodicity());
706
707 vec_rain[0]->setVal(zero);
708 vec_rain[0]->ParallelCopy(*states[AtmosFluxes::Rain], 0, 0, 1);
709 vec_rain[0]->FillBoundary(geom[0].periodicity());
710
711 vec_evap[0]->setVal(zero);
712 vec_evap[0]->ParallelCopy(*states[AtmosFluxes::Evap], 0, 0, 1);
713 vec_evap[0]->FillBoundary(geom[0].periodicity());
714
715 vec_lrflx[0]->setVal(zero);
716 vec_lhflx[0]->setVal(zero);
717 vec_shflx[0]->setVal(zero);
718 vec_stflux[0]->setVal(zero);
719
720 for (MFIter mfi(*vec_sustr[0], TilingIfNotGPU()); mfi.isValid(); ++mfi) {
721 Array4<Real> const& sustr = vec_sustr[0]->array(mfi);
722 Array4<const Real> const& msku = vec_msku[0]->const_array(mfi);
723 Array4<const Real> const& tau_x = tau_x_tmp.const_array(mfi);
724 Box ubx = mfi.grownnodaltilebox(0, IntVect(NGROW,NGROW,0));
725 Box ubxD = ubx;
726 ubxD.makeSlab(2,0);
727 ParallelFor(ubxD, [=] AMREX_GPU_DEVICE (int i, int j, int ) {
728 sustr(i,j,0) = -tau_x(i,j,0) / rho0 * msku(i,j,0);
729 });
730 }
731
732 for (MFIter mfi(*vec_svstr[0], TilingIfNotGPU()); mfi.isValid(); ++mfi) {
733 Array4<Real> const& svstr = vec_svstr[0]->array(mfi);
734 Array4<const Real> const& mskv = vec_mskv[0]->const_array(mfi);
735 Array4<const Real> const& tau_y = tau_y_tmp.const_array(mfi);
736 Box vbx = mfi.grownnodaltilebox(1, IntVect(NGROW,NGROW,0));
737 Box vbxD = vbx;
738 vbxD.makeSlab(2,0);
739
740 ParallelFor(vbxD, [=] AMREX_GPU_DEVICE (int i, int j, int ) {
741 svstr(i,j,0) = -tau_y(i,j,0) / rho0 * mskv(i,j,0);
742 });
743 }
744
745 for (MFIter mfi(*vec_stflux[0], TilingIfNotGPU()); mfi.isValid(); ++mfi) {
746 Array4<Real> const& stflux = vec_stflux[0]->array(mfi);
747 Array4<Real> const& lrflx = vec_lrflx[0]->array(mfi);
748 Array4<Real> const& lhflx = vec_lhflx[0]->array(mfi);
749 Array4<Real> const& shflx = vec_shflx[0]->array(mfi);
750 Array4<const Real> const& mskr = vec_mskr[0]->const_array(mfi);
751 Array4<const Real> const& srflx = vec_srflx[0]->const_array(mfi);
752 Array4<const Real> const& rain = vec_rain[0]->const_array(mfi);
753 Array4<const Real> const& evap = vec_evap[0]->const_array(mfi);
754 Array4<const Real> const& shflux = shflux_tmp.const_array(mfi);
755 Array4<const Real> const& lhflux = lhflux_tmp.const_array(mfi);
756 Array4<const Real> const& lwflux = lwflux_tmp.const_array(mfi);
757
758 Box gbx2 = mfi.growntilebox(IntVect(NGROW,NGROW,0));
759 Box gbx2D = gbx2;
760 gbx2D.makeSlab(2,0);
761
762 ParallelFor(gbx2D, [=] AMREX_GPU_DEVICE (int i, int j, int ) {
763 lrflx(i,j,0) = lwflux(i,j,0) * Hscale2;
764 lhflx(i,j,0) = -lhflux(i,j,0) * Hscale2;
765 shflx(i,j,0) = -shflux(i,j,0) * Hscale2;
766 stflux(i,j,0,Temp_comp) =
767 (srflx(i,j,0) * Hscale2 + lrflx(i,j,0) + lhflx(i,j,0) + shflx(i,j,0))
768 * mskr(i,j,0);
769 stflux(i,j,0,Salt_comp) =
770 mskr(i,j,0) * (evap(i,j,0) - rain(i,j,0)) / rhow;
771 });
772 }
773
774 vec_sustr[0]->FillBoundary(geom[0].periodicity());
775 vec_svstr[0]->FillBoundary(geom[0].periodicity());
776 vec_srflx[0]->FillBoundary(geom[0].periodicity());
777 vec_lrflx[0]->FillBoundary(geom[0].periodicity());
778 vec_lhflx[0]->FillBoundary(geom[0].periodicity());
779 vec_shflx[0]->FillBoundary(geom[0].periodicity());
780 vec_stflux[0]->FillBoundary(geom[0].periodicity());
781 vec_rain[0]->FillBoundary(geom[0].periodicity());
782 vec_evap[0]->FillBoundary(geom[0].periodicity());
783 vec_stflux[0]->FillBoundary(geom[0].periodicity());
784
785 // Once per run, or every exchange at remora.v >= 1. verbose is a ParmParse
786 // value, so every rank takes the same branch around the collectives.
787 if (m_reported_atm_flux_validation && verbose < 1) { return; }
789
790 struct Row { const char* name; const MultiFab* mf; int comp; const MultiFab* mask; };
791 const Row rows[] = {
792 {"sustr", vec_sustr[0].get(), 0, vec_msku[0].get()},
793 {"svstr", vec_svstr[0].get(), 0, vec_mskv[0].get()},
794 {"stflux(Temp)", vec_stflux[0].get(), Temp_comp, vec_mskr[0].get()},
795 {"stflux(Salt)", vec_stflux[0].get(), Salt_comp, vec_mskr[0].get()},
796 {"srflx", vec_srflx[0].get(), 0, vec_mskr[0].get()},
797 {"lrflx", vec_lrflx[0].get(), 0, vec_mskr[0].get()},
798 {"lhflx", vec_lhflx[0].get(), 0, vec_mskr[0].get()},
799 {"shflx", vec_shflx[0].get(), 0, vec_mskr[0].get()},
800 };
801 amrex::Print() << "REMORA ApplyAtmosphericFluxes validation (wet cells only; srflx W/m2, "
802 << "others kinematic):\n";
803 for (const auto& r : rows) {
804 const WetExtrema e = WetMinMax(*r.mf, r.comp, *r.mask);
805 if (e.wet_cells == 0) {
806 amrex::Print() << " " << r.name << ": no wet cells\n";
807 } else {
808 amrex::Print() << " " << r.name << ": min=" << e.min_value
809 << " max=" << e.max_value << "\n";
810 }
811 }
812}
constexpr amrex::Real one
constexpr amrex::Real zero
constexpr amrex::Real rhow
constexpr amrex::Real Cp
#define NGROW
#define Temp_comp
#define Salt_comp
mf_h setVal(geomdata.ProbHi(2))
void ConfigureDriverAtmosToOceanCoupling(bool use_coupling_driver, bool use_two_way_coupling, DriverAtmosForcingMode active_mode)
double stop_time
Whether max_step was set in the inputs; the default above is not a distinguishable sentinel.
Definition REMORA.H:1774
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_evap
evaporation rate [kg/m^2/s]
Definition REMORA.H:517
amrex::Long m_warned_surface_temp_nan
Definition REMORA.H:257
bool running_with_coupling_driver
True once REMORA has received forcing through the coupling driver.
Definition REMORA.H:526
void GetLandSeaMasks(const amrex::MultiFab *&mskr, const amrex::MultiFab *&msku, const amrex::MultiFab *&mskv) const
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_lrflx
longwave radiation
Definition REMORA.H:497
amrex::Vector< amrex::MultiFab * > cons_new
multilevel data container for current step's scalar data: temperature, salinity, passive tracer
Definition REMORA.H:393
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_vwind
Wind in the v direction, defined at rho-points.
Definition REMORA.H:486
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_mskr
land/sea mask at cell centers (2D)
Definition REMORA.H:584
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_rain
precipitation rate [kg/m^2/s]
Definition REMORA.H:515
int do_substep
Whether to substep fine levels in time.
Definition REMORA.H:1831
amrex::Real m_warned_surface_temp_min
Definition REMORA.H:255
void GetAtmosToOceanPsiLonLat(const amrex::MultiFab *&lon_psi, const amrex::MultiFab *&lat_psi) const
void ApplyAtmosphericFluxes(const amrex::Vector< amrex::MultiFab * > &states, amrex::Real time)
Receives atmospheric flux lanes from the driver and assembles REMORA flux inputs.
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_sustr
Surface stress in the u direction.
Definition REMORA.H:479
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_yp
y_grid on psi-points (2D)
Definition REMORA.H:632
void GetAtmosToOceanVFaceLayout(amrex::BoxArray &ba, amrex::DistributionMapping &dm) const
void GetAtmosToOceanRhoLayout(amrex::BoxArray &ba, amrex::DistributionMapping &dm) const
void WriteAtIntermediateTime(int step, amrex::Real cur_time)
Write checkpoint and plotfiles at intermediate point of simulation, if needed.
Definition REMORA.cpp:360
amrex::Real EvolveOneStep(amrex::Real time, amrex::Real dt_request)
DriverAtmosForcingMode
Definition REMORA.H:95
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_msku
land/sea mask at x-faces (2D)
Definition REMORA.H:586
std::array< bool, AtmosState::NumTypes > driver_atmos_state_from_driver
provenance flags for driver-supplied atmospheric forcing lanes
Definition REMORA.H:524
void GetAtmosToOceanPsiCoordinates(const amrex::MultiFab *&x_psi, const amrex::MultiFab *&y_psi) const
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_uwind
Wind in the u direction, defined at rho-points.
Definition REMORA.H:484
DriverAtmosForcingMode driver_atmos_forcing_mode
Active atmosphere-to-ocean forcing contract on the most recent driver apply.
Definition REMORA.H:530
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_shflx
sensible heat flux
Definition REMORA.H:503
void post_timestep(int nstep, amrex::Real time, amrex::Real dt_lev)
Called after every level 0 timestep.
Definition REMORA.cpp:440
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_lhflx
latent heat flux
Definition REMORA.H:501
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_lonp
longitude on psi-points (2D, degrees east); only filled when the grid NetCDF file carries lon_psi
Definition REMORA.H:636
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_mskv
land/sea mask at y-faces (2D)
Definition REMORA.H:588
void ComputeDt()
a wrapper for estTimeStep()
amrex::Vector< int > istep
which step?
Definition REMORA.H:1689
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_svstr
Surface stress in the v direction.
Definition REMORA.H:481
void GetDeckConfiguredAtmosStateLanes(std::array< bool, AtmosState::NumTypes > &deck_configured) const
amrex::Real m_warned_surface_temp_max
Definition REMORA.H:256
amrex::Vector< amrex::Real > t_new
new time at each level, in seconds since start_time
Definition REMORA.H:1700
static SolverChoice solverChoice
Container for algorithmic choices.
Definition REMORA.H:1949
void ApplyAtmosphericStates(const amrex::Vector< amrex::MultiFab * > &states, amrex::Real time)
Receives atmospheric states from the driver and applies unit conversions.
bool m_reported_atm_flux_validation
Latch so the flux validation block prints once per run unless remora.v >= 1.
Definition REMORA.H:252
void SetLongwaveFromDriver()
bool driver_uses_two_way_coupling
Driver-level direction flag copied in before InitData.
Definition REMORA.H:528
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_xp
x_grid on psi-points (2D)
Definition REMORA.H:630
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_longwave_down
Downward longwave radiation.
Definition REMORA.H:499
void GetAtmosToOceanUFaceLayout(amrex::BoxArray &ba, amrex::DistributionMapping &dm) const
void SetDriverAtmosToOceanForcingMode(DriverAtmosForcingMode mode)
void timeStep(int lev, amrex::Real time, int iteration)
advance a level by dt, includes a recursive call for finer levels
void timeStepML(amrex::Real time, int iteration)
advance all levels by dt, loops over finer levels
amrex::Real elapsed_time(double time) const noexcept
Elapsed time since start_time of a time on the model clock.
Definition REMORA.H:2176
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_stflux
Surface tracer flux; input arrays.
Definition REMORA.H:508
static int verbose
Verbosity level of output.
Definition REMORA.H:1985
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_cloud
cloud cover fraction [0-1], defined at rho-points
Definition REMORA.H:519
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_srflx
Shortwave radiation flux [W/m²], defined at rho-points.
Definition REMORA.H:495
amrex::Vector< amrex::Real > dt
time step at each level
Definition REMORA.H:1704
void PackSurfaceState(amrex::Vector< amrex::MultiFab * > &state, amrex::Real time, const amrex::MultiFab *weight_o2a_mf, const amrex::iMultiFab *index_o2a_mf, int max_stencil_size, const amrex::iMultiFab *dst_land_mask=nullptr)
Extracts SST from the 3D conservative state for the atmospheric driver.
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_Pair
Air pressure [mb], defined at rho-points.
Definition REMORA.H:492
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_qair
Specific humidity [kg/kg], defined at rho-points.
Definition REMORA.H:490
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_Tair
Air temperature [°C], defined at rho-points.
Definition REMORA.H:488
amrex::Vector< std::unique_ptr< amrex::MultiFab > > vec_latp
latitude on psi-points (2D, degrees north); only filled when the grid NetCDF file carries lat_psi
Definition REMORA.H:639
@ LWrad
longwave flux lane
@ LHflux
latent heat flux lane
@ Rain
precipitation rate lane
@ Evap
evaporation rate lane
@ SHflux
sensible heat flux lane
@ TauX
surface zonal stress lane
@ TauY
surface meridional stress lane
@ SWrad
shortwave radiation lane
@ Pair
atmospheric pressure [Pa from driver, mb in REMORA]
@ Vwind
10-m meridional wind [m/s]
@ Qair
specific humidity [kg/kg]
@ SWrad
downward shortwave radiation [W/m^2]
@ LWrad
downward longwave radiation [W/m^2]
@ Uwind
10-m zonal wind [m/s]
@ Rain
precipitation rate [kg/m^2/s]
@ Cloud
cloud fraction [0-1]
@ Tair
air temperature [K from driver, degC in REMORA]
@ Vwind
10-m meridional wind [m/s]
@ Pair
atmospheric pressure [mb]
@ Uwind
10-m zonal wind [m/s]
@ LWrad
longwave radiation [W/m^2]
@ Tair
air temperature [degC]
@ Qair
specific humidity or relative humidity [kg/kg or fraction]
@ Cloud
cloud fraction [0-1]
@ SWrad
downward shortwave radiation [W/m^2]
@ Rain
precipitation rate [kg/m^2/s]
@ data
annual climatology of Laurent et al. (2017)
std::array< bool, BulkFlux::NumTypes > bulk_flux_type_specified
std::array< bool, BulkFlux::NumTypes > bulk_flux_value_specified
std::array< BulkForcingType, BulkFlux::NumTypes > bulk_flux_type