Mesh Oriented datABase  (version 5.6.0)
An array-based unstructured mesh library
NCWriteScrip.cpp
Go to the documentation of this file.
1 //-------------------------------------------------------------------------
2 // Filename : NCWriteScrip.cpp
3 //
4 // Purpose : Implementation of the SCRIP grid file writer. See
5 // NCWriteScrip.hpp for design notes.
6 //
7 // Creator : Vijay Mahadevan, 2026-06-13
8 //-------------------------------------------------------------------------
9 
10 #include "NCWriteScrip.hpp"
11 #include "MBTagConventions.hpp"
12 
13 #include <algorithm>
14 #include <cmath>
15 #include <cstring>
16 
17 namespace moab
18 {
19 
21 
22 namespace
23 {
24 
25 constexpr double kPi = 3.14159265358979323846;
26 constexpr double kRadToDegree = 180.0 / kPi;
27 
28 /// Convert Cartesian XYZ (on or near the unit sphere) to lat/lon in
29 /// degrees. Normalizes internally so callers don't have to. Longitude
30 /// is shifted to [0, 360).
31 inline void xyz_to_latlon_deg( double x, double y, double z, double& lat, double& lon )
32 {
33  const double r = std::sqrt( x * x + y * y + z * z );
34  if( r < 1.0e-30 )
35  {
36  lat = 0.0;
37  lon = 0.0;
38  return;
39  }
40  lat = std::asin( z / r ) * kRadToDegree;
41  lon = std::atan2( y, x ) * kRadToDegree;
42  if( lon < 0.0 ) lon += 360.0;
43 }
44 
45 } // namespace
46 
47 // ============================================================================
48 // collect_mesh_info: gather local owned cells, compute per-cell center and
49 // corner lat/lon arrays, find the global max-corners value via Allreduce.
50 // ============================================================================
52 {
53  Interface*& mbImpl = _writeNC->mbImpl;
54  Tag& mGlobalIdTag = _writeNC->mGlobalIdTag;
55  DebugOutput& dbgOut = _writeNC->dbgOut;
56 
57  // Pick up all 2-D cells (polygonal: tris, quads, polygons).
58  Range allCells;
59  MB_CHK_SET_ERR( mbImpl->get_entities_by_dimension( _fileSet, 2, allCells ),
60  "Failed to gather 2-D cells for SCRIP write" );
61  if( allCells.empty() ) MB_SET_ERR( MB_FAILURE, "No 2-D cells in file set; cannot write SCRIP grid" );
62 
63  // In parallel, restrict to owned cells so each rank contributes
64  // exactly its share (no double-writing of ghosts).
65  localCellsOwned = allCells;
66 #ifdef MOAB_HAVE_MPI
67  bool& isParallel = _writeNC->isParallel;
68  if( isParallel )
69  {
70  ParallelComm*& myPcomm = _writeNC->myPcomm;
71  if( myPcomm && myPcomm->proc_config().proc_size() > 1 )
72  {
74  "Failed to filter owned cells for SCRIP write" );
75  }
76  }
77 #endif
78 
79  mLocalCells = static_cast< long >( localCellsOwned.size() );
80 
81  // Allreduce to find the global maximum corner count so every rank's
82  // arrays are sized identically — required for the final write because
83  // grid_corner_lon/lat is a 2-D variable.
84  int localMaxCorners = 0;
85  for( Range::iterator cit = localCellsOwned.begin(); cit != localCellsOwned.end(); ++cit )
86  {
87  const EntityHandle* conn = nullptr;
88  int numConn = 0;
89  ErrorCode rc = mbImpl->get_connectivity( *cit, conn, numConn );
90  if( MB_SUCCESS == rc && numConn > localMaxCorners ) localMaxCorners = numConn;
91  }
92 #ifdef MOAB_HAVE_MPI
93  if( _writeNC->isParallel && _writeNC->myPcomm )
94  {
95  MPI_Allreduce( &localMaxCorners, &mMaxCornersGlobal, 1, MPI_INT, MPI_MAX,
96  _writeNC->myPcomm->proc_config().proc_comm() );
97  }
98  else
99 #endif
100  {
101  mMaxCornersGlobal = localMaxCorners;
102  }
103  if( mMaxCornersGlobal <= 0 )
104  MB_SET_ERR( MB_FAILURE, "Mesh contains cells but all reported zero connectivity; cannot write SCRIP" );
105 
106  // Pre-allocate per-cell arrays.
107  mLocalGids.resize( mLocalCells );
108  mCenterLon.resize( mLocalCells );
109  mCenterLat.resize( mLocalCells );
110  mCornerLon.assign( static_cast< size_t >( mLocalCells ) * mMaxCornersGlobal, 0.0 );
111  mCornerLat.assign( static_cast< size_t >( mLocalCells ) * mMaxCornersGlobal, 0.0 );
112  mImask.assign( mLocalCells, 1 );
113 
114  // Optional: pull a GRID_IMASK tag if it exists (matches the reader).
115  Tag maskTag = 0;
116  if( MB_SUCCESS == mbImpl->tag_get_handle( "GRID_IMASK", 1, MB_TYPE_INTEGER, maskTag ) && maskTag )
117  {
118  // Read per-cell mask; if call fails (e.g. tag isn't densely set),
119  // just keep the default 1's — the mask field is still well-formed.
120  ErrorCode mrc = mbImpl->tag_get_data( maskTag, localCellsOwned, mImask.data() );
121  if( MB_SUCCESS != mrc )
122  {
123  dbgOut.tprintf( 1, " GRID_IMASK tag exists but read failed; writing all-1 mask\n" );
124  std::fill( mImask.begin(), mImask.end(), 1 );
125  }
126  }
127 
128  // Optional: cell area tag.
129  Tag areaTag = 0;
130  if( MB_SUCCESS == mbImpl->tag_get_handle( "GRID_AREA", 1, MB_TYPE_DOUBLE, areaTag ) && areaTag )
131  {
132  mAreas.assign( mLocalCells, 0.0 );
133  if( MB_SUCCESS == mbImpl->tag_get_data( areaTag, localCellsOwned, mAreas.data() ) )
134  mHasAreas = true;
135  else
136  mAreas.clear();
137  }
138 
139  // Fill the global-id, center, and corner arrays.
140  long ci = 0;
141  for( Range::iterator cit = localCellsOwned.begin(); cit != localCellsOwned.end(); ++cit, ++ci )
142  {
143  // Global ID for this cell — used to make the on-disk ordering
144  // deterministic after a gather to rank 0.
145  int gid = 0;
146  if( mGlobalIdTag ) mbImpl->tag_get_data( mGlobalIdTag, &( *cit ), 1, &gid );
147  mLocalGids[ci] = gid;
148 
149  const EntityHandle* conn = nullptr;
150  int numConn = 0;
151  MB_CHK_SET_ERR( mbImpl->get_connectivity( *cit, conn, numConn ), "Cell connectivity lookup failed" );
152 
153  // Accumulate XYZ for the cell centroid as we go.
154  double cx = 0.0, cy = 0.0, cz = 0.0;
155  // First-corner fallback used to pad smaller polygons up to the
156  // global max corner count (standard SCRIP convention).
157  double firstLat = 0.0, firstLon = 0.0;
158 
159  for( int k = 0; k < numConn; ++k )
160  {
161  double vc[3] = { 0.0, 0.0, 0.0 };
162  MB_CHK_SET_ERR( mbImpl->get_coords( &conn[k], 1, vc ), "Vertex coordinate lookup failed" );
163  cx += vc[0];
164  cy += vc[1];
165  cz += vc[2];
166  double vlat = 0.0, vlon = 0.0;
167  xyz_to_latlon_deg( vc[0], vc[1], vc[2], vlat, vlon );
168  mCornerLat[ci * mMaxCornersGlobal + k] = vlat;
169  mCornerLon[ci * mMaxCornersGlobal + k] = vlon;
170  if( k == 0 )
171  {
172  firstLat = vlat;
173  firstLon = vlon;
174  }
175  }
176  // Pad unused corners with the first corner (SCRIP convention).
177  for( int k = numConn; k < mMaxCornersGlobal; ++k )
178  {
179  mCornerLat[ci * mMaxCornersGlobal + k] = firstLat;
180  mCornerLon[ci * mMaxCornersGlobal + k] = firstLon;
181  }
182 
183  // Center: average XYZ projected to the sphere.
184  cx /= numConn;
185  cy /= numConn;
186  cz /= numConn;
187  xyz_to_latlon_deg( cx, cy, cz, mCenterLat[ci], mCenterLon[ci] );
188  }
189 
190  // Global cell count for the file-level grid_size dim.
192 #ifdef MOAB_HAVE_MPI
193  if( _writeNC->isParallel && _writeNC->myPcomm )
194  {
195  long localN = mLocalCells;
196  MPI_Allreduce( &localN, &mGlobalCells, 1, MPI_LONG, MPI_SUM,
197  _writeNC->myPcomm->proc_config().proc_comm() );
198  }
199 #endif
200 
201  dbgOut.tprintf( 1, " SCRIP write: local_cells=%ld global_cells=%ld max_corners=%d hasAreas=%d\n", mLocalCells,
203 
204  return MB_SUCCESS;
205 }
206 
207 // ============================================================================
208 // init_file: define dimensions and variables for the SCRIP schema.
209 // All ranks call collectively — the dispatch layer routes the def_dim /
210 // def_var calls to the right backend.
211 // ============================================================================
212 ErrorCode NCWriteScrip::init_file( std::vector< std::string >& /*var_names*/,
213  std::vector< std::string >& /*desired_names*/,
214  bool /*_append*/ )
215 {
216  // Dimensions
217  MB_CHK_SET_ERR( ( NCFUNC( def_dim )( _fileId, "grid_size", static_cast< size_t >( mGlobalCells ), &mDimGridSize ) )
218  ? MB_FAILURE
219  : MB_SUCCESS,
220  "Failed to define grid_size dimension" );
222  ( NCFUNC( def_dim )( _fileId, "grid_corners", static_cast< size_t >( mMaxCornersGlobal ), &mDimGridCorners ) )
223  ? MB_FAILURE
224  : MB_SUCCESS,
225  "Failed to define grid_corners dimension" );
226  MB_CHK_SET_ERR( ( NCFUNC( def_dim )( _fileId, "grid_rank", static_cast< size_t >( 1 ), &mDimGridRank ) )
227  ? MB_FAILURE
228  : MB_SUCCESS,
229  "Failed to define grid_rank dimension" );
230 
231  int dim1[1] = { mDimGridRank };
232  int dim1c[1] = { mDimGridSize };
233  int dim2[2] = { mDimGridSize, mDimGridCorners };
234 
235  // Variables — match NCHelperScrip's reader expectations.
237  ( NCFUNC( def_var )( _fileId, "grid_dims", NC_INT, 1, dim1, &mVarGridDims ) ) ? MB_FAILURE : MB_SUCCESS,
238  "Failed to define grid_dims variable" );
239 
240  MB_CHK_SET_ERR( ( NCFUNC( def_var )( _fileId, "grid_center_lat", NC_DOUBLE, 1, dim1c, &mVarCenterLat ) )
241  ? MB_FAILURE
242  : MB_SUCCESS,
243  "Failed to define grid_center_lat variable" );
244  MB_CHK_SET_ERR( ( NCFUNC( def_var )( _fileId, "grid_center_lon", NC_DOUBLE, 1, dim1c, &mVarCenterLon ) )
245  ? MB_FAILURE
246  : MB_SUCCESS,
247  "Failed to define grid_center_lon variable" );
248  MB_CHK_SET_ERR( ( NCFUNC( def_var )( _fileId, "grid_corner_lat", NC_DOUBLE, 2, dim2, &mVarCornerLat ) )
249  ? MB_FAILURE
250  : MB_SUCCESS,
251  "Failed to define grid_corner_lat variable" );
252  MB_CHK_SET_ERR( ( NCFUNC( def_var )( _fileId, "grid_corner_lon", NC_DOUBLE, 2, dim2, &mVarCornerLon ) )
253  ? MB_FAILURE
254  : MB_SUCCESS,
255  "Failed to define grid_corner_lon variable" );
257  ( NCFUNC( def_var )( _fileId, "grid_imask", NC_INT, 1, dim1c, &mVarImask ) ) ? MB_FAILURE : MB_SUCCESS,
258  "Failed to define grid_imask variable" );
259 
260  if( mHasAreas )
261  {
263  ( NCFUNC( def_var )( _fileId, "grid_area", NC_DOUBLE, 1, dim1c, &mVarArea ) ) ? MB_FAILURE : MB_SUCCESS,
264  "Failed to define grid_area variable" );
265  }
266 
267  // Unit attributes — readers (including NCHelperScrip) key off these.
268  const char* deg = "degrees";
269  NCFUNC( put_att_text )( _fileId, mVarCenterLat, "units", 7, deg );
270  NCFUNC( put_att_text )( _fileId, mVarCenterLon, "units", 7, deg );
271  NCFUNC( put_att_text )( _fileId, mVarCornerLat, "units", 7, deg );
272  NCFUNC( put_att_text )( _fileId, mVarCornerLon, "units", 7, deg );
273  if( mHasAreas )
274  {
275  const char* rad2 = "radians^2";
276  NCFUNC( put_att_text )( _fileId, mVarArea, "units", 9, rad2 );
277  }
278 
279  // Global title attribute mirrors what the reader looks for in a few places.
280  const char* title = "MOAB:NCWriteScrip generated SCRIP grid file";
281  NCFUNC( put_att_text )( _fileId, NC_GLOBAL, "title", std::strlen( title ), title );
282 
283  // Close define mode so subsequent writes are data-mode operations.
284  int endrc = NCFUNC( enddef )( _fileId );
285  if( endrc ) MB_SET_ERR( MB_FAILURE, "enddef failed for SCRIP write" );
286 
287  return MB_SUCCESS;
288 }
289 
290 // ============================================================================
291 // write_values: gather per-rank arrays to rank 0, sort by global id for
292 // deterministic ordering, then have rank 0 write the entire SCRIP grid
293 // via the dispatch layer.
294 //
295 // SCRIP grids are small enough (cells, not climate fields) that the
296 // gather-and-write pattern is fine; full parallel collective writes are
297 // a future enhancement if the file size ever grows large enough to matter.
298 // ============================================================================
299 ErrorCode NCWriteScrip::write_values( std::vector< std::string >& /*var_names*/,
300  std::vector< int >& /*tstep_nums*/ )
301 {
302  DebugOutput& dbgOut = _writeNC->dbgOut;
303 
304  // Helper to assemble the global-ordered arrays on rank 0.
305  auto sortAndWrite = [&]( const std::vector< int >& allGids, const std::vector< double >& allCenterLat,
306  const std::vector< double >& allCenterLon, const std::vector< double >& allCornerLat,
307  const std::vector< double >& allCornerLon, const std::vector< int >& allImask,
308  const std::vector< double >& allAreas ) -> ErrorCode {
309  const long N = static_cast< long >( allGids.size() );
310  const int ncpc = mMaxCornersGlobal;
311 
312  // Permutation by global id for BfB output across rank counts.
313  std::vector< long > perm( N );
314  for( long i = 0; i < N; ++i )
315  perm[i] = i;
316  std::sort( perm.begin(), perm.end(),
317  [&]( long a, long b ) { return allGids[a] < allGids[b]; } );
318 
319  std::vector< double > sCenterLat( N ), sCenterLon( N ), sCornerLat( N * ncpc ), sCornerLon( N * ncpc );
320  std::vector< int > sImask( N );
321  std::vector< double > sAreas;
322  if( !allAreas.empty() ) sAreas.resize( N );
323 
324  for( long i = 0; i < N; ++i )
325  {
326  const long src = perm[i];
327  sCenterLat[i] = allCenterLat[src];
328  sCenterLon[i] = allCenterLon[src];
329  sImask[i] = allImask[src];
330  if( !sAreas.empty() ) sAreas[i] = allAreas[src];
331  for( int k = 0; k < ncpc; ++k )
332  {
333  sCornerLat[i * ncpc + k] = allCornerLat[src * ncpc + k];
334  sCornerLon[i * ncpc + k] = allCornerLon[src * ncpc + k];
335  }
336  }
337 
338  // grid_dims = [grid_size]
339  int gridDimsValue = static_cast< int >( N );
340  size_t startD = 0, countD = 1;
341  if( NCFUNCAP( _vara_int )( _fileId, mVarGridDims, &startD, &countD, &gridDimsValue ) )
342  MB_SET_ERR( MB_FAILURE, "Failed to write grid_dims" );
343 
344  size_t s1 = 0, c1 = static_cast< size_t >( N );
345  if( NCFUNCAP( _vara_double )( _fileId, mVarCenterLat, &s1, &c1, sCenterLat.data() ) )
346  MB_SET_ERR( MB_FAILURE, "Failed to write grid_center_lat" );
347  if( NCFUNCAP( _vara_double )( _fileId, mVarCenterLon, &s1, &c1, sCenterLon.data() ) )
348  MB_SET_ERR( MB_FAILURE, "Failed to write grid_center_lon" );
349  if( NCFUNCAP( _vara_int )( _fileId, mVarImask, &s1, &c1, sImask.data() ) )
350  MB_SET_ERR( MB_FAILURE, "Failed to write grid_imask" );
351  if( !sAreas.empty() )
352  {
353  if( NCFUNCAP( _vara_double )( _fileId, mVarArea, &s1, &c1, sAreas.data() ) )
354  MB_SET_ERR( MB_FAILURE, "Failed to write grid_area" );
355  }
356 
357  const size_t s2[2] = { 0, 0 };
358  const size_t c2[2] = { static_cast< size_t >( N ), static_cast< size_t >( ncpc ) };
359  if( NCFUNCAP( _vara_double )( _fileId, mVarCornerLat, s2, c2, sCornerLat.data() ) )
360  MB_SET_ERR( MB_FAILURE, "Failed to write grid_corner_lat" );
361  if( NCFUNCAP( _vara_double )( _fileId, mVarCornerLon, s2, c2, sCornerLon.data() ) )
362  MB_SET_ERR( MB_FAILURE, "Failed to write grid_corner_lon" );
363 
364  return MB_SUCCESS;
365  };
366 
367 #ifdef MOAB_HAVE_MPI
368  if( _writeNC->isParallel && _writeNC->myPcomm && _writeNC->myPcomm->proc_config().proc_size() > 1 )
369  {
370  MPI_Comm comm = _writeNC->myPcomm->proc_config().proc_comm();
371  const int rank = _writeNC->myPcomm->proc_config().proc_rank();
372  const int size = _writeNC->myPcomm->proc_config().proc_size();
373  const int ncpc = mMaxCornersGlobal;
374 
375  // Counts of cells per rank (Gatherv setup).
376  int myN = static_cast< int >( mLocalCells );
377  std::vector< int > counts( size, 0 ), displs( size, 0 );
378  MPI_Gather( &myN, 1, MPI_INT, counts.data(), 1, MPI_INT, 0, comm );
379  if( rank == 0 )
380  {
381  int acc = 0;
382  for( int i = 0; i < size; ++i )
383  {
384  displs[i] = acc;
385  acc += counts[i];
386  }
387  }
388 
389  // Per-cell gather targets on rank 0.
390  std::vector< int > allGids;
391  std::vector< double > allCenterLat, allCenterLon, allCornerLat, allCornerLon;
392  std::vector< int > allImask;
393  std::vector< double > allAreas;
394  if( rank == 0 )
395  {
396  allGids.resize( mGlobalCells );
397  allCenterLat.resize( mGlobalCells );
398  allCenterLon.resize( mGlobalCells );
399  allCornerLat.resize( static_cast< size_t >( mGlobalCells ) * ncpc );
400  allCornerLon.resize( static_cast< size_t >( mGlobalCells ) * ncpc );
401  allImask.resize( mGlobalCells );
402  if( mHasAreas ) allAreas.resize( mGlobalCells );
403  }
404 
405  MPI_Gatherv( mLocalGids.data(), myN, MPI_INT, allGids.data(), counts.data(), displs.data(), MPI_INT, 0, comm );
406  MPI_Gatherv( mCenterLat.data(), myN, MPI_DOUBLE, allCenterLat.data(), counts.data(), displs.data(), MPI_DOUBLE,
407  0, comm );
408  MPI_Gatherv( mCenterLon.data(), myN, MPI_DOUBLE, allCenterLon.data(), counts.data(), displs.data(), MPI_DOUBLE,
409  0, comm );
410  MPI_Gatherv( mImask.data(), myN, MPI_INT, allImask.data(), counts.data(), displs.data(), MPI_INT, 0, comm );
411 
412  // Corner arrays have a different per-rank count (myN * ncpc), so
413  // build a second counts/displs.
414  std::vector< int > countsC( size, 0 ), displsC( size, 0 );
415  if( rank == 0 )
416  {
417  int acc = 0;
418  for( int i = 0; i < size; ++i )
419  {
420  countsC[i] = counts[i] * ncpc;
421  displsC[i] = acc;
422  acc += countsC[i];
423  }
424  }
425  MPI_Gatherv( mCornerLat.data(), myN * ncpc, MPI_DOUBLE, allCornerLat.data(), countsC.data(), displsC.data(),
426  MPI_DOUBLE, 0, comm );
427  MPI_Gatherv( mCornerLon.data(), myN * ncpc, MPI_DOUBLE, allCornerLon.data(), countsC.data(), displsC.data(),
428  MPI_DOUBLE, 0, comm );
429  if( mHasAreas )
430  {
431  MPI_Gatherv( mAreas.data(), myN, MPI_DOUBLE, allAreas.data(), counts.data(), displs.data(), MPI_DOUBLE, 0,
432  comm );
433  }
434 
435  // Rank 0 writes. Other ranks no-op (their file handle is still
436  // open via the dispatch layer; close happens later in WriteNC).
437  ErrorCode rc = MB_SUCCESS;
438  if( rank == 0 )
439  {
440  rc = sortAndWrite( allGids, allCenterLat, allCenterLon, allCornerLat, allCornerLon, allImask, allAreas );
441  }
442  int rcInt = static_cast< int >( rc );
443  MPI_Bcast( &rcInt, 1, MPI_INT, 0, comm );
444  if( rcInt != MB_SUCCESS ) MB_SET_ERR( MB_FAILURE, "SCRIP write failed on rank 0" );
445 
446  dbgOut.tprintf( 1, " SCRIP write: gathered+wrote %ld cells from %d ranks\n", mGlobalCells, size );
447  return MB_SUCCESS;
448  }
449 #endif
450 
451  // Serial path: rank-0-only arrays are already complete; sort + write.
452  return sortAndWrite( mLocalGids, mCenterLat, mCenterLon, mCornerLat, mCornerLon, mImask, mAreas );
453 }
454 
455 // SCRIP grid files don't have time-varying "nonset" variables; the abstract
456 // base requires us to implement this hook even though it has nothing to do.
457 ErrorCode NCWriteScrip::write_nonset_variables( std::vector< WriteNC::VarData >& /*vdatas*/,
458  std::vector< int >& /*tstep_nums*/ )
459 {
460  return MB_SUCCESS;
461 }
462 
463 } // namespace moab