X-Git-Url: https://git.sesse.net/?p=stockfish;a=blobdiff_plain;f=src%2Fsearch.cpp;h=ec884b3c145afc20dbcdff887e056ba671552827;hp=b48ead09ead7b44e72d1b72a687f0817808947d9;hb=80f5ca88f6eaead0c5460c4bc13c1c7d9477c2be;hpb=43c93cb15129692e600a5857668c3a83ec5c3b64 diff --git a/src/search.cpp b/src/search.cpp index b48ead09..ec884b3c 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -1,7 +1,7 @@ /* Stockfish, a UCI chess playing engine derived from Glaurung 2.1 Copyright (C) 2004-2008 Tord Romstad (Glaurung author) - Copyright (C) 2008-2009 Marco Costalba + Copyright (C) 2008-2010 Marco Costalba, Joona Kiiski, Tord Romstad Stockfish is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -84,10 +84,10 @@ namespace { void put_threads_to_sleep(); void idle_loop(int threadID, SplitPoint* waitSp); bool split(const Position& pos, SearchStack* ss, int ply, Value* alpha, const Value beta, Value* bestValue, - Depth depth, int* moves, MovePicker* mp, int master, bool pvNode); + Depth depth, bool mateThreat, int* moves, MovePicker* mp, int master, bool pvNode); private: - friend void poll(SearchStack ss[], int ply); + friend void poll(); int ActiveThreads; volatile bool AllThreadsShouldExit, AllThreadsShouldSleep; @@ -254,13 +254,10 @@ namespace { int MultiPV; // Time managment variables - int RootMoveNumber, SearchStartTime, MaxNodes, MaxDepth; - int MaxSearchTime, AbsoluteMaxSearchTime, ExtraSearchTime, ExactMaxTime; + int SearchStartTime, MaxNodes, MaxDepth, MaxSearchTime; + int AbsoluteMaxSearchTime, ExtraSearchTime, ExactMaxTime; bool UseTimeManagement, InfiniteSearch, PonderSearch, StopOnPonderhit; - bool AbortSearch, Quit, AspirationFailLow; - - // Show current line? - bool ShowCurrentLine; + bool FirstRootMove, AbortSearch, Quit, AspirationFailLow; // Log file bool UseLogFile; @@ -282,7 +279,7 @@ namespace { /// Local functions Value id_loop(const Position& pos, Move searchMoves[]); - Value root_search(Position& pos, SearchStack ss[], RootMoveList& rml, Value& oldAlpha, Value& beta); + Value root_search(Position& pos, SearchStack ss[], RootMoveList& rml, Value* alphaPtr, Value* betaPtr); Value search_pv(Position& pos, SearchStack ss[], Value alpha, Value beta, Depth depth, int ply, int threadID); Value search(Position& pos, SearchStack ss[], Value beta, Depth depth, int ply, bool allowNullmove, int threadID, Move excludedMove = MOVE_NONE); Value qsearch(Position& pos, SearchStack ss[], Value alpha, Value beta, Depth depth, int ply, int threadID); @@ -305,7 +302,7 @@ namespace { int current_search_time(); int nps(); - void poll(SearchStack ss[], int ply); + void poll(); void ponderhit(); void wait_for_stop_or_ponderhit(); void init_ss_array(SearchStack ss[]); @@ -373,6 +370,7 @@ bool think(const Position& pos, bool infinite, bool ponder, int side_to_move, // Initialize global search variables StopOnPonderhit = AbortSearch = Quit = AspirationFailLow = false; + MaxSearchTime = AbsoluteMaxSearchTime = ExtraSearchTime = 0; NodesSincePoll = 0; TM.resetNodeCounters(); SearchStartTime = get_system_time(); @@ -389,7 +387,7 @@ bool think(const Position& pos, bool infinite, bool ponder, int side_to_move, if (get_option_value_string("Book File") != OpeningBook.file_name()) OpeningBook.open(get_option_value_string("Book File")); - Move bookMove = OpeningBook.get_move(pos); + Move bookMove = OpeningBook.get_move(pos, get_option_value_bool("Best Book Move")); if (bookMove != MOVE_NONE) { if (PonderSearch) @@ -424,7 +422,6 @@ bool think(const Position& pos, bool infinite, bool ponder, int side_to_move, MinimumSplitDepth = get_option_value_int("Minimum Split Depth") * OnePly; MaxThreadsPerSplitPoint = get_option_value_int("Maximum Number of Threads per Split Point"); - ShowCurrentLine = get_option_value_bool("UCI_ShowCurrLine"); MultiPV = get_option_value_int("MultiPV"); Chess960 = get_option_value_bool("UCI_Chess960"); UseLogFile = get_option_value_bool("Use Search Log"); @@ -440,10 +437,6 @@ bool think(const Position& pos, bool infinite, bool ponder, int side_to_move, { TM.set_active_threads(newActiveThreads); init_eval(TM.active_threads()); - // HACK: init_eval() destroys the static castleRightsMask[] array in the - // Position class. The below line repairs the damage. - Position p(pos.to_fen()); - assert(pos.is_ok()); } // Wake up sleeping threads @@ -567,7 +560,7 @@ void init_search() { for (int j = 0; j < 64; j++) // j == moveNumber { // FIXME: test using log instead of BSR - FutilityMarginsMatrix[i][j] = (i < 2 ? 0 : 112 * bitScanReverse32(i * i / 2)) - 8 * j; + FutilityMarginsMatrix[i][j] = (i < 2 ? 0 : 112 * bitScanReverse32(i * i / 2)) - 8 * j + 45; } // Init futility move count array @@ -647,8 +640,6 @@ namespace { // Initialize iteration Iteration++; BestMoveChangesByIteration[Iteration] = 0; - if (Iteration <= 5) - ExtraSearchTime = 0; cout << "info depth " << Iteration << endl; @@ -665,8 +656,8 @@ namespace { beta = Min(ValueByIteration[Iteration - 1] + AspirationDelta, VALUE_INFINITE); } - // Search to the current depth, rml is updated and sorted - value = root_search(p, ss, rml, alpha, beta); + // Search to the current depth, rml is updated and sorted, alpha and beta could change + value = root_search(p, ss, rml, &alpha, &beta); // Write PV to transposition table, in case the relevant entries have // been overwritten during the search. @@ -786,33 +777,53 @@ namespace { // scheme, prints some information to the standard output and handles // the fail low/high loops. - Value root_search(Position& pos, SearchStack ss[], RootMoveList& rml, Value& oldAlpha, Value& beta) { + Value root_search(Position& pos, SearchStack ss[], RootMoveList& rml, Value* alphaPtr, Value* betaPtr) { EvalInfo ei; StateInfo st; + CheckInfo ci(pos); int64_t nodes; Move move; Depth depth, ext, newDepth; - Value value, alpha; + Value value, alpha, beta; bool isCheck, moveIsCheck, captureOrPromotion, dangerous; - int researchCount = 0; - CheckInfo ci(pos); - alpha = oldAlpha; + int researchCountFH, researchCountFL; + + researchCountFH = researchCountFL = 0; + alpha = *alphaPtr; + beta = *betaPtr; isCheck = pos.is_check(); - // Evaluate the position statically - ss[0].eval = !isCheck ? evaluate(pos, ei, 0) : VALUE_NONE; + // Step 1. Initialize node and poll (omitted at root, but I can see no good reason for this, FIXME) + // Step 2. Check for aborted search (omitted at root, because we do not initialize root node) + // Step 3. Mate distance pruning (omitted at root) + // Step 4. Transposition table lookup (omitted at root) - while (1) // Fail low loop + // Step 5. Evaluate the position statically + // At root we do this only to get reference value for child nodes + if (!isCheck) + ss[0].eval = evaluate(pos, ei, 0); + else + ss[0].eval = VALUE_NONE; // HACK because we do not initialize root node + + // Step 6. Razoring (omitted at root) + // Step 7. Static null move pruning (omitted at root) + // Step 8. Null move search with verification search (omitted at root) + // Step 9. Internal iterative deepening (omitted at root) + + // Step extra. Fail low loop + // We start with small aspiration window and in case of fail low, we research + // with bigger window until we are not failing low anymore. + while (1) { // Sort the moves before to (re)search rml.sort(); - // Loop through all the moves in the root move list + // Step 10. Loop through all moves in the root move list for (int i = 0; i < rml.move_count() && !AbortSearch; i++) { - // This is used by time management and starts from 1 - RootMoveNumber = i + 1; + // This is used by time management + FirstRootMove = (i == 0); // Save the current node count before the move is searched nodes = TM.nodes_searched(); @@ -826,23 +837,31 @@ namespace { if (current_search_time() >= 1000) cout << "info currmove " << move - << " currmovenumber " << RootMoveNumber << endl; + << " currmovenumber " << i + 1 << endl; - // Decide search depth for this move moveIsCheck = pos.move_is_check(move); captureOrPromotion = pos.move_is_capture_or_promotion(move); + + // Step 11. Decide the new search depth depth = (Iteration - 2) * OnePly + InitialDepth; ext = extension(pos, move, true, captureOrPromotion, moveIsCheck, false, false, &dangerous); newDepth = depth + ext; - // Reset value before the search + // Step 12. Futility pruning (omitted at root) + + // Step extra. Fail high loop + // If move fails high, we research with bigger window until we are not failing + // high anymore. value = - VALUE_INFINITE; - while (1) // Fail high loop + while (1) { - // Make the move, and search it + // Step 13. Make the move pos.do_move(move, st, ci, moveIsCheck); + // Step extra. pv search + // We do pv search for first moves (i < MultiPV) + // and for fail high research (value > alpha) if (i < MultiPV || value > alpha) { // Aspiration window is disabled in multi-pv case @@ -854,11 +873,11 @@ namespace { } else { - // Try to reduce non-pv search depth by one ply if move seems not problematic, - // if the move fails high will be re-searched at full depth. + // Step 14. Reduced search + // if the move fails high will be re-searched at full depth bool doFullDepthSearch = true; - if ( depth >= 3 * OnePly // FIXME was newDepth + if ( depth >= 3 * OnePly && !dangerous && !captureOrPromotion && !move_is_castle(move)) @@ -872,6 +891,7 @@ namespace { } } + // Step 15. Full depth search if (doFullDepthSearch) { // Full depth non-pv search using alpha as upperbound @@ -885,6 +905,7 @@ namespace { } } + // Step 16. Undo move pos.undo_move(move); // Can we exit fail high loop ? @@ -902,8 +923,8 @@ namespace { print_pv_info(pos, ss, alpha, beta, value); // Prepare for a research after a fail high, each time with a wider window - researchCount++; - beta = Min(beta + AspirationDelta * (1 << researchCount), VALUE_INFINITE); + *betaPtr = beta = Min(beta + AspirationDelta * (1 << researchCountFH), VALUE_INFINITE); + researchCountFH++; } // End of fail high loop @@ -923,7 +944,9 @@ namespace { rml.set_move_nodes(i, TM.nodes_searched() - nodes); assert(value >= -VALUE_INFINITE && value <= VALUE_INFINITE); + assert(value < beta); + // Step 17. Check for new best move if (value <= alpha && i >= MultiPV) rml.set_move_score(i, -VALUE_INFINITE); else @@ -947,8 +970,7 @@ namespace { // Print information to the standard output print_pv_info(pos, ss, alpha, beta, value); - // Raise alpha to setup proper non-pv search upper bound, note - // that we can end up with alpha >= beta and so get a fail high. + // Raise alpha to setup proper non-pv search upper bound if (value > alpha) alpha = value; } @@ -974,22 +996,21 @@ namespace { } } // PV move or new best move - assert(alpha >= oldAlpha); + assert(alpha >= *alphaPtr); - AspirationFailLow = (alpha == oldAlpha); + AspirationFailLow = (alpha == *alphaPtr); if (AspirationFailLow && StopOnPonderhit) StopOnPonderhit = false; } // Can we exit fail low loop ? - if (AbortSearch || alpha > oldAlpha) + if (AbortSearch || !AspirationFailLow) break; // Prepare for a research after a fail low, each time with a wider window - researchCount++; - alpha = Max(alpha - AspirationDelta * (1 << researchCount), -VALUE_INFINITE); - oldAlpha = alpha; + *alphaPtr = alpha = Max(alpha - AspirationDelta * (1 << researchCountFL), -VALUE_INFINITE); + researchCountFL++; } // Fail low loop @@ -1076,14 +1097,13 @@ namespace { tte = TT.retrieve(pos.get_key()); } - // Step 10. Loop through moves - // Loop through all legal moves until no moves remain or a beta cutoff occurs - // Initialize a MovePicker object for the current position mateThreat = pos.has_mate_threat(opposite_color(pos.side_to_move())); MovePicker mp = MovePicker(pos, ttMove, depth, H, &ss[ply]); CheckInfo ci(pos); + // Step 10. Loop through moves + // Loop through all legal moves until no moves remain or a beta cutoff occurs while ( alpha < beta && (move = mp.get_next_move()) != MOVE_NONE && !TM.thread_should_stop(threadID)) @@ -1191,7 +1211,7 @@ namespace { && !AbortSearch && !TM.thread_should_stop(threadID) && TM.split(pos, ss, ply, &alpha, beta, &bestValue, - depth, &moveCount, &mp, threadID, true)) + depth, mateThreat, &moveCount, &mp, threadID, true)) break; } @@ -1281,6 +1301,9 @@ namespace { if (tte && ok_to_use_TT(tte, depth, beta, ply)) { + // Refresh tte entry to avoid aging + TT.store(posKey, tte->value(), tte->type(), tte->depth(), ttMove); + ss[ply].currentMove = ttMove; // Can be MOVE_NONE return value_from_tt(tte->value(), ply); } @@ -1300,27 +1323,31 @@ namespace { } // Step 6. Razoring - if ( !value_is_mate(beta) + if ( refinedValue < beta - razor_margin(depth) + && ttMove == MOVE_NONE + && ss[ply - 1].currentMove != MOVE_NULL + && depth < RazorDepth && !isCheck - && depth < RazorDepth - && refinedValue < beta - razor_margin(depth) - && ss[ply - 1].currentMove != MOVE_NULL - && ttMove == MOVE_NONE + && !value_is_mate(beta) && !pos.has_pawn_on_7th(pos.side_to_move())) { Value rbeta = beta - razor_margin(depth); Value v = qsearch(pos, ss, rbeta-1, rbeta, Depth(0), ply, threadID); if (v < rbeta) - return v; //FIXME: Logically should be: return (v + razor_margin(depth)); + // Logically we should return (v + razor_margin(depth)), but + // surprisingly this did slightly weaker in tests. + return v; } // Step 7. Static null move pruning // We're betting that the opponent doesn't have a move that will reduce - // the score by more than fuility_margin(depth) if we do a null move. - if ( !isCheck - && allowNullmove - && depth < RazorDepth - && refinedValue - futility_margin(depth, 0) >= beta) + // the score by more than futility_margin(depth) if we do a null move. + if ( allowNullmove + && depth < RazorDepth + && !isCheck + && !value_is_mate(beta) + && ok_to_do_nullmove(pos) + && refinedValue >= beta + futility_margin(depth, 0)) return refinedValue - futility_margin(depth, 0); // Step 8. Null move search with verification search @@ -1336,8 +1363,6 @@ namespace { { ss[ply].currentMove = MOVE_NULL; - pos.do_null_move(st); - // Null move dynamic reduction based on depth int R = 3 + (depth >= 5 * OnePly ? depth / 8 : 0); @@ -1345,19 +1370,25 @@ namespace { if (refinedValue - beta > PawnValueMidgame) R++; + pos.do_null_move(st); + nullValue = -search(pos, ss, -(beta-1), depth-R*OnePly, ply+1, false, threadID); pos.undo_null_move(); if (nullValue >= beta) { + // Do not return unproven mate scores + if (nullValue >= value_mate_in(PLY_MAX)) + nullValue = beta; + if (depth < 6 * OnePly) - return beta; + return nullValue; // Do zugzwang verification search Value v = search(pos, ss, beta, depth-5*OnePly, ply, false, threadID); if (v >= beta) - return beta; + return nullValue; } else { // The null move failed low, which means that we may be faced with // some kind of threat. If the previous move was reduced, check if @@ -1387,13 +1418,12 @@ namespace { tte = TT.retrieve(posKey); } - // Step 10. Loop through moves - // Loop through all legal moves until no moves remain or a beta cutoff occurs - // Initialize a MovePicker object for the current position MovePicker mp = MovePicker(pos, ttMove, depth, H, &ss[ply], beta); CheckInfo ci(pos); + // Step 10. Loop through moves + // Loop through all legal moves until no moves remain or a beta cutoff occurs while ( bestValue < beta && (move = mp.get_next_move()) != MOVE_NONE && !TM.thread_should_stop(threadID)) @@ -1416,7 +1446,7 @@ namespace { if ( depth >= SingularExtensionDepthAtNonPVNodes && tte && move == tte->move() - && !excludedMove // Do not allow recursive single-reply search + && !excludedMove // Do not allow recursive singular extension search && ext < OnePly && is_lower_bound(tte->type()) && tte->depth() >= depth - 3 * OnePly) @@ -1453,7 +1483,7 @@ namespace { // Value based pruning Depth predictedDepth = newDepth - nonpv_reduction(depth, moveCount); // We illogically ignore reduction condition depth >= 3*OnePly futilityValueScaled = ss[ply].eval + futility_margin(predictedDepth, moveCount) - + H.gain(pos.piece_on(move_from(move)), move_to(move)) + 45; + + H.gain(pos.piece_on(move_from(move)), move_to(move)); if (futilityValueScaled < beta) { @@ -1466,8 +1496,8 @@ namespace { // Step 13. Make the move pos.do_move(move, st, ci, moveIsCheck); - // Step 14. Reduced search - // if the move fails high will be re-searched at full depth. + // Step 14. Reduced search, if the move fails high + // will be re-searched at full depth. bool doFullDepthSearch = true; if ( depth >= 3*OnePly @@ -1516,16 +1546,16 @@ namespace { && !AbortSearch && !TM.thread_should_stop(threadID) && TM.split(pos, ss, ply, NULL, beta, &bestValue, - depth, &moveCount, &mp, threadID, false)) + depth, mateThreat, &moveCount, &mp, threadID, false)) break; } // Step 19. Check for mate and stalemate - // All legal moves have been searched and if there were + // All legal moves have been searched and if there are // no legal moves, it must be mate or stalemate. - // If one move was excluded return fail low. + // If one move was excluded return fail low score. if (!moveCount) - return excludedMove ? beta - 1 : (pos.is_check() ? value_mated_in(ply) : VALUE_DRAW); + return excludedMove ? beta - 1 : (isCheck ? value_mated_in(ply) : VALUE_DRAW); // Step 20. Update tables // If the search is not aborted, update the transposition table, @@ -1634,7 +1664,7 @@ namespace { alpha = bestValue; // If we are near beta then try to get a cutoff pushing checks a bit further - bool deepChecks = depth == -OnePly && staticValue >= beta - PawnValueMidgame / 8; + bool deepChecks = (depth == -OnePly && staticValue >= beta - PawnValueMidgame / 8); // Initialize a MovePicker object for the current position, and prepare // to search the moves. Because the depth is <= 0 here, only captures, @@ -1645,8 +1675,7 @@ namespace { enoughMaterial = pos.non_pawn_material(pos.side_to_move()) > RookValueMidgame; futilityBase = staticValue + FutilityMarginQS + ei.futilityMargin[pos.side_to_move()]; - // Loop through the moves until no moves remain or a beta cutoff - // occurs. + // Loop through the moves until no moves remain or a beta cutoff occurs while ( alpha < beta && (move = mp.get_next_move()) != MOVE_NONE) { @@ -1681,7 +1710,7 @@ namespace { // Detect blocking evasions that are candidate to be pruned evasionPrunable = isCheck - && bestValue != -VALUE_INFINITE + && bestValue > value_mated_in(PLY_MAX) && !pos.move_is_capture(move) && pos.type_of_piece_on(move_from(move)) != KING && !pos.can_castle(pos.side_to_move()); @@ -1715,7 +1744,7 @@ namespace { // All legal moves have been searched. A special case: If we're in check // and no legal moves were found, it is checkmate. - if (!moveCount && pos.is_check()) // Mate! + if (!moveCount && isCheck) // Mate! return value_mated_in(ply); // Update transposition table @@ -1752,7 +1781,6 @@ namespace { // splitting, we don't have to repeat all this work in sp_search(). We // also don't need to store anything to the hash table here: This is taken // care of after we return from the split point. - // FIXME: We are currently ignoring mateThreat flag here void sp_search(SplitPoint* sp, int threadID) { @@ -1789,7 +1817,7 @@ namespace { captureOrPromotion = pos.move_is_capture_or_promotion(move); // Step 11. Decide the new search depth - ext = extension(pos, move, false, captureOrPromotion, moveIsCheck, false, false, &dangerous); + ext = extension(pos, move, false, captureOrPromotion, moveIsCheck, false, sp->mateThreat, &dangerous); newDepth = sp->depth - OnePly + ext; // Update current move @@ -1813,7 +1841,7 @@ namespace { // Value based pruning Depth predictedDepth = newDepth - nonpv_reduction(sp->depth, moveCount); futilityValueScaled = ss[sp->ply].eval + futility_margin(predictedDepth, moveCount) - + H.gain(pos.piece_on(move_from(move)), move_to(move)) + 45; + + H.gain(pos.piece_on(move_from(move)), move_to(move)); if (futilityValueScaled < sp->beta) { @@ -1887,7 +1915,6 @@ namespace { // don't have to repeat all this work in sp_search_pv(). We also don't // need to store anything to the hash table here: This is taken care of // after we return from the split point. - // FIXME: We are ignoring mateThreat flag! void sp_search_pv(SplitPoint* sp, int threadID) { @@ -1923,7 +1950,7 @@ namespace { captureOrPromotion = pos.move_is_capture_or_promotion(move); // Step 11. Decide the new search depth - ext = extension(pos, move, true, captureOrPromotion, moveIsCheck, false, false, &dangerous); + ext = extension(pos, move, true, captureOrPromotion, moveIsCheck, false, sp->mateThreat, &dangerous); newDepth = sp->depth - OnePly + ext; // Update current move @@ -2022,7 +2049,7 @@ namespace { NodesSincePoll++; if (NodesSincePoll >= NodesBetweenPolls) { - poll(ss, ply); + poll(); NodesSincePoll = 0; } } @@ -2385,7 +2412,7 @@ namespace { // looks at the time consumed so far and decides if it's time to abort the // search. - void poll(SearchStack ss[], int ply) { + void poll() { static int lastInfoTime; int t = current_search_time(); @@ -2436,23 +2463,13 @@ namespace { cout << "info nodes " << TM.nodes_searched() << " nps " << nps() << " time " << t << " hashfull " << TT.full() << endl; - - // We only support current line printing in single thread mode - if (ShowCurrentLine && TM.active_threads() == 1) - { - cout << "info currline"; - for (int p = 0; p < ply; p++) - cout << " " << ss[p].currentMove; - - cout << endl; - } } // Should we stop the search? if (PonderSearch) return; - bool stillAtFirstMove = RootMoveNumber == 1 + bool stillAtFirstMove = FirstRootMove && !AspirationFailLow && t > MaxSearchTime + ExtraSearchTime; @@ -2475,7 +2492,7 @@ namespace { int t = current_search_time(); PonderSearch = false; - bool stillAtFirstMove = RootMoveNumber == 1 + bool stillAtFirstMove = FirstRootMove && !AspirationFailLow && t > MaxSearchTime + ExtraSearchTime; @@ -2750,7 +2767,7 @@ namespace { } // Wait until the thread has finished launching and is gone to sleep - while (threads[i].state != THREAD_SLEEPING); + while (threads[i].state != THREAD_SLEEPING) {} } } @@ -2791,7 +2808,7 @@ namespace { SplitPoint* sp; - for (sp = threads[threadID].splitPoint; sp && !sp->stopRequest; sp = sp->parent); + for (sp = threads[threadID].splitPoint; sp && !sp->stopRequest; sp = sp->parent) {} return sp != NULL; } @@ -2864,7 +2881,7 @@ namespace { bool ThreadsManager::split(const Position& p, SearchStack* sstck, int ply, Value* alpha, const Value beta, Value* bestValue, - Depth depth, int* moves, MovePicker* mp, int master, bool pvNode) { + Depth depth, bool mateThreat, int* moves, MovePicker* mp, int master, bool pvNode) { assert(p.is_ok()); assert(sstck != NULL); @@ -2899,6 +2916,7 @@ namespace { splitPoint->stopRequest = false; splitPoint->ply = ply; splitPoint->depth = depth; + splitPoint->mateThreat = mateThreat; splitPoint->alpha = pvNode ? *alpha : beta - 1; splitPoint->beta = beta; splitPoint->pvNode = pvNode;