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
39// One pass over the wet cells (mask > 0.5) of one component of a 2D field, on
40// the valid region at the field's lowest k. Land cells hold zeros or whatever
41// the atmosphere sent over land, so an unmasked min/max says nothing about what
42// the ocean actually receives.
43//
44// min/max skip NaN, which nan_count reports: NaN in a min/max reduction is
45// kept or dropped depending on the reduction order, so the result would depend
46// on the decomposition. With no finite wet cell, has_finite() is false.
47//
48// bad_count counts the wet cells outside [lo, hi], NaN included; the test is
49// !(lo <= v <= hi) because every comparison with NaN is false. One of them is
50// named: the smallest flat (i,j) index, the same cell under any decomposition.
51// Collective.
52struct WetRangeCheck
53{
54 amrex::Long wet_cells;
55 amrex::Real min_value;
56 amrex::Real max_value;
57 amrex::Long bad_count;
58 amrex::Long nan_count;
59 amrex::IntVect example;
60 amrex::Real example_value;
61
62 bool has_finite () const { return min_value <= max_value; }
63};
64
65WetRangeCheck
66CheckWetRange (const amrex::MultiFab& mf, int comp, const amrex::MultiFab& mask,
67 amrex::Real lo_bound = -std::numeric_limits<amrex::Real>::max(),
68 amrex::Real hi_bound = std::numeric_limits<amrex::Real>::max())
69{
70 using namespace amrex;
71 const Box domain = mf.boxArray().minimalBox();
72 const Long nx = domain.length(0);
73 const int k0 = domain.smallEnd(2);
74 constexpr Long no_cell = std::numeric_limits<Long>::max();
75 constexpr Real big = std::numeric_limits<Real>::max();
76
80 using Tuple = typename decltype(data)::Type;
81 for (MFIter mfi(mf); mfi.isValid(); ++mfi) {
82 Box bx = mfi.validbox();
83 bx.makeSlab(2, k0);
84 const auto f = mf.const_array(mfi, comp);
85 const auto m = mask.const_array(mfi);
86 const auto dlo = lbound(domain);
87 ops.eval(bx, data,
88 [=] AMREX_GPU_DEVICE (int i, int j, int k) -> Tuple
89 {
90 const Real v = f(i,j,k);
91 const bool wet = m(i,j,k) > Real(0.5);
92 const bool bad = wet && !(v >= lo_bound && v <= hi_bound);
93 const bool nan = wet && (v != v);
94 const bool finite = wet && !nan;
95 const Long flat = Long(j - dlo.y) * nx + Long(i - dlo.x);
96 return { wet ? Long(1) : Long(0),
97 bad ? Long(1) : Long(0),
98 nan ? Long(1) : Long(0),
99 finite ? v : big,
100 finite ? v : -big,
101 bad ? flat : no_cell };
102 });
103 }
104 auto r = data.value(ops);
105 Long counts[3] = {get<0>(r), get<1>(r), get<2>(r)};
106 Real extrema[2] = {get<3>(r), -get<4>(r)}; // max(v) = -min(-v)
107 Long first = get<5>(r);
108 ParallelDescriptor::ReduceLongSum(counts, 3);
109 ParallelDescriptor::ReduceRealMin(extrema, 2);
110 ParallelDescriptor::ReduceLongMin(first);
111
112 WetRangeCheck out{counts[0], extrema[0], -extrema[1], counts[1],
113 counts[2], IntVect(0), big};
114 if (out.bad_count == 0) { return out; }
115
116 // Only when something is out of range: fetch the named cell's value.
117 out.example = IntVect(domain.smallEnd(0) + static_cast<int>(first % nx),
118 domain.smallEnd(1) + static_cast<int>(first / nx),
119 k0);
120 // Empty off the owning rank. Exactly one rank owns the cell, so a sum with
121 // zero elsewhere carries its value, NaN included, which a min would not.
122 const auto here = get_cell_data(mf, out.example);
123 out.example_value = here.empty() ? Real(0) : here[comp];
124 ParallelDescriptor::ReduceRealSum(out.example_value);
125 return out;
126}
127
128// -------------------------------------------------------------------------
129// NEW: Conservative Sparse Matrix Remap Engine (Reverse: OCN -> ATM)
130// -------------------------------------------------------------------------
131
132// Source index region each destination box's stencils actually reference.
133//
134// The entries in index_mf are *source* indices, and for non-conformal grids they
135// routinely fall far outside the destination box's own index range, so staging the
136// source on `dst.boxArray()` plus a fixed ghost halo leaves those reads outside
137// the valid region - zero at best, out of bounds at worst. Mirror of the same
138// helper on the ERF side of the coupling.
139//
140// The result becomes a BoxArray, which must be globally consistent, hence the
141// reduction.
142amrex::BoxArray
143StagedSourceBoxArray (const amrex::MultiFab& src,
144 const amrex::MultiFab& dst,
145 const amrex::iMultiFab& index_mf,
147{
148 using namespace amrex;
149
150 const Box src_domain = src.boxArray().minimalBox();
151 const int nboxes = static_cast<int>(dst.boxArray().size());
152 constexpr int int_big = std::numeric_limits<int>::max();
153
154 Vector<int> lo(2 * nboxes, int_big);
155 Vector<int> hi(2 * nboxes, -int_big);
156
157 for (MFIter mfi(dst); mfi.isValid(); ++mfi) {
158 const int b = mfi.index();
159 const Box bx = mfi.validbox();
160 auto const& idx = index_mf.const_array(mfi);
161
164 using ReduceTuple = typename decltype(reduce_data)::Type;
165
167 [=] AMREX_GPU_DEVICE (int i, int j, int k) -> ReduceTuple
168 {
170 for (int m = 0; m < max_stencil_size; ++m) {
171 const int si = idx(i, j, k, m * 3);
172 const int sj = idx(i, j, k, m * 3 + 1);
173 if (si < 0 || sj < 0) { continue; } // -1 marks an unused slot
174 i_min = amrex::min(i_min, si); i_max = amrex::max(i_max, si);
175 j_min = amrex::min(j_min, sj); j_max = amrex::max(j_max, sj);
176 }
177 return {i_min, j_min, i_max, j_max};
178 });
179
180 auto const& hv = reduce_data.value(reduce_op);
181 lo[2*b ] = amrex::get<0>(hv); lo[2*b+1] = amrex::get<1>(hv);
182 hi[2*b ] = amrex::get<2>(hv); hi[2*b+1] = amrex::get<3>(hv);
183 }
184
185 ParallelDescriptor::ReduceIntMin(lo.dataPtr(), static_cast<int>(lo.size()));
186 ParallelDescriptor::ReduceIntMax(hi.dataPtr(), static_cast<int>(hi.size()));
187
188 // The staged boxes must carry the source's index type from the start:
189 // intersecting a cell-centered box with a staggered one trips
190 // Box::operator&='s sameType assertion.
191 const IndexType src_ixtype = src.boxArray().ixType();
192
194 for (int b = 0; b < nboxes; ++b) {
195 Box need;
196 if (lo[2*b] > hi[2*b] || lo[2*b+1] > hi[2*b+1]) {
197 // No stencil anywhere in this destination box. A BoxArray cannot hold
198 // an empty box, so stage a single cell; nothing reads it.
199 need = Box(src_domain.smallEnd(), src_domain.smallEnd(), src_ixtype);
200 } else {
201 need = Box(IntVect(lo[2*b], lo[2*b+1], src_domain.smallEnd(2)),
202 IntVect(hi[2*b], hi[2*b+1], src_domain.bigEnd(2)),
203 src_ixtype);
204 need &= src_domain;
205 }
206 bl.push_back(need);
207 }
208 return BoxArray(std::move(bl));
209}
210
211// fallback_val fills destination cells that no source cell overlaps.
212//
213// Zero is the wrong default for a physical field: the receiving model cannot tell
214// "no data here" from "the ocean says zero", and for an intensive quantity zero is
215// not merely inaccurate, it drives the receiver's flux formulas outside their valid
216// domain. This mirrors the rationale on the atmosphere->ocean twin at
217// ERF/Source/Coupling/ERF_to_REMORA.cpp:122.
218//
219// The ocean->atmosphere SST lane deliberately leaves this at zero and relies on the
220// per-cell coverage flag instead: where REMORA does not cover an ERF cell, ERF keeps
221// its own wrflowinp SST rather than consuming a value invented here. That is a
222// withholding contract, not a value, so no climatological constant belongs at that
223// call site. The parameter exists because the mask blend below needs it and because
224// other lanes have a meaningful value to pass.
225void
226ApplyConservativeRemap (const amrex::MultiFab& src,
227 amrex::MultiFab& dst,
228 const amrex::MultiFab& weight_mf,
229 const amrex::iMultiFab& index_mf,
231 const amrex::MultiFab* dst_mask = nullptr,
232 const amrex::iMultiFab* dst_land_mask = nullptr,
233 amrex::Real fallback_val = amrex::Real(0.0))
234{
235 using namespace amrex;
236
237 // 1. Data Routing: Route REMORA SST data onto the ERF atmospheric layout,
238 // staging exactly the source region the stencils reference.
240 dst.DistributionMap(), src.nComp(), 0);
241 src_on_dst.setVal(0.0);
242 src_on_dst.ParallelCopy(src);
243
244 dst.setVal(0.0);
245
246 // 2. Stencil Application: Execute the local sparse dot product
247 for (MFIter mfi(dst, TilingIfNotGPU()); mfi.isValid(); ++mfi) {
248 Box bx = mfi.tilebox();
249
250 auto const& w_arr = weight_mf.const_array(mfi);
251 auto const& idx_arr = index_mf.const_array(mfi);
252 auto const& src_arr = src_on_dst.const_array(mfi);
253 auto dst_arr = dst.array(mfi);
254 const bool has_mask = (dst_mask != nullptr);
255 auto const& mask_arr = has_mask ? dst_mask->const_array(mfi) : Array4<const Real>{};
256 // ERF land mask: 0 = water, anything non-zero is land. Destination cells
257 // over land are zeroed out (wet/dry masking only, no vector rotation).
258 const bool has_land_mask = (dst_land_mask != nullptr);
259 auto const& land_arr = has_land_mask ? dst_land_mask->const_array(mfi) : Array4<const int>{};
260
261 ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) {
262 // No stencil entry at all means no source cell overlapped this
263 // destination cell. Hand back the fallback rather than an accumulated
264 // zero, and do not apply the masks to it: a masked-out cell still gets
265 // evaluated by the receiving model's flux formulas before the mask is
266 // applied, so it too must hold an admissible value. Mirrors
267 // ERF/Source/Coupling/ERF_to_REMORA.cpp:174.
268 if (idx_arr(i, j, k, 0) < 0) {
269 dst_arr(i, j, k) = fallback_val;
270 return;
271 }
272
273 Real sum = 0.0;
274 for (int m = 0; m < max_stencil_size; ++m) {
275 Real w = w_arr(i, j, k, m);
276 if (w > 0.0) {
277 int src_i = idx_arr(i, j, k, m * 3);
278 int src_j = idx_arr(i, j, k, m * 3 + 1);
279 int src_k = idx_arr(i, j, k, m * 3 + 2);
280
281 sum += w * src_arr(src_i, src_j, src_k);
282 }
283 }
284 // Blend toward the fallback rather than multiplying by the mask.
285 // Multiplying drives partially masked cells toward exactly zero, which
286 // is right for a flux lane (fallback_val = 0, so this reduces to
287 // sum *= mask and is bit-identical) but destructive for an intensive
288 // state lane such as SST, where it scales a temperature toward 0 K.
289 // Mirrors ERF/Source/Coupling/ERF_to_REMORA.cpp:202.
290 if (has_mask) {
291 const Real mask = mask_arr(i, j, k);
292 sum = mask * sum + (Real(1.0) - mask) * fallback_val;
293 }
294 // Test against zero, not against 1: ERF stamps lmask = 2 for cells
295 // under ImmersedForcing buildings (ERF/Source/ERF_MakeNewArrays.cpp:890),
296 // so an == 1 test reads every building as water and hands it a remapped
297 // SST. The driver's atmosphere->ocean side already uses the tolerant
298 // form (ERFRemoraMultiBlockContainer.cpp:1767), so == 1 also made the
299 // two directions disagree about the same cell within one timestep.
300 if (has_land_mask && land_arr(i, j, k) != 0) { sum = Real(0.0); }
301 dst_arr(i, j, k) = sum;
302 });
303 }
304}
305}
306
307amrex::Real
308REMORA::EvolveOneStep (amrex::Real /*time*/, amrex::Real /*dt_request*/)
309{
310 Real cur_time = t_new[0];
311 const int step = istep[0];
312
314 return zero;
315 }
316
317 ComputeDt();
318
319 int lev = 0;
320 int iteration = 1;
321 // Must match Evolve's choice: the subcycle-only paths are gated on do_substep, not on
322 // which driver ran, and timeStepML does not register the mass flux they read.
323 if (max_level == 0 || do_substep) {
325 } else {
327 }
328
329 cur_time += dt[0];
330
332
334
335 return dt[0];
336}
337
338void
347
348void
350{
351 // "constant" is any non-computed type: it makes setup_step pass
352 // vec_longwave_down to bulk_fluxes instead of leaving lw_ptr null. The two
353 // flags below are what ReadParameters would have derived for that type.
357}
358
359void
361 std::array<bool, AtmosState::NumTypes>& deck_configured) const
362{
363 // AtmosState and BulkFlux agree on lanes 0-8 today, but they are separate
364 // enums of separate lengths (BulkFlux carries EminusP, which has no driver
365 // lane). Map explicitly, so inserting into either breaks the build here
366 // rather than silently transposing the answer.
367 static constexpr std::array<int, AtmosState::NumTypes> lane_to_bulk_flux {{
368 BulkFlux::Uwind, // AtmosState::Uwind
369 BulkFlux::Vwind, // AtmosState::Vwind
370 BulkFlux::Pair, // AtmosState::Pair
371 BulkFlux::Qair, // AtmosState::Qair
372 BulkFlux::Tair, // AtmosState::Tair
373 BulkFlux::Cloud, // AtmosState::Cloud
374 BulkFlux::Rain, // AtmosState::Rain
375 BulkFlux::SWrad, // AtmosState::SWrad
376 BulkFlux::LWrad // AtmosState::LWrad
377 }};
378
379 deck_configured.fill(false);
380 for (int lane = 0; lane < AtmosState::NumTypes; ++lane) {
381 const int idx = lane_to_bulk_flux[lane];
384 }
385}
386
387void
392
393void
395 amrex::DistributionMapping& dm) const
396{
398 !vec_srflx.empty() && vec_srflx[0] != nullptr,
399 "REMORA::GetAtmosToOceanRhoLayout requires post-InitData rho-point forcing storage.");
400 ba = vec_srflx[0]->boxArray();
401 dm = vec_srflx[0]->DistributionMap();
402}
403
404void
406 amrex::DistributionMapping& dm) const
407{
409 !vec_sustr.empty() && vec_sustr[0] != nullptr,
410 "REMORA::GetAtmosToOceanUFaceLayout requires post-InitData u-face forcing storage.");
411 ba = vec_sustr[0]->boxArray();
412 dm = vec_sustr[0]->DistributionMap();
413}
414
415void
417 amrex::DistributionMapping& dm) const
418{
420 !vec_svstr.empty() && vec_svstr[0] != nullptr,
421 "REMORA::GetAtmosToOceanVFaceLayout requires post-InitData v-face forcing storage.");
422 ba = vec_svstr[0]->boxArray();
423 dm = vec_svstr[0]->DistributionMap();
424}
425
426void
428 const amrex::MultiFab*& y_psi) const
429{
430 if (vec_xp.empty() || vec_yp.empty() ||
431 vec_xp[0] == nullptr || vec_yp[0] == nullptr) {
432 x_psi = nullptr;
433 y_psi = nullptr;
434 return;
435 }
436 x_psi = vec_xp[0].get();
437 y_psi = vec_yp[0].get();
438}
439
440void
442 const amrex::MultiFab*& lat_psi) const
443{
444 if (vec_lonp.empty() || vec_latp.empty() ||
445 vec_lonp[0] == nullptr || vec_latp[0] == nullptr) {
446 lon_psi = nullptr;
447 lat_psi = nullptr;
448 return;
449 }
450 lon_psi = vec_lonp[0].get();
451 lat_psi = vec_latp[0].get();
452}
453
454void
455REMORA::GetLandSeaMasks (const amrex::MultiFab*& mskr,
456 const amrex::MultiFab*& msku,
457 const amrex::MultiFab*& mskv) const
458{
459 mskr = (!vec_mskr.empty() && vec_mskr[0]) ? vec_mskr[0].get() : nullptr;
460 msku = (!vec_msku.empty() && vec_msku[0]) ? vec_msku[0].get() : nullptr;
461 mskv = (!vec_mskv.empty() && vec_mskv[0]) ? vec_mskv[0].get() : nullptr;
462}
463
464/*
465 * \brief Extracts SST from the 3D conservative state for the atmospheric driver.
466 *
467 * Reads Temp_comp at the top water-column cell (k_sfc), converts from
468 * Celsius to Kelvin, and conservatively remaps the result into state[SSTIndex].
469 */
470void
472 Real /*time*/,
473 const amrex::MultiFab* weight_o2a_mf,
474 const amrex::iMultiFab* index_o2a_mf,
476 const amrex::iMultiFab* dst_land_mask)
477{
478 if (state.empty() || state[SSTIndex] == nullptr) { return; }
479 const int lev = 0;
480
481 // REMORA stores temperature in Celsius. Surface is at k=N (top of water column).
482 const int k_sfc = cons_new[lev]->boxArray().minimalBox().bigEnd(2);
483
484 // Build a temp MultiFab on REMORA's ba2d (k=0) derived from cons_new's BoxArray.
485 BoxList bl2d = cons_new[lev]->boxArray().boxList();
486 for (auto& b : bl2d) { b.setRange(2, 0); }
487 BoxArray ba2d(std::move(bl2d));
488 MultiFab tmp(ba2d, cons_new[lev]->DistributionMap(), 1, 0);
489
490 for (MFIter mfi(*cons_new[lev]); mfi.isValid(); ++mfi) {
491 auto const& c = cons_new[lev]->const_array(mfi);
492 auto t = tmp.array(mfi);
493 Box bx = makeSlab(mfi.validbox(), 2, k_sfc);
494 ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int) {
495 // Write to k=0 in tmp (ba2d range); convert Celsius → Kelvin.
496 t(i, j, 0) = c(i, j, k_sfc, Temp_comp) + Real(273.15);
497 });
498 }
499
500 // Surface temperature sanity check on REMORA's own wet cells, in the SST
501 // that is about to be sent to the atmosphere: one pass, once per exchange
502 // (this runs at pack time, not every ERF substep). Warned the first time
503 // and again only when it gets worse; remora.v >= 1 also prints the wet
504 // min/max.
505 // min/max exclude NaN, so the NaN count is tracked separately. verbose and
506 // the reduced values are rank-uniform, so every rank takes the same
507 // branches.
508 if (!vec_mskr.empty() && vec_mskr[lev] != nullptr) {
509 MultiFab wet(ba2d, cons_new[lev]->DistributionMap(), 1, 0);
510 wet.setVal(zero);
511 wet.ParallelCopy(*vec_mskr[lev], 0, 0, 1);
512
513 constexpr Real t_lo = Real(273.15 - 2.5); // below seawater freezing
514 constexpr Real t_hi = Real(273.15 + 40.0);
515 const WetRangeCheck c = CheckWetRange(tmp, 0, wet, t_lo, t_hi);
516 if (verbose >= 1) {
517 amrex::Print os;
518 os << "REMORA surface temperature over " << c.wet_cells
519 << " wet cells: ";
520 if (c.has_finite()) {
521 os << "min/max = " << c.min_value << " / " << c.max_value
522 << " K";
523 } else {
524 os << "no finite values";
525 }
526 os << "\n";
527 }
528 // With no finite wet cell, min/max are the +/-max sentinels, which
529 // never compare as worse; the NaN count still does.
530 const bool worse = c.min_value < m_warned_surface_temp_min ||
531 c.max_value > m_warned_surface_temp_max ||
532 c.nan_count > m_warned_surface_temp_nan;
533 if (c.bad_count > 0 && worse) {
535 std::min(m_warned_surface_temp_min, c.min_value);
537 std::max(m_warned_surface_temp_max, c.max_value);
539 std::max(m_warned_surface_temp_nan, c.nan_count);
540 amrex::Print os;
541 os << "WARNING: REMORA surface temperature outside [" << t_lo
542 << ", " << t_hi << "] K on " << c.bad_count << " of "
543 << c.wet_cells << " wet cells (" << c.nan_count << " NaN";
544 if (c.has_finite()) {
545 os << "; min/max " << c.min_value << " / " << c.max_value
546 << " K";
547 }
548 os << "; e.g. REMORA (" << c.example[0] << "," << c.example[1]
549 << ") = " << c.example_value
550 << " K). Repeated only if it gets worse.\n";
551 }
552 }
553
554 MultiFab& dst = *state[SSTIndex];
555
556 if (weight_o2a_mf != nullptr && index_o2a_mf != nullptr) {
557 // Execute sparse conservative remap from REMORA SST to ERF layout
559 nullptr, dst_land_mask);
560 } else {
561 // Fallback for un-stenciled or synthetic runs
562 dst.setVal(zero);
563 dst.ParallelCopy(tmp, 0, 0, 1);
564 }
565}
566
567/*
568 * \brief Receives atmospheric states from the driver and applies unit conversions.
569 */
570void
572{
576 if (finest_level < 0) { return; }
577
578 // Wind (m/s) — no unit conversion
579 if (vec_uwind[0] != nullptr) {
580 if (states.size() > AtmosState::Uwind && states[AtmosState::Uwind] != nullptr) {
581 vec_uwind[0]->ParallelCopy(*states[AtmosState::Uwind], 0, 0, 1);
582 vec_uwind[0]->FillBoundary(geom[0].periodicity());
584 }
585 }
586 if (vec_vwind[0] != nullptr) {
587 if (states.size() > AtmosState::Vwind && states[AtmosState::Vwind] != nullptr) {
588 vec_vwind[0]->ParallelCopy(*states[AtmosState::Vwind], 0, 0, 1);
589 vec_vwind[0]->FillBoundary(geom[0].periodicity());
591 }
592 }
593
594 // Atmospheric pressure: Pa → mb (REMORA bulk flux expects mb)
595 if (vec_Pair[0] != nullptr) {
596 if (states.size() > AtmosState::Pair && states[AtmosState::Pair] != nullptr) {
597 vec_Pair[0]->ParallelCopy(*states[AtmosState::Pair], 0, 0, 1);
598 vec_Pair[0]->mult(Real(0.01), 0, 1);
599 vec_Pair[0]->FillBoundary(geom[0].periodicity());
601 }
602 }
603
604 // Specific humidity (kg/kg) — no conversion
605 if (vec_qair[0] != nullptr) {
606 if (states.size() > AtmosState::Qair && states[AtmosState::Qair] != nullptr) {
607 vec_qair[0]->ParallelCopy(*states[AtmosState::Qair], 0, 0, 1);
608 vec_qair[0]->FillBoundary(geom[0].periodicity());
610 }
611 }
612
613 // Air temperature: K → °C (REMORA stores/uses Celsius internally)
614 if (vec_Tair[0] != nullptr) {
615 if (states.size() > AtmosState::Tair && states[AtmosState::Tair] != nullptr) {
616 vec_Tair[0]->ParallelCopy(*states[AtmosState::Tair], 0, 0, 1);
617 vec_Tair[0]->plus(Real(-273.15), 0, 1);
618 vec_Tair[0]->FillBoundary(geom[0].periodicity());
620 }
621 }
622
623 // Cloud fraction [0-1], rain, SW/LW radiation — no unit conversion
624 if (vec_cloud[0] != nullptr) {
625 if (states.size() > AtmosState::Cloud && states[AtmosState::Cloud] != nullptr) {
626 vec_cloud[0]->ParallelCopy(*states[AtmosState::Cloud], 0, 0, 1);
627 vec_cloud[0]->FillBoundary(geom[0].periodicity());
629 }
630 }
631 if (vec_rain[0] != nullptr) {
632 if (states.size() > AtmosState::Rain && states[AtmosState::Rain] != nullptr) {
633 vec_rain[0]->ParallelCopy(*states[AtmosState::Rain], 0, 0, 1);
634 vec_rain[0]->FillBoundary(geom[0].periodicity());
636 }
637 }
638 if (vec_srflx[0] != nullptr) {
639 if (states.size() > AtmosState::SWrad && states[AtmosState::SWrad] != nullptr) {
640 vec_srflx[0]->ParallelCopy(*states[AtmosState::SWrad], 0, 0, 1);
641 vec_srflx[0]->FillBoundary(geom[0].periodicity());
643 }
644 }
645 if (vec_longwave_down[0] != nullptr) {
646 if (states.size() > AtmosState::LWrad && states[AtmosState::LWrad] != nullptr) {
647 vec_longwave_down[0]->ParallelCopy(*states[AtmosState::LWrad], 0, 0, 1);
648 vec_longwave_down[0]->FillBoundary(geom[0].periodicity());
650 }
651 }
652
653}
654
655void
657{
661 if (finest_level < 0) { return; }
662
663 if (states.size() <= AtmosFluxes::Evap ||
664 states[AtmosFluxes::TauX] == nullptr ||
665 states[AtmosFluxes::TauY] == nullptr ||
666 states[AtmosFluxes::SHflux] == nullptr ||
667 states[AtmosFluxes::LHflux] == nullptr ||
668 states[AtmosFluxes::SWrad] == nullptr ||
669 states[AtmosFluxes::LWrad] == nullptr ||
670 states[AtmosFluxes::Rain] == nullptr ||
671 states[AtmosFluxes::Evap] == nullptr ||
672 vec_sustr[0] == nullptr || vec_svstr[0] == nullptr ||
673 vec_stflux[0] == nullptr || vec_mskr[0] == nullptr ||
674 vec_msku[0] == nullptr || vec_mskv[0] == nullptr ||
675 vec_srflx[0] == nullptr || vec_lrflx[0] == nullptr ||
676 vec_lhflx[0] == nullptr || vec_shflx[0] == nullptr ||
677 vec_rain[0] == nullptr || vec_evap[0] == nullptr) {
678 return;
679 }
680
681 const Real Hscale2 = one / (solverChoice.rho0 * Cp);
682 const Real rho0 = solverChoice.rho0;
683
684 MultiFab tau_x_tmp(vec_sustr[0]->boxArray(), vec_sustr[0]->DistributionMap(), 1,
685 vec_sustr[0]->nGrowVect());
686 MultiFab tau_y_tmp(vec_svstr[0]->boxArray(), vec_svstr[0]->DistributionMap(), 1,
687 vec_svstr[0]->nGrowVect());
688 MultiFab shflux_tmp(vec_shflx[0]->boxArray(), vec_shflx[0]->DistributionMap(), 1,
689 vec_shflx[0]->nGrowVect());
690 MultiFab lhflux_tmp(vec_lhflx[0]->boxArray(), vec_lhflx[0]->DistributionMap(), 1,
691 vec_lhflx[0]->nGrowVect());
692 MultiFab lwflux_tmp(vec_lrflx[0]->boxArray(), vec_lrflx[0]->DistributionMap(), 1,
693 vec_lrflx[0]->nGrowVect());
694
695 tau_x_tmp.setVal(zero);
696 tau_x_tmp.ParallelCopy(*states[AtmosFluxes::TauX], 0, 0, 1);
697 tau_x_tmp.FillBoundary(geom[0].periodicity());
698
699 tau_y_tmp.setVal(zero);
700 tau_y_tmp.ParallelCopy(*states[AtmosFluxes::TauY], 0, 0, 1);
701 tau_y_tmp.FillBoundary(geom[0].periodicity());
702
703 shflux_tmp.setVal(zero);
704 shflux_tmp.ParallelCopy(*states[AtmosFluxes::SHflux], 0, 0, 1);
705 shflux_tmp.FillBoundary(geom[0].periodicity());
706
707 lhflux_tmp.setVal(zero);
708 lhflux_tmp.ParallelCopy(*states[AtmosFluxes::LHflux], 0, 0, 1);
709 lhflux_tmp.FillBoundary(geom[0].periodicity());
710
711 lwflux_tmp.setVal(zero);
712 lwflux_tmp.ParallelCopy(*states[AtmosFluxes::LWrad], 0, 0, 1);
713 lwflux_tmp.FillBoundary(geom[0].periodicity());
714
715 vec_srflx[0]->setVal(zero);
716 vec_srflx[0]->ParallelCopy(*states[AtmosFluxes::SWrad], 0, 0, 1);
717 vec_srflx[0]->FillBoundary(geom[0].periodicity());
718
719 vec_rain[0]->setVal(zero);
720 vec_rain[0]->ParallelCopy(*states[AtmosFluxes::Rain], 0, 0, 1);
721 vec_rain[0]->FillBoundary(geom[0].periodicity());
722
723 vec_evap[0]->setVal(zero);
724 vec_evap[0]->ParallelCopy(*states[AtmosFluxes::Evap], 0, 0, 1);
725 vec_evap[0]->FillBoundary(geom[0].periodicity());
726
727 vec_lrflx[0]->setVal(zero);
728 vec_lhflx[0]->setVal(zero);
729 vec_shflx[0]->setVal(zero);
730 vec_stflux[0]->setVal(zero);
731
732 for (MFIter mfi(*vec_sustr[0], TilingIfNotGPU()); mfi.isValid(); ++mfi) {
733 Array4<Real> const& sustr = vec_sustr[0]->array(mfi);
734 Array4<const Real> const& msku = vec_msku[0]->const_array(mfi);
735 Array4<const Real> const& tau_x = tau_x_tmp.const_array(mfi);
736 Box ubx = mfi.grownnodaltilebox(0, IntVect(NGROW,NGROW,0));
737 Box ubxD = ubx;
738 ubxD.makeSlab(2,0);
739 ParallelFor(ubxD, [=] AMREX_GPU_DEVICE (int i, int j, int ) {
740 sustr(i,j,0) = -tau_x(i,j,0) / rho0 * msku(i,j,0);
741 });
742 }
743
744 for (MFIter mfi(*vec_svstr[0], TilingIfNotGPU()); mfi.isValid(); ++mfi) {
745 Array4<Real> const& svstr = vec_svstr[0]->array(mfi);
746 Array4<const Real> const& mskv = vec_mskv[0]->const_array(mfi);
747 Array4<const Real> const& tau_y = tau_y_tmp.const_array(mfi);
748 Box vbx = mfi.grownnodaltilebox(1, IntVect(NGROW,NGROW,0));
749 Box vbxD = vbx;
750 vbxD.makeSlab(2,0);
751
752 ParallelFor(vbxD, [=] AMREX_GPU_DEVICE (int i, int j, int ) {
753 svstr(i,j,0) = -tau_y(i,j,0) / rho0 * mskv(i,j,0);
754 });
755 }
756
757 for (MFIter mfi(*vec_stflux[0], TilingIfNotGPU()); mfi.isValid(); ++mfi) {
758 Array4<Real> const& stflux = vec_stflux[0]->array(mfi);
759 Array4<Real> const& lrflx = vec_lrflx[0]->array(mfi);
760 Array4<Real> const& lhflx = vec_lhflx[0]->array(mfi);
761 Array4<Real> const& shflx = vec_shflx[0]->array(mfi);
762 Array4<const Real> const& mskr = vec_mskr[0]->const_array(mfi);
763 Array4<const Real> const& srflx = vec_srflx[0]->const_array(mfi);
764 Array4<const Real> const& rain = vec_rain[0]->const_array(mfi);
765 Array4<const Real> const& evap = vec_evap[0]->const_array(mfi);
766 Array4<const Real> const& shflux = shflux_tmp.const_array(mfi);
767 Array4<const Real> const& lhflux = lhflux_tmp.const_array(mfi);
768 Array4<const Real> const& lwflux = lwflux_tmp.const_array(mfi);
769
770 Box gbx2 = mfi.growntilebox(IntVect(NGROW,NGROW,0));
771 Box gbx2D = gbx2;
772 gbx2D.makeSlab(2,0);
773
774 ParallelFor(gbx2D, [=] AMREX_GPU_DEVICE (int i, int j, int ) {
775 lrflx(i,j,0) = lwflux(i,j,0) * Hscale2;
776 lhflx(i,j,0) = -lhflux(i,j,0) * Hscale2;
777 shflx(i,j,0) = -shflux(i,j,0) * Hscale2;
778 stflux(i,j,0,Temp_comp) =
779 (srflx(i,j,0) * Hscale2 + lrflx(i,j,0) + lhflx(i,j,0) + shflx(i,j,0))
780 * mskr(i,j,0);
781 stflux(i,j,0,Salt_comp) =
782 mskr(i,j,0) * (evap(i,j,0) - rain(i,j,0)) / rhow;
783 });
784 }
785
786 vec_sustr[0]->FillBoundary(geom[0].periodicity());
787 vec_svstr[0]->FillBoundary(geom[0].periodicity());
788 vec_srflx[0]->FillBoundary(geom[0].periodicity());
789 vec_lrflx[0]->FillBoundary(geom[0].periodicity());
790 vec_lhflx[0]->FillBoundary(geom[0].periodicity());
791 vec_shflx[0]->FillBoundary(geom[0].periodicity());
792 vec_stflux[0]->FillBoundary(geom[0].periodicity());
793 vec_rain[0]->FillBoundary(geom[0].periodicity());
794 vec_evap[0]->FillBoundary(geom[0].periodicity());
795 vec_stflux[0]->FillBoundary(geom[0].periodicity());
796
797 // Once per run, or every exchange at remora.v >= 1. verbose is a ParmParse
798 // value, so every rank takes the same branch around the collectives.
799 if (m_reported_atm_flux_validation && verbose < 1) { return; }
801
802 struct Row { const char* name; const MultiFab* mf; int comp; const MultiFab* mask; };
803 const Row rows[] = {
804 {"sustr", vec_sustr[0].get(), 0, vec_msku[0].get()},
805 {"svstr", vec_svstr[0].get(), 0, vec_mskv[0].get()},
806 {"stflux(Temp)", vec_stflux[0].get(), Temp_comp, vec_mskr[0].get()},
807 {"stflux(Salt)", vec_stflux[0].get(), Salt_comp, vec_mskr[0].get()},
808 {"srflx", vec_srflx[0].get(), 0, vec_mskr[0].get()},
809 {"lrflx", vec_lrflx[0].get(), 0, vec_mskr[0].get()},
810 {"lhflx", vec_lhflx[0].get(), 0, vec_mskr[0].get()},
811 {"shflx", vec_shflx[0].get(), 0, vec_mskr[0].get()},
812 };
813 amrex::Print() << "REMORA ApplyAtmosphericFluxes validation (wet cells only; srflx W/m2, "
814 << "others kinematic):\n";
815 for (const auto& r : rows) {
816 const WetRangeCheck e = CheckWetRange(*r.mf, r.comp, *r.mask);
817 amrex::Print os;
818 os << " " << r.name << ": ";
819 if (e.wet_cells == 0) {
820 os << "no wet cells";
821 } else if (!e.has_finite()) {
822 os << "all " << e.wet_cells << " wet cells NaN";
823 } else {
824 os << "min=" << e.min_value << " max=" << e.max_value;
825 if (e.nan_count > 0) { os << " (" << e.nan_count << " NaN)"; }
826 }
827 os << "\n";
828 }
829}
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
Time to stop, in seconds on the model clock (not since start_time)
Definition REMORA.H:1816
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:1873
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:361
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:441
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:1733
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:1744
static SolverChoice solverChoice
Container for algorithmic choices.
Definition REMORA.H:2017
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:2216
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:2053
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:1748
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