Commit c293a380 authored by Yohann Bearzi's avatar Yohann Bearzi

PEXtractDataArrayOverTime ghost aware

This filter was handing when the input had empty blocks.
Now, vtkPExtractDataArraysOverTime shares array meta data among ranks so
MPI calls are done correctly

Ghosts are also now handled.
parent b49cf441
Pipeline #196661 running with stage
......@@ -34,11 +34,13 @@
#include "vtkSplitColumnComponents.h"
#include "vtkStreamingDemandDrivenPipeline.h"
#include "vtkTable.h"
#include "vtkUnsignedCharArray.h"
#include "vtkWeakPointer.h"
#include <algorithm>
#include <cassert>
#include <map>
#include <numeric>
#include <sstream>
#include <string>
#include <vector>
......@@ -386,14 +388,10 @@ vtkSmartPointer<vtkDataObject> vtkExtractDataArraysOverTime::vtkInternal::Summar
assert(input != nullptr);
const int attributeType = this->Self->GetFieldAssociation();
vtkFieldData* inFD = input->GetAttributesAsFieldData(attributeType);
assert(inFD != nullptr);
vtkDataSetAttributes* inDSA = input->GetAttributes(attributeType);
assert(inDSA != nullptr);
const vtkIdType numIDs = inFD->GetNumberOfTuples();
if (numIDs <= 0)
{
return nullptr;
}
const vtkIdType numIDs = inDSA->GetNumberOfTuples();
// Make a vtkTable containing all fields plus possibly point coordinates.
// We'll pass the table, after splitting multi-component arrays, to
......@@ -412,7 +410,10 @@ vtkSmartPointer<vtkDataObject> vtkExtractDataArraysOverTime::vtkInternal::Summar
orderStats->SetAssessOption(false);
vtkDataSetAttributes* statInDSA = statInput->GetRowData();
statInDSA->ShallowCopy(inFD);
statInDSA->ShallowCopy(inDSA);
this->Self->SynchronizeBlocksMetaData(statInput);
// Add point coordinates to selected data if we are tracking point-data.
if (attributeType == vtkDataObject::POINT)
{
......@@ -433,6 +434,9 @@ vtkSmartPointer<vtkDataObject> vtkExtractDataArraysOverTime::vtkInternal::Summar
}
vtkExtractArraysAssignUniqueCoordNames(statInDSA, pX[0], pX[1], pX[2]);
}
vtkIdType numberOfTotalInputTuples = this->Self->SynchronizeNumberOfTotalInputTuples(inDSA);
splitColumns->SetInputDataObject(0, statInput);
splitColumns->SetCalculateMagnitudes(true);
splitColumns->Update();
......@@ -441,7 +445,7 @@ vtkSmartPointer<vtkDataObject> vtkExtractDataArraysOverTime::vtkInternal::Summar
orderStats->SetInputConnection(splitColumns->GetOutputPort());
// Add a column holding the number of points/cells/rows
// in the data at this timestep.
vtkExtractArraysAddColumnValue(statSummary, "N", VTK_DOUBLE, numIDs);
vtkExtractArraysAddColumnValue(statSummary, "N", VTK_DOUBLE, numberOfTotalInputTuples);
// Compute statistics 1 column at a time to save space (esp. for order stats)
for (int i = 0; i < splits->GetNumberOfColumns(); ++i)
{
......@@ -812,3 +816,16 @@ vtkSmartPointer<vtkOrderStatistics> vtkExtractDataArraysOverTime::NewOrderStatis
{
return vtkSmartPointer<vtkOrderStatistics>::New();
}
//------------------------------------------------------------------------------
vtkIdType vtkExtractDataArraysOverTime::SynchronizeNumberOfTotalInputTuples(
vtkDataSetAttributes* dsa)
{
if (auto ghosts =
vtkArrayDownCast<vtkUnsignedCharArray>(dsa->GetAbstractArray(dsa->GhostArrayName())))
{
auto ghostsRange = vtk::DataArrayValueRange<1>(ghosts);
return dsa->GetNumberOfTuples() - std::accumulate(ghostsRange.cbegin(), ghostsRange.cend(), 0);
}
return dsa->GetNumberOfTuples();
}
......@@ -152,6 +152,24 @@ protected:
virtual vtkSmartPointer<vtkDescriptiveStatistics> NewDescriptiveStatistics();
virtual vtkSmartPointer<vtkOrderStatistics> NewOrderStatistics();
/**
* This method returns the total amount of non ghost tuples on which
* statistics are computed.
*/
virtual vtkIdType SynchronizeNumberOfTotalInputTuples(vtkDataSetAttributes* dsa);
/**
* This method does nothing. But
* in the case of multi process environment, where
* `vtkPExtractDataArraysOverTime` should be used to compute statistics,
* this method synchronizes
* all ranks such that the `vtkTables` in each rank have the same amount of
* columns, with the same names. This is particularly important in the
* presence of ranks having empty inputs, who are still expected to compute
* statistics for each input field.
*/
virtual void SynchronizeBlocksMetaData(vtkTable* vtkNotUsed(splits)) {}
private:
vtkExtractDataArraysOverTime(const vtkExtractDataArraysOverTime&) = delete;
void operator=(const vtkExtractDataArraysOverTime&) = delete;
......
......@@ -12,21 +12,28 @@
PURPOSE. See the above copyright notice for more information.
=========================================================================*/
#include "vtkMPI.h"
#include "vtkPExtractDataArraysOverTime.h"
#include "vtkDataArray.h"
#include "vtkDataSetAttributes.h"
#include "vtkExtractDataArraysOverTime.h"
#include "vtkExtractSelection.h"
#include "vtkExtractTimeSteps.h"
#include "vtkGenerateGlobalIds.h"
#include "vtkInformation.h"
#include "vtkMPI.h"
#include "vtkMPIController.h"
#include "vtkMathUtilities.h"
#include "vtkMultiBlockDataSet.h"
#include "vtkNew.h"
#include "vtkPExodusIIReader.h"
#include "vtkRedistributeDataSetFilter.h"
#include "vtkSelectionNode.h"
#include "vtkSelectionSource.h"
#include "vtkTable.h"
#include "vtkTestUtilities.h"
#include <cmath>
#include <sstream>
#include <string>
......@@ -39,6 +46,68 @@
namespace
{
bool TablesAreTheSame(vtkMultiBlockDataSet* singleProcessMBS, vtkMultiBlockDataSet* multiProcessMBS)
{
int count = 0;
for (unsigned int blockId = 0; blockId < singleProcessMBS->GetNumberOfBlocks(); ++blockId)
{
vtkTable* singleProcessTable = vtkTable::SafeDownCast(singleProcessMBS->GetBlock(blockId));
vtkTable* multiProcessTable = vtkTable::SafeDownCast(multiProcessMBS->GetBlock(blockId));
auto singleRowData = singleProcessTable->GetRowData();
auto multiRowData = multiProcessTable->GetRowData();
for (int arrayId = 0; arrayId < singleRowData->GetNumberOfArrays(); ++arrayId)
{
vtkDataArray* singleArray =
vtkArrayDownCast<vtkDataArray>(singleRowData->GetAbstractArray(arrayId));
vtkDataArray* multiArray =
vtkArrayDownCast<vtkDataArray>(multiRowData->GetAbstractArray(singleArray->GetName()));
// There is a mismatch between global id array names being overriden by
// vtkGenerateGlobalIds
if (!singleArray || !multiArray)
{
continue;
}
if (!multiArray || singleArray->GetNumberOfValues() != multiArray->GetNumberOfValues())
{
return false;
}
for (vtkIdType id = 0; id < singleArray->GetNumberOfTuples(); ++id)
{
// We arbitrarely use 8 significative digits.
// The way stats are currently computed result in numeric imprecision...
// Reason is that stats are incrementally computed. At each step i of the
// computation, the resulting statistic of the i first inputs is
// computed. To add a new element, one need to do a lot of numeric
// operations to retrieve a numeric state such that an element can be added.
//
// A better solution would be to accumulate inputs in a form such that
// accumulating data requires a small amount of operations (three
// maximum, ideally), and with a routine to compute the wanted
// statistics with this accumulated data. In other words, one does not
// carry the whole result at each step, but rather some buffer that can
// be used to easily compute the wanted statistics. This can be easily done for
// most statistics by expanding whatever is expandable in the formula
// in term of monomials, then computing those monomials one by one in some
// small buffer. The wanted statistic can be easily retrieved from it.
//
// In the case of the median / quantiles (any statistics not having a
// closed form), a more involved process is
// needed, but it can still be worked around.
if (std::fabs(singleArray->GetTuple1(id) - multiArray->GetTuple1(id)) >
1e-8 * std::fabs(std::max(singleArray->GetTuple1(id), multiArray->GetTuple1(id))))
{
return false;
}
}
++count;
}
}
// We are supposed to check at least 230 arrays in our setup
return count >= 230;
}
bool ValidateStats(vtkMultiBlockDataSet* mb, int num_timesteps, int rank)
{
if (rank != 0)
......@@ -124,7 +193,7 @@ bool ValidateID(vtkMultiBlockDataSet* mb, int num_timesteps, const char* bname,
expect(name != nullptr, "expecting non-null name.");
std::ostringstream stream;
stream << bname << " rank=" << cc;
stream << bname;
expect(stream.str() == name,
"block name not matching, expected '" << stream.str() << "', got '" << name << "'");
}
......@@ -164,6 +233,8 @@ int TestPExtractDataArraysOverTime(int argc, char* argv[])
{
Initializer init(&argc, &argv);
int ret = EXIT_SUCCESS;
vtkMultiProcessController* contr = vtkMultiProcessController::GetGlobalController();
if (contr == nullptr || contr->GetNumberOfProcesses() != 2)
{
......@@ -172,7 +243,6 @@ int TestPExtractDataArraysOverTime(int argc, char* argv[])
}
const int myrank = contr->GetLocalProcessId();
const int numranks = contr->GetNumberOfProcesses();
char* fname = vtkTestUtilities::ExpandDataFileName(argc, argv, "Data/can.ex2");
......@@ -184,7 +254,6 @@ int TestPExtractDataArraysOverTime(int argc, char* argv[])
reader->SetAllArrayStatus(vtkExodusIIReader::ELEM_BLOCK, 1);
reader->SetGenerateGlobalElementIdArray(true);
reader->SetGenerateGlobalNodeIdArray(true);
delete[] fname;
// lets limit to 10 timesteps to reduce test time.
vtkNew<vtkExtractTimeSteps> textracter;
......@@ -193,17 +262,69 @@ int TestPExtractDataArraysOverTime(int argc, char* argv[])
textracter->GenerateTimeStepIndices(1, 11, 1);
const int num_timesteps = 10;
/**
* Those 2 following filters compute statistics on distributed data
*/
vtkNew<vtkRedistributeDataSetFilter> redistributeFilter;
redistributeFilter->SetInputConnection(textracter->GetOutputPort());
vtkNew<vtkGenerateGlobalIds> generateGlobalIds;
generateGlobalIds->SetInputConnection(redistributeFilter->GetOutputPort());
vtkNew<vtkPExtractDataArraysOverTime> distributedExtractorWithGhosts;
distributedExtractorWithGhosts->SetReportStatisticsOnly(true);
distributedExtractorWithGhosts->SetInputConnection(generateGlobalIds->GetOutputPort());
distributedExtractorWithGhosts->Update();
std::cout << "Computing stats " << std::endl;
vtkNew<vtkPExtractDataArraysOverTime> extractor;
extractor->SetReportStatisticsOnly(true);
extractor->SetInputConnection(textracter->GetOutputPort());
extractor->UpdatePiece(myrank, numranks, 0);
extractor->Update();
if (myrank == 0)
{
// We check computed statistics in different setups:
//
// - P filter between distributed memory vs rank 0 has all the data
// - Non P filter and P filter with rank 0 having all the data.
std::cout << "Comparing computed stats between distributed and non-distributed memory"
<< std::endl;
vtkNew<vtkExtractDataArraysOverTime> singleProcessExtractor;
singleProcessExtractor->SetReportStatisticsOnly(true);
singleProcessExtractor->SetInputConnection(textracter->GetOutputPort());
singleProcessExtractor->Update();
if (!TablesAreTheSame(
vtkMultiBlockDataSet::SafeDownCast(singleProcessExtractor->GetOutputDataObject(0)),
vtkMultiBlockDataSet::SafeDownCast(
distributedExtractorWithGhosts->GetOutputDataObject(0))))
{
std::cerr << "Single process and multiple process with distributed data"
<< " do not compute the same statistics." << std::endl;
ret = EXIT_FAILURE;
}
std::cout << "end " << std::endl;
if (!TablesAreTheSame(
vtkMultiBlockDataSet::SafeDownCast(singleProcessExtractor->GetOutputDataObject(0)),
vtkMultiBlockDataSet::SafeDownCast(extractor->GetOutputDataObject(0))))
{
std::cerr << "Single process and multiple process with empty ranks"
<< " do not compute the same statistics." << std::endl;
ret = EXIT_FAILURE;
}
}
std::cout << "Checking if rank " << myrank << " has correct memory layout on output" << std::endl;
if (!AllRanksSucceeded(
ValidateStats(vtkMultiBlockDataSet::SafeDownCast(extractor->GetOutputDataObject(0)),
num_timesteps, myrank)))
{
cerr << "ERROR: Failed to validate dataset at line: " << __LINE__ << endl;
return EXIT_FAILURE;
ret = EXIT_FAILURE;
}
// let's try non-summary extraction.
......@@ -219,25 +340,26 @@ int TestPExtractDataArraysOverTime(int argc, char* argv[])
extractor->SetReportStatisticsOnly(false);
extractor->SetInputConnection(iextractor->GetOutputPort());
extractor->SetFieldAssociation(vtkDataObject::CELL);
extractor->UpdatePiece(myrank, numranks, 0);
extractor->Update();
if (!AllRanksSucceeded(
ValidateGID(vtkMultiBlockDataSet::SafeDownCast(extractor->GetOutputDataObject(0)),
num_timesteps, "gid=100", myrank)))
{
cerr << "Failed to validate dataset at line: " << __LINE__ << endl;
return EXIT_FAILURE;
ret = EXIT_FAILURE;
}
// this time, simply use element id.
extractor->SetUseGlobalIDs(false);
extractor->UpdatePiece(myrank, numranks, 0);
extractor->Update();
if (!AllRanksSucceeded(
ValidateID(vtkMultiBlockDataSet::SafeDownCast(extractor->GetOutputDataObject(0)),
num_timesteps, "originalId=99 block=2", myrank)))
{
cerr << "Failed to validate dataset at line: " << __LINE__ << endl;
return EXIT_FAILURE;
ret = EXIT_FAILURE;
}
return EXIT_SUCCESS;
delete[] fname;
return ret;
}
......@@ -17,6 +17,7 @@ DEPENDS
VTK::FiltersGeometry
VTK::FiltersHybrid
VTK::FiltersModeling
VTK::FiltersParallelStatistics
VTK::FiltersSources
VTK::FiltersTexture
PRIVATE_DEPENDS
......@@ -26,6 +27,7 @@ PRIVATE_DEPENDS
VTK::ParallelCore
TEST_DEPENDS
VTK::FiltersFlowPaths
VTK::FiltersParallelDIY2
VTK::FiltersParallelImaging
VTK::IOExodus
VTK::IOImage
......
......@@ -14,19 +14,23 @@
=========================================================================*/
#include "vtkPExtractDataArraysOverTime.h"
#include "vtkAbstractArray.h"
#include "vtkDataSetAttributes.h"
#include "vtkInformation.h"
#include "vtkMultiBlockDataSet.h"
#include "vtkMultiProcessController.h"
#include "vtkMultiProcessStream.h"
#include "vtkObjectFactory.h"
#include "vtkPDescriptiveStatistics.h"
#include "vtkPOrderStatistics.h"
#include "vtkSmartPointer.h"
#include "vtkTable.h"
#include "vtkUnsignedCharArray.h"
#include <cassert>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <vector>
namespace
{
......@@ -75,7 +79,10 @@ vtkSmartPointer<vtkTable> vtkMergeTable(vtkTable* dest, vtkTable* src)
}
return dest;
}
}
static constexpr int NUMBER_OF_COLUMNS_COM = 25096;
static constexpr int ARRAY_NAME_LENGTH_COM = 25097;
static constexpr int BUFFER_COM = 25098;
} // anonymous namespace
vtkStandardNewMacro(vtkPExtractDataArraysOverTime);
vtkCxxSetObjectMacro(vtkPExtractDataArraysOverTime, Controller, vtkMultiProcessController);
......@@ -100,15 +107,130 @@ void vtkPExtractDataArraysOverTime::PrintSelf(ostream& os, vtkIndent indent)
}
//------------------------------------------------------------------------------
void vtkPExtractDataArraysOverTime::PostExecute(
vtkInformation* request, vtkInformationVector** inputVector, vtkInformationVector* outputVector)
void vtkPExtractDataArraysOverTime::SynchronizeBlocksMetaData(vtkTable* splits)
{
this->Superclass::PostExecute(request, inputVector, outputVector);
if (!this->Controller || this->Controller->GetNumberOfProcesses() < 2)
// We share among processes array information (type, name and number of
// components) so MPI calls can be done for each array
// The final buffer layout is
// ArrayType - NumberOfComponents - ArrayName
int localNumberOfColumns = splits->GetNumberOfColumns();
int numberOfProcesses = this->Controller->GetNumberOfProcesses();
std::vector<int> globalNumberOfColumns(numberOfProcesses);
int localProcessId = this->Controller->GetLocalProcessId();
this->Controller->AllGather(&localNumberOfColumns, globalNumberOfColumns.data(), 1);
int maxId = 0;
{
return;
int processId = 0;
for (; processId < numberOfProcesses; ++processId)
{
if (localNumberOfColumns < globalNumberOfColumns[processId])
{
maxId = processId;
break;
}
else if (localNumberOfColumns > globalNumberOfColumns[processId])
{
break;
}
else
{
maxId = processId;
}
}
// No need to exchange data, no process are empty
if (processId == numberOfProcesses)
{
return;
}
}
// I have to send array information to processes lacking some.
if (maxId == localProcessId)
{
std::set<int> emptyProcessIds;
for (int processId = 0; processId < numberOfProcesses; ++processId)
{
if (localNumberOfColumns > globalNumberOfColumns[processId])
{
emptyProcessIds.insert(processId);
}
}
vtkIdType numberOfColumns = splits->GetNumberOfColumns();
for (int processId : emptyProcessIds)
{
this->Controller->Send(&numberOfColumns, 1, processId, NUMBER_OF_COLUMNS_COM);
}
std::size_t numberOfChars = 0;
for (int processId : emptyProcessIds)
{
std::vector<std::size_t> arrayNameLength(numberOfColumns);
for (vtkIdType colId = 0; colId < splits->GetNumberOfColumns(); ++colId)
{
arrayNameLength[colId] = strlen(splits->GetColumnName(colId));
numberOfChars += arrayNameLength[colId];
}
this->Controller->Send(
arrayNameLength.data(), numberOfColumns, processId, ARRAY_NAME_LENGTH_COM);
}
std::size_t bufferSize = numberOfChars + (2 + sizeof(int)) * numberOfColumns;
std::vector<char> sendBuffer(bufferSize);
char* sendBufferCurrent = sendBuffer.data();
for (int processId : emptyProcessIds)
{
for (vtkIdType colId = 0; colId < splits->GetNumberOfColumns(); ++colId)
{
*(sendBufferCurrent++) = static_cast<char>(splits->GetColumn(colId)->GetDataType());
*(reinterpret_cast<int*>(sendBufferCurrent)) =
splits->GetColumn(colId)->GetNumberOfComponents();
sendBufferCurrent += sizeof(int);
std::size_t len = strlen(splits->GetColumnName(colId));
memcpy(sendBufferCurrent, splits->GetColumnName(colId), len + 1);
sendBufferCurrent += len + 1;
}
this->Controller->Send(sendBuffer.data(), bufferSize, processId, BUFFER_COM);
}
}
// I need array informations from process of id maxId
else if (maxId != numberOfProcesses)
{
vtkIdType numberOfColumns;
this->Controller->Receive(&numberOfColumns, 1, maxId, NUMBER_OF_COLUMNS_COM);
std::vector<std::size_t> arrayNameLength(numberOfColumns);
this->Controller->Receive(
arrayNameLength.data(), numberOfColumns, maxId, ARRAY_NAME_LENGTH_COM);
vtkIdType numberOfChars = 0;
for (vtkIdType colId = 0; colId < numberOfColumns; ++colId)
{
numberOfChars += arrayNameLength[colId];
}
std::size_t bufferSize = numberOfChars + (2 + sizeof(int)) * numberOfColumns;
std::vector<char> receiveBuffer(bufferSize);
this->Controller->Receive(receiveBuffer.data(), bufferSize, maxId, BUFFER_COM);
char* receiveBufferCurrent = receiveBuffer.data();
for (vtkIdType colId = 0; colId < numberOfColumns; ++colId)
{
auto array = vtkSmartPointer<vtkAbstractArray>::Take(
vtkAbstractArray::CreateArray(*(receiveBufferCurrent++)));
array->SetNumberOfComponents(*(reinterpret_cast<int*>(receiveBufferCurrent)));
receiveBufferCurrent += sizeof(int);
array->SetName(receiveBufferCurrent);
receiveBufferCurrent += arrayNameLength[colId] + 1;
splits->AddColumn(array);
}
}
}
void vtkPExtractDataArraysOverTime::PostExecute(
vtkInformation* request, vtkInformationVector** inputVector, vtkInformationVector* outputVector)
{
this->Superclass::PostExecute(request, inputVector, outputVector);
vtkMultiBlockDataSet* output = vtkMultiBlockDataSet::GetData(outputVector, 0);
assert(output);
this->ReorganizeData(output);
......@@ -117,17 +239,29 @@ void vtkPExtractDataArraysOverTime::PostExecute(
//------------------------------------------------------------------------------
void vtkPExtractDataArraysOverTime::ReorganizeData(vtkMultiBlockDataSet* dataset)
{
// If we only report statistics, we empty blocks of rank different than 0 to
// eliminate duplicate information.
// Else:
// 1. Send all blocks to 0.
// 2. Rank 0 then reorganizes blocks. This is done as follows:
// i. If blocks form different ranks have same names, then we check if they
// are referring to the same global-id. If so, the tables are merged
// into one. If not, we the tables separately, with their names
// uniquified with rank number.
// i. The tables of blocks of same id are merged
// into one.
// 3. Rank 0 send info about number blocks and their names to everyone
// 4. Satellites, then, simply initialize their output to and make it match
// the structure reported by rank 0.
const int myRank = this->Controller->GetLocalProcessId();
if (this->ReportStatisticsOnly)
{
if (myRank)
{
for (vtkIdType blockId = 0; blockId < dataset->GetNumberOfBlocks(); ++blockId)
{
dataset->SetBlock(blockId, nullptr);
}
}
return;
}
const int numRanks = this->Controller->GetNumberOfProcesses();
if (myRank != 0)
{
......@@ -179,38 +313,42 @@ void vtkPExtractDataArraysOverTime::ReorganizeData(vtkMultiBlockDataSet* dataset
for (auto& item : collection)
{
const std::string& name = item.first;
// as we using global ids, if so merge the tables.
if (strncmp(name.c_str(), "gid=", 4) == 0)
vtkSmartPointer<vtkTable> mergedTable;
for (auto& sitem : item.second)
{
vtkSmartPointer<vtkTable> mergedTable;
for (auto& sitem : item.second)
{
mergedTable = vtkMergeTable(mergedTable, sitem.second);
}
auto idx = mb->GetNumberOfBlocks();
mb->SetBlock(idx, mergedTable);
mb->GetMetaData(idx)->Set(vtkCompositeDataSet::NAME(), name.c_str());
stream << name;
mergedTable = vtkMergeTable(mergedTable, sitem.second);
}
else
{
// if not gids, then add each table separately with rank info.
for (auto& sitem : item.second)
{
auto idx = mb->GetNumberOfBlocks();
mb->SetBlock(idx, sitem.second);
std::ostringstream str;
str << name << " rank=" << sitem.first;
mb->GetMetaData(idx)->Set(vtkCompositeDataSet::NAME(), str.str().c_str());
stream << str.str();
}
}
auto idx = mb->GetNumberOfBlocks();
mb->SetBlock(idx, mergedTable);
mb->GetMetaData(idx)->Set(vtkCompositeDataSet::NAME(), name.c_str());
stream << name;
}
this->Controller->Broadcast(stream, 0);
dataset->ShallowCopy(mb);
} // end rank 0
}
//------------------------------------------------------------------------------
vtkSmartPointer<vtkDescriptiveStatistics> vtkPExtractDataArraysOverTime::NewDescriptiveStatistics()
{
return vtkSmartPointer<vtkPDescriptiveStatistics>::New();
}
//------------------------------------------------------------------------------
vtkSmartPointer<vtkOrderStatistics> vtkPExtractDataArraysOverTime::NewOrderStatistics()
{
return vtkSmartPointer<vtkPOrderStatistics>::New();
}