From e95f7a603fc63713dc32145592b9720d927c507b Mon Sep 17 00:00:00 2001 From: Petr Strakos <petrstrakos@eduroam-227-74.vsb.cz> Date: Wed, 9 Jan 2019 14:54:11 +0100 Subject: [PATCH] adding sources for a simple simulation code, setting user --- docs.it4i/software/viz/insitu/CMakeLists.txt | 41 +++++ docs.it4i/software/viz/insitu/FEAdaptor.cxx | 162 ++++++++++++++++++ docs.it4i/software/viz/insitu/FEAdaptor.h | 17 ++ .../software/viz/insitu/FEDataStructures.cxx | 155 +++++++++++++++++ .../software/viz/insitu/FEDataStructures.h | 42 +++++ docs.it4i/software/viz/insitu/FEDriver.cxx | 65 +++++++ .../software/viz/insitu/feslicescript.py | 93 ++++++++++ docs.it4i/software/viz/insitu/insitu.tar.gz | Bin 0 -> 5687 bytes 8 files changed, 575 insertions(+) create mode 100644 docs.it4i/software/viz/insitu/CMakeLists.txt create mode 100644 docs.it4i/software/viz/insitu/FEAdaptor.cxx create mode 100644 docs.it4i/software/viz/insitu/FEAdaptor.h create mode 100644 docs.it4i/software/viz/insitu/FEDataStructures.cxx create mode 100644 docs.it4i/software/viz/insitu/FEDataStructures.h create mode 100644 docs.it4i/software/viz/insitu/FEDriver.cxx create mode 100644 docs.it4i/software/viz/insitu/feslicescript.py create mode 100644 docs.it4i/software/viz/insitu/insitu.tar.gz diff --git a/docs.it4i/software/viz/insitu/CMakeLists.txt b/docs.it4i/software/viz/insitu/CMakeLists.txt new file mode 100644 index 000000000..c1f7b97df --- /dev/null +++ b/docs.it4i/software/viz/insitu/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.3) +project(CatalystCxxFullExample) + +include_directories(/apps/all/ParaView/5.6.0-intel-2017a-mpi/include/paraview-5.6/) +link_directories(/apps/all/ParaView/5.6.0-intel-2017a-mpi/lib/) + +set(USE_CATALYST ON CACHE BOOL "Link the simulator with Catalyst") +if(USE_CATALYST) + find_package(ParaView 4.1 REQUIRED COMPONENTS vtkPVPythonCatalyst) + include("${PARAVIEW_USE_FILE}") + set(Adaptor_SRCS + FEAdaptor.cxx + ) + add_library(CxxFullExampleAdaptor ${Adaptor_SRCS}) + target_link_libraries(CxxFullExampleAdaptor vtkPVPythonCatalyst vtkParallelMPI) + add_definitions("-DUSE_CATALYST") + if(NOT PARAVIEW_USE_MPI) + message(SEND_ERROR "ParaView must be built with MPI enabled") + endif() +else() + find_package(MPI REQUIRED) + include_directories(${MPI_C_INCLUDE_PATH}) +endif() + +add_executable(CxxFullExample FEDriver.cxx FEDataStructures.cxx) +if(USE_CATALYST) + target_link_libraries(CxxFullExample LINK_PRIVATE CxxFullExampleAdaptor) + include(vtkModuleMacros) + include(vtkMPI) + vtk_mpi_link(CxxFullExample) +else() + target_link_libraries(CxxFullExample LINK_PRIVATE ${MPI_LIBRARIES}) +endif() + +option(BUILD_TESTING "Build Testing" OFF) +# Setup testing. +if (BUILD_TESTING) + include(CTest) + add_test(NAME CxxFullExampleTest COMMAND CxxFullExample ${CMAKE_CURRENT_SOURCE_DIR}/SampleScripts/feslicescript.py) + set_tests_properties(CxxFullExampleTest PROPERTIES LABELS "PARAVIEW;CATALYST") +endif() diff --git a/docs.it4i/software/viz/insitu/FEAdaptor.cxx b/docs.it4i/software/viz/insitu/FEAdaptor.cxx new file mode 100644 index 000000000..6409f18ff --- /dev/null +++ b/docs.it4i/software/viz/insitu/FEAdaptor.cxx @@ -0,0 +1,162 @@ +#include "FEAdaptor.h" +#include "FEDataStructures.h" +#include <iostream> + +#include <vtkCPDataDescription.h> +#include <vtkCPInputDataDescription.h> +#include <vtkCPProcessor.h> +#include <vtkCPPythonScriptPipeline.h> +#include <vtkCellData.h> +#include <vtkCellType.h> +#include <vtkDoubleArray.h> +#include <vtkFloatArray.h> +#include <vtkNew.h> +#include <vtkPointData.h> +#include <vtkPoints.h> +#include <vtkUnstructuredGrid.h> + +namespace +{ +vtkCPProcessor* Processor = NULL; +vtkUnstructuredGrid* VTKGrid; + +void BuildVTKGrid(Grid& grid) +{ + // create the points information + vtkNew<vtkDoubleArray> pointArray; + pointArray->SetNumberOfComponents(3); + pointArray->SetArray( + grid.GetPointsArray(), static_cast<vtkIdType>(grid.GetNumberOfPoints() * 3), 1); + vtkNew<vtkPoints> points; + points->SetData(pointArray.GetPointer()); + VTKGrid->SetPoints(points.GetPointer()); + + // create the cells + size_t numCells = grid.GetNumberOfCells(); + VTKGrid->Allocate(static_cast<vtkIdType>(numCells * 9)); + for (size_t cell = 0; cell < numCells; cell++) + { + unsigned int* cellPoints = grid.GetCellPoints(cell); + vtkIdType tmp[8] = { cellPoints[0], cellPoints[1], cellPoints[2], cellPoints[3], cellPoints[4], + cellPoints[5], cellPoints[6], cellPoints[7] }; + VTKGrid->InsertNextCell(VTK_HEXAHEDRON, 8, tmp); + } +} + +void UpdateVTKAttributes(Grid& grid, Attributes& attributes, vtkCPInputDataDescription* idd) +{ + if (idd->IsFieldNeeded("velocity", vtkDataObject::POINT) == true) + { + if (VTKGrid->GetPointData()->GetNumberOfArrays() == 0) + { + // velocity array + vtkNew<vtkDoubleArray> velocity; + velocity->SetName("velocity"); + velocity->SetNumberOfComponents(3); + velocity->SetNumberOfTuples(static_cast<vtkIdType>(grid.GetNumberOfPoints())); + VTKGrid->GetPointData()->AddArray(velocity.GetPointer()); + } + vtkDoubleArray* velocity = + vtkDoubleArray::SafeDownCast(VTKGrid->GetPointData()->GetArray("velocity")); + // The velocity array is ordered as vx0,vx1,vx2,..,vy0,vy1,vy2,..,vz0,vz1,vz2,.. + // so we need to create a full copy of it with VTK's ordering of + // vx0,vy0,vz0,vx1,vy1,vz1,.. + double* velocityData = attributes.GetVelocityArray(); + vtkIdType numTuples = velocity->GetNumberOfTuples(); + for (vtkIdType i = 0; i < numTuples; i++) + { + double values[3] = { velocityData[i], velocityData[i + numTuples], + velocityData[i + 2 * numTuples] }; + velocity->SetTypedTuple(i, values); + } + } + if (idd->IsFieldNeeded("pressure", vtkDataObject::CELL) == true) + { + if (VTKGrid->GetCellData()->GetNumberOfArrays() == 0) + { + // pressure array + vtkNew<vtkFloatArray> pressure; + pressure->SetName("pressure"); + pressure->SetNumberOfComponents(1); + VTKGrid->GetCellData()->AddArray(pressure.GetPointer()); + } + vtkFloatArray* pressure = + vtkFloatArray::SafeDownCast(VTKGrid->GetCellData()->GetArray("pressure")); + // The pressure array is a scalar array so we can reuse + // memory as long as we ordered the points properly. + float* pressureData = attributes.GetPressureArray(); + pressure->SetArray(pressureData, static_cast<vtkIdType>(grid.GetNumberOfCells()), 1); + } +} + +void BuildVTKDataStructures(Grid& grid, Attributes& attributes, vtkCPInputDataDescription* idd) +{ + if (VTKGrid == NULL) + { + // The grid structure isn't changing so we only build it + // the first time it's needed. If we needed the memory + // we could delete it and rebuild as necessary. + VTKGrid = vtkUnstructuredGrid::New(); + BuildVTKGrid(grid); + } + UpdateVTKAttributes(grid, attributes, idd); +} +} + +namespace FEAdaptor +{ + +void Initialize(int numScripts, char* scripts[]) +{ + if (Processor == NULL) + { + Processor = vtkCPProcessor::New(); + Processor->Initialize(); + } + else + { + Processor->RemoveAllPipelines(); + } + for (int i = 0; i < numScripts; i++) + { + vtkNew<vtkCPPythonScriptPipeline> pipeline; + pipeline->Initialize(scripts[i]); + Processor->AddPipeline(pipeline.GetPointer()); + } +} + +void Finalize() +{ + if (Processor) + { + Processor->Delete(); + Processor = NULL; + } + if (VTKGrid) + { + VTKGrid->Delete(); + VTKGrid = NULL; + } +} + +void CoProcess( + Grid& grid, Attributes& attributes, double time, unsigned int timeStep, bool lastTimeStep) +{ + vtkNew<vtkCPDataDescription> dataDescription; + dataDescription->AddInput("input"); + dataDescription->SetTimeData(time, timeStep); + if (lastTimeStep == true) + { + // assume that we want to all the pipelines to execute if it + // is the last time step. + dataDescription->ForceOutputOn(); + } + if (Processor->RequestDataDescription(dataDescription.GetPointer()) != 0) + { + vtkCPInputDataDescription* idd = dataDescription->GetInputDescriptionByName("input"); + BuildVTKDataStructures(grid, attributes, idd); + idd->SetGrid(VTKGrid); + Processor->CoProcess(dataDescription.GetPointer()); + } +} +} // end of Catalyst namespace diff --git a/docs.it4i/software/viz/insitu/FEAdaptor.h b/docs.it4i/software/viz/insitu/FEAdaptor.h new file mode 100644 index 000000000..9bc277048 --- /dev/null +++ b/docs.it4i/software/viz/insitu/FEAdaptor.h @@ -0,0 +1,17 @@ +#ifndef FEADAPTOR_HEADER +#define FEADAPTOR_HEADER + +class Attributes; +class Grid; + +namespace FEAdaptor +{ +void Initialize(int numScripts, char* scripts[]); + +void Finalize(); + +void CoProcess( + Grid& grid, Attributes& attributes, double time, unsigned int timeStep, bool lastTimeStep); +} + +#endif diff --git a/docs.it4i/software/viz/insitu/FEDataStructures.cxx b/docs.it4i/software/viz/insitu/FEDataStructures.cxx new file mode 100644 index 000000000..858e450aa --- /dev/null +++ b/docs.it4i/software/viz/insitu/FEDataStructures.cxx @@ -0,0 +1,155 @@ +#include "FEDataStructures.h" + +#include <iostream> +#include <mpi.h> + +Grid::Grid() +{ +} + +void Grid::Initialize(const unsigned int numPoints[3], const double spacing[3]) +{ + if (numPoints[0] == 0 || numPoints[1] == 0 || numPoints[2] == 0) + { + std::cerr << "Must have a non-zero amount of points in each direction.\n"; + } + // in parallel, we do a simple partitioning in the x-direction. + int mpiSize = 1; + int mpiRank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &mpiRank); + MPI_Comm_size(MPI_COMM_WORLD, &mpiSize); + + unsigned int startXPoint = mpiRank * numPoints[0] / mpiSize; + unsigned int endXPoint = (mpiRank + 1) * numPoints[0] / mpiSize; + if (mpiSize != mpiRank + 1) + { + endXPoint++; + } + + // create the points -- slowest in the x and fastest in the z directions + double coord[3] = { 0, 0, 0 }; + for (unsigned int i = startXPoint; i < endXPoint; i++) + { + coord[0] = i * spacing[0]; + for (unsigned int j = 0; j < numPoints[1]; j++) + { + coord[1] = j * spacing[1]; + for (unsigned int k = 0; k < numPoints[2]; k++) + { + coord[2] = k * spacing[2]; + // add the coordinate to the end of the vector + std::copy(coord, coord + 3, std::back_inserter(this->Points)); + } + } + } + // create the hex cells + unsigned int cellPoints[8]; + unsigned int numXPoints = endXPoint - startXPoint; + for (unsigned int i = 0; i < numXPoints - 1; i++) + { + for (unsigned int j = 0; j < numPoints[1] - 1; j++) + { + for (unsigned int k = 0; k < numPoints[2] - 1; k++) + { + cellPoints[0] = i * numPoints[1] * numPoints[2] + j * numPoints[2] + k; + cellPoints[1] = (i + 1) * numPoints[1] * numPoints[2] + j * numPoints[2] + k; + cellPoints[2] = (i + 1) * numPoints[1] * numPoints[2] + (j + 1) * numPoints[2] + k; + cellPoints[3] = i * numPoints[1] * numPoints[2] + (j + 1) * numPoints[2] + k; + cellPoints[4] = i * numPoints[1] * numPoints[2] + j * numPoints[2] + k + 1; + cellPoints[5] = (i + 1) * numPoints[1] * numPoints[2] + j * numPoints[2] + k + 1; + cellPoints[6] = (i + 1) * numPoints[1] * numPoints[2] + (j + 1) * numPoints[2] + k + 1; + cellPoints[7] = i * numPoints[1] * numPoints[2] + (j + 1) * numPoints[2] + k + 1; + std::copy(cellPoints, cellPoints + 8, std::back_inserter(this->Cells)); + } + } + } +} + +size_t Grid::GetNumberOfPoints() +{ + return this->Points.size() / 3; +} + +size_t Grid::GetNumberOfCells() +{ + return this->Cells.size() / 8; +} + +double* Grid::GetPointsArray() +{ + if (this->Points.empty()) + { + return NULL; + } + return &(this->Points[0]); +} + +double* Grid::GetPoint(size_t pointId) +{ + if (pointId >= this->Points.size()) + { + return NULL; + } + return &(this->Points[pointId * 3]); +} + +unsigned int* Grid::GetCellPoints(size_t cellId) +{ + if (cellId >= this->Cells.size()) + { + return NULL; + } + return &(this->Cells[cellId * 8]); +} + +Attributes::Attributes() +{ + this->GridPtr = NULL; +} + +void Attributes::Initialize(Grid* grid) +{ + this->GridPtr = grid; +} + +void Attributes::UpdateFields(double time) +{ + size_t numPoints = this->GridPtr->GetNumberOfPoints(); + this->Velocity.resize(numPoints * 3); + + // provide different update setting for different parallel process + int mpiSize = 1; + int mpiRank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &mpiRank); + MPI_Comm_size(MPI_COMM_WORLD, &mpiSize); + double setting = 1.0 + (double) mpiRank / (double) mpiSize; + + for (size_t pt = 0; pt < numPoints; pt++) + { + double* coord = this->GridPtr->GetPoint(pt); + this->Velocity[pt] = coord[1] * time * setting; + } + std::fill(this->Velocity.begin() + numPoints, this->Velocity.end(), 0.0); + + size_t numCells = this->GridPtr->GetNumberOfCells(); + this->Pressure.resize(numCells); + std::fill(this->Pressure.begin(), this->Pressure.end(), setting); +} + +double* Attributes::GetVelocityArray() +{ + if (this->Velocity.empty()) + { + return NULL; + } + return &this->Velocity[0]; +} + +float* Attributes::GetPressureArray() +{ + if (this->Pressure.empty()) + { + return NULL; + } + return &this->Pressure[0]; +} diff --git a/docs.it4i/software/viz/insitu/FEDataStructures.h b/docs.it4i/software/viz/insitu/FEDataStructures.h new file mode 100644 index 000000000..b9d39db96 --- /dev/null +++ b/docs.it4i/software/viz/insitu/FEDataStructures.h @@ -0,0 +1,42 @@ +#ifndef FEDATASTRUCTURES_HEADER +#define FEDATASTRUCTURES_HEADER + +#include <cstddef> +#include <vector> + +class Grid +{ +public: + Grid(); + void Initialize(const unsigned int numPoints[3], const double spacing[3]); + size_t GetNumberOfPoints(); + size_t GetNumberOfCells(); + double* GetPointsArray(); + double* GetPoint(size_t pointId); + unsigned int* GetCellPoints(size_t cellId); + +private: + std::vector<double> Points; + std::vector<unsigned int> Cells; +}; + +class Attributes +{ + // A class for generating and storing point and cell fields. + // Velocity is stored at the points and pressure is stored + // for the cells. The current velocity profile is for a + // shearing flow with U(y,t) = y*t, V = 0 and W = 0. + // Pressure is constant through the domain. +public: + Attributes(); + void Initialize(Grid* grid); + void UpdateFields(double time); + double* GetVelocityArray(); + float* GetPressureArray(); + +private: + std::vector<double> Velocity; + std::vector<float> Pressure; + Grid* GridPtr; +}; +#endif diff --git a/docs.it4i/software/viz/insitu/FEDriver.cxx b/docs.it4i/software/viz/insitu/FEDriver.cxx new file mode 100644 index 000000000..9c7d9ec4e --- /dev/null +++ b/docs.it4i/software/viz/insitu/FEDriver.cxx @@ -0,0 +1,65 @@ +#include "FEDataStructures.h" +#include <mpi.h> +#include <stdio.h> +#include <unistd.h> +#include <iostream> +#include <stdlib.h> + +#ifdef USE_CATALYST +#include "FEAdaptor.h" +#endif + +// Example of a C++ adaptor for a simulation code + +int main(int argc, char** argv) +{ + // Check the input arguments for area size + if (argc < 4) { + printf("Not all arguments for grid definition supplied\n"); + return 0; + } + + unsigned int pointsX = abs(std::stoi(argv[1])); + unsigned int pointsY = abs(std::stoi(argv[2])); + unsigned int pointsZ = abs(std::stoi(argv[3])); + + //MPI_Init(&argc, &argv); + MPI_Init(NULL, NULL); + Grid grid; + + unsigned int numPoints[3] = { pointsX, pointsY, pointsZ }; + double spacing[3] = { 1, 1.1, 1.3 }; + grid.Initialize(numPoints, spacing); + Attributes attributes; + attributes.Initialize(&grid); + +#ifdef USE_CATALYST + // The first argument is the program name + FEAdaptor::Initialize(argc - 4, &argv[4]); +#endif + unsigned int numberOfTimeSteps = 1000; + for (unsigned int timeStep = 0; timeStep < numberOfTimeSteps; timeStep++) + { + // use a time step length of 0.1 + double time = timeStep * 0.1; + attributes.UpdateFields(time); +#ifdef USE_CATALYST + FEAdaptor::CoProcess(grid, attributes, time, timeStep, timeStep == numberOfTimeSteps - 1); +#endif + + // Get the name of the processor + char processor_name[MPI_MAX_PROCESSOR_NAME]; + int name_len; + MPI_Get_processor_name(processor_name, &name_len); + + printf("This is processor %s, time step: %0.3f\n", processor_name, time); + usleep(500000); + } + +#ifdef USE_CATALYST + FEAdaptor::Finalize(); +#endif + MPI_Finalize(); + + return 0; +} diff --git a/docs.it4i/software/viz/insitu/feslicescript.py b/docs.it4i/software/viz/insitu/feslicescript.py new file mode 100644 index 000000000..21e9d7522 --- /dev/null +++ b/docs.it4i/software/viz/insitu/feslicescript.py @@ -0,0 +1,93 @@ +from paraview.simple import * +from paraview import coprocessing + +#-------------------------------------------------------------- +# Code generated from cpstate.py to create the CoProcessor. + + +# ----------------------- CoProcessor definition ----------------------- + +def CreateCoProcessor(): + def _CreatePipeline(coprocessor, datadescription): + class Pipeline: + filename_3_pvtu = coprocessor.CreateProducer( datadescription, "input" ) + + Slice1 = Slice( guiName="Slice1", Crinkleslice=0, SliceOffsetValues=[0.0], Triangulatetheslice=1, SliceType="Plane" ) + Slice1.SliceType.Offset = 0.0 + Slice1.SliceType.Origin = [34.5, 32.45, 27.95] + Slice1.SliceType.Normal = [1.0, 0.0, 0.0] + + # create a new 'Parallel PolyData Writer' + parallelPolyDataWriter1 = servermanager.writers.XMLPPolyDataWriter(Input=Slice1) + + # register the writer with coprocessor + # and provide it with information such as the filename to use, + # how frequently to write the data, etc. + coprocessor.RegisterWriter(parallelPolyDataWriter1, filename='slice_%t.pvtp', freq=10) + + # create a new 'Parallel UnstructuredGrid Writer' + unstructuredGridWriter1 = servermanager.writers.XMLPUnstructuredGridWriter(Input=filename_3_pvtu) + + # register the writer with coprocessor + # and provide it with information such as the filename to use, + # how frequently to write the data, etc. + coprocessor.RegisterWriter(unstructuredGridWriter1, filename='fullgrid_%t.pvtu', freq=100) + + return Pipeline() + + class CoProcessor(coprocessing.CoProcessor): + def CreatePipeline(self, datadescription): + self.Pipeline = _CreatePipeline(self, datadescription) + + coprocessor = CoProcessor() + freqs = {'input': [10, 100]} + coprocessor.SetUpdateFrequencies(freqs) + return coprocessor + +#-------------------------------------------------------------- +# Global variables that will hold the pipeline for each timestep +# Creating the CoProcessor object, doesn't actually create the ParaView pipeline. +# It will be automatically setup when coprocessor.UpdateProducers() is called the +# first time. +coprocessor = CreateCoProcessor() + +#-------------------------------------------------------------- +# Enable Live-Visualizaton with ParaView +coprocessor.EnableLiveVisualization(False) + + +# ---------------------- Data Selection method ---------------------- + +def RequestDataDescription(datadescription): + "Callback to populate the request for current timestep" + global coprocessor + if datadescription.GetForceOutput() == True: + # We are just going to request all fields and meshes from the simulation + # code/adaptor. + for i in range(datadescription.GetNumberOfInputDescriptions()): + datadescription.GetInputDescription(i).AllFieldsOn() + datadescription.GetInputDescription(i).GenerateMeshOn() + return + + # setup requests for all inputs based on the requirements of the + # pipeline. + coprocessor.LoadRequestedData(datadescription) + +# ------------------------ Processing method ------------------------ + +def DoCoProcessing(datadescription): + "Callback to do co-processing for current timestep" + global coprocessor + + # Update the coprocessor by providing it the newly generated simulation data. + # If the pipeline hasn't been setup yet, this will setup the pipeline. + coprocessor.UpdateProducers(datadescription) + + # Write output data, if appropriate. + coprocessor.WriteData(datadescription); + + # Write image capture (Last arg: rescale lookup table), if appropriate. + coprocessor.WriteImages(datadescription, rescale_lookuptable=False) + + # Live Visualization, if enabled. + coprocessor.DoLiveVisualization(datadescription, "localhost", 22222) diff --git a/docs.it4i/software/viz/insitu/insitu.tar.gz b/docs.it4i/software/viz/insitu/insitu.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..d207465c54760da5dc15d3fae7c05591f7bf5547 GIT binary patch literal 5687 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 literal 0 HcmV?d00001 -- GitLab