Mesh Oriented datABase  (version 5.6.0)
An array-based unstructured mesh library
NCWriteDomain.cpp
Go to the documentation of this file.
1 //-------------------------------------------------------------------------
2 // Filename : NCWriteDomain.cpp
3 //
4 // Purpose : CESM domain file writer. See NCWriteDomain.hpp for design.
5 //
6 // Creator : Vijay Mahadevan, 2026-06-13
7 //-------------------------------------------------------------------------
8 
9 #include "NCWriteDomain.hpp"
10 #include "MBTagConventions.hpp"
11 
12 #include <algorithm>
13 #include <cmath>
14 #include <cstring>
15 
16 namespace moab
17 {
18 
20 
21 namespace
22 {
23 
24 constexpr double kPi = 3.14159265358979323846;
25 constexpr double kRadToDegree = 180.0 / kPi;
26 
27 inline void xyz_to_latlon_deg( double x, double y, double z, double& lat, double& lon )
28 {
29  const double r = std::sqrt( x * x + y * y + z * z );
30  if( r < 1.0e-30 )
31  {
32  lat = 0.0;
33  lon = 0.0;
34  return;
35  }
36  lat = std::asin( z / r ) * kRadToDegree;
37  lon = std::atan2( y, x ) * kRadToDegree;
38  if( lon < 0.0 ) lon += 360.0;
39 }
40 
41 } // namespace
42 
43 // ============================================================================
44 // collect_mesh_info — gather owned cells, compute center + corner lat/lon
45 // per cell. (Domain is a per-cell layout; no vertex dedup needed, unlike
46 // NCWriteESMF.)
47 // ============================================================================
49 {
50  Interface*& mbImpl = _writeNC->mbImpl;
51  Tag& mGlobalIdTag = _writeNC->mGlobalIdTag;
52  DebugOutput& dbgOut = _writeNC->dbgOut;
53 
54  Range allCells;
55  MB_CHK_SET_ERR( mbImpl->get_entities_by_dimension( _fileSet, 2, allCells ),
56  "Failed to gather 2-D cells for Domain write" );
57  if( allCells.empty() ) MB_SET_ERR( MB_FAILURE, "No 2-D cells in file set; cannot write Domain grid" );
58 
59  localCellsOwned = allCells;
60 #ifdef MOAB_HAVE_MPI
61  bool& isParallel = _writeNC->isParallel;
62  if( isParallel )
63  {
64  ParallelComm*& myPcomm = _writeNC->myPcomm;
65  if( myPcomm && myPcomm->proc_config().proc_size() > 1 )
66  {
68  "Failed to filter owned cells for Domain write" );
69  }
70  }
71 #endif
72 
73  mLocalCells = static_cast< long >( localCellsOwned.size() );
74 
75  // Global max corners per cell.
76  int localMaxCorners = 0;
77  for( Range::iterator cit = localCellsOwned.begin(); cit != localCellsOwned.end(); ++cit )
78  {
79  const EntityHandle* conn = nullptr;
80  int numConn = 0;
81  if( MB_SUCCESS == mbImpl->get_connectivity( *cit, conn, numConn ) && numConn > localMaxCorners )
82  localMaxCorners = numConn;
83  }
84 #ifdef MOAB_HAVE_MPI
85  if( _writeNC->isParallel && _writeNC->myPcomm )
86  {
87  MPI_Allreduce( &localMaxCorners, &mMaxCornersGlobal, 1, MPI_INT, MPI_MAX,
88  _writeNC->myPcomm->proc_config().proc_comm() );
89  }
90  else
91 #endif
92  {
93  mMaxCornersGlobal = localMaxCorners;
94  }
95  if( mMaxCornersGlobal <= 0 ) MB_SET_ERR( MB_FAILURE, "Cells reported zero connectivity; cannot write Domain" );
96 
97  mLocalGids.assign( mLocalCells, 0 );
98  mXc.assign( mLocalCells, 0.0 );
99  mYc.assign( mLocalCells, 0.0 );
100  mXv.assign( static_cast< size_t >( mLocalCells ) * mMaxCornersGlobal, 0.0 );
101  mYv.assign( static_cast< size_t >( mLocalCells ) * mMaxCornersGlobal, 0.0 );
102  mMask.assign( mLocalCells, 1 );
103 
104  long ci = 0;
105  for( Range::iterator cit = localCellsOwned.begin(); cit != localCellsOwned.end(); ++cit, ++ci )
106  {
107  int gid = 0;
108  if( mGlobalIdTag ) mbImpl->tag_get_data( mGlobalIdTag, &( *cit ), 1, &gid );
109  mLocalGids[ci] = gid;
110 
111  const EntityHandle* conn = nullptr;
112  int numConn = 0;
113  MB_CHK_SET_ERR( mbImpl->get_connectivity( *cit, conn, numConn ), "Cell connectivity lookup failed" );
114 
115  double cx = 0.0, cy = 0.0, cz = 0.0;
116  double firstLat = 0.0, firstLon = 0.0;
117  for( int k = 0; k < numConn; ++k )
118  {
119  double vc[3] = { 0.0, 0.0, 0.0 };
120  MB_CHK_SET_ERR( mbImpl->get_coords( &conn[k], 1, vc ), "Vertex coordinate lookup failed" );
121  cx += vc[0];
122  cy += vc[1];
123  cz += vc[2];
124  double vlat = 0.0, vlon = 0.0;
125  xyz_to_latlon_deg( vc[0], vc[1], vc[2], vlat, vlon );
126  mYv[ci * mMaxCornersGlobal + k] = vlat;
127  mXv[ci * mMaxCornersGlobal + k] = vlon;
128  if( k == 0 )
129  {
130  firstLat = vlat;
131  firstLon = vlon;
132  }
133  }
134  for( int k = numConn; k < mMaxCornersGlobal; ++k )
135  {
136  mYv[ci * mMaxCornersGlobal + k] = firstLat;
137  mXv[ci * mMaxCornersGlobal + k] = firstLon;
138  }
139  cx /= numConn;
140  cy /= numConn;
141  cz /= numConn;
142  xyz_to_latlon_deg( cx, cy, cz, mYc[ci], mXc[ci] );
143  }
144 
145  // Mask is required by the CESM domain schema; pull values from
146  // DOMAIN_MASK if present, otherwise the default-1 mMask buffer
147  // populated above is what gets written.
148  Tag maskTag = 0;
149  if( MB_SUCCESS == mbImpl->tag_get_handle( "DOMAIN_MASK", 1, MB_TYPE_INTEGER, maskTag ) && maskTag )
150  {
151  (void)mbImpl->tag_get_data( maskTag, localCellsOwned, mMask.data() );
152  }
153  mHasMask = true; // schema-required field; always emit
154 
155  Tag areaTag = 0;
156  if( MB_SUCCESS == mbImpl->tag_get_handle( "GRID_AREA", 1, MB_TYPE_DOUBLE, areaTag ) && areaTag )
157  {
158  mAreas.assign( mLocalCells, 0.0 );
159  if( MB_SUCCESS == mbImpl->tag_get_data( areaTag, localCellsOwned, mAreas.data() ) )
160  mHasAreas = true;
161  else
162  mAreas.clear();
163  }
164  Tag fracTag = 0;
165  if( MB_SUCCESS == mbImpl->tag_get_handle( "DOMAIN_FRAC", 1, MB_TYPE_DOUBLE, fracTag ) && fracTag )
166  {
167  mFrac.assign( mLocalCells, 1.0 );
168  if( MB_SUCCESS == mbImpl->tag_get_data( fracTag, localCellsOwned, mFrac.data() ) )
169  mHasFrac = true;
170  else
171  mFrac.clear();
172  }
173  // frac is also conventionally always present in CESM domain files; emit 1.0's by default.
174  if( !mHasFrac )
175  {
176  mFrac.assign( mLocalCells, 1.0 );
177  mHasFrac = true;
178  }
179 
181 #ifdef MOAB_HAVE_MPI
182  if( _writeNC->isParallel && _writeNC->myPcomm )
183  {
184  long localN = mLocalCells;
185  MPI_Allreduce( &localN, &mGlobalCells, 1, MPI_LONG, MPI_SUM,
186  _writeNC->myPcomm->proc_config().proc_comm() );
187  }
188 #endif
189  // Flat layout: nj=1, ni=total cells. Matches the CESM convention for
190  // domain files derived from unstructured meshes (see e.g.
191  // domain.ocn.ne4np4_oQU240 ships with ni=866, nj=1).
192  mNj = 1;
193  mNi = mGlobalCells;
194 
195  dbgOut.tprintf( 1, " Domain write: local_cells=%ld global_cells=%ld ni=%ld nj=%ld max_corners=%d\n", mLocalCells,
197 
198  return MB_SUCCESS;
199 }
200 
201 ErrorCode NCWriteDomain::init_file( std::vector< std::string >& /*var_names*/,
202  std::vector< std::string >& /*desired_names*/,
203  bool /*_append*/ )
204 {
205  if( NCFUNC( def_dim )( _fileId, "n", static_cast< size_t >( mGlobalCells ), &mDimN ) )
206  MB_SET_ERR( MB_FAILURE, "Failed to define n dim" );
207  if( NCFUNC( def_dim )( _fileId, "ni", static_cast< size_t >( mNi ), &mDimNi ) )
208  MB_SET_ERR( MB_FAILURE, "Failed to define ni dim" );
209  if( NCFUNC( def_dim )( _fileId, "nj", static_cast< size_t >( mNj ), &mDimNj ) )
210  MB_SET_ERR( MB_FAILURE, "Failed to define nj dim" );
211  if( NCFUNC( def_dim )( _fileId, "nv", static_cast< size_t >( mMaxCornersGlobal ), &mDimNv ) )
212  MB_SET_ERR( MB_FAILURE, "Failed to define nv dim" );
213 
214  // Variable dims: arrays are (nj, ni) or (nj, ni, nv). j is the slow axis.
215  const int dimsCenter[2] = { mDimNj, mDimNi };
216  const int dimsVertex[3] = { mDimNj, mDimNi, mDimNv };
217 
218  if( NCFUNC( def_var )( _fileId, "xc", NC_DOUBLE, 2, dimsCenter, &mVarXc ) )
219  MB_SET_ERR( MB_FAILURE, "Failed to define xc var" );
220  if( NCFUNC( def_var )( _fileId, "yc", NC_DOUBLE, 2, dimsCenter, &mVarYc ) )
221  MB_SET_ERR( MB_FAILURE, "Failed to define yc var" );
222  if( NCFUNC( def_var )( _fileId, "xv", NC_DOUBLE, 3, dimsVertex, &mVarXv ) )
223  MB_SET_ERR( MB_FAILURE, "Failed to define xv var" );
224  if( NCFUNC( def_var )( _fileId, "yv", NC_DOUBLE, 3, dimsVertex, &mVarYv ) )
225  MB_SET_ERR( MB_FAILURE, "Failed to define yv var" );
226  if( NCFUNC( def_var )( _fileId, "mask", NC_INT, 2, dimsCenter, &mVarMask ) )
227  MB_SET_ERR( MB_FAILURE, "Failed to define mask var" );
228  if( NCFUNC( def_var )( _fileId, "area", NC_DOUBLE, 2, dimsCenter, &mVarArea ) )
229  MB_SET_ERR( MB_FAILURE, "Failed to define area var" );
230  if( NCFUNC( def_var )( _fileId, "frac", NC_DOUBLE, 2, dimsCenter, &mVarFrac ) )
231  MB_SET_ERR( MB_FAILURE, "Failed to define frac var" );
232 
233  // CF-style metadata matching the canonical CESM domain file schema.
234  const char* deg_e = "degrees_east";
235  const char* deg_n = "degrees_north";
236  const char* rad2 = "radian2";
237  const char* xv_name = "longitude of grid cell verticies";
238  const char* yv_name = "latitude of grid cell verticies";
239  const char* xc_name = "longitude of grid cell center";
240  const char* yc_name = "latitude of grid cell center";
241  const char* m_name = "domain mask";
242  const char* a_name = "area of grid cell in radians squared";
243  const char* f_name = "fraction of grid cell that is active";
244 
245  NCFUNC( put_att_text )( _fileId, mVarXc, "long_name", std::strlen( xc_name ), xc_name );
246  NCFUNC( put_att_text )( _fileId, mVarXc, "units", std::strlen( deg_e ), deg_e );
247  NCFUNC( put_att_text )( _fileId, mVarXc, "bounds", 2, "xv" );
248 
249  NCFUNC( put_att_text )( _fileId, mVarYc, "long_name", std::strlen( yc_name ), yc_name );
250  NCFUNC( put_att_text )( _fileId, mVarYc, "units", std::strlen( deg_n ), deg_n );
251  NCFUNC( put_att_text )( _fileId, mVarYc, "bounds", 2, "yv" );
252 
253  NCFUNC( put_att_text )( _fileId, mVarXv, "long_name", std::strlen( xv_name ), xv_name );
254  NCFUNC( put_att_text )( _fileId, mVarXv, "units", std::strlen( deg_e ), deg_e );
255 
256  NCFUNC( put_att_text )( _fileId, mVarYv, "long_name", std::strlen( yv_name ), yv_name );
257  NCFUNC( put_att_text )( _fileId, mVarYv, "units", std::strlen( deg_n ), deg_n );
258 
259  NCFUNC( put_att_text )( _fileId, mVarMask, "long_name", std::strlen( m_name ), m_name );
260  NCFUNC( put_att_text )( _fileId, mVarMask, "note", 8, "unitless" );
261  NCFUNC( put_att_text )( _fileId, mVarMask, "coordinates", 5, "xc yc" );
262  NCFUNC( put_att_text )( _fileId, mVarMask, "comment", 36, "0 value indicates cell is not active" );
263 
264  NCFUNC( put_att_text )( _fileId, mVarArea, "long_name", std::strlen( a_name ), a_name );
265  NCFUNC( put_att_text )( _fileId, mVarArea, "units", std::strlen( rad2 ), rad2 );
266  NCFUNC( put_att_text )( _fileId, mVarArea, "coordinates", 5, "xc yc" );
267 
268  NCFUNC( put_att_text )( _fileId, mVarFrac, "long_name", std::strlen( f_name ), f_name );
269  NCFUNC( put_att_text )( _fileId, mVarFrac, "coordinates", 5, "xc yc" );
270  NCFUNC( put_att_text )( _fileId, mVarFrac, "note", 8, "unitless" );
271 
272  const char* title = "MOAB:NCWriteDomain generated CESM-style domain file";
273  NCFUNC( put_att_text )( _fileId, NC_GLOBAL, "title", std::strlen( title ), title );
274  const char* conv = "CF-1.0";
275  NCFUNC( put_att_text )( _fileId, NC_GLOBAL, "Conventions", std::strlen( conv ), conv );
276 
277  if( NCFUNC( enddef )( _fileId ) ) MB_SET_ERR( MB_FAILURE, "enddef failed for Domain write" );
278 
279  return MB_SUCCESS;
280 }
281 
282 ErrorCode NCWriteDomain::write_values( std::vector< std::string >& /*var_names*/,
283  std::vector< int >& /*tstep_nums*/ )
284 {
285  DebugOutput& dbgOut = _writeNC->dbgOut;
286 
287  auto writeAll = [&]( const std::vector< int >& allGids, const std::vector< double >& allXc,
288  const std::vector< double >& allYc, const std::vector< double >& allXv,
289  const std::vector< double >& allYv, const std::vector< int >& allMask,
290  const std::vector< double >& allAreas, const std::vector< double >& allFrac ) -> ErrorCode {
291  const long N = static_cast< long >( allGids.size() );
292  const int ncpc = mMaxCornersGlobal;
293 
294  // Sort by global id for BfB output across rank counts.
295  std::vector< long > perm( N );
296  for( long i = 0; i < N; ++i )
297  perm[i] = i;
298  std::sort( perm.begin(), perm.end(), [&]( long a, long b ) { return allGids[a] < allGids[b]; } );
299 
300  std::vector< double > sXc( N ), sYc( N ), sXv( N * ncpc ), sYv( N * ncpc );
301  std::vector< int > sMask( N );
302  std::vector< double > sArea( N, 0.0 ), sFrac( N, 1.0 );
303  const bool haveArea = !allAreas.empty();
304  for( long i = 0; i < N; ++i )
305  {
306  const long src = perm[i];
307  sXc[i] = allXc[src];
308  sYc[i] = allYc[src];
309  sMask[i] = allMask[src];
310  if( haveArea ) sArea[i] = allAreas[src];
311  if( !allFrac.empty() ) sFrac[i] = allFrac[src];
312  for( int k = 0; k < ncpc; ++k )
313  {
314  sXv[i * ncpc + k] = allXv[src * ncpc + k];
315  sYv[i * ncpc + k] = allYv[src * ncpc + k];
316  }
317  }
318 
319  // Writes are 2-D (nj=1, ni=N) for centers and mask/area/frac;
320  // 3-D (nj=1, ni=N, nv=ncpc) for the vertex arrays.
321  const size_t s2[2] = { 0, 0 };
322  const size_t c2[2] = { 1, static_cast< size_t >( N ) };
323  const size_t s3[3] = { 0, 0, 0 };
324  const size_t c3[3] = { 1, static_cast< size_t >( N ), static_cast< size_t >( ncpc ) };
325 
326  if( NCFUNCAP( _vara_double )( _fileId, mVarXc, s2, c2, sXc.data() ) )
327  MB_SET_ERR( MB_FAILURE, "Failed to write xc" );
328  if( NCFUNCAP( _vara_double )( _fileId, mVarYc, s2, c2, sYc.data() ) )
329  MB_SET_ERR( MB_FAILURE, "Failed to write yc" );
330  if( NCFUNCAP( _vara_double )( _fileId, mVarXv, s3, c3, sXv.data() ) )
331  MB_SET_ERR( MB_FAILURE, "Failed to write xv" );
332  if( NCFUNCAP( _vara_double )( _fileId, mVarYv, s3, c3, sYv.data() ) )
333  MB_SET_ERR( MB_FAILURE, "Failed to write yv" );
334  if( NCFUNCAP( _vara_int )( _fileId, mVarMask, s2, c2, sMask.data() ) )
335  MB_SET_ERR( MB_FAILURE, "Failed to write mask" );
336  if( NCFUNCAP( _vara_double )( _fileId, mVarArea, s2, c2, sArea.data() ) )
337  MB_SET_ERR( MB_FAILURE, "Failed to write area" );
338  if( NCFUNCAP( _vara_double )( _fileId, mVarFrac, s2, c2, sFrac.data() ) )
339  MB_SET_ERR( MB_FAILURE, "Failed to write frac" );
340  return MB_SUCCESS;
341  };
342 
343 #ifdef MOAB_HAVE_MPI
344  if( _writeNC->isParallel && _writeNC->myPcomm && _writeNC->myPcomm->proc_config().proc_size() > 1 )
345  {
346  MPI_Comm comm = _writeNC->myPcomm->proc_config().proc_comm();
347  const int rank = _writeNC->myPcomm->proc_config().proc_rank();
348  const int size = _writeNC->myPcomm->proc_config().proc_size();
349  const int ncpc = mMaxCornersGlobal;
350 
351  int myN = static_cast< int >( mLocalCells );
352  std::vector< int > counts( size, 0 ), displs( size, 0 );
353  MPI_Gather( &myN, 1, MPI_INT, counts.data(), 1, MPI_INT, 0, comm );
354  if( rank == 0 )
355  {
356  int acc = 0;
357  for( int i = 0; i < size; ++i )
358  {
359  displs[i] = acc;
360  acc += counts[i];
361  }
362  }
363 
364  std::vector< int > allGids;
365  std::vector< double > allXc, allYc, allXv, allYv;
366  std::vector< int > allMask;
367  std::vector< double > allAreas, allFrac;
368  if( rank == 0 )
369  {
370  allGids.resize( mGlobalCells );
371  allXc.resize( mGlobalCells );
372  allYc.resize( mGlobalCells );
373  allXv.resize( static_cast< size_t >( mGlobalCells ) * ncpc );
374  allYv.resize( static_cast< size_t >( mGlobalCells ) * ncpc );
375  allMask.resize( mGlobalCells );
376  if( mHasAreas ) allAreas.resize( mGlobalCells );
377  allFrac.resize( mGlobalCells );
378  }
379 
380  MPI_Gatherv( mLocalGids.data(), myN, MPI_INT, allGids.data(), counts.data(), displs.data(), MPI_INT, 0, comm );
381  MPI_Gatherv( mXc.data(), myN, MPI_DOUBLE, allXc.data(), counts.data(), displs.data(), MPI_DOUBLE, 0, comm );
382  MPI_Gatherv( mYc.data(), myN, MPI_DOUBLE, allYc.data(), counts.data(), displs.data(), MPI_DOUBLE, 0, comm );
383  MPI_Gatherv( mMask.data(), myN, MPI_INT, allMask.data(), counts.data(), displs.data(), MPI_INT, 0, comm );
384  if( mHasAreas )
385  MPI_Gatherv( mAreas.data(), myN, MPI_DOUBLE, allAreas.data(), counts.data(), displs.data(), MPI_DOUBLE, 0,
386  comm );
387  MPI_Gatherv( mFrac.data(), myN, MPI_DOUBLE, allFrac.data(), counts.data(), displs.data(), MPI_DOUBLE, 0, comm );
388 
389  std::vector< int > countsC( size, 0 ), displsC( size, 0 );
390  if( rank == 0 )
391  {
392  int acc = 0;
393  for( int i = 0; i < size; ++i )
394  {
395  countsC[i] = counts[i] * ncpc;
396  displsC[i] = acc;
397  acc += countsC[i];
398  }
399  }
400  MPI_Gatherv( mXv.data(), myN * ncpc, MPI_DOUBLE, allXv.data(), countsC.data(), displsC.data(), MPI_DOUBLE, 0,
401  comm );
402  MPI_Gatherv( mYv.data(), myN * ncpc, MPI_DOUBLE, allYv.data(), countsC.data(), displsC.data(), MPI_DOUBLE, 0,
403  comm );
404 
405  ErrorCode rc = MB_SUCCESS;
406  if( rank == 0 ) rc = writeAll( allGids, allXc, allYc, allXv, allYv, allMask, allAreas, allFrac );
407  int rcInt = static_cast< int >( rc );
408  MPI_Bcast( &rcInt, 1, MPI_INT, 0, comm );
409  if( rcInt != MB_SUCCESS ) MB_SET_ERR( MB_FAILURE, "Domain write failed on rank 0" );
410 
411  dbgOut.tprintf( 1, " Domain write: gathered+wrote %ld cells from %d ranks\n", mGlobalCells, size );
412  return MB_SUCCESS;
413  }
414 #endif
415 
416  return writeAll( mLocalGids, mXc, mYc, mXv, mYv, mMask, mAreas, mFrac );
417 }
418 
419 ErrorCode NCWriteDomain::write_nonset_variables( std::vector< WriteNC::VarData >& /*vdatas*/,
420  std::vector< int >& /*tstep_nums*/ )
421 {
422  return MB_SUCCESS;
423 }
424 
425 } // namespace moab