MOAB: Mesh Oriented datABase
(version 5.4.1)
|
00001 /* 00002 * NCHelperScrip.cpp 00003 */ 00004 00005 #include "NCHelperScrip.hpp" 00006 #include "moab/ReadUtilIface.hpp" 00007 #include "AEntityFactory.hpp" 00008 #include "moab/IntxMesh/IntxUtils.hpp" 00009 #ifdef MOAB_HAVE_MPI 00010 #include "moab/ParallelMergeMesh.hpp" 00011 #endif 00012 #ifdef MOAB_HAVE_ZOLTAN 00013 #include "moab/ZoltanPartitioner.hpp" 00014 #endif 00015 00016 namespace moab 00017 { 00018 00019 bool NCHelperScrip::can_read_file( ReadNC* readNC, int /*fileId*/ ) 00020 { 00021 std::vector< std::string >& dimNames = readNC->dimNames; 00022 00023 // If dimension names "grid_size" AND "grid_corners" AND "grid_rank" exist then it should be the Scrip grid 00024 if( ( std::find( dimNames.begin(), dimNames.end(), std::string( "grid_size" ) ) != dimNames.end() ) && 00025 ( std::find( dimNames.begin(), dimNames.end(), std::string( "grid_corners" ) ) != dimNames.end() ) && 00026 ( std::find( dimNames.begin(), dimNames.end(), std::string( "grid_rank" ) ) != dimNames.end() ) ) 00027 { 00028 00029 return true; 00030 } 00031 00032 return false; 00033 } 00034 ErrorCode NCHelperScrip::init_mesh_vals() 00035 { 00036 Interface*& mbImpl = _readNC->mbImpl; 00037 std::vector< std::string >& dimNames = _readNC->dimNames; 00038 std::vector< int >& dimLens = _readNC->dimLens; 00039 00040 unsigned int idx; 00041 std::vector< std::string >::iterator vit; 00042 00043 // get grid_size 00044 if( ( vit = std::find( dimNames.begin(), dimNames.end(), "grid_size" ) ) != dimNames.end() ) 00045 { 00046 idx = vit - dimNames.begin(); 00047 grid_size = dimLens[idx]; 00048 } 00049 00050 // get grid_corners 00051 if( ( vit = std::find( dimNames.begin(), dimNames.end(), "grid_corners" ) ) != dimNames.end() ) 00052 { 00053 idx = vit - dimNames.begin(); 00054 grid_corners = dimLens[idx]; 00055 } 00056 // do not need conventional tags 00057 Tag convTagsCreated = 0; 00058 int def_val = 0; 00059 ErrorCode rval = mbImpl->tag_get_handle( "__CONV_TAGS_CREATED", 1, MB_TYPE_INTEGER, convTagsCreated, 00060 MB_TAG_SPARSE | MB_TAG_CREAT, &def_val );MB_CHK_SET_ERR( rval, "Trouble getting _CONV_TAGS_CREATED tag" ); 00061 int create_conv_tags_flag = 1; 00062 rval = mbImpl->tag_set_data( convTagsCreated, &_fileSet, 1, &create_conv_tags_flag );MB_CHK_SET_ERR( rval, "Trouble setting _CONV_TAGS_CREATED tag" ); 00063 00064 // decide now the units, by looking at grid_center_lon 00065 int xCellVarId; 00066 int success = NCFUNC( inq_varid )( _fileId, "grid_center_lon", &xCellVarId ); 00067 if( success ) MB_CHK_SET_ERR( MB_FAILURE, "Trouble getting grid_center_lon" ); 00068 std::map< std::string, ReadNC::VarData >& varInfo = _readNC->varInfo; 00069 auto vmit = varInfo.find( "grid_center_lon" ); 00070 if( varInfo.end() == vmit ) 00071 MB_SET_ERR( MB_FAILURE, "Couldn't find variable " 00072 << "grid_center_lon" ); 00073 ReadNC::VarData& glData = vmit->second; 00074 auto attIt = glData.varAtts.find( "units" ); 00075 if( attIt != glData.varAtts.end() ) 00076 { 00077 unsigned int sz = attIt->second.attLen; 00078 std::string att_data; 00079 att_data.resize( sz + 1 ); 00080 att_data[sz] = '\000'; 00081 success = 00082 NCFUNC( get_att_text )( _fileId, attIt->second.attVarId, attIt->second.attName.c_str(), &att_data[0] ); 00083 if( 0 == success && att_data.find( "radians" ) != std::string::npos ) degrees = false; 00084 } 00085 00086 return MB_SUCCESS; 00087 } 00088 ErrorCode NCHelperScrip::create_mesh( Range& faces ) 00089 { 00090 Interface*& mbImpl = _readNC->mbImpl; 00091 DebugOutput& dbgOut = _readNC->dbgOut; 00092 Tag& mGlobalIdTag = _readNC->mGlobalIdTag; 00093 ErrorCode rval; 00094 00095 #ifdef MOAB_HAVE_MPI 00096 int rank = 0; 00097 int procs = 1; 00098 bool& isParallel = _readNC->isParallel; 00099 ParallelComm* myPcomm = NULL; 00100 if( isParallel ) 00101 { 00102 myPcomm = _readNC->myPcomm; 00103 rank = myPcomm->proc_config().proc_rank(); 00104 procs = myPcomm->proc_config().proc_size(); 00105 } 00106 00107 if( procs >= 2 ) 00108 { 00109 // Shift rank to obtain a rotated trivial partition 00110 int shifted_rank = rank; 00111 int& trivialPartitionShift = _readNC->trivialPartitionShift; 00112 if( trivialPartitionShift > 0 ) shifted_rank = ( rank + trivialPartitionShift ) % procs; 00113 00114 // Compute the number of local cells on this proc 00115 nLocalCells = int( std::floor( 1.0 * grid_size / procs ) ); 00116 00117 // The starting global cell index in the MPAS file for this proc 00118 int start_cell_idx = shifted_rank * nLocalCells; 00119 00120 // Number of extra cells after equal split over procs 00121 int iextra = grid_size % procs; 00122 00123 // Allocate extra cells over procs 00124 if( shifted_rank < iextra ) nLocalCells++; 00125 start_cell_idx += std::min( shifted_rank, iextra ); 00126 00127 start_cell_idx++; // 0 based -> 1 based 00128 00129 // Redistribute local cells after trivial partition (e.g. apply Zoltan partition) 00130 ErrorCode rval = redistribute_local_cells( start_cell_idx, myPcomm );MB_CHK_SET_ERR( rval, "Failed to redistribute local cells after trivial partition" ); 00131 } 00132 else 00133 { 00134 nLocalCells = grid_size; 00135 localGidCells.insert( 1, nLocalCells ); 00136 } 00137 #else 00138 nLocalCells = grid_size; 00139 localGidCells.insert( 1, nLocalCells ); 00140 #endif 00141 dbgOut.tprintf( 1, " localGidCells.psize() = %d\n", (int)localGidCells.psize() ); 00142 dbgOut.tprintf( 1, " localGidCells.size() = %d\n", (int)localGidCells.size() ); 00143 00144 // double grid_corner_lat(grid_size, grid_corners) ; 00145 // double grid_corner_lon(grid_size, grid_corners) ; 00146 int xvId, yvId; 00147 int success = NCFUNC( inq_varid )( _fileId, "grid_corner_lon", &xvId ); 00148 if( success ) MB_SET_ERR( MB_FAILURE, "Failed to get variable id of grid_corner_lon" ); 00149 success = NCFUNC( inq_varid )( _fileId, "grid_corner_lat", &yvId ); 00150 if( success ) MB_SET_ERR( MB_FAILURE, "Failed to get variable id of grid_corner_lat" ); 00151 00152 // important upgrade: read masks if they exist, and save them as tags 00153 int gmId = -1; 00154 int sizeMasks = 0; 00155 #ifdef MOAB_HAVE_PNETCDF 00156 int factorRequests = 2; // we would read in general only 2 variables, xv and yv 00157 #endif 00158 success = NCFUNC( inq_varid )( _fileId, "grid_imask", &gmId ); 00159 Tag maskTag = 0; // not sure yet if we have the masks or not 00160 if( success ) 00161 { 00162 gmId = -1; // we do not have masks 00163 } 00164 else 00165 { 00166 sizeMasks = nLocalCells; 00167 #ifdef MOAB_HAVE_PNETCDF 00168 factorRequests = 3; // we also need to read masks distributed 00169 #endif 00170 // create the maskTag GRID_IMASK, with default value of 1 00171 int def_val = 1; 00172 rval = 00173 mbImpl->tag_get_handle( "GRID_IMASK", 1, MB_TYPE_INTEGER, maskTag, MB_TAG_DENSE | MB_TAG_CREAT, &def_val );MB_CHK_SET_ERR( rval, "Trouble creating GRID_IMASK tag" ); 00174 } 00175 00176 std::vector< double > xv( nLocalCells * grid_corners ); 00177 std::vector< double > yv( nLocalCells * grid_corners ); 00178 std::vector< int > masks( sizeMasks ); 00179 #ifdef MOAB_HAVE_PNETCDF 00180 size_t nb_reads = localGidCells.psize(); 00181 std::vector< int > requests( nb_reads * factorRequests ); 00182 std::vector< int > statuss( nb_reads * factorRequests ); 00183 size_t idxReq = 0; 00184 #endif 00185 size_t indexInArray = 0; 00186 size_t indexInMaskArray = 0; 00187 for( Range::pair_iterator pair_iter = localGidCells.pair_begin(); pair_iter != localGidCells.pair_end(); 00188 ++pair_iter ) 00189 { 00190 EntityHandle starth = pair_iter->first; 00191 EntityHandle endh = pair_iter->second; 00192 NCDF_SIZE read_starts[2] = { static_cast< NCDF_SIZE >( starth - 1 ), 0 }; 00193 NCDF_SIZE read_counts[2] = { static_cast< NCDF_SIZE >( endh - starth + 1 ), 00194 static_cast< NCDF_SIZE >( grid_corners ) }; 00195 00196 // Do a partial read in each subrange 00197 #ifdef MOAB_HAVE_PNETCDF 00198 success = NCFUNCREQG( _vara_double )( _fileId, xvId, read_starts, read_counts, &( xv[indexInArray] ), 00199 &requests[idxReq++] ); 00200 #else 00201 success = NCFUNCAG( _vara_double )( _fileId, xvId, read_starts, read_counts, &( xv[indexInArray] ) ); 00202 #endif 00203 if( success ) MB_SET_ERR( MB_FAILURE, "Failed to read grid_corner_lon data in a loop" ); 00204 00205 // Do a partial read in each subrange 00206 #ifdef MOAB_HAVE_PNETCDF 00207 success = NCFUNCREQG( _vara_double )( _fileId, yvId, read_starts, read_counts, &( yv[indexInArray] ), 00208 &requests[idxReq++] ); 00209 #else 00210 success = NCFUNCAG( _vara_double )( _fileId, yvId, read_starts, read_counts, &( yv[indexInArray] ) ); 00211 #endif 00212 if( success ) MB_SET_ERR( MB_FAILURE, "Failed to read grid_corner_lat data in a loop" ); 00213 // Increment the index for next subrange 00214 indexInArray += ( endh - starth + 1 ) * grid_corners; 00215 00216 if( gmId >= 0 ) // it means we need to read masks too, distributed: 00217 { 00218 NCDF_SIZE read_st = static_cast< NCDF_SIZE >( starth - 1 ); 00219 NCDF_SIZE read_ct = static_cast< NCDF_SIZE >( endh - starth + 1 ); 00220 // Do a partial read in each subrange, for mask variable: 00221 #ifdef MOAB_HAVE_PNETCDF 00222 success = NCFUNCREQG( _vara_int )( _fileId, gmId, &read_st, &read_ct, &( masks[indexInMaskArray] ), 00223 &requests[idxReq++] ); 00224 #else 00225 success = NCFUNCAG( _vara_int )( _fileId, gmId, &read_st, &read_ct, &( masks[indexInMaskArray] ) ); 00226 #endif 00227 if( success ) MB_SET_ERR( MB_FAILURE, "Failed on mask read " ); 00228 indexInMaskArray += endh - starth + 1; 00229 } 00230 } 00231 00232 #ifdef MOAB_HAVE_PNETCDF 00233 // Wait outside the loop 00234 success = NCFUNC( wait_all )( _fileId, requests.size(), &requests[0], &statuss[0] ); 00235 if( success ) MB_SET_ERR( MB_FAILURE, "Failed on wait_all" ); 00236 #endif 00237 00238 // so we read xv, yv for all corners in the local mesh, and masks if they exist 00239 00240 // Create vertices; first identify different ones, with a tolerance 00241 std::map< Node3D, EntityHandle > vertex_map; 00242 00243 // Set vertex coordinates 00244 // will read all xv, yv, but use only those with correct mask on 00245 00246 int elem_index = 0; // local index in netcdf arrays 00247 double pideg = 1.; // radians 00248 if( degrees ) pideg = acos( -1.0 ) / 180.0; 00249 00250 for( ; elem_index < nLocalCells; elem_index++ ) 00251 { 00252 // set area and fraction on those elements too 00253 for( int k = 0; k < grid_corners; k++ ) 00254 { 00255 int index_v_arr = grid_corners * elem_index + k; 00256 double x, y; 00257 x = xv[index_v_arr]; 00258 y = yv[index_v_arr]; 00259 double cosphi = cos( pideg * y ); 00260 double zmult = sin( pideg * y ); 00261 double xmult = cosphi * cos( x * pideg ); 00262 double ymult = cosphi * sin( x * pideg ); 00263 Node3D pt( xmult, ymult, zmult ); 00264 vertex_map[pt] = 0; 00265 } 00266 } 00267 int nLocalVertices = (int)vertex_map.size(); 00268 std::vector< double* > arrays; 00269 EntityHandle start_vertex, vtx_handle; 00270 rval = _readNC->readMeshIface->get_node_coords( 3, nLocalVertices, 0, start_vertex, arrays );MB_CHK_SET_ERR( rval, "Failed to create local vertices" ); 00271 00272 vtx_handle = start_vertex; 00273 // Copy vertex coordinates into entity sequence coordinate arrays 00274 // and copy handle into vertex_map. 00275 double *x = arrays[0], *y = arrays[1], *z = arrays[2]; 00276 for( auto i = vertex_map.begin(); i != vertex_map.end(); ++i ) 00277 { 00278 i->second = vtx_handle; 00279 ++vtx_handle; 00280 *x = i->first.coords[0]; 00281 ++x; 00282 *y = i->first.coords[1]; 00283 ++y; 00284 *z = i->first.coords[2]; 00285 ++z; 00286 } 00287 00288 EntityHandle start_cell; 00289 int nv = grid_corners; 00290 EntityType mdb_type = MBVERTEX; 00291 if( nv == 3 ) 00292 mdb_type = MBTRI; 00293 else if( nv == 4 ) 00294 mdb_type = MBQUAD; 00295 else if( nv > 4 ) // (nv > 4) 00296 mdb_type = MBPOLYGON; 00297 00298 Range tmp_range; 00299 EntityHandle* conn_arr; 00300 00301 rval = _readNC->readMeshIface->get_element_connect( nLocalCells, nv, mdb_type, 0, start_cell, conn_arr );MB_CHK_SET_ERR( rval, "Failed to create local cells" ); 00302 tmp_range.insert( start_cell, start_cell + nLocalCells - 1 ); 00303 00304 elem_index = 0; 00305 00306 for( ; elem_index < nLocalCells; elem_index++ ) 00307 { 00308 for( int k = 0; k < nv; k++ ) 00309 { 00310 int index_v_arr = nv * elem_index + k; 00311 if( nv > 1 ) 00312 { 00313 double x = xv[index_v_arr]; 00314 double y = yv[index_v_arr]; 00315 double cosphi = cos( pideg * y ); 00316 double zmult = sin( pideg * y ); 00317 double xmult = cosphi * cos( x * pideg ); 00318 double ymult = cosphi * sin( x * pideg ); 00319 Node3D pt( xmult, ymult, zmult ); 00320 conn_arr[elem_index * nv + k] = vertex_map[pt]; 00321 } 00322 } 00323 EntityHandle cell = start_cell + elem_index; 00324 // set other tags, like xc, yc, frac, area 00325 /*rval = mbImpl->tag_set_data( xcTag, &cell, 1, &xc[elem_index] );MB_CHK_SET_ERR( rval, "Failed to set xc tag" ); 00326 rval = mbImpl->tag_set_data( ycTag, &cell, 1, &yc[elem_index] );MB_CHK_SET_ERR( rval, "Failed to set yc tag" ); 00327 rval = mbImpl->tag_set_data( areaTag, &cell, 1, &area[elem_index] );MB_CHK_SET_ERR( rval, "Failed to set area tag" ); 00328 rval = mbImpl->tag_set_data( fracTag, &cell, 1, &frac[elem_index] );MB_CHK_SET_ERR( rval, "Failed to set frac tag" ); 00329 */ 00330 // set the global id too: 00331 int globalId = localGidCells[elem_index]; 00332 00333 rval = mbImpl->tag_set_data( mGlobalIdTag, &cell, 1, &globalId );MB_CHK_SET_ERR( rval, "Failed to set global id tag" ); 00334 if( gmId >= 0 ) 00335 { 00336 int localMask = masks[elem_index]; 00337 rval = mbImpl->tag_set_data( maskTag, &cell, 1, &localMask );MB_CHK_SET_ERR( rval, "Failed to set mask tag" ); 00338 } 00339 } 00340 00341 rval = mbImpl->add_entities( _fileSet, tmp_range );MB_CHK_SET_ERR( rval, "Failed to add new cells to current file set" ); 00342 00343 // modify local file set, to merge coincident vertices, and to correct repeated vertices in elements 00344 std::vector< Tag > tagList; 00345 tagList.push_back( mGlobalIdTag ); 00346 if( gmId >= 0 ) tagList.push_back( maskTag ); 00347 rval = IntxUtils::remove_padded_vertices( mbImpl, _fileSet, tagList );MB_CHK_SET_ERR( rval, "Failed to remove duplicate vertices" ); 00348 00349 rval = mbImpl->get_entities_by_dimension( _fileSet, 2, faces );MB_CHK_ERR( rval ); 00350 Range all_verts; 00351 rval = mbImpl->get_connectivity( faces, all_verts );MB_CHK_ERR( rval ); 00352 rval = mbImpl->add_entities( _fileSet, all_verts );MB_CHK_ERR( rval ); 00353 // need to add adjacencies; TODO: fix this for all nc readers 00354 // copy this logic from migrate mesh in par comm graph 00355 Core* mb = (Core*)mbImpl; 00356 AEntityFactory* adj_fact = mb->a_entity_factory(); 00357 if( !adj_fact->vert_elem_adjacencies() ) 00358 adj_fact->create_vert_elem_adjacencies(); 00359 else 00360 { 00361 for( Range::iterator it = faces.begin(); it != faces.end(); ++it ) 00362 { 00363 EntityHandle eh = *it; 00364 const EntityHandle* conn = NULL; 00365 int num_nodes = 0; 00366 rval = mb->get_connectivity( eh, conn, num_nodes );MB_CHK_ERR( rval ); 00367 adj_fact->notify_create_entity( eh, conn, num_nodes ); 00368 } 00369 } 00370 00371 #ifdef MOAB_HAVE_MPI 00372 if( myPcomm ) 00373 { 00374 double tol = 1.e-12; // this is the same as static tolerance in NCHelper 00375 ParallelMergeMesh pmm( myPcomm, tol ); 00376 rval = pmm.merge( _fileSet, 00377 /* do not do local merge*/ false, 00378 /* 2d cells*/ 2 );MB_CHK_SET_ERR( rval, "Failed to merge vertices in parallel" ); 00379 00380 // assign global ids only for vertices, cells have them fine 00381 rval = myPcomm->assign_global_ids( _fileSet, /*dim*/ 0 );MB_CHK_ERR( rval ); 00382 // remove all sets, edges and vertices from the file set 00383 Range edges, vertices; 00384 rval = mbImpl->get_entities_by_dimension( _fileSet, 1, edges, /*recursive*/ true );MB_CHK_ERR( rval ); 00385 rval = mbImpl->get_entities_by_dimension( _fileSet, 0, vertices, /*recursive*/ true );MB_CHK_ERR( rval ); 00386 rval = mbImpl->remove_entities( _fileSet, edges );MB_CHK_ERR( rval ); 00387 rval = mbImpl->remove_entities( _fileSet, vertices );MB_CHK_ERR( rval ); 00388 00389 Range intfSets = myPcomm->interface_sets(); 00390 // empty intf sets 00391 rval = mbImpl->clear_meshset( intfSets );MB_CHK_ERR( rval ); 00392 // delete the sets without shame :) 00393 //sets.merge(intfSets); 00394 //rval = myPcomm->delete_entities(sets);MB_CHK_ERR( rval ); // will also clean shared ents ! 00395 rval = myPcomm->delete_entities( edges );MB_CHK_ERR( rval ); // will also clean shared ents ! 00396 } 00397 #else 00398 rval = mbImpl->remove_entities( _fileSet, all_verts );MB_CHK_ERR( rval ); 00399 #endif 00400 00401 return MB_SUCCESS; 00402 } 00403 00404 #ifdef MOAB_HAVE_MPI 00405 ErrorCode NCHelperScrip::redistribute_local_cells( int start_cell_idx, ParallelComm * pco ) 00406 { 00407 // If possible, apply Zoltan partition 00408 #ifdef MOAB_HAVE_ZOLTAN 00409 if( ScdParData::RCBZOLTAN == _readNC->partMethod ) 00410 { 00411 // Read grid_center_lat coordinates of cell centers 00412 int xCellVarId; 00413 int success = NCFUNC( inq_varid )( _fileId, "grid_center_lon", &xCellVarId ); 00414 if( success ) MB_SET_ERR( MB_FAILURE, "Failed to get variable id of grid_center_lon" ); 00415 std::vector< double > xc( nLocalCells ); 00416 NCDF_SIZE read_start = static_cast< NCDF_SIZE >( start_cell_idx - 1 ); 00417 NCDF_SIZE read_count = static_cast< NCDF_SIZE >( nLocalCells ); 00418 success = NCFUNCAG( _vara_double )( _fileId, xCellVarId, &read_start, &read_count, &xc[0] ); 00419 if( success ) MB_SET_ERR( MB_FAILURE, "Failed to read grid_center_lat data" ); 00420 00421 // Read grid_center_lon coordinates of cell centers 00422 int yCellVarId; 00423 success = NCFUNC( inq_varid )( _fileId, "grid_center_lat", &yCellVarId ); 00424 if( success ) MB_SET_ERR( MB_FAILURE, "Failed to get variable id of grid_center_lat" ); 00425 std::vector< double > yc( nLocalCells ); 00426 success = NCFUNCAG( _vara_double )( _fileId, yCellVarId, &read_start, &read_count, &yc[0] ); 00427 if( success ) MB_SET_ERR( MB_FAILURE, "Failed to read grid_center_lon data" ); 00428 00429 // Zoltan partition using RCB; maybe more studies would be good, as to which partition 00430 // is better 00431 Interface*& mbImpl = _readNC->mbImpl; 00432 DebugOutput& dbgOut = _readNC->dbgOut; 00433 ZoltanPartitioner* mbZTool = new ZoltanPartitioner( mbImpl, pco, false, 0, NULL ); 00434 std::vector< double > xCell( nLocalCells ); 00435 std::vector< double > yCell( nLocalCells ); 00436 std::vector< double > zCell( nLocalCells ); 00437 double pideg = 1.; // radians 00438 if( degrees ) pideg = acos( -1.0 ) / 180.0; 00439 double x, y, cosphi; 00440 for( int i = 0; i < nLocalCells; i++ ) 00441 { 00442 x = xc[i]; 00443 y = yc[i]; 00444 cosphi = cos( pideg * y ); 00445 zCell[i] = sin( pideg * y ); 00446 xCell[i] = cosphi * cos( x * pideg ); 00447 yCell[i] = cosphi * sin( x * pideg ); 00448 } 00449 ErrorCode rval = mbZTool->repartition( xCell, yCell, zCell, start_cell_idx, "RCB", localGidCells );MB_CHK_SET_ERR( rval, "Error in Zoltan partitioning" ); 00450 delete mbZTool; 00451 00452 dbgOut.tprintf( 1, "After Zoltan partitioning, localGidCells.psize() = %d\n", (int)localGidCells.psize() ); 00453 dbgOut.tprintf( 1, " localGidCells.size() = %d\n", (int)localGidCells.size() ); 00454 00455 // This is important: local cells are now redistributed, so nLocalCells might be different! 00456 nLocalCells = localGidCells.size(); 00457 00458 return MB_SUCCESS; 00459 } 00460 #endif 00461 00462 // By default, apply trivial partition 00463 localGidCells.insert( start_cell_idx, start_cell_idx + nLocalCells - 1 ); 00464 00465 return MB_SUCCESS; 00466 } 00467 #endif 00468 00469 } /* namespace moab */