Mesh Oriented datABase  (version 5.6.0)
An array-based unstructured mesh library
mbtempest.cpp
Go to the documentation of this file.
1 /**
2  * @file mbtempest.cpp
3  * @brief MOAB-Tempest: A powerful mesh generation and remapping tool for climate and weather applications
4  *
5  * @section overview Overview
6  * MOAB-Tempest is a command-line tool that provides mesh generation and conservative remapping capabilities
7  * for climate and weather modeling. It combines the power of MOAB (Mesh-Oriented datABase) with the
8  * TempestRemap library to enable high-performance, parallel mesh generation and remapping operations.
9  *
10  * @section features Key Features
11  * - Generation of various spherical mesh types (Cubed-Sphere, RLL, Icosahedral)
12  * - Support for high-order discretization methods (FV, CGLL, DGLL)
13  * - Conservative remapping between different mesh types
14  * - Parallel processing support via MPI
15  * - Flexible I/O with support for multiple file formats
16  * - Built-in analytical functions for testing and validation
17  *
18  * @section algorithms Supported Algorithms
19  * - Mesh Generation:
20  * - Cubed-Sphere (CS) meshes
21  * - Regular Latitude-Longitude (RLL) meshes
22  * - Icosahedral (ICO) meshes
23  * - Overlap meshes for remapping
24  * - Remapping Methods:
25  * - Finite Volume (FV)
26  * - Continuous Galerkin (CGLL)
27  * - Discontinuous Galerkin (DGLL)
28  * - Monotonic and high-order variants
29  *
30  * @section usage Basic Usage Examples
31  * @code
32  * # Generate a Cubed-Sphere mesh with resolution 25
33  * ./mbtempest --type 0 --res 25 --file cubed_sphere_mesh.h5m
34  *
35  * # Generate a RLL mesh with resolution 90x180 (lon x lat)
36  * ./mbtempest --type 1 --res 90 --file rll_mesh.h5m
37  *
38  * # Generate an Icosahedral mesh with resolution 25 (dual mesh)
39  * ./mbtempest --type 2 --res 25 --dual --file icosahedral_dual_mesh.h5m
40  *
41  * # Compute overlap between two meshes
42  * ./mbtempest --type 5 --load mesh1.h5m --load mesh2.h5m intx intersection_mesh.h5m
43  *
44  * # Generate a remapping weights file between two meshes: FV to FV (default)
45  * ./mbtempest --type 5 --load source_mesh.h5m --load target_mesh.h5m --file weights.nc
46  *
47  * # Generate remapping weights file between two meshes: making it explicit (SE to FV)
48  * ./mbtempest --type 5 --load source_mesh.h5m --load target_mesh.h5m \
49  * --order 4 --method cgll --global_id GLOBAL_DOFS \
50  * --order 1 --method fv --global_id GLOBAL_ID \
51  * --file weights_se_to_fv.nc
52  * @endcode
53  *
54  * @section options Command Line Options
55  * Run './mbtempest --help' for a complete list of available options.
56  *
57  * @section notes Notes
58  * - For parallel execution, use MPI launcher (e.g., mpirun, mpiexec)
59  * - Output formats: .h5m (MOAB), .nc (NetCDF), .exo (ExodusII)
60  * - Requires MOAB and TempestRemap libraries
61  *
62  * @author MOAB Development Team
63  * @date Created: 2023
64  */
65 
66 // standard C++ includes
67 #include <iostream>
68 #include <iomanip>
69 #include <cstdlib>
70 #include <vector>
71 #include <string>
72 #include <memory>
73 #include <sstream>
74 #include <cassert>
75 
76 // MOAB includes
77 #include "moab/Core.hpp"
81 #include "moab/ProgOptions.hpp"
82 #include "moab/CpuTimer.hpp"
83 #include "DebugOutput.hpp"
84 
85 #ifdef MOAB_HAVE_MPI
86 // MPI includes
87 #include "moab_mpi.h"
88 #include "moab/ParallelComm.hpp"
89 #include "MBParallelConventions.h"
90 #endif
91 
92 /**
93  * @brief Context class for MOAB-TempestRemap tool configuration and state management
94  */
96 {
97  public:
98  // Core components
99  moab::Core* const mbcore; ///< MOAB Core instance for mesh operations
100 #ifdef MOAB_HAVE_MPI
101  moab::ParallelComm* const pcomm; ///< Parallel communicator (nullptr in serial)
102 #endif
103  const int proc_id; ///< MPI process rank (0 for serial)
104  const int n_procs; ///< Total number of MPI processes (1 for serial)
105  moab::DebugOutput outputFormatter; ///< Formatter for debug output
106 
107  // Mesh and remapping configuration
109  std::vector< std::string > inFilenames; ///< Input filenames for source and target meshes
110  std::vector< int > disc_orders; ///< Discretization orders for source and target
111  std::vector< std::string > disc_methods; ///< Discretization methods (fv, cgll, dgll) for source and target
112  std::vector< std::string > doftag_names; ///< Degree of freedom tag names for source and target
113  std::string outFilename{ "outputFile.nc" }; ///< Output filename for remapping results
114  std::string intxFilename; ///< Intersection mesh filename (optional)
115  std::string baselineFile; ///< Baseline file for verification (optional)
116  std::string variableToVerify; ///< Variable name for verification (optional)
117  std::string fvMethod{ "none" }; ///< Finite volume method specification
118 
119  // Remapping options
120  GenerateOfflineMapAlgorithmOptions mapOptions; ///< Configuration for offline map generation
122  moab::TempestOnlineMap::CAAS_NONE }; ///< Conservative and accurate advection scheme type
123  int ensureMonotonicity{ 0 }; ///< Monotonicity enforcement level (0=none, 1=basic, 2=full, 3=strict)
124  bool rrmGrids{ false }; ///< Flag to use RRM (Regional Refinement Meshes)
125  bool kdtreeSearch{ true }; ///< Enable KD-tree for spatial searches
126  bool advFrontSearch{ false }; ///< Set by "--advfront/-a"; disables kdtreeSearch after parsing
127  bool fCheck{ false }; ///< Enable additional checking during remapping
128  bool fVolumetric{ false }; ///< Enable volumetric (3D) remapping
129  bool useGnomonicProjection{ false }; ///< Use gnomonic projection for certain operations
130  bool print_diagnostics{ false }; ///< Print detailed diagnostic information
131  bool skip_intersection{ false }; ///< Skip intersection computation (for debugging)
132  double boxeps{ 1e-7 }; ///< Epsilon for bounding box checks
133  double epsrel{ ReferenceTolerance }; ///< Relative tolerance for convergence
134  std::string areaMethodName{ "vos" }; ///< Spherical area formula (see --area_method)
136 
137  // Mesh operations control
138  bool skip_io{ false }; ///< Skip file I/O operations (for testing)
139  bool computeDual{ false }; ///< Compute dual mesh
140  bool computeWeights{ false }; ///< Compute interpolation weights
141  bool verifyConservation{ false }; ///< Verify conservation properties
142  bool verifyWeights{ false }; ///< Verify interpolation weights
143  bool enforceConvexity{ false }; ///< Enforce convexity in mesh elements
144 
145  // Performance and debugging
146  std::unique_ptr< moab::CpuTimer > timer; ///< Timer for performance measurement
147  double timer_ops{ 0.0 }; ///< Operation timer value
148  std::string opName; ///< Name of current operation being timed
149  int nlayers{ 0 }; ///< Number of ghost layers for parallel operations
150  int blockSize{ 5 }; ///< Block size for vectorized operations
151 
152  // Mesh data
153  std::vector< Mesh* > meshes; ///< Collection of TempestRemap meshes
154  std::vector< moab::EntityHandle > meshsets; ///< MOAB entity sets for meshes
155 
156  /**
157  * @brief Construct a new ToolContext object with MPI support
158  * @param icore MOAB Core instance (must not be null)
159  * @param p_pcomm Parallel communicator (must not be null in MPI mode)
160  * @throw std::invalid_argument if icore is null or p_pcomm is null in MPI mode
161  */
162 #ifdef MOAB_HAVE_MPI
163  ToolContext( moab::Core* icore, moab::ParallelComm* p_pcomm )
164  : mbcore( icore ), pcomm( p_pcomm ), proc_id( p_pcomm ? p_pcomm->rank() : 0 ),
165  n_procs( p_pcomm ? p_pcomm->size() : 1 ), outputFormatter( std::cout, p_pcomm ? p_pcomm->rank() : 0, 0 )
166  {
167  if( !icore ) throw std::invalid_argument( "MOAB Core instance cannot be null" );
168  if( !p_pcomm ) throw std::invalid_argument( "ParallelComm cannot be null in MPI mode" );
169 #else
170  /**
171  * @brief Construct a new ToolContext object (serial version)
172  * @param icore MOAB Core instance (must not be null)
173  * @throw std::invalid_argument if icore is null
174  */
175  explicit ToolContext( moab::Core* icore )
176  : mbcore( icore ), proc_id( 0 ), n_procs( 1 ), outputFormatter( std::cout, 0, 0 )
177  {
178 #endif
179  // Initialize default values
180  inFilenames.reserve( 2 );
181  doftag_names = { "GLOBAL_ID", "GLOBAL_ID" };
182  disc_orders = { 1, 1 };
183  disc_methods = { "fv", "fv" };
184 
185  // Initialize timer and output formatter
186  timer = std::make_unique< moab::CpuTimer >();
187  outputFormatter.set_prefix( "[MBTempest]: " );
188 
189  // Set default map options
190  mapOptions.fNoConservation = false;
191  mapOptions.fMonotone = false;
192  mapOptions.fNoCorrectAreas = false;
193  mapOptions.fNoCheck = false;
194  mapOptions.nPin = 1;
195  mapOptions.nPout = 1;
196  }
197 
198  // Rule of Five - Delete copy/move operations as mbcore is const
199  ~ToolContext() = default;
200  ToolContext( const ToolContext& ) = delete;
201  ToolContext& operator=( const ToolContext& ) = delete;
202  ToolContext( ToolContext&& ) = delete;
204 
205  /**
206  * @brief Start timing an operation
207  * @param operation Name of the operation being timed
208  */
209  void timer_push( const std::string& operation )
210  {
211  timer_ops = timer->time_since_birth();
212  opName = operation;
213  }
214 
215  /**
216  * @brief Stop timing and log the operation duration
217  */
218  void timer_pop()
219  {
220  double locElapsed = timer->time_since_birth() - timer_ops;
221  double avgElapsed = locElapsed;
222  double maxElapsed = locElapsed;
223 
224 #ifdef MOAB_HAVE_MPI
225  MPI_Reduce( &locElapsed, &maxElapsed, 1, MPI_DOUBLE, MPI_MAX, 0, pcomm->comm() );
226  MPI_Reduce( &locElapsed, &avgElapsed, 1, MPI_DOUBLE, MPI_SUM, 0, pcomm->comm() );
227  avgElapsed /= n_procs;
228 #endif
229 
230  if( proc_id == 0 )
231  {
232  std::cout << "[LOG] Time taken to " << opName << ": max = " << maxElapsed << ", avg = " << avgElapsed
233  << "\n";
234  }
235  opName.clear();
236  }
237 
238  /**
239  * @brief Parse command line arguments
240  * @param argc Argument count
241  * @param argv Argument values
242  * @return moab::ErrorCode indicating success or failure
243  * @throw std::invalid_argument for invalid command line arguments
244  */
245  moab::ErrorCode ParseCLOptions( int argc, char** argv )
246  {
247  // Initialize variables for command line options
248  int imeshType = 0;
249  std::string expectedFName = "output.exo";
250  std::string expectedMethod = "fv";
251  std::string expectedFVMethod = "none";
252  std::string expectedDofTagName = "GLOBAL_ID";
253  int expectedOrder = 1;
254  int useCAAS = 0;
255  int nlayer_input = -1; // -1 means not set by user
256  bool version_info = false;
257 
258  // Print command line for debugging
259  if( proc_id == 0 )
260  {
261  std::cout << "Command line options provided to mbtempest:\n ";
262  for( int i = 0; i < argc; ++i )
263  {
264  std::cout << argv[i] << " ";
265  }
266  std::cout << "\n" << std::endl;
267  }
268 
269  // Create options object with description
270  ProgOptions opts( "mbtempest - A mesh generation and remapping tool" );
271 
272  // Mesh generation options
273  opts.addOpt< int >( "type,t",
274  "Type of mesh (default=CS; Choose from [CS=0, RLL=1, ICO=2, OVERLAP_FILES=3, "
275  "OVERLAP_MEMORY=4, OVERLAP_MOAB=5])",
276  &imeshType );
277 
278  opts.addOpt< int >( "res,r", "Resolution of the mesh (default=5)", &blockSize );
279 
280  opts.addOpt< void >( "dual,d", "Output the dual of the mesh (relevant only for ICO mesh type)", &computeDual );
281 
282  opts.addOpt< std::string >( "file,f", "Output computed mesh or remapping weights to specified filename",
283  &outFilename );
284 
285  // Input/Output options
286  opts.addOpt< std::string >(
287  "load,l", "Input mesh filenames for source and target meshes. (relevant only when computing weights)",
288  &expectedFName );
289 
290  // NOTE: addOpt<void> sets the target bool to true when the flag is present.
291  // Pointing it at kdtreeSearch (which already defaults to true) made "-a" a no-op,
292  // so the advancing-front algorithm could never be selected. Use a separate flag
293  // and invert it after parsing instead.
294  opts.addOpt< void >( "advfront,a",
295  "Use the advancing front intersection instead of the Kd-tree based algorithm to compute "
296  "mesh intersections.",
297  &advFrontSearch );
298 
299  opts.addOpt< std::string >( "intx,i", "Output TempestRemap intersection mesh filename", &intxFilename );
300 
301  opts.addOpt< void >(
302  "weights,w",
303  "Compute and output the weights using the overlap mesh (generally relevant only for OVERLAP mesh)",
304  &computeWeights );
305 
306  // Discretization options
307  opts.addOpt< void >(
308  "verbose,v", "Print verbose diagnostic messages during intersection and map computation (default=false)",
310 
311  opts.addOpt< std::string >( "method,m", "Discretization method for the source and target solution fields",
312  &expectedMethod );
313 
314  opts.addOpt< int >( "order,o", "Discretization orders for the source and target solution fields",
315  &expectedOrder );
316 
317  opts.addOpt< std::string >( "global_id,g",
318  "Tag name that contains the global DoF IDs for source and target solution fields",
319  &expectedDofTagName );
320 
321  // Advanced options
322  opts.addOpt< std::string >( "fvmethod",
323  "Sub-type method for FV-FV projections (invdist, delaunay, bilin, intbilin, "
324  "intbilingb, none. Default: none)",
325  &expectedFVMethod );
326 
327  opts.addOpt< std::string >(
328  "area_method",
329  "Formula used to compute spherical areas: vos (Van Oosterom-Strackee, default), "
330  "lhuiller, girard, or gquad (Gauss quadrature).",
331  &areaMethodName );
332 
333  opts.addOpt< void >(
334  "noconserve", "Do not apply conservation to the resultant weights (relevant only when computing weights)",
335  &mapOptions.fNoConservation );
336 
337  opts.addOpt< void >(
338  "volumetric", "Apply a volumetric projection to compute the weights (relevant only when computing weights)",
339  &fVolumetric );
340 
341  opts.addOpt< void >( "skip_intersection", "Skip mesh intersection computation.", &skip_intersection );
342 
343  opts.addOpt< void >( "skip_output", "For performance studies, skip all I/O operations.", &skip_io );
344 
345  opts.addOpt< void >( "gnomonic", "Use Gnomonic plane projections to compute coverage mesh.",
347 
348  opts.addOpt< void >( "enforce_convexity", "Check convexity of input meshes to compute mesh intersections",
349  &enforceConvexity );
350 
351  opts.addOpt< void >( "nobubble", "Do not use bubble on interior of spectral element nodes",
352  &mapOptions.fNoBubble );
353 
354  opts.addOpt< void >(
355  "sparseconstraints",
356  "Use sparse solver for constraints when we have high-valence (typical with high-res RLL mesh)",
357  &mapOptions.fSparseConstraints );
358 
359  opts.addOpt< void >(
360  "rrmgrids",
361  "At least one of the meshes is a regionally refined grid (relevant to accelerate intersection computation)",
362  &rrmGrids );
363 
364  opts.addOpt< void >( "checkmap", "Check the generated map for conservation and consistency", &fCheck );
365 
366  opts.addOpt< void >( "verify",
367  "Verify the accuracy of the maps by projecting analytical functions from source to target "
368  "grid by applying the maps",
369  &verifyWeights );
370 
371  opts.addOpt< std::string >( "var",
372  "Tag name of the variable to use in the verification study (error metrics for user "
373  "defined variables may not be available)",
374  &variableToVerify );
375 
376  opts.addOpt< int >( "monotonicity", "Ensure monotonicity in the weight generation. Options=[0,1,2,3]",
378 
379  opts.addOpt< int >( "ghost",
380  "Number of ghost layers in coverage mesh (overrides automatic selection: 0 for FV order 1, "
381  "p+1 for FV order p>1)",
382  &nlayer_input );
383 
384  opts.addOpt< double >( "boxeps", "The tolerance for boxes (default=1e-7)", &boxeps );
385 
386  opts.addOpt< int >( "limiter", "Apply nonlinear filter after linear map application", &useCAAS );
387 
388  opts.addOpt< std::string >( "baseline", "Output baseline file", &baselineFile );
389 
390  opts.addOpt< void >( "manual", "Show documentation about usage with examples" );
391 
392  opts.addOpt< void >( "version", "Show version information", &version_info );
393 
394  // Parse command line
395  opts.parseCommandLine( argc, argv );
396 
397  // "--advfront/-a" requests the advancing-front algorithm, i.e. NOT the Kd-tree search.
398  if( advFrontSearch ) kdtreeSearch = false;
399 
401  {
402  if( !proc_id )
403  std::cerr << "Unknown --area_method \"" << areaMethodName
404  << "\"; expected one of: vos, lhuiller, girard, gquad" << std::endl;
405  exit( 1 );
406  }
407 
408  // Handle call for detailed information
409  if( opts.numOptSet( "manual" ) > 0 )
410  {
411  if( this->proc_id == 0 )
412  {
413  this->printHelp( argv[0] );
414  }
415  exit( 0 );
416  }
417 
418  if( version_info )
419  {
420  if( this->proc_id == 0 )
421  {
422  std::cout << "mbtempest is part of the MOAB library version " << std::string( MOAB_PACKAGE_VERSION )
423  << "\n";
424  }
425  exit( 0 );
426  }
427 
428  // Process mesh type
429  switch( imeshType )
430  {
431  case 0:
433  break;
434  case 1:
436  break;
437  case 2:
439  break;
440  case 3:
442  break;
443  case 4:
445  break;
446  case 5:
448  break;
449  default:
451  break;
452  }
453 
454  // Process CAAS type
455  switch( useCAAS )
456  {
457  case 1:
459  break;
460  case 2:
462  break;
463  case 3:
465  break;
466  case 4:
468  break;
469  default:
471  break;
472  }
473 
474  // Process input files if provided
475  if( !expectedFName.empty() )
476  {
477  this->inFilenames = { expectedFName };
478  }
479 
480  // Process discretization options through processMeshOptions to handle both single and multiple values
481  // Set initial defaults that can be overridden by processMeshOptions
482  this->fvMethod = expectedFVMethod;
483  this->disc_orders = { expectedOrder, expectedOrder };
484  this->disc_methods = { expectedMethod, expectedMethod };
485  this->doftag_names = { expectedDofTagName, expectedDofTagName };
486 
487  // Let processMeshOptions handle all the discretization option processing
488  this->processMeshOptions( opts );
489 
490  // Now use the processed values for map configuration
491  this->mapOptions.nPin = this->disc_orders[0];
492  this->mapOptions.nPout = this->disc_orders[1];
493  this->mapOptions.fSourceConcave = false;
494  this->mapOptions.fTargetConcave = false;
495  this->mapOptions.strMethod = "";
496 
497  // Configure map options with the processed values - this handles all remaining setup
498  this->configureMapOptions( nlayer_input );
499 
500  // Print runtime parameters
501  this->printRuntimeParameters();
502 
503  return moab::MB_SUCCESS;
504  }
505 
506  /**
507  * @brief Get the appropriate MOAB read options based on file extension and parallel configuration
508  *
509  * @param ctx Tool context containing parallel information
510  * @param filename Input filename to determine read options
511  * @return std::string MOAB read options string
512  */
513  std::string get_file_read_options( const std::string& filename )
514  {
515  // For serial execution, return default options
516  if( n_procs <= 1 )
517  {
518  return "";
519  }
520 
521  // Extract file extension
522  const size_t last_dot = filename.find_last_of( "." );
523  if( last_dot == std::string::npos )
524  {
525  return ""; // No extension found
526  }
527 
528  const std::string extension = filename.substr( last_dot + 1 );
529 
530  // Handle H5M files
531  if( extension == "h5m" )
532  {
533  return "PARALLEL=READ_PART;PARTITION=PARALLEL_PARTITION;PARALLEL_RESOLVE_SHARED_ENTS;";
534  }
535 
536  // Handle NetCDF files
537  if( extension == "nc" )
538  {
539  // Default NetCDF options
540 #ifdef MOAB_HAVE_ZOLTAN
541  std::string netcdf_options = "PARALLEL=READ_PART;PARTITION_METHOD=RCBZOLTAN;";
542 #else
543  std::string netcdf_options = "PARALLEL=READ_PART;PARTITION_METHOD=TRIVIAL;";
544 #endif
545  // Only rank 0 needs to determine the NetCDF file type
546 #ifdef MOAB_HAVE_NETCDF
547  if( proc_id == 0 )
548  {
549  NcFile ncFile( filename.c_str(), NcFile::ReadOnly );
550  if( !ncFile.is_valid() )
551  {
552  // Handle invalid file
553  return netcdf_options;
554  }
555 
556  // Check for different NetCDF formats
557  int format_flags = 0;
558  for( int i = 0; i < ncFile.num_dims(); i++ )
559  {
560  const std::string dim_name = ncFile.get_dim( i )->name();
561 
562  if( dim_name == "grid_size" || dim_name == "grid_corners" || dim_name == "grid_rank" )
563  {
564  format_flags |= 1; // SCRIP format
565  }
566  else if( dim_name == "nodeCount" || dim_name == "elementCount" || dim_name == "maxNodePElement" )
567  {
568  format_flags |= 2; // ESMF format
569  }
570  else if( dim_name == "nCells" || dim_name == "nEdges" || dim_name == "nVertices" ||
571  dim_name == "vertexDegree" )
572  {
573  format_flags |= 4; // MPAS format
574  }
575  }
576 
577  // Apply format-specific options
578  if( format_flags & 2 )
579  { // ESMF format
580  netcdf_options += "PARALLEL_RESOLVE_SHARED_ENTS;VARIABLE=;";
581  }
582  else if( format_flags & 1 )
583  { // SCRIP format
584  netcdf_options += ""; // no extra options necessary for now
585  }
586  else if( format_flags & 4 )
587  { // MPAS format
588  netcdf_options += "PARALLEL_RESOLVE_SHARED_ENTS;NO_EDGES;NO_MIXED_ELEMENTS;VARIABLE=;";
589  }
590  }
591 #endif // MOAB_HAVE_NETCDF
592 
593  // Broadcast the options to all processes
594 #ifdef MOAB_HAVE_MPI
595  int line_size = netcdf_options.size();
596  MPI_Bcast( &line_size, 1, MPI_INT, 0, MPI_COMM_WORLD );
597  if( proc_id != 0 )
598  {
599  netcdf_options.resize( line_size );
600  }
601  MPI_Bcast( const_cast< char* >( netcdf_options.data() ), line_size, MPI_CHAR, 0, MPI_COMM_WORLD );
602 #endif
603 
604  return netcdf_options;
605  }
606 
607  // Default options for other file types
608  return "PARALLEL=BCAST_DELETE;PARTITION=TRIVIAL;PARALLEL_RESOLVE_SHARED_ENTS;";
609  }
610 
611  private:
612  /**
613  * @brief Print detailed help message with usage examples
614  * @param progName Program name
615  */
616  void printHelp( const char* progName ) const
617  {
618  if( this->proc_id != 0 ) return;
619 
620  std::cout << "MOAB-Tempest: A mesh generation and remapping tool\n"
621  << "==================================================\n\n"
622  << "Usage: " << progName << " [OPTIONS]\n\n"
623  << "Mesh Generation Options:\n"
624  << " -t, --type TYPE Type of mesh to generate (required for mesh generation):\n"
625  << " 0 = Cubed-Sphere (CS)\n"
626  << " 1 = Regular Latitude-Longitude (RLL)\n"
627  << " 2 = Icosahedral (ICO)\n"
628  << " 3 = TempestRemap overlap (thin interface))\n"
629  << " 4 = MOAB with TempestRemap overlap in memory\n"
630  << " 5 = Parallel handling of Overlap meshes with MOAB (recommended)\n\n"
631  << " -r, --res N Resolution (number of elements on edge, default: 10)\n"
632  << " -f, --file FILE Output filename (default: output.h5m)\n\n"
633  << "Discretization Options:\n"
634  << " -m, --method METHOD Discretization method (default: fv):\n"
635  << " fv = Finite Volume\n"
636  << " cgll = Continuous Galerkin with Legendre-Gauss-Lobatto\n"
637  << " dgll = Discontinuous Galerkin with Legendre-Gauss-Lobatto\n\n"
638  << " -o, --order N Discretization order (default: 1, range: 1-4)\n\n"
639  << "Remapping Options:\n"
640  << " --mono N Monotonicity constraints (default: 0):\n"
641  << " 0 = No monotonicity\n"
642  << " 1 = Basic monotonicity\n"
643  << " 2 = Full monotonicity with bounds\n"
644  << " 3 = Strict monotonicity\n\n"
645  << " --limiter TYPE Nonlinear limiting (optional):\n"
646  << " none = No limiting (default)\n"
647  << " global = Global CAAS limiting\n"
648  << " local = Localized CAAS limiting\n"
649  << " qlt = Quasi-Local Tree-based limiting\n\n"
650  << "Input/Output Options:\n"
651  << " -l, --load FILE Load input mesh file (use twice for source and target)\n"
652  << " -i, --global_id TAG Global ID tag name (default: GLOBAL_ID)\n"
653  << " --diagnostics Print diagnostic information\n\n"
654  << "Miscellaneous Options:\n"
655  << " --manual Show this help message and exit\n"
656  << " --version Show version information\n\n"
657  << "Examples:\n"
658  << " # Generate a cubed-sphere mesh with resolution 25\n"
659  << " " << progName << " --type 0 --res 25 -f cs_mesh.h5m\n\n"
660  << " # Generate a latitude-longitude mesh with resolution 180\n"
661  << " " << progName << " --type 1 --res 180 -f rll_mesh.h5m\n\n"
662  << " # Create a map between two meshes with order 4\n"
663  << " " << progName << " --type 5 --load source_mesh.h5m --load target_mesh.h5m \\\n"
664  << " --method cgll --order 4 --global_id GLOBAL_DOFS \\\n"
665  << " --method fv --order 1 --limiter 1 --file map.nc\n";
666  }
667 
668  /**
669  * @brief Get mesh type as string
670  * @return String representation of mesh type
671  */
672  std::string getMeshTypeName() const
673  {
674  switch( this->meshType )
675  {
677  return "Cubed-Sphere";
679  return "Latitude-Longitude";
681  return "Icosahedral";
683  return "Overlap (files)";
685  return "Overlap (memory)";
687  return "Overlap (MOAB)";
688  default:
689  return "Unknown";
690  }
691  }
692 
693  /**
694  * @brief Process mesh options from command line
695  * @param opts Program options
696  * @param expectedFVMethod Expected finite volume method
697  * @param nlayer_input Number of ghost layers
698  */
700  {
701  if( this->meshType <= moab::TempestRemapper::ICO ) return;
702 
703  // Process input files
704  std::vector< std::string > inputFiles;
705  opts.getOptAllArgs( "load,l", inputFiles );
706  if( !inputFiles.empty() )
707  {
708  this->inFilenames = inputFiles;
709  if( this->inFilenames.size() != 2 )
710  {
711  throw std::runtime_error( "Exactly two input filenames must be provided with -l/--load" );
712  }
713  }
714 
715  // Process discretization orders
716  std::vector< int > orders;
717  opts.getOptAllArgs( "order,o", orders );
718  if( !orders.empty() )
719  {
720  this->disc_orders = orders;
721  if( this->disc_orders.size() == 1 )
722  {
723  this->disc_orders.push_back( this->disc_orders[0] );
724  }
725  else if( this->disc_orders.size() != 2 )
726  {
727  throw std::runtime_error( "Must specify 1 or 2 values for order (source [target])" );
728  }
729 
730  for( const auto& order : this->disc_orders )
731  {
732  if( order < 1 || order > 4 )
733  {
734  throw std::runtime_error( "Discretization order must be between 1 and 4" );
735  }
736  }
737  }
738 
739  // Process discretization methods
740  std::vector< std::string > methods;
741  opts.getOptAllArgs( "method,m", methods );
742  if( !methods.empty() )
743  {
744  this->disc_methods = methods;
745  if( this->disc_methods.size() == 1 )
746  {
747  // Use same method for both source and target
748  this->disc_methods.push_back( this->disc_methods[0] );
749  }
750  else if( this->disc_methods.size() != 2 )
751  {
752  throw std::runtime_error( "Must specify 1 or 2 values for method (source [target])" );
753  }
754 
755  // Validate method values
756  for( const auto& method : this->disc_methods )
757  {
758  if( method != "fv" && method != "cgll" && method != "dgll" && method != "pcloud" )
759  {
760  throw std::runtime_error( "Invalid method '" + method + "'. Must be one of: fv, cgll, dgll" );
761  }
762  }
763  }
764 
765  // Process DOF tag names
766  std::vector< std::string > tags;
767  opts.getOptAllArgs( "global_id,i", tags );
768  if( !tags.empty() )
769  {
770  this->doftag_names = tags;
771  if( this->doftag_names.size() == 1 )
772  {
773  // Use same tag name for both source and target
774  this->doftag_names.push_back( this->doftag_names[0] );
775  }
776  else if( this->doftag_names.size() != 2 )
777  {
778  throw std::runtime_error( "Must specify 1 or 2 values for DOF tag names (source [target])" );
779  }
780  }
781 
782  // Process output filename if specified
783  std::string outFile;
784  if( opts.getOpt( "file,f", &outFile ) )
785  {
786  this->outFilename = outFile;
787  }
788  // Note: configureMapOptions is now called from ParseCLOptions after processMeshOptions completes
789  }
790 
791  /**
792  * @brief Print all runtime parameters in a formatted way
793  */
795  {
796  if( this->proc_id != 0 ) return;
797 
798  constexpr int width = 60;
799 
800  std::cout << std::string( width, '=' ) << "\n";
801  std::cout << " MOAB-TempestRemap Runtime Configuration " << "\n";
802  std::cout << std::string( width, '=' );
803 
804  // Input files
806  {
807  if( !this->inFilenames.empty() )
808  {
809  std::cout << "\n\nInput Files:";
810  std::cout << "\n Source mesh: " << this->inFilenames[0];
811  std::cout << "\n Target mesh: " << this->inFilenames[1];
812  }
813 
814  std::cout << "\n\nOutput Files:";
815  if( !skip_intersection )
816  std::cout << "\n Intersection mesh: "
817  << ( this->computeWeights ? this->intxFilename : this->outFilename );
818  if( computeWeights ) std::cout << "\n Remap weights: " << this->outFilename;
819  }
820 
821  // Mesh configuration
822  std::cout << "\n\nMesh Configuration:";
823  std::cout << "\n Mesh type: " << this->getMeshTypeName();
824  if( this->meshType <= moab::TempestRemapper::ICO )
825  std::cout << "\n Resolution: " << this->blockSize;
827  std::cout << "\n Compute dual: " << ( this->computeDual ? "Yes" : "No" );
828 
829  if( computeWeights )
830  {
831  std::cout << "\n Gnomonic projection: " << ( this->useGnomonicProjection ? "Yes" : "No" );
832  std::cout << "\n Intersection algorithm: " << ( this->kdtreeSearch ? "KdTree search" : "Advancing front" );
833  std::cout << "\n Area computation: " << moab::IntxAreaUtils::area_method_name( this->areaMethod );
834 
835  // Discretization settings
836  std::cout << "\n\nDiscretization:";
837  std::cout << "\n Source: " << this->disc_methods[0] << " (order " << this->disc_orders[0]
838  << ")";
839  std::cout << "\n Target: " << this->disc_methods[1] << " (order " << this->disc_orders[1]
840  << ")";
841 
842  // Remapping options
843  std::cout << "\n\nRemapping Options:";
844  std::cout << "\n Method: "
845  << ( this->mapOptions.strMethod.empty() ? "Default" : this->mapOptions.strMethod );
846  std::cout << "\n Monotonicity: " << ( this->ensureMonotonicity ? "Yes" : "No" );
847  std::cout << "\n Volumetric: " << ( this->fVolumetric ? "Yes" : "No" );
848  std::cout << "\n Check consistency: " << ( this->fCheck ? "Yes" : "No" );
849  std::cout << "\n Skip intersection: " << ( this->skip_intersection ? "Yes" : "No" );
850  }
851 
852  // Parallel configuration
853  std::cout << "\n\nParallel Configuration:";
854  std::cout << "\n MPI Processes: " << this->n_procs;
855  if( this->meshType > moab::TempestRemapper::ICO ) std::cout << "\n Number of Ghost Layers: " << this->nlayers;
856 
857  std::cout << "\n\n" << std::string( width, '=' ) << "\n\n";
858  }
859 
860  /**
861  * @brief Configure map options based on command line parameters
862  * @param nlayer_input Number of ghost layers
863  */
864  void configureMapOptions( int nlayer_input )
865  {
866  // Set polynomial orders with bounds checking
867  this->mapOptions.nPin = ( this->disc_orders.empty() ) ? 1 : this->disc_orders[0];
868  this->mapOptions.nPout = ( this->disc_orders.size() > 1 ) ? this->disc_orders[1] : this->mapOptions.nPin;
869 
870  // Initialize flags
871  this->mapOptions.fSourceConcave = false;
872  this->mapOptions.fTargetConcave = false;
873  this->mapOptions.strMethod.clear();
874 
875  // Configure finite volume method if specified
876  if( this->fvMethod != "none" )
877  {
878  this->mapOptions.strMethod = this->fvMethod + ";";
879  this->mapOptions.fNoConservation = true;
880  }
881 
882  // Configure monotonicity with validation
883  this->ensureMonotonicity = std::max( 0, std::min( 3, this->ensureMonotonicity ) ); // Clamp to 0-3
884  switch( this->ensureMonotonicity )
885  {
886  case 0:
887  this->mapOptions.fMonotone = false;
888  break;
889  case 3:
890  this->mapOptions.strMethod += "mono3;";
891  this->mapOptions.fMonotone = true;
892  break;
893  case 2:
894  this->mapOptions.strMethod += "mono2;";
895  this->mapOptions.fMonotone = true;
896  break;
897  case 1:
898  default:
899  this->mapOptions.fMonotone = true;
900  break;
901  }
902 
903  // Set other options
904  this->mapOptions.fNoCorrectAreas = false;
905  this->mapOptions.fNoCheck = !this->fCheck;
906 
907  // Add volumetric flag if needed
908  if( this->fVolumetric )
909  {
910  this->mapOptions.strMethod += "volumetric;";
911  }
912 
913  // Set number of ghost layers based on method and order.
914  //
915  // Every bilinear-family kernel reconstructs a DUAL mesh of the source
916  // (Dual()/ConstructLocalDualFace() via meshInput.revnodearray), so it
917  // needs the full ring of cells around each source vertex. In parallel
918  // the coverage mesh is cut at partition boundaries, and without ghost
919  // layers the dual of a boundary vertex is incomplete -- which shows up
920  // as wrong weights near partition boundaries rather than as an error.
921  // The kernels involved are:
922  // bilin -> LinearRemapBilinear
923  // intbilin -> LinearRemapIntegratedBilinear
924  // intbilingb -> LinearRemapIntegratedGeneralizedBarycentric
925  // and delaunay likewise needs a neighbourhood for its triangulation.
926  //
927  // "intbilin" and "intbilingb" were previously omitted here purely
928  // because this test matched on the user-facing --fvmethod string and
929  // only listed two of the four names; they silently ran with 0 layers.
930  const bool needsDualMesh = ( this->fvMethod == "delaunay" || this->fvMethod == "bilin" ||
931  this->fvMethod == "intbilin" || this->fvMethod == "intbilingb" );
932 
933  if( needsDualMesh )
934  {
935  this->nlayers = 3; // conservative
936  // Only "bilin" and "delaunay" work purely off the coverage mesh.
937  // The integrated variants quadrature over the overlap polygons
938  // (they take meshOverlap), so the intersection is still required.
939  if( this->fvMethod == "delaunay" || this->fvMethod == "bilin" ) this->skip_intersection = true;
940  }
941  else
942  {
943  // order 1: no ghost layers
944  // order p: p+1 layers (again, being conservative)
945  this->nlayers = ( this->mapOptions.nPin > 1 ) ? this->mapOptions.nPin + 1 : 0;
946  }
947 
948  // User-supplied value always overrides the internal default (even 0 is valid).
949  if( nlayer_input >= 0 )
950  {
951  this->nlayers = nlayer_input;
952  }
953 
954  // Configure output
955  this->mapOptions.strOutputMapFile = this->outFilename;
956  this->mapOptions.strOutputFormat = "Netcdf4";
957  }
958 };
959 
960 // Forward declare some methods
962 static inline constexpr double sample_constant( double dLon, double dLat ) noexcept;
963 static inline double sample_slow_harmonic( double dLon, double dLat ) noexcept;
964 static inline double sample_fast_harmonic( double dLon, double dLat ) noexcept;
965 static inline double sample_stationary_vortex( double dLon, double dLat ) noexcept;
966 
967 /////////////////////////////////////////////////////////////
968 
969 //#define MOAB_DBG
970 int main( int argc, char* argv[] )
971 {
972  try
973  {
974 #ifdef MOAB_HAVE_NETCDF
975  NcError error( NcError::verbose_nonfatal );
976 #endif
977  std::stringstream sstr;
978  std::string historyStr;
979 
980  int proc_id = 0, nprocs = 1;
981 #ifdef MOAB_HAVE_MPI
982  MPI_Init( &argc, &argv );
983  MPI_Comm_rank( MPI_COMM_WORLD, &proc_id );
984  MPI_Comm_size( MPI_COMM_WORLD, &nprocs );
985 #endif
986 
987  moab::Core* mbCore = new( std::nothrow ) moab::Core;
988 
989  if( nullptr == mbCore )
990  {
991  return 1;
992  }
993 
994  // Build the history string
995  for( int ia = 0; ia < argc; ++ia )
996  historyStr += std::string( argv[ia] ) + " ";
997 
998  ToolContext* runCtx;
999 #ifdef MOAB_HAVE_MPI
1000  moab::ParallelComm* pcomm = new moab::ParallelComm( mbCore, MPI_COMM_WORLD, 0 );
1001 
1002  runCtx = new ToolContext( mbCore, pcomm );
1003  const char* writeOptions = ( nprocs > 1 ? "PARALLEL=WRITE_PART" : "" );
1004 #else
1005  runCtx = new ToolContext( mbCore );
1006  const char* writeOptions = "";
1007 #endif
1008  runCtx->ParseCLOptions( argc, argv );
1009 
1010  const double radius_src = 1.0 /*2.0*acos(-1.0)*/;
1011  const double radius_dest = 1.0 /*2.0*acos(-1.0)*/;
1012 
1014 
1015 #ifdef MOAB_HAVE_MPI
1016  moab::TempestRemapper remapper( mbCore, pcomm );
1017 #else
1018  moab::TempestRemapper remapper( mbCore );
1019 #endif
1020  remapper.meshValidate = true;
1021  remapper.constructEdgeMap = true;
1022  remapper.initialize();
1023  // The command-line choice supersedes the remapper default, so that intersection,
1024  // coverage construction and orientation fixups all use one formula.
1025  remapper.SetAreaMethod( runCtx->areaMethod );
1026  // --rrmgrids: the two domains need not coincide, so cells may be partially covered.
1027  remapper.SetRegionalMesh( runCtx->rrmGrids );
1028 
1029  // Area formula selected by --area_method (default: Van Oosterom-Strackee)
1030  moab::IntxAreaUtils areaAdaptor( runCtx->areaMethod );
1031 
1032  Mesh* tempest_mesh = new Mesh();
1033  MB_CHK_SET_ERR( CreateTempestMesh( *runCtx, remapper, tempest_mesh ), "Failed to create tempest mesh" );
1034 
1036  {
1037  // Compute intersections with MOAB
1038  // For the overlap method, choose between: "fuzzy", "exact" or "mixed"
1039  assert( runCtx->meshes.size() == 3 );
1040 
1041 #ifdef MOAB_HAVE_MPI
1042  MB_CHK_SET_ERR( pcomm->check_all_shared_handles(), "Failed to check all shared handles" );
1043 #endif
1044 
1045  // Load the meshes and validate
1046  MB_CHK_SET_ERR( remapper.ConvertTempestMesh( moab::Remapper::SourceMesh ), "Failed to convert source mesh" );
1047  MB_CHK_SET_ERR( remapper.ConvertTempestMesh( moab::Remapper::TargetMesh ), "Failed to convert target mesh" );
1048  MB_CHK_SET_ERR( remapper.ConvertTempestMesh( moab::Remapper::OverlapMesh ), "Failed to convert overlap mesh" );
1049  if( !runCtx->skip_io )
1050  {
1051  MB_CHK_SET_ERR( mbCore->write_mesh( "tempest_intersection.h5m", &runCtx->meshsets[2], 1 ),
1052  "Failed to write TempestRemap intersection mesh in MOAB format" );
1053  }
1054 
1055  // print verbosely about the problem setting
1056  size_t velist[6], gvelist[6];
1057  {
1058  moab::Range rintxverts, rintxelems;
1059  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[0], 0, rintxverts ),
1060  "Failed to get vertices" );
1061  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[0], 2, rintxelems ),
1062  "Failed to get elements" );
1063  velist[0] = rintxverts.size();
1064  velist[1] = rintxelems.size();
1065 
1066  moab::Range bintxverts, bintxelems;
1067  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[1], 0, bintxverts ),
1068  "Failed to get vertices" );
1069  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[1], 2, bintxelems ),
1070  "Failed to get elements" );
1071  velist[2] = bintxverts.size();
1072  velist[3] = bintxelems.size();
1073  }
1074 
1075  moab::EntityHandle intxset; // == remapper.GetMeshSet(moab::Remapper::OverlapMesh);
1076 
1077  // Compute intersections with MOAB
1078  {
1079  // Create the intersection on the sphere object
1080  runCtx->timer_push( "setup the intersector" );
1081 
1082  moab::Intx2MeshOnSphere* mbintx = new moab::Intx2MeshOnSphere( mbCore, runCtx->areaMethod );
1083  mbintx->set_error_tolerance( runCtx->epsrel );
1084  mbintx->set_box_error( runCtx->boxeps );
1085  mbintx->set_radius_source_mesh( radius_src );
1086  mbintx->set_radius_destination_mesh( radius_dest );
1087 #ifdef MOAB_HAVE_MPI
1088  mbintx->set_parallel_comm( pcomm );
1089 #endif
1090  MB_CHK_SET_ERR( mbintx->FindMaxEdges( runCtx->meshsets[0], runCtx->meshsets[1] ),
1091  "Failed to find max edges" );
1092 
1093 #ifdef MOAB_HAVE_MPI
1094  moab::Range local_verts;
1095  MB_CHK_SET_ERR( mbintx->build_processor_euler_boxes( runCtx->meshsets[1], local_verts ),
1096  "Failed to build processor euler boxes" );
1097 
1098  runCtx->timer_pop();
1099 
1100  moab::EntityHandle covering_set;
1101  runCtx->timer_push( "communicate the mesh" );
1102  // we compute just intersection here, no need for extra ghost layers anyway
1103  // ghost layers are needed in coverage for bilinear map, which does not actually need intersection
1104  // this will be fixed in the future, bilinear map needs just coverage, not intersection
1105  // so I am not passing the ghost layer here, even though there is an option in runCtx for a ghost layer
1106  // NOTE: This is a communication-heavy kernel if mesh is distributed very differently
1107  MB_CHK_SET_ERR( mbintx->construct_covering_set( runCtx->meshsets[0], covering_set ),
1108  "Failed to construct covering set" );
1109  runCtx->timer_pop();
1110 
1111  // print verbosely about the problem setting
1112  {
1113  moab::Range cintxverts, cintxelems;
1114  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( covering_set, 0, cintxverts ),
1115  "Failed to get vertices" );
1116  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( covering_set, 2, cintxelems ),
1117  "Failed to get elements" );
1118  velist[4] = cintxverts.size();
1119  velist[5] = cintxelems.size();
1120  }
1121 
1122  MPI_Reduce( velist, gvelist, 6, MPI_UINT64_T, MPI_SUM, 0, MPI_COMM_WORLD );
1123 
1124 #else
1125  moab::EntityHandle covering_set = runCtx->meshsets[0];
1126  for( int i = 0; i < 6; i++ )
1127  gvelist[i] = velist[i];
1128 #endif
1129 
1130  if( !proc_id )
1131  {
1132  outputFormatter.printf( 0, "The source set contains %lu vertices and %lu elements \n", gvelist[0],
1133  gvelist[0] );
1134  outputFormatter.printf( 0, "The covering set contains %lu vertices and %lu elements \n", gvelist[2],
1135  gvelist[2] );
1136  outputFormatter.printf( 0, "The target set contains %lu vertices and %lu elements \n", gvelist[1],
1137  gvelist[1] );
1138  }
1139 
1140  // Now let's invoke the MOAB intersection algorithm in parallel with a
1141  // source and target mesh set representing two different decompositions
1142  runCtx->timer_push( "compute intersections with MOAB" );
1143  MB_CHK_SET_ERR( mbCore->create_meshset( moab::MESHSET_SET, intxset ), "Can't create new set" );
1144  MB_CHK_SET_ERR( mbintx->intersect_meshes( covering_set, runCtx->meshsets[1], intxset ),
1145  "Can't compute the intersection of meshes on the sphere" );
1146  runCtx->timer_pop();
1147 
1148  // free the memory
1149  delete mbintx;
1150  }
1151 
1152  {
1153  moab::Range intxelems, intxverts;
1154  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( intxset, 2, intxelems ), "Failed to get elements" );
1155  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( intxset, 0, intxverts, true ),
1156  "Failed to get vertices" );
1157  outputFormatter.printf( 0, "The intersection set contains %lu elements and %lu vertices \n",
1158  intxelems.size(), intxverts.size() );
1159 
1160  double initial_sarea =
1161  areaAdaptor.area_on_sphere( mbCore, runCtx->meshsets[0],
1162  radius_src ); // use the target to compute the initial area
1163  double initial_tarea =
1164  areaAdaptor.area_on_sphere( mbCore, runCtx->meshsets[1],
1165  radius_dest ); // use the target to compute the initial area
1166  double intx_area = areaAdaptor.area_on_sphere( mbCore, intxset, radius_src );
1167 
1168  outputFormatter.printf( 0, "mesh areas: source = %12.10f, target = %12.10f, intersection = %12.10f \n",
1169  initial_sarea, initial_tarea, intx_area );
1170  outputFormatter.printf( 0, "relative error w.r.t source = %12.10e, target = %12.10e \n",
1171  fabs( intx_area - initial_sarea ) / initial_sarea,
1172  fabs( intx_area - initial_tarea ) / initial_tarea );
1173  }
1174 
1175  // Write out our computed intersection file
1176  if( !runCtx->skip_io )
1177  {
1178  MB_CHK_SET_ERR( mbCore->write_mesh( "moab_intersection.h5m", &intxset, 1 ),
1179  "Failed to write the intersection" );
1180  }
1181 
1182  if( runCtx->computeWeights )
1183  {
1184  runCtx->timer_push( "compute weights with the Tempest meshes" );
1185  // Call to generate an offline map with the tempest meshes
1186  OfflineMap weightMap;
1187  if( GenerateOfflineMapWithMeshes( *runCtx->meshes[0], *runCtx->meshes[1], *runCtx->meshes[2],
1188  runCtx->disc_methods[0], // std::string strInputType
1189  runCtx->disc_methods[1], // std::string strOutputType,
1190  runCtx->mapOptions, weightMap ) != 0 )
1191  throw std::runtime_error( "Could not generate offline map with TempestRemap" );
1192  runCtx->timer_pop();
1193 
1194  std::map< std::string, std::string > mapAttributes;
1195 #ifdef MOAB_HAVE_NETCDF
1196  if( !runCtx->skip_io ) weightMap.Write( "outWeights.nc", mapAttributes );
1197 #else
1198  (void)mapAttributes;
1199  MB_CHK_SET_ERR( moab::MB_FAILURE,
1200  "Writing a TempestRemap OfflineMap (OVERLAP_MEMORY path) requires NetCDF; use the "
1201  "MOAB (OVERLAP_MOAB) workflow, which writes weights via PnetCDF/HDF5 instead" );
1202 #endif
1203  }
1204  }
1205  else if( runCtx->meshType == moab::TempestRemapper::OVERLAP_MOAB )
1206  {
1207  // Usage: mpiexec -n 2 tools/mbtempest -t 5 -l mycs_2.h5m -l myico_2.h5m -f myoverlap_2.h5m
1208 #ifdef MOAB_HAVE_MPI
1209  MB_CHK_SET_ERR( pcomm->check_all_shared_handles(), "Checking shared handles failed." );
1210 #endif
1211 
1212  // print verbosely about the problem setting
1213  size_t velist[4] = { 0, 0, 0, 0 }, gvelist[4] = { 0, 0, 0, 0 };
1214  {
1215  moab::Range srcverts, srcelems;
1216  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[0], 0, srcverts ),
1217  "Failed to get vertices" );
1218  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[0], 2, srcelems ),
1219  "Failed to get elements" );
1221  "Failed to fix degenerate quads" );
1222  if( runCtx->enforceConvexity )
1223  {
1225  "Failed to enforce convexity" );
1226  }
1227  MB_CHK_SET_ERR( areaAdaptor.positive_orientation( mbCore, runCtx->meshsets[0], radius_src ),
1228  "Failed to enforce positive orientation" );
1229  velist[0] = srcverts.size();
1230  velist[1] = srcelems.size();
1231 
1232  moab::Range tgtverts, tgtelems;
1233  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[1], 0, tgtverts ),
1234  "Failed to get vertices" );
1235  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[1], 2, tgtelems ),
1236  "Failed to get elements" );
1238  "Failed to fix degenerate quads" );
1239  if( runCtx->enforceConvexity )
1240  {
1242  "Failed to enforce convexity" );
1243  }
1244  MB_CHK_SET_ERR( areaAdaptor.positive_orientation( mbCore, runCtx->meshsets[1], radius_dest ),
1245  "Failed to enforce positive orientation" );
1246  velist[2] = tgtverts.size();
1247  velist[3] = tgtelems.size();
1248  }
1249  //MB_CHK_SET_ERR( mbCore->write_file( "source_mesh.h5m", nullptr, writeOptions, &runCtx->meshsets[0], 1 ), "Could not write source mesh" );
1250  //MB_CHK_SET_ERR( mbCore->write_file( "target_mesh.h5m", nullptr, writeOptions, &runCtx->meshsets[1], 1 ), "Could not write target mesh" );
1251 
1252  // if( runCtx->nlayers && nprocs > 1 )
1253  // {
1254  // remapper.ResetMeshSet( moab::Remapper::SourceMesh, runCtx->meshsets[3] );
1255  // runCtx->meshes[0] = remapper.GetMesh( moab::Remapper::SourceMesh ); // ?
1256  // }
1257 
1258  // First compute the covering set such that the target elements are fully covered by the
1259  // local source grid
1260  runCtx->timer_push( "construct covering set for intersection" );
1261  // Ghosting and the gnomonic-projection coverage path are mutually
1262  // exclusive. Ghost layers are now enabled by default for the
1263  // bilinear-family methods, so warn rather than silently dropping an
1264  // explicit --gnomonic request.
1265  if( runCtx->nlayers > 0 && runCtx->useGnomonicProjection )
1266  {
1267  if( !proc_id )
1268  std::cout << " [WARNING] --gnomonic is ignored when ghost layers are used (nlayers = "
1269  << runCtx->nlayers << "); pass '--ghost 0' to force the gnomonic coverage path.\n";
1270  runCtx->useGnomonicProjection = false;
1271  }
1272  MB_CHK_SET_ERR( remapper.ConstructCoveringSet( runCtx->epsrel, 1.0, 1.0, runCtx->boxeps, runCtx->rrmGrids,
1273  runCtx->useGnomonicProjection, runCtx->nlayers ),
1274  "Failed to construct covering set" );
1275  runCtx->timer_pop();
1276 
1277 #ifdef MOAB_HAVE_MPI
1278  MPI_Reduce( velist, gvelist, 4, MPI_UINT64_T, MPI_SUM, 0, MPI_COMM_WORLD );
1279 #else
1280  for( int i = 0; i < 4; i++ )
1281  gvelist[i] = velist[i];
1282 #endif
1283  if( !proc_id && runCtx->print_diagnostics )
1284  {
1285  outputFormatter.printf( 0, "The source set contains %lu vertices and %lu elements \n", gvelist[0],
1286  gvelist[1] );
1287  outputFormatter.printf( 0, "The target set contains %lu vertices and %lu elements \n", gvelist[2],
1288  gvelist[3] );
1289  }
1290 
1291  if( runCtx->skip_intersection )
1292  {
1293  if( !proc_id ) outputFormatter.printf( 0, "Skipping mesh intersection computation.\n" );
1294  }
1295  else
1296  {
1297  // Compute intersections with MOAB with either the Kd-tree or the advancing front algorithm
1298  runCtx->timer_push( "setup and compute mesh intersections" );
1299  MB_CHK_SET_ERR( remapper.ComputeOverlapMesh( runCtx->kdtreeSearch, false ),
1300  "Failed to compute mesh intersections" );
1301  runCtx->timer_pop();
1302  }
1303 
1304  // print some diagnostic checks to see if the overlap grid resolved the input meshes
1305  // correctly
1306  // Compute ghost overlap elements once; reused for both area diagnostics and intx file write
1307  moab::Range ghostOverlapElems;
1308 #ifdef MOAB_HAVE_MPI
1309  if( nprocs > 1 && !runCtx->skip_intersection )
1310  MB_CHK_SET_ERR( remapper.GetOverlapAugmentedEntities( ghostOverlapElems ),
1311  "Failed to get ghost overlap entities" );
1312 #endif
1313 
1314  double dTotalOverlapArea = 0.0;
1315  if( runCtx->print_diagnostics && !runCtx->skip_intersection )
1316  {
1317  // Areas for source, target, overlap meshes
1318  double local_areas[3] = { 0, 0, 0 },
1319  global_areas[3] = { 0, 0, 0 };
1320 
1321  // Helper: compute area of a meshset excluding cells with GRID_IMASK==0.
1322  // Both source and target SCRIP grids may have a land/sea mask; the intersection
1323  // only covers unmasked cells, so comparing full-mesh areas gives a misleading error.
1324  auto area_unmasked = [&]( moab::EntityHandle meshset, double radius ) -> double {
1325  moab::Tag imaskTag = 0;
1326  mbCore->tag_get_handle( "GRID_IMASK", imaskTag );
1327  if( !imaskTag ) return areaAdaptor.area_on_sphere( mbCore, meshset, radius );
1328  moab::Range cells;
1329  mbCore->get_entities_by_dimension( meshset, 2, cells );
1330  std::vector< int > masks( cells.size(), 1 );
1331  mbCore->tag_get_data( imaskTag, cells, masks.data() );
1332  moab::Range maskedCells;
1333  size_t idx = 0;
1334  for( auto it = cells.begin(); it != cells.end(); ++it, ++idx )
1335  if( !masks[idx] ) maskedCells.insert( *it );
1336  moab::Range unmasked = moab::subtract( cells, maskedCells );
1337  moab::EntityHandle tmpSet;
1338  mbCore->create_meshset( moab::MESHSET_SET, tmpSet );
1339  mbCore->add_entities( tmpSet, unmasked );
1340  double area = areaAdaptor.area_on_sphere( mbCore, tmpSet, radius );
1341  mbCore->delete_entities( &tmpSet, 1 );
1342  return area;
1343  };
1344 
1345  local_areas[0] = area_unmasked( runCtx->meshsets[0], radius_src );
1346  local_areas[1] = area_unmasked( runCtx->meshsets[1], radius_dest );
1347  // Exclude ghost overlap elements from area sum to avoid double-counting after MPI_Allreduce
1348  {
1349  moab::Range ownedOverlapElems;
1350  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[2], 2, ownedOverlapElems ),
1351  "Failed to get overlap elements" );
1352  ownedOverlapElems = moab::subtract( ownedOverlapElems, ghostOverlapElems );
1353  moab::EntityHandle ownedOverlapSet;
1354  MB_CHK_SET_ERR( mbCore->create_meshset( moab::MESHSET_SET, ownedOverlapSet ),
1355  "Can't create owned overlap meshset" );
1356  MB_CHK_SET_ERR( mbCore->add_entities( ownedOverlapSet, ownedOverlapElems ),
1357  "Can't add owned overlap elements" );
1358  local_areas[2] = areaAdaptor.area_on_sphere( mbCore, ownedOverlapSet, radius_src );
1359  MB_CHK_SET_ERR( mbCore->delete_entities( &ownedOverlapSet, 1 ), "Can't delete temp meshset" );
1360  }
1361 
1362 #ifdef MOAB_HAVE_MPI
1363  MPI_Allreduce( &local_areas[0], &global_areas[0], 3, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD );
1364 #else
1365  global_areas[0] = local_areas[0];
1366  global_areas[1] = local_areas[1];
1367  global_areas[2] = local_areas[2];
1368 #endif
1369  if( !proc_id )
1370  {
1372  "initial area: source mesh = %12.14f, target mesh = "
1373  "%12.14f, overlap mesh = %12.14f\n",
1374  global_areas[0], global_areas[1], global_areas[2] );
1375  outputFormatter.printf( 0, "relative error w.r.t source = %12.14e, and target = %12.14e\n",
1376  fabs( global_areas[0] - global_areas[2] ) / global_areas[0],
1377  fabs( global_areas[1] - global_areas[2] ) / global_areas[1] );
1378  if( runCtx->rrmGrids )
1379  {
1380  // For coincident domains the shortfall below is roundoff and the
1381  // relative errors above already say so. For a regional mesh it is
1382  // the physically meaningful quantity: how much of each mesh the
1383  // other one actually covers.
1385  "regional mesh: overlap covers %6.2f%% of source and %6.2f%% of "
1386  "target area\n",
1387  100.0 * global_areas[2] / global_areas[0],
1388  100.0 * global_areas[2] / global_areas[1] );
1389  }
1390  }
1391  dTotalOverlapArea = global_areas[2];
1392  }
1393 
1394  if( runCtx->intxFilename.size() && !runCtx->skip_intersection )
1395  {
1396  moab::EntityHandle writableOverlapSet;
1397  MB_CHK_SET_ERR( mbCore->create_meshset( moab::MESHSET_SET, writableOverlapSet ), "Can't create new set" );
1398  moab::EntityHandle meshOverlapSet = remapper.GetMeshSet( moab::Remapper::OverlapMesh );
1399  moab::Range ovEnts;
1400  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( meshOverlapSet, 2, ovEnts ), "Can't create new set" );
1401  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( meshOverlapSet, 0, ovEnts ), "Can't create new set" );
1402 
1403 #ifdef MOAB_HAVE_MPI
1404  // Exclude ghost overlap elements from the write: each ghost element is owned by another
1405  // rank and will be written from there. Including ghosts here causes duplicate entity
1406  // handles in the parallel HDF5 output and deadlocks the collective write.
1407  if( nprocs > 1 )
1408  {
1409  ovEnts = moab::subtract( ovEnts, ghostOverlapElems );
1410 #ifdef MOAB_DBG
1411  if( !runCtx->skip_io )
1412  {
1413  std::stringstream filename;
1414  filename << "aug_overlap" << runCtx->pcomm->rank() << ".h5m";
1415  MB_CHK_SET_ERR( mbCore->write_file( filename.str().c_str(), 0, 0, &meshOverlapSet, 1 ),
1416  "Failed to write the overlap set" );
1417  }
1418 #endif
1419  }
1420 #endif
1421  MB_CHK_SET_ERR( mbCore->add_entities( writableOverlapSet, ovEnts ), "adding local intx cells failed" );
1422 
1423 #ifdef MOAB_HAVE_MPI
1424 #ifdef MOAB_DBG
1425  if( nprocs > 1 && !runCtx->skip_io )
1426  {
1427  std::stringstream filename;
1428  filename << "writable_intx_" << runCtx->pcomm->rank() << ".h5m";
1429  MB_CHK_SET_ERR( mbCore->write_file( filename.str().c_str(), 0, 0, &writableOverlapSet, 1 ),
1430  "Failed to write the writable overlap set" );
1431  }
1432 #endif
1433 #endif
1434 
1435  size_t lastindex = runCtx->intxFilename.find_last_of( "." );
1436  sstr.str( "" );
1437  sstr << runCtx->intxFilename.substr( 0, lastindex ) << ".h5m";
1438  if( !runCtx->proc_id )
1439  std::cout << "Writing out the MOAB intersection mesh file to " << sstr.str() << std::endl;
1440 
1441  // Write out our computed intersection file
1442  if( !runCtx->skip_io )
1443  {
1444  MB_CHK_SET_ERR( mbCore->write_file( sstr.str().c_str(), nullptr, writeOptions, &writableOverlapSet, 1 ),
1445  "Failed to write the writable overlap set" );
1446  }
1447  }
1448 
1449  if( runCtx->computeWeights )
1450  {
1451  runCtx->meshes[2] = remapper.GetMesh( moab::Remapper::OverlapMesh );
1452  if( !runCtx->proc_id ) std::cout << std::endl;
1453 
1454  runCtx->timer_push( "setup computation of weights" );
1455  // Call to generate the remapping weights with the tempest meshes
1456  moab::TempestOnlineMap* weightMap = new moab::TempestOnlineMap( &remapper );
1457  runCtx->timer_pop();
1458 
1459  runCtx->timer_push( "compute weights with TempestRemap" );
1461  runCtx->disc_methods[0], // std::string strInputType
1462  runCtx->disc_methods[1], // std::string strOutputType,
1463  runCtx->mapOptions, // const GenerateOfflineMapAlgorithmOptions& options
1464  runCtx->doftag_names[0], // const std::string& source_tag_name
1465  runCtx->doftag_names[1] // const std::string& target_tag_name
1466  ),
1467  "Failed to generate remapping weights" );
1468  runCtx->timer_pop();
1469 
1470  weightMap->PrintMapStatistics();
1471 
1472  // Invoke the CheckMap routine on the TempestRemap serial interface directly, if running
1473  // on a single process
1474  if( runCtx->fCheck )
1475  {
1476  const double dNormalTolerance = 1.0E-8;
1477  const double dStrictTolerance = 1.0E-12;
1478  weightMap->CheckMap( runCtx->fCheck, runCtx->fCheck, runCtx->fCheck && ( runCtx->ensureMonotonicity ),
1479  dNormalTolerance, dStrictTolerance, dTotalOverlapArea );
1480  }
1481 
1482  if( runCtx->outFilename.size() && !runCtx->skip_io )
1483  {
1484  std::map< std::string, std::string > attrMap;
1485  attrMap["MOABversion"] = std::string( MOAB_PACKAGE_VERSION );
1486  attrMap["Title"] = "MOAB-TempestRemap (mbtempest) Offline Regridding Weight Generator";
1487  attrMap["normalization"] = "ovarea";
1488  attrMap["remap_options"] = runCtx->mapOptions.strMethod;
1489  attrMap["domain_a"] = runCtx->inFilenames[0];
1490  attrMap["domain_b"] = runCtx->inFilenames[1];
1491  if( runCtx->intxFilename.size() ) attrMap["domain_aUb"] = runCtx->intxFilename;
1492  attrMap["map_aPb"] = runCtx->outFilename;
1493  attrMap["methodorder_a"] = runCtx->disc_methods[0] + ":" + std::to_string( runCtx->disc_orders[0] ) +
1494  ":" + std::string( runCtx->doftag_names[0] );
1495  attrMap["concave_a"] = runCtx->mapOptions.fSourceConcave ? "true" : "false";
1496  attrMap["methodorder_b"] = runCtx->disc_methods[1] + ":" + std::to_string( runCtx->disc_orders[1] ) +
1497  ":" + std::string( runCtx->doftag_names[1] );
1498  attrMap["concave_b"] = runCtx->mapOptions.fTargetConcave ? "true" : "false";
1499  attrMap["bubble"] = runCtx->mapOptions.fNoBubble ? "false" : "true";
1500  attrMap["history"] = historyStr;
1501 
1502  // Write the map file to disk in parallel using either HDF5 or SCRIP interface
1503  // in extra case; maybe need a better solution, just create it with the right meshset
1504  // from the beginning;
1505  MB_CHK_SET_ERR( weightMap->WriteParallelMap( runCtx->outFilename.c_str(), attrMap ),
1506  "Failed writing the parallel map to disk" );
1507  }
1508 
1509  if( runCtx->verifyWeights )
1510  {
1511  // Let us pick a sampling test function for solution evaluation
1512  // SH, SV, FH, C, USERVAR
1513  bool userVariable = false;
1515  if( !runCtx->variableToVerify.compare( "SH" ) )
1516  testFunction = &sample_slow_harmonic;
1517  else if( !runCtx->variableToVerify.compare( "FH" ) )
1518  testFunction = &sample_fast_harmonic;
1519  else if( !runCtx->variableToVerify.compare( "SV" ) )
1520  testFunction = &sample_stationary_vortex;
1521  else if( !runCtx->variableToVerify.compare( "C" ) )
1522  testFunction = &sample_constant;
1523  else
1524  {
1525  userVariable = runCtx->variableToVerify.size() ? true : false;
1526  testFunction = runCtx->variableToVerify.size() ? nullptr : sample_stationary_vortex;
1527  }
1528 
1529  moab::Tag srcAnalyticalFunction;
1530  moab::Tag tgtAnalyticalFunction;
1531  moab::Tag tgtProjectedFunction;
1532  if( testFunction )
1533  {
1534  runCtx->timer_push( "describe a solution on source grid" );
1535  // MB_CHK_SET_ERR( mbCore->tag_get_handle( runCtx->variableToVerify.c_str(), srcAnalyticalFunction ),
1536  // "Failed to get analytical solution on source grid" );
1537  MB_CHK_SET_ERR( weightMap->DefineAnalyticalSolution( srcAnalyticalFunction,
1538  "AnalyticalSolnSrcExact",
1539  moab::Remapper::SourceMesh, testFunction ),
1540  "Failed to define analytical solution on source grid" );
1541  runCtx->timer_pop();
1542 
1543  // runCtx->timer_push( "exchange solution on source grid" );
1544  // moab::Range& srccovEnts = remapper.GetMeshEntities( moab::Remapper::CoveringMesh );
1545  // MB_CHK_SET_ERR( pcomm->exchange_tags( srcAnalyticalFunction, srccovEnts ),
1546  // "Failed to exchange analytical solution on source grid" );
1547  // runCtx->timer_pop();
1548 
1549  runCtx->timer_push( "describe a solution on target grid" );
1551  tgtAnalyticalFunction, "AnalyticalSolnTgtExact", moab::Remapper::TargetMesh,
1552  testFunction, &tgtProjectedFunction, "ProjectedSolnTgt" ),
1553  "Failed to define analytical solution on target grid" );
1554  runCtx->timer_pop();
1555  }
1556  else
1557  {
1558  MB_CHK_SET_ERR( mbCore->tag_get_handle( runCtx->variableToVerify.c_str(), srcAnalyticalFunction ),
1559  "Failed to get analytical solution on source grid" );
1560  MB_CHK_SET_ERR( mbCore->tag_get_handle( "ProjectedSolnTgt", 1, moab::MB_TYPE_DOUBLE,
1561  tgtProjectedFunction,
1563  "Failed to get projected solution on target grid" );
1564  }
1565 
1566  // if( !runCtx->skip_io )
1567  {
1568  MB_CHK_SET_ERR( mbCore->write_file( "srcWithSolnTag.h5m", nullptr, writeOptions,
1569  &runCtx->meshsets[0], 1 ),
1570  "Failed to write the source mesh with solution tag" );
1571  }
1572 
1573  runCtx->timer_push( "compute solution projection on target grid" );
1574  MB_CHK_SET_ERR( weightMap->ApplyWeights( srcAnalyticalFunction, tgtProjectedFunction, false,
1575  runCtx->cassType ),
1576  "Failed to apply weights" );
1577  runCtx->timer_pop();
1578 
1579  // if( !runCtx->skip_io )
1580  {
1581  MB_CHK_SET_ERR( mbCore->write_file( "tgtWithSolnTag2.h5m", nullptr, writeOptions,
1582  &runCtx->meshsets[1], 1 ),
1583  "Failed to write the target mesh with projected solution tag" );
1584  }
1585 
1586  if( nprocs == 1 && runCtx->baselineFile.size() )
1587  {
1588  // save the field from tgtWithSolnTag2 in a text file, and global ids for cells
1589  moab::Range tgtEntities;
1590  if( runCtx->disc_methods[1] == "pcloud" )
1591  {
1592  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[1], 0, tgtEntities ),
1593  "Failed to get entities by dimension" );
1594  }
1595  else
1596  {
1597  MB_CHK_SET_ERR( mbCore->get_entities_by_dimension( runCtx->meshsets[1], 2, tgtEntities ),
1598  "Failed to get entities by dimension" );
1599  }
1600  std::vector< int > globIds( tgtEntities.size() );
1601  std::vector< double > vals( tgtEntities.size() );
1602  moab::Tag projTag;
1603  MB_CHK_SET_ERR( mbCore->tag_get_handle( "ProjectedSolnTgt", projTag ),
1604  "Failed to get projected solution tag" );
1605  moab::Tag gid = mbCore->globalId_tag();
1606  MB_CHK_SET_ERR( mbCore->tag_get_data( gid, tgtEntities, &globIds[0] ), "Failed to get global ids" );
1607  MB_CHK_SET_ERR( mbCore->tag_get_data( projTag, tgtEntities, &vals[0] ),
1608  "Failed to get projected solution" );
1609  std::fstream fs;
1610  fs.open( runCtx->baselineFile.c_str(), std::fstream::out );
1611  fs << std::setprecision( 15 ); // maximum precision for doubles
1612  for( size_t i = 0; i < tgtEntities.size(); i++ )
1613  fs << globIds[i] << " " << vals[i] << "\n";
1614  fs.close();
1615  // for good measure, save the source file too, with the tag AnalyticalSolnSrcExact
1616  // it will be used later to test, along with a target file
1617  if( !runCtx->skip_io )
1618  {
1619  MB_CHK_SET_ERR( mbCore->write_file( "srcWithSolnTag.h5m", nullptr, writeOptions,
1620  &runCtx->meshsets[0], 1 ),
1621  "Failed to write the source mesh with solution tag" );
1622  }
1623  }
1624 
1625  // compute error metrics if it is a known analytical functional
1626  if( !userVariable )
1627  {
1628  runCtx->timer_push( "compute error metrics against analytical solution on target grid" );
1629  std::map< std::string, double > errMetrics;
1630  MB_CHK_SET_ERR( weightMap->ComputeMetrics( moab::Remapper::TargetMesh, tgtAnalyticalFunction,
1631  tgtProjectedFunction, errMetrics, true ),
1632  "Failed to compute error metrics" );
1633  runCtx->timer_pop();
1634  }
1635  }
1636 
1637  delete weightMap;
1638  }
1639  }
1640 
1641  // Clean up
1642  remapper.clear();
1643  delete runCtx;
1644  delete mbCore;
1645 
1646 #ifdef MOAB_HAVE_MPI
1647  MPI_Finalize();
1648 #endif
1649  return 0;
1650  }
1651  catch( const std::exception& e )
1652  {
1653  std::cerr << "[mbtempest] Fatal error: " << e.what() << std::endl;
1654 #ifdef MOAB_HAVE_MPI
1655  MPI_Abort( MPI_COMM_WORLD, 1 );
1656 #endif
1657  return 1;
1658  }
1659  catch( ... )
1660  {
1661  std::cerr << "[mbtempest] Fatal: unknown exception caught" << std::endl;
1662 #ifdef MOAB_HAVE_MPI
1663  MPI_Abort( MPI_COMM_WORLD, 1 );
1664 #endif
1665  return 1;
1666  }
1667 }
1668 
1669 ///////////////////////////////////////////////////////////////////////////////
1670 
1671 // Helper functions for each mesh type
1672 namespace
1673 {
1674 
1675 #define TR_CHK_SET_ERR( err, msg ) \
1676  if( err ) \
1677  { \
1678  std::cout << "MOAB-TempestRemap Failure. ErrorCode (" << ( err ) << ") "; \
1679  MB_CHK_SET_ERR( moab::MB_FAILURE, msg ); \
1680  }
1681 
1682 #ifdef MOAB_HAVE_NETCDF
1683 // TempestRemap-native overlap path: loads source/target meshes as TempestRemap Mesh
1684 // objects (which require NetCDF) and generates the overlap in memory. Only available when
1685 // NetCDF is enabled; otherwise use the MOAB-native path (handleOverlapMOAB).
1686 moab::ErrorCode handleOverlapMemory( ToolContext& ctx, moab::TempestRemapper& remapper, Mesh* tempest_mesh )
1687 {
1688  using namespace moab;
1689 
1690  // resize the meshsets and meshes vectors
1691  ctx.meshsets.resize( 3 );
1692  ctx.meshes.resize( 3 );
1693 
1694  ctx.meshsets[0] = remapper.GetMeshSet( Remapper::SourceMesh );
1695  ctx.meshsets[1] = remapper.GetMeshSet( Remapper::TargetMesh );
1696  ctx.meshsets[2] = remapper.GetMeshSet( Remapper::OverlapMesh );
1697 
1698  // Load and process source mesh
1699  MB_CHK_SET_ERR( remapper.LoadMesh( Remapper::SourceMesh, ctx.inFilenames[0], TempestRemapper::DEFAULT ),
1700  "Failed to load MOAB Source mesh" );
1701 
1702  // Load and process target mesh
1703  MB_CHK_SET_ERR( remapper.LoadMesh( Remapper::TargetMesh, ctx.inFilenames[1], TempestRemapper::DEFAULT ),
1704  "Failed to load MOAB Target mesh" );
1705 
1706  // Generate overlap mesh
1707  TR_CHK_SET_ERR( GenerateOverlapWithMeshes( *ctx.meshes[0], *ctx.meshes[1], *tempest_mesh, "", "NetCDF4", "exact",
1708  false ),
1709  "Failed to generate TempestRemap OverlapMesh" );
1710 
1711  remapper.SetMesh( Remapper::OverlapMesh, tempest_mesh );
1712  ctx.meshes[2] = remapper.GetMesh( Remapper::OverlapMesh );
1713 
1714  return moab::MB_SUCCESS;
1715 }
1716 #endif // MOAB_HAVE_NETCDF
1717 
1719 {
1720  using namespace moab;
1721 
1722  // resize the meshsets and meshes vectors
1723  ctx.meshsets.resize( 3 );
1724  ctx.meshes.resize( 3 );
1725 
1726  ctx.meshsets[0] = remapper.GetMeshSet( Remapper::SourceMesh );
1727  ctx.meshsets[1] = remapper.GetMeshSet( Remapper::TargetMesh );
1728  ctx.meshsets[2] = remapper.GetMeshSet( Remapper::OverlapMesh );
1729 
1730  constexpr double radius_src = 1.0;
1731  constexpr double radius_dest = 1.0;
1732 
1733  // Load and process target mesh
1734  {
1735  std::vector< int > metadata;
1736  std::string additional_read_opts_tgt = ctx.get_file_read_options( ctx.inFilenames[1] );
1737  if( ctx.n_procs > 1 && ctx.disc_methods[1].compare( "fv" ) != 0 ) // target discretization is cgll or dgll
1738  {
1739  // auto pcomm = new ParallelComm( ctx.mbcore, MPI_COMM_WORLD );
1740  // add one ghost layer to the target mesh
1741  // additional_read_opts_tgt = additional_read_opts_tgt + "PARALLEL_GHOSTS=3.0.2;PARALLEL_THIN_GHOST_LAYER;SKIP_AUGMENT_WITH_GHOSTS;PRINT_PARALLEL;";
1742  // additional_read_opts_tgt = additional_read_opts_tgt + "PARALLEL_COMM=1;";
1743  // additional_read_opts_tgt = additional_read_opts_tgt + "PARALLEL_GHOSTS=3.0.1;";
1744  // additional_read_opts_tgt = additional_read_opts_tgt + "PARALLEL_COMM=" + std::to_string(ctx.pcomm->get_id()) + ";";
1745  }
1746 
1747  MB_CHK_SET_ERR( remapper.LoadNativeMesh( ctx.inFilenames[1], ctx.meshsets[1], metadata,
1748  additional_read_opts_tgt.c_str() ),
1749  "Failed to load MOAB Target mesh" );
1750 
1751 #ifdef MOAB_HAVE_MPI
1752  if( ctx.n_procs > 1 && ctx.disc_methods[1].compare( "fv" ) != 0 &&
1753  false ) // target discretization is cgll or dgll
1754  {
1755  Range beforeGhost, afterGhost;
1756  ctx.mbcore->get_entities_by_dimension( ctx.meshsets[1], 2, beforeGhost );
1757 
1758  ctx.pcomm->set_debug_verbosity( 5 );
1759  MB_CHK_SET_ERR( ctx.pcomm->exchange_ghost_cells( 2, 0, 1, 0, true, true, &ctx.meshsets[1] ),
1760  "Failed to exchange ghost cells for MOAB Target mesh" );
1761  ctx.pcomm->set_debug_verbosity( 0 );
1762 
1763  ctx.mbcore->get_entities_by_dimension( ctx.meshsets[1], 2, afterGhost );
1764  std::cout << ctx.proc_id << ": N(before) = " << beforeGhost.size() << ", N(after) = " << afterGhost.size()
1765  << std::endl;
1766 
1767  std::vector< Tag > taglist;
1768  taglist.push_back( ctx.mbcore->globalId_tag() );
1769  Tag gdofTag;
1770  MB_CHK_SET_ERR( ctx.mbcore->tag_get_handle( "GLOBAL_DOFS", gdofTag ),
1771  "Failed to get global dofs tag for MOAB Target mesh" );
1772  taglist.push_back( gdofTag );
1773  MB_CHK_SET_ERR( ctx.pcomm->exchange_tags( taglist, taglist, afterGhost ),
1774  "Failed to exchange global dofs for MOAB Target mesh" );
1775  // std::set< unsigned int > commprocs;
1776  // MB_CHK_SET_ERR( ctx.pcomm->get_comm_procs( commprocs ),
1777  // "Failed to get commprocs for MOAB Target mesh" );
1778  // if (ctx.proc_id == 0)
1779  // {
1780  // std::cout << ctx.proc_id << ": commprocs = [";
1781  // for( auto p : commprocs ) std::cout << p << ", ";
1782  // std::cout << "]\n";
1783 
1784  // std::cout << ctx.proc_id << ": N(after) = " << afterGhost.size() << std::endl;
1785  // for (auto eh: afterGhost)
1786  // {
1787  // std::cout << ctx.mbcore->type_from_handle(eh) << ": " << eh << std::endl;
1788  // }
1789  // }
1790  }
1791 #endif
1792 
1793  if( !metadata.empty() )
1794  {
1795  remapper.SetMeshType( Remapper::TargetMesh, metadata );
1796  }
1797 
1798  MB_CHK_SET_ERR( IntxUtils::ScaleToRadius( ctx.mbcore, ctx.meshsets[1], radius_dest ),
1799  "Failed to preprocess MOAB Target mesh" );
1800  }
1801 
1802  // Load and process source mesh
1803  {
1804  std::vector< int > metadata;
1805  auto additional_read_opts_src = ctx.get_file_read_options( ctx.inFilenames[0] );
1806 #ifdef MOAB_HAVE_MPI
1807  if( ctx.n_procs > 1 )
1808  {
1809  // auto pcomm = new ParallelComm( ctx.mbcore, MPI_COMM_WORLD );
1810  additional_read_opts_src =
1811  additional_read_opts_src + "PARALLEL_COMM=" + std::to_string( ctx.pcomm->get_id() ) + ";";
1812  }
1813 #endif
1814  MB_CHK_SET_ERR( remapper.LoadNativeMesh( ctx.inFilenames[0], ctx.meshsets[0], metadata,
1815  additional_read_opts_src.c_str() ),
1816  "Failed to load MOAB Source mesh" );
1817 
1818  if( !metadata.empty() )
1819  {
1820  remapper.SetMeshType( Remapper::SourceMesh, metadata );
1821  }
1822 
1823  MB_CHK_SET_ERR( IntxUtils::ScaleToRadius( ctx.mbcore, ctx.meshsets[0], radius_src ),
1824  "Failed to preprocess MOAB Source mesh" );
1825  }
1826 
1827  if( ctx.computeWeights )
1828  {
1829  // Convert MOAB to TempestRemap meshes
1830  MB_CHK_SET_ERR( remapper.ConvertMeshToTempest( Remapper::SourceMesh ),
1831  "Failed to convert MOAB Source mesh to TempestRemap mesh" );
1832  ctx.meshes[0] = remapper.GetMesh( Remapper::SourceMesh );
1833 
1834  MB_CHK_SET_ERR( remapper.ConvertMeshToTempest( Remapper::TargetMesh ),
1835  "Failed to convert MOAB Target mesh to TempestRemap mesh" );
1836  ctx.meshes[1] = remapper.GetMesh( Remapper::TargetMesh );
1837  }
1838 
1839  return moab::MB_SUCCESS;
1840 }
1841 
1842 #ifdef MOAB_HAVE_NETCDF
1843 // TempestRemap-native, file-based overlap generation (GenerateOverlapMesh reads the mesh
1844 // files and writes the overlap). Requires NetCDF; use the MOAB-native path otherwise.
1845 moab::ErrorCode handleOverlapFiles( ToolContext& ctx, Mesh* tempest_mesh )
1846 {
1847  ctx.timer_push( "create Tempest OverlapMesh" );
1848  TR_CHK_SET_ERR( GenerateOverlapMesh( ctx.inFilenames[0], ctx.inFilenames[1], *tempest_mesh, ctx.outFilename,
1849  "NetCDF4", "exact", true ),
1850  "Failed to create Tempest OverlapMesh" );
1851  ctx.timer_pop();
1852 
1853  // Add the overlap mesh to the list of meshes
1854  ctx.meshes.push_back( tempest_mesh );
1855  return moab::MB_SUCCESS;
1856 }
1857 #endif // MOAB_HAVE_NETCDF
1858 
1859 /**
1860  * @brief Convert a generated TempestRemap mesh to MOAB format and write as h5m file.
1861  *
1862  * When the output filename has a .h5m extension, the mesh is converted from TempestRemap
1863  * format to MOAB format in memory and written as a native MOAB HDF5 file. This allows
1864  * the generated meshes to be loaded by mbtempest type 5 (OVERLAP_MOAB) workflows.
1865  * The TempestRemap format file is still written (with .g extension) for compatibility.
1866  */
1868 {
1869  // Check if output filename has .h5m extension
1870  const std::string& outFile = ctx.outFilename;
1871  const size_t dot = outFile.find_last_of( "." );
1872  if( dot == std::string::npos ) return moab::MB_SUCCESS;
1873 
1874  const std::string ext = outFile.substr( dot + 1 );
1875  if( ext != "h5m" ) return moab::MB_SUCCESS;
1876 
1877  // Register the TempestRemap mesh with the remapper as SourceMesh
1878  remapper.SetMesh( moab::Remapper::SourceMesh, tempest_mesh, false );
1879 
1880  // Convert TempestRemap mesh to MOAB format
1881  ctx.timer_push( "convert TempestRemap mesh to MOAB format" );
1883  "Failed to convert TempestRemap mesh to MOAB format" );
1884  ctx.timer_pop();
1885 
1886  // Fix degenerate quads: RLL meshes from TempestRemap have polar cells stored as
1887  // 4-node quads with duplicate vertices. Convert these to proper triangles so the
1888  // intersection algorithm can handle them correctly.
1891  "Failed to fix degenerate quads in converted mesh" );
1892  ctx.timer_push( "write MOAB mesh to h5m file" );
1893  MB_CHK_SET_ERR( ctx.mbcore->write_file( outFile.c_str(), nullptr, nullptr, &meshSet, 1 ),
1894  "Failed to write MOAB mesh to h5m file" );
1895  ctx.timer_pop();
1896 
1897  if( !ctx.proc_id )
1898  ctx.outputFormatter.printf( 0, "Wrote MOAB mesh to %s\n", outFile.c_str() );
1899 
1900  return moab::MB_SUCCESS;
1901 }
1902 
1903 moab::ErrorCode handleICOMesh( ToolContext& ctx, moab::TempestRemapper& remapper, Mesh* tempest_mesh )
1904 {
1905  std::string trFilename = ctx.outFilename;
1906  const size_t dot = trFilename.find_last_of( "." );
1907  if( dot != std::string::npos && trFilename.substr( dot + 1 ) == "h5m" )
1908  trFilename = trFilename.substr( 0, dot ) + ".g";
1909 
1910  ctx.timer_push( "generate ICO mesh with TempestRemap" );
1911  TR_CHK_SET_ERR( GenerateICOMesh( *tempest_mesh, ctx.blockSize, ctx.computeDual, trFilename, "NetCDF4" ),
1912  "Failed to generate ICO mesh with TempestRemap" );
1913  ctx.timer_pop();
1914 
1915  // Add the ICO mesh to the list of meshes
1916  ctx.meshes.push_back( tempest_mesh );
1917 
1918  MB_CHK_SET_ERR( convertAndWriteMOABMesh( ctx, remapper, tempest_mesh ),
1919  "Failed to convert and write MOAB mesh" );
1920 
1921  return moab::MB_SUCCESS;
1922 }
1923 
1924 moab::ErrorCode handleRLLMesh( ToolContext& ctx, moab::TempestRemapper& remapper, Mesh* tempest_mesh )
1925 {
1926  std::string trFilename = ctx.outFilename;
1927  const size_t dot = trFilename.find_last_of( "." );
1928  if( dot != std::string::npos && trFilename.substr( dot + 1 ) == "h5m" )
1929  trFilename = trFilename.substr( 0, dot ) + ".g";
1930 
1931  ctx.timer_push( "generate RLL mesh with TempestRemap" );
1932  TR_CHK_SET_ERR( GenerateRLLMesh( *tempest_mesh, // Mesh& meshOut,
1933  ctx.blockSize * 2, ctx.blockSize, // int nLongitudes, int nLatitudes,
1934  0.0, 360.0, // double dLonBegin, double dLonEnd,
1935  -90.0, 90.0, // double dLatBegin, double dLatEnd,
1936  false, false, false, // bool fGlobalCap, bool fFlipLatLon, bool fForceGlobal,
1937  "" /*ctx.inFilename*/,
1938  "", // std::string strInputFile, std::string strInputFileLonName
1939  "", // std::string strInputFileLatName
1940  trFilename, // std::string strOutputFile
1941  "NetCDF4", // std::string strOutputFormat
1942  true // bool fVerbose
1943  ),
1944  "Failed to generate RLL mesh with TempestRemap" );
1945  ctx.timer_pop();
1946 
1947  // Add the RLL mesh to the list of meshes
1948  ctx.meshes.push_back( tempest_mesh );
1949 
1950  MB_CHK_SET_ERR( convertAndWriteMOABMesh( ctx, remapper, tempest_mesh ),
1951  "Failed to convert and write MOAB mesh" );
1952 
1953  return moab::MB_SUCCESS;
1954 }
1955 
1956 moab::ErrorCode handleCSMesh( ToolContext& ctx, moab::TempestRemapper& remapper, Mesh* tempest_mesh )
1957 {
1958  // Generate the TempestRemap Exodus mesh (always written as .g for TempestRemap compatibility)
1959  std::string trFilename = ctx.outFilename;
1960  const size_t dot = trFilename.find_last_of( "." );
1961  if( dot != std::string::npos && trFilename.substr( dot + 1 ) == "h5m" )
1962  trFilename = trFilename.substr( 0, dot ) + ".g";
1963 
1964  ctx.timer_push( "generate CS mesh with TempestRemap" );
1965  TR_CHK_SET_ERR( GenerateCSMesh( *tempest_mesh, ctx.blockSize, trFilename, "NetCDF4" ),
1966  "Failed to generate CS mesh with TempestRemap" );
1967  ctx.timer_pop();
1968 
1969  // Add the CS mesh to the list of meshes
1970  ctx.meshes.push_back( tempest_mesh );
1971 
1972  // Convert and write as MOAB h5m if requested
1973  MB_CHK_SET_ERR( convertAndWriteMOABMesh( ctx, remapper, tempest_mesh ),
1974  "Failed to convert and write MOAB mesh" );
1975 
1976  return moab::MB_SUCCESS;
1977 }
1978 
1979 } // namespace
1980 
1981 /**
1982  * @brief Creates a TempestRemap mesh based on the provided context and mesh type
1983  *
1984  * @param ctx Tool context containing configuration and state
1985  * @param remapper TempestRemap instance for mesh operations
1986  * @param tempest_mesh Output parameter for the created mesh
1987  * @return moab::ErrorCode Status of the operation
1988  */
1989 static moab::ErrorCode CreateTempestMesh( ToolContext& ctx, moab::TempestRemapper& remapper, Mesh* tempest_mesh )
1990 {
1991  using namespace moab;
1992  using RemapperType = moab::TempestRemapper;
1993 
1994  auto& outputFormatter = ctx.outputFormatter;
1995 
1996  try
1997  {
1998  switch( ctx.meshType )
1999  {
2000  case RemapperType::OVERLAP_FILES:
2001 #ifdef MOAB_HAVE_NETCDF
2002  if( !ctx.proc_id ) outputFormatter.printf( 0, "Creating TempestRemap overlap mesh ...\n" );
2003  return handleOverlapFiles( ctx, tempest_mesh );
2004 #else
2005  MB_CHK_SET_ERR( moab::MB_FAILURE,
2006  "OVERLAP_FILES mode requires NetCDF (TempestRemap file-based overlap "
2007  "generation); build with NetCDF or use OVERLAP_MOAB mode instead" );
2008 #endif
2009 
2010  case RemapperType::OVERLAP_MEMORY:
2011 #ifdef MOAB_HAVE_NETCDF
2012  if( !ctx.proc_id )
2013  outputFormatter.printf( 0, "Convert MOAB overlap files to TempestRemap format in-memory ...\n" );
2014  return handleOverlapMemory( ctx, remapper, tempest_mesh );
2015 #else
2016  MB_CHK_SET_ERR( moab::MB_FAILURE,
2017  "OVERLAP_MEMORY mode requires NetCDF (TempestRemap-native mesh loading); "
2018  "build with NetCDF or use OVERLAP_MOAB mode instead" );
2019 #endif
2020 
2021  case RemapperType::OVERLAP_MOAB:
2022  if( !ctx.proc_id )
2023  outputFormatter.printf( 0, "Convert MOAB meshes to TempestRemap format in-memory ...\n" );
2024  return handleOverlapMOAB( ctx, remapper );
2025 
2026  case RemapperType::ICO:
2027  if( !ctx.proc_id ) outputFormatter.printf( 0, "Creating TempestRemap ICO mesh ...\n" );
2028  return handleICOMesh( ctx, remapper, tempest_mesh );
2029 
2030  case RemapperType::RLL:
2031  if( !ctx.proc_id ) outputFormatter.printf( 0, "Creating TempestRemap RLL mesh ...\n" );
2032  return handleRLLMesh( ctx, remapper, tempest_mesh );
2033 
2034  default: // Default to CS mesh
2035  if( !ctx.proc_id ) outputFormatter.printf( 0, "Creating TempestRemap CS mesh ...\n" );
2036  return handleCSMesh( ctx, remapper, tempest_mesh );
2037  }
2038  }
2039  catch( const std::exception& e )
2040  {
2041  std::cerr << "Error in CreateTempestMesh: " << e.what() << "\n";
2042  return MB_FAILURE;
2043  }
2044 }
2045 
2046 #undef MOAB_DBG
2047 
2048 ///////////////////////////////////////////////
2049 /**
2050  * @brief Sample functions for testing remapping operations
2051  *
2052  * These functions provide analytical test cases with different spatial patterns
2053  * for verifying remapping accuracy and performance.
2054  */
2055 
2056 // Constants for sample functions
2057 namespace
2058 {
2059 // Constants for sample_stationary_vortex
2060 constexpr double VORTEX_LON0 = 0.0;
2061 constexpr double VORTEX_LAT0 = 0.6;
2062 constexpr double VORTEX_R0 = 3.0;
2063 constexpr double VORTEX_D = 5.0;
2064 constexpr double VORTEX_T = 6.0;
2065 
2066 } // namespace
2067 
2068 /**
2069  * @brief Constant sample function
2070  *
2071  * @return double Always returns 1.0
2072  */
2073 static inline constexpr double sample_constant( double /*dLon*/, double /*dLat*/ ) noexcept
2074 {
2075  return 1.0;
2076 }
2077 
2078 /**
2079  * @brief Sample function with slow harmonic variation
2080  *
2081  * @param dLon Longitude in radians
2082  * @param dLat Latitude in radians
2083  * @return double Function value at (dLon, dLat)
2084  */
2085 static inline double sample_slow_harmonic( double dLon, double dLat ) noexcept
2086 {
2087  const double cosLat = std::cos( dLat );
2088  return 2.0 + cosLat * cosLat * std::cos( 2.0 * dLon );
2089 }
2090 
2091 /**
2092  * @brief Sample function with fast harmonic variation
2093  *
2094  * @param dLon Longitude in radians
2095  * @param dLat Latitude in radians
2096  * @return double Function value at (dLon, dLat)
2097  */
2098 static inline double sample_fast_harmonic( double dLon, double dLat ) noexcept
2099 {
2100  const double sin2Lat = std::sin( 2.0 * dLat );
2101  return 2.0 +
2102  sin2Lat * sin2Lat * sin2Lat * sin2Lat * sin2Lat * sin2Lat * sin2Lat * sin2Lat * std::cos( 16.0 * dLon );
2103 }
2104 
2105 /**
2106  * @brief Sample function representing a stationary vortex
2107  *
2108  * @param dLon Longitude in radians
2109  * @param dLat Latitude in radians
2110  * @return double Function value at (dLon, dLat)
2111  */
2112 static inline double sample_stationary_vortex( double dLon, double dLat ) noexcept
2113 {
2114  // Find the rotated longitude and latitude of a point on a sphere
2115  // with pole at (dLonC, dLatC)
2116  const double dSinC = std::sin( VORTEX_LAT0 );
2117  const double dCosC = std::cos( VORTEX_LAT0 );
2118  const double dSinT = std::sin( dLat );
2119  const double dCosT = std::cos( dLat );
2120 
2121  const double dTrm = dCosT * std::cos( dLon - VORTEX_LON0 );
2122  const double dX = dSinC * dTrm - dCosC * dSinT;
2123  const double dY = dCosT * std::sin( dLon - VORTEX_LON0 );
2124  const double dZ = dSinC * dSinT + dCosC * dTrm;
2125 
2126  // Calculate new longitude and latitude in rotated coordinate system
2127  double dNewLon = std::atan2( dY, dX );
2128  if( dNewLon < 0.0 )
2129  {
2130  dNewLon += 2.0 * M_PI;
2131  }
2132  const double dNewLat = std::asin( dZ );
2133 
2134  // Calculate vortex profile
2135  const double dRho = VORTEX_R0 * std::cos( dNewLat );
2136  const double dVt = 3.0 * std::sqrt( 3.0 ) / 2.0 / std::cosh( dRho ) / std::cosh( dRho ) * std::tanh( dRho );
2137 
2138  // Calculate angular velocity (avoid division by zero)
2139  const double dOmega = ( dRho == 0.0 ) ? 0.0 : ( dVt / dRho );
2140 
2141  // Return the final vortex profile
2142  return ( 1.0 - std::tanh( dRho / VORTEX_D * std::sin( dNewLon - dOmega * VORTEX_T ) ) );
2143 }
2144 
2145 ///////////////////////////////////////////////