|
MASA-Core
|
00001 /******************************************************************************* 00002 * 00003 * Copyright (c) 2010-2015 Edans Sandes 00004 * 00005 * This file is part of MASA-Core. 00006 * 00007 * MASA-Core is free software: you can redistribute it and/or modify 00008 * it under the terms of the GNU General Public License as published by 00009 * the Free Software Foundation, either version 3 of the License, or 00010 * (at your option) any later version. 00011 * 00012 * MASA-Core is distributed in the hope that it will be useful, 00013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 00014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00015 * GNU General Public License for more details. 00016 * 00017 * You should have received a copy of the GNU General Public License 00018 * along with MASA-Core. If not, see <http://www.gnu.org/licenses/>. 00019 * 00020 ******************************************************************************/ 00021 00022 #include "AbstractBlockAligner.hpp" 00023 00024 #include <stdio.h> 00025 #include <stdlib.h> 00026 #include <unistd.h> 00027 00028 #include "config.h" 00029 #include "../processors/CPUBlockProcessor.hpp" 00030 00031 /** 00032 * Set to (1) in order to print debug information in the stdout. This 00033 * significantly degrades the performance. 00034 */ 00035 #define DEBUG (0) 00036 00037 /* 00038 * The score constants 00039 */ 00040 #define DNA_MATCH (1) 00041 #define DNA_MISMATCH (-3) 00042 #define DNA_GAP_EXT (2) 00043 #define DNA_GAP_OPEN (3) 00044 #define DNA_GAP_FIRST (DNA_GAP_EXT+DNA_GAP_OPEN) 00045 00046 00047 #ifdef ENABLE_PROFILING 00048 #include "../libs/masa-core/src/common/Timer.hpp" 00049 static FILE* bitmap = NULL; 00050 #define PROFILING_INIT(name) bitmap = fopen(name, "wt"); 00051 #define PROFILING_TIME(var) float var = Timer::getGlobalTime(); 00052 #define PROFILING_PRINT(bx, by, score, done, time) fprintf(bitmap, "%d %d %d %d %d %p\n", by, bx, score, done, (int)((time)*1000), pthread_self()); 00053 #else 00054 #define PROFILING_INIT 00055 #define PROFILING_TIME(var) 00056 #define PROFILING_PRINT(bx, by, score, done, time) 00057 #endif 00058 00059 /** 00060 * Maximum recommended block size for better performance 00061 */ 00062 #define RECOMMENDED_BLOCK_SIZE (1024) 00063 00064 /** 00065 * Minimum recommended grid size for better performance 00066 */ 00067 #define RECOMMENDED_GRID_SIZE (8) 00068 00069 00070 /** 00071 * AbstractBlockAligner Constructor. 00072 */ 00073 AbstractBlockAligner::AbstractBlockAligner(AbstractBlockProcessor* blockProcessor, BlockAlignerParameters* params) { 00074 /* 00075 * Initializations 00076 */ 00077 col = NULL; 00078 row = NULL; 00079 00080 /* 00081 * defines the constant parameters to be returned in the 00082 * getScoreParameters() method 00083 */ 00084 score_params.match = DNA_MATCH; 00085 score_params.mismatch = DNA_MISMATCH; 00086 score_params.gap_open = DNA_GAP_OPEN; 00087 score_params.gap_ext = DNA_GAP_EXT; 00088 00089 /* 00090 * creates the custom parameters 00091 */ 00092 if (params == NULL) { 00093 this->params = new BlockAlignerParameters(); 00094 } else { 00095 this->params = params; 00096 } 00097 if (blockProcessor == NULL) { 00098 this->blockProcessor = new CPUBlockProcessor(); 00099 } else { 00100 this->blockProcessor = blockProcessor; 00101 } 00102 this->blockPruner = new BlockPruningGenericN2(); 00103 00104 /* 00105 * Must be called to enable --fork parameter. This will allow 00106 * 2 process for each CPU. Each process will receive the same 00107 * workload. 00108 */ 00109 int numCPU = sysconf( _SC_NPROCESSORS_ONLN ); 00110 setForkCount(numCPU*2); 00111 00112 preferredBlockSize = RECOMMENDED_BLOCK_SIZE; 00113 preferredGridSize = RECOMMENDED_GRID_SIZE; 00114 } 00115 00116 /** 00117 * AbstractBlockAligner destructor. 00118 */ 00119 AbstractBlockAligner::~AbstractBlockAligner() { 00120 } 00121 00122 /* 00123 * This method defines which capabilities are implemented by this aligner. 00124 * The dispatch_special_column and variable_penalties capabilities are 00125 * not used yet by the MASA framework. The block_pruning and dispatch_last_cell 00126 * capabilities are not supported yet by this example aligner. 00127 */ 00128 aligner_capabilities_t AbstractBlockAligner::getCapabilities() { 00129 aligner_capabilities_t capabilities; 00130 00131 /* This is the supported capabilities of this Aligner subclass */ 00132 capabilities.smith_waterman = SUPPORTED; 00133 capabilities.needleman_wunsch = SUPPORTED; 00134 capabilities.block_pruning = SUPPORTED; 00135 capabilities.customize_first_column = SUPPORTED; 00136 capabilities.customize_first_row = SUPPORTED; 00137 capabilities.dispatch_last_cell = NOT_SUPPORTED; 00138 capabilities.dispatch_last_column = SUPPORTED; 00139 capabilities.dispatch_last_row = SUPPORTED; 00140 capabilities.dispatch_special_column = SUPPORTED; 00141 capabilities.dispatch_special_row = SUPPORTED; 00142 capabilities.dispatch_block_scores = SUPPORTED; 00143 capabilities.dispatch_scores = SUPPORTED; 00144 capabilities.process_partition = SUPPORTED; 00145 capabilities.variable_penalties = NOT_SUPPORTED; 00146 capabilities.fork_processes = SUPPORTED; 00147 00148 capabilities.maximum_seq0_len = 0; 00149 capabilities.maximum_seq1_len = 0; 00150 00151 return capabilities; 00152 } 00153 00154 /* 00155 * Returns the constant match/mismatch/gaps scores. 00156 */ 00157 const score_params_t* AbstractBlockAligner::getScoreParameters() { 00158 return &score_params; 00159 } 00160 00161 /* 00162 * See MicParameters and AbstractAlignerParameters classes. 00163 */ 00164 IAlignerParameters* AbstractBlockAligner::getParameters() { 00165 return this->params; 00166 } 00167 00168 /* 00169 * Initializes some structures of the Aligner. This method 00170 * is called only once for each stage, and only the sequence data may be 00171 * obtained in this time. The parameters must also be used here. 00172 * The partition boundaries must only be used in the alignPartition() method. 00173 * 00174 * See the IAligner and AbstractAligner classes documentation. 00175 */ 00176 void AbstractBlockAligner::initialize() { 00177 00178 } 00179 00180 /* 00181 * The finalize method is called once for each stage, right before 00182 * the printFinalStatistics. Here we must deallocate all the previously 00183 * allocated structure. 00184 * 00185 * See the IAligner and AbstractAligner classes documentation. 00186 */ 00187 void AbstractBlockAligner::finalize() { 00188 if (getGrid() != NULL) { 00189 deallocateStructures(); 00190 } 00191 } 00192 00193 00194 /** 00195 * Configures the grid for the given partition. 00196 * @param partition partition to be aligned. 00197 */ 00198 Grid* AbstractBlockAligner::configureGrid(Partition partition) { 00199 /* creates the grid */ 00200 Grid* grid = this->createGrid(partition); 00201 00202 grid->setMinBlockSize(MIN_BLOCK_SIZE, MIN_BLOCK_SIZE); 00203 00204 /* Block/Grid height */ 00205 if (params->getBlockHeight() > 0) { 00206 grid->setBlockHeight(params->getBlockHeight()); 00207 } else if (params->getGridHeight() > 0) { 00208 grid->splitGridVertically(params->getGridHeight()); 00209 } else { 00210 // Automatic configuration 00211 if (!mustDispatchLastColumn()) { 00212 int height = preferredBlockSize; 00213 if (grid->getHeight() / height < preferredGridSize) { 00214 height = grid->getHeight() / preferredGridSize; 00215 } 00216 grid->setBlockHeight(height); 00217 } else { 00218 grid->setBlockHeight(grid->getWidth()/preferredGridSize/2); 00219 } 00220 } 00221 00222 /* Block/Grid width */ 00223 if (params->getBlockWidth() > 0) { 00224 grid->setBlockWidth(params->getBlockWidth()); 00225 } else if (params->getGridWidth() > 0) { 00226 grid->splitGridHorizontally(params->getGridWidth()); 00227 } else { 00228 // Automatic configuration 00229 if (!mustDispatchLastColumn()) { 00230 int width = preferredBlockSize; 00231 if (grid->getWidth() / width < preferredGridSize) { 00232 width = grid->getWidth() / preferredGridSize; 00233 } 00234 grid->setBlockWidth(width); 00235 } else { 00236 grid->splitGridHorizontally(preferredGridSize); 00237 } 00238 } 00239 00240 /* block statistics */ 00241 int block_width = grid->getBlockWidth(0,0); 00242 int block_height = grid->getBlockHeight(0,0); 00243 statMinBlockWidth = std::min(statMinBlockWidth , block_width); 00244 statMinBlockHeight = std::min(statMinBlockHeight, block_height); 00245 statMaxBlockWidth = std::max(statMaxBlockWidth , block_width); 00246 statMaxBlockHeight = std::max(statMaxBlockHeight, block_height); 00247 00248 /* grid statistics */ 00249 int grid_width = grid->getGridWidth(); 00250 int grid_height = grid->getGridHeight(); 00251 statMinGridWidth = std::min(statMinGridWidth , grid_width); 00252 statMinGridHeight = std::min(statMinGridHeight, grid_height); 00253 statMaxGridWidth = std::max(statMaxGridWidth , grid_width); 00254 statMaxGridHeight = std::max(statMaxGridHeight, grid_height); 00255 00256 if (DEBUG) printf("Configured Grid: B: %d x %d G: %d x %d\n", block_width, block_height, grid_width, grid_height); 00257 return grid; 00258 } 00259 00260 00261 void AbstractBlockAligner::setSequences(const char* seq0, const char* seq1, int seq0_len, int seq1_len) { 00262 blockProcessor->setSequences(seq0, seq1, seq0_len, seq1_len); 00263 } 00264 00265 void AbstractBlockAligner::unsetSequences() { 00266 blockProcessor->unsetSequences(); 00267 } 00268 00269 /** 00270 * Aligns the given partition, processing it block by block using a customized 00271 * scheduler. 00272 * 00273 * @param partition partition to be aligned. 00274 * @see IAligner::alignPartition 00275 */ 00276 void AbstractBlockAligner::alignPartition(Partition partition) { 00277 //fprintf(stderr, "alignPartition()\n"); 00278 //createDispatcherQueue(); // Uncomment this if you want thread-safe calls 00279 00280 /* configures the grid */ 00281 Grid* grid = configureGrid(partition); 00282 00283 /* the block pruning initialization must be done after grid configuration */ 00284 initializeBlockPruning(blockPruner); 00285 00286 /* allocates the memory structures. It must be called after grid configuration */ 00287 allocateStructures(); 00288 00289 /* reads the first top-left cell of the partition. */ 00290 cell_t dummy; 00291 receiveFirstColumn(&dummy, 1); // initializes the AbstractAligner::firstColumnTail 00292 receiveFirstRow(&dummy, 1); // initializes the AbstractAligner::firstRowTail 00293 // TODO assert if both tails are equal? 00294 00295 /* statistics initializations */ 00296 statTotalBlocks = 0; 00297 statPrunedBlocks = 0; 00298 00299 static char str[500]; 00300 sprintf(str, "profiling.%08d.%08d.%08d.%08d.%d.txt", partition.getI0(), partition.getJ0(), partition.getI1(), partition.getJ1(), mustDispatchLastColumn()); 00301 PROFILING_INIT(str); 00302 00303 /* local initializations */ 00304 int grid_width = grid->getGridWidth(); 00305 int grid_height = grid->getGridHeight(); 00306 scheduleBlocks(grid_width, grid_height); 00307 00308 // TODO o dispatch score deve ser feito on-the-fly durante o estagio 2, 00309 // caso contrario o processamento nao ira parar se encontrarmos o goal score. 00310 for (int bx = 0; bx < grid_width; bx++) { 00311 for (int by = 0; by < grid_height; by++) { 00312 /* Dispatch the best score found in block (bx,by) */ 00313 dispatchScore(grid_scores[bx][by], bx, by); 00314 } 00315 } 00316 00317 if (mustDispatchLastCell()) { 00318 score_t score; 00319 score.score = row[grid_width-1][grid->getBlockWidth(grid_width-1, grid_height-1)-1].h; 00320 score.i = partition.getI1()-1; 00321 score.j = partition.getJ1()-1; 00322 dispatchScore(score, grid_width-1, grid_height-1); 00323 } 00324 00325 deallocateStructures(); 00326 //destroyDispatcherQueue(); // Uncomment this if you want thread-safe calls 00327 } 00328 00329 00330 /** 00331 * This method aligns block (bx,by). This method calls the 00332 * AbstractBlockAligner::alignBlock(int,int,int,int,int,int) 00333 * with additional information about block coordinates. 00334 * 00335 * @param bx horizontal coordinate of the block in the grid 00336 * @param by vertical coordinate of the block in the grid 00337 */ 00338 void AbstractBlockAligner::alignBlock(int bx, int by) { 00339 int i0; 00340 int i1; 00341 int j0; 00342 int j1; 00343 00344 /* obtains the boundaries of block (bx, by) */ 00345 getGrid()->getBlockPosition(bx, by, &i0, &j0, &i1, &j1); 00346 00347 alignBlock(bx, by, i0, j0, i1, j1); 00348 } 00349 00350 /** 00351 * This method calls AbstractBlockProcessor::processBlock(int,int,int,int,int,int,int) 00352 * to execute the recurrence relation for a block. 00353 * 00354 * @param bx horizontal block coordinate 00355 * @param by vertical block coordinate 00356 * @param i0 vertical first row of the block 00357 * @param j0 horizontal first column of the block 00358 * @param i1 vertical last row of the block 00359 * @param j1 horizontal last column of the block 00360 * @param true if the block was processed or false if it was pruned. 00361 */ 00362 bool AbstractBlockAligner::processBlock(int bx, int by, int i0, int j0, int i1, int j1) { 00363 if (!isBlockPruned(bx, by)) { 00364 /* the block was not pruned */ 00365 if (DEBUG) printf(">>>AbstractBlockAligner::processBlock(%d, %d, %d, %d, %d, %d)\n", bx, by, i0, j0, i1, j1); 00366 00367 00368 //if (!mustContinue()) return; // MASA-core is telling to stop 00369 00370 PROFILING_TIME(t0); 00371 00372 /* processes the block */ 00373 grid_scores[bx][by] = blockProcessor->processBlock(row[bx], col[by], i0, j0, i1, j1, getRecurrenceType()); 00374 00375 PROFILING_TIME(t1); 00376 PROFILING_PRINT(bx, by, grid_scores[bx][by].score, 1, t1-t0); 00377 00378 /* Updates the block pruning status */ 00379 pruningUpdate(bx, by, grid_scores[bx][by].score); 00380 00381 increaseBlockStat(false); 00382 00383 /* Dispatch the best score found in block (bx,by) */ 00384 //dispatchScore(grid_scores[bx][by], bx, by); 00385 return true; 00386 } else { 00387 /* the block was pruned */ 00388 ignoreBlock(bx, by); 00389 return false; 00390 } 00391 00392 } 00393 00394 /* 00395 * Profiles this block as "pruned" 00396 */ 00397 void AbstractBlockAligner::ignoreBlock(int bx, int by) { 00398 PROFILING_PRINT(bx, by, 0, 0, 0); 00399 increaseBlockStat(true); 00400 } 00401 00402 /* 00403 * Updates statTotalBlocks and statPrunedBlocks variables 00404 */ 00405 void AbstractBlockAligner::increaseBlockStat(const bool pruned) { 00406 statTotalBlocks++; 00407 if (pruned) { 00408 statPrunedBlocks++; 00409 } 00410 } 00411 00412 00413 /** 00414 * Indicates if the blocks on row $by$ must dispatch its last row. 00415 * 00416 * @param by the row of blocks. 00417 * @return true if the last row of the block will be stored (special row). 00418 */ 00419 bool AbstractBlockAligner::isSpecialRow(int by) { 00420 /* 00421 * Dispatch special rows with a minimum defined distance (getSpecialRowInterval()). 00422 */ 00423 if (mustDispatchLastRow() && by == getGrid()->getGridHeight()-1) { 00424 return true; 00425 } else if (mustDispatchSpecialRows()) { 00426 /* 00427 * Note that only the last rows of the blocks are suitable to be a 00428 * special row. 00429 */ 00430 const int block_height = getGrid()->getBlockHeight(0, 0); // considering that all the blocks has the same height 00431 int flush_block_interval = (getSpecialRowInterval()+block_height-1)/block_height; 00432 if (flush_block_interval <= 0) { 00433 flush_block_interval = 1; 00434 } 00435 00436 return ((by+1) % flush_block_interval == 0); 00437 } else { 00438 return false; 00439 } 00440 } 00441 00442 /** 00443 * Indicates if the blocks on column $bx$ must dispatch its last column. 00444 * 00445 * @param bx the column of blocks. 00446 * @return true if the last column of the block will be stored (special column). 00447 */ 00448 bool AbstractBlockAligner::isSpecialColumn(int bx) { 00449 return (mustDispatchLastColumn() && bx == getGrid()->getGridWidth()-1); 00450 } 00451 00452 /** 00453 * Prints the pruning statistics and grid used range. 00454 * @param file handler to print out the statistics. 00455 * @see IAligner::printStatistics 00456 */ 00457 void AbstractBlockAligner::printStatistics(FILE* file) { 00458 // Print final statistics to the statistics file; 00459 fprintf(file, "\n===== PRUNING STATS =====\n"); 00460 fprintf(file, "Pruned Blocks: %d\n", statPrunedBlocks); 00461 fprintf(file, "Pruned Blocks: %.4f%%\n", 00462 (statPrunedBlocks * 100.0f) / statTotalBlocks); 00463 00464 fprintf(file, "\n===== RUNTIME VARIABLES =====\n"); 00465 fprintf(file, " Block Width: %d-%d\n", statMinBlockWidth, statMaxBlockWidth); 00466 fprintf(file, " Block Height: %d-%d\n", statMinBlockHeight, statMaxBlockHeight); 00467 fprintf(file, " Grid Width: %d-%d\n", statMinGridWidth, statMaxGridWidth); 00468 fprintf(file, " Grid Height: %d-%d\n", statMinGridHeight, statMaxGridHeight); 00469 00470 fflush(file); 00471 } 00472 00473 /** 00474 * Prints the runtime parameters. 00475 * @param file handler to print out the statistics. 00476 * @see IAligner::printInitialStatistics 00477 */ 00478 void AbstractBlockAligner::printInitialStatistics(FILE* file) { 00479 fprintf(file, "\n===== RUNTIME PARAMETERS =====\n"); 00480 fprintf(file, " %s height: %d\n", 00481 params->getBlockHeight() ? "Block" : "Grid", 00482 params->getBlockHeight() ? params->getBlockHeight() : params->getGridHeight()); 00483 fprintf(file, " %s width: %d\n", 00484 params->getBlockWidth() ? "Block" : "Grid", 00485 params->getBlockWidth() ? params->getBlockWidth() : params->getGridWidth()); 00486 fprintf(file, " ForkID: %d\n", params->getForkId()); 00487 } 00488 00489 /** 00490 * @copydoc IAligner::clearStatistics 00491 */ 00492 void AbstractBlockAligner::clearStatistics() { 00493 statTotalBlocks = 0; 00494 statPrunedBlocks = 0; 00495 00496 statMinBlockWidth = INF; 00497 statMaxBlockWidth = 0; 00498 statMinBlockHeight = INF; 00499 statMaxBlockHeight = 0; 00500 00501 statMinGridWidth = INF; 00502 statMaxGridWidth = 0; 00503 statMinGridHeight = INF; 00504 statMaxGridHeight = 0; 00505 } 00506 00507 /** Empty stub for the superclass virtual method. 00508 * @copydoc IAligner::printStageStatistics 00509 */ 00510 void AbstractBlockAligner::printStageStatistics(FILE* file) { 00511 } 00512 00513 /** Empty stub for the superclass virtual method. 00514 * @copydoc IAligner::printFinalStatistics 00515 */ 00516 void AbstractBlockAligner::printFinalStatistics(FILE* file) { 00517 } 00518 00519 /* 00520 * This method returns a progress string that is printed periodically. 00521 */ 00522 const char* AbstractBlockAligner::getProgressString() const { 00523 return ""; 00524 } 00525 00526 /* 00527 * 00528 */ 00529 long long AbstractBlockAligner::getProcessedCells() { 00530 return 0; // Used to calculate the MCUPS performance metric 00531 } 00532 00533 /** 00534 * Allocate vectors after sequence is set. 00535 */ 00536 void AbstractBlockAligner::allocateStructures() { 00537 /* 00538 * Allocates the first row of each block. This vector is transferred 00539 * from on block to the other, in the vertical direction. 00540 */ 00541 int grid_width = getGrid()->getGridWidth(); 00542 row = new cell_t*[grid_width]; 00543 for (int j=0; j<grid_width; j++) { 00544 int block_width = getGrid()->getBlockWidth(j,0); 00545 row[j] = new cell_t[block_width]; 00546 } 00547 00548 /* 00549 * Allocates the first column of each block. This vector is transferred 00550 * from on block to the other, in the horizontal direction. 00551 */ 00552 int grid_height = getGrid()->getGridHeight(); 00553 col = new cell_t*[grid_height]; 00554 for (int i=0; i<grid_height; i++) { 00555 int block_height = getGrid()->getBlockHeight(0,i); 00556 col[i] = new cell_t[block_height+1]; 00557 } 00558 00559 00560 00561 grid_scores = new score_t*[ grid_width ]; 00562 for( int j = 0; j < grid_width; ++j ){ 00563 grid_scores[j] = new score_t[ grid_height ]; 00564 for( int i = 0; i < grid_height; ++i ) { 00565 grid_scores[j][i].score = -INF; 00566 } 00567 } 00568 } 00569 00570 /** 00571 * Deallocate vectors after sequence is unset. 00572 */ 00573 void AbstractBlockAligner::deallocateStructures() { 00574 if (getGrid() == NULL) { 00575 return; 00576 } 00577 int grid_width = getGrid()->getGridWidth(); 00578 int grid_height = getGrid()->getGridHeight(); 00579 00580 if (row != NULL) { 00581 for (int j = 0; j < grid_width; ++j) { 00582 delete[] row[j]; 00583 } 00584 delete[] row; 00585 row = NULL; 00586 } 00587 if (col != NULL) { 00588 for (int i = 0; i < grid_height; ++i) { 00589 delete[] col[i]; 00590 } 00591 delete[] col; 00592 col = NULL; 00593 } 00594 if(grid_scores != NULL) { 00595 for( int j = 0; j < grid_width; ++j ){ 00596 delete[] grid_scores[j]; 00597 } 00598 delete[] grid_scores; 00599 grid_scores = NULL; 00600 } 00601 } 00602 00603 /** 00604 * Returns true if block (bx, by) can be pruned. 00605 * 00606 * @param bx horizontal block coordinate 00607 * @param by vertical block coordinate 00608 * @return pruned status of the block. 00609 */ 00610 bool AbstractBlockAligner::isBlockPruned(int bx, int by) const { 00611 return (blockPruner != NULL && blockPruner->isBlockPruned(bx, by)); 00612 } 00613 00614 /** 00615 * Updates the pruning status accordingly to the block score. 00616 * 00617 * @param bx horizontal block coordinate 00618 * @param by vertical block coordinate 00619 * @param score the score of block at coordinate $(bx,by)$ 00620 */ 00621 void AbstractBlockAligner::pruningUpdate(int bx, int by, int score) { 00622 if (blockPruner != NULL) { 00623 blockPruner->pruningUpdate(bx, by, score); 00624 } 00625 } 00626 00627 /** 00628 * Defines the preferred block/grid sizes. Used as a hint. 00629 * 00630 * @param preferredBlockSize the preferred maximum size of a block. 00631 * @param preferredGridSize the preferred minimum grid size. 00632 */ 00633 void AbstractBlockAligner::setPreferredSizes(int preferredBlockSize, int preferredGridSize) { 00634 if (preferredBlockSize > 0) { 00635 this->preferredBlockSize = preferredBlockSize; 00636 } 00637 if (preferredGridSize > 0) { 00638 this->preferredGridSize = preferredGridSize; 00639 } 00640 } 00641 00642 00643 00644
1.7.6.1