From 91d063c80cfdd90bdf68c78721fa98475762180e Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 13 Feb 2024 10:14:03 -0500 Subject: [PATCH 001/167] Fix for exception handler in SLGridSph and allow deprecated parameter 'scale' as a proxy for 'rmapping' [no ci] --- exputil/SLGridMP2.cc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 37ed197f9..299716660 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -2258,10 +2258,17 @@ bool SLGridSph::ReadH5Cache(void) if (not checkInt(cmap, "cmap")) return false; if (not checkDbl(rmin, "rmin")) return false; if (not checkDbl(rmax, "rmax")) return false; - if (not checkDbl(rmap, "rmapping")) return false; if (not checkInt(diverge, "diverge")) return false; if (not checkDbl(dfac, "dfac")) return false; + // Backward compatibility for old 'scale' key word + // + if (h5file.hasAttribute("scale")) { + if (not checkDbl(rmap, "scale")) return false; + } else { + if (not checkDbl(rmap, "rmapping")) return false; + } + // Harmonic order // auto harmonic = h5file.getGroup("Harmonic"); @@ -2291,8 +2298,9 @@ bool SLGridSph::ReadH5Cache(void) } catch (HighFive::Exception& err) { if (myid==0) std::cerr << "---- SLGridSph::ReadH5Cache: " - << "error opening <" << sph_cache_name - << "> as HDF5 basis cache" << std::endl; + << "error reading <" << sph_cache_name << ">" << std::endl + << "---- SLGridSph::ReadH5Cache: HDF5 error is <" << err.what() + << ">" << std::endl; } return false; From cb5b109d5b8c828e75cbca0c8836b75ec1383900 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 19 Feb 2024 12:23:35 -0500 Subject: [PATCH 002/167] Change to keep the compute-sanitzer happy [no ci] --- src/cudaMultistep.cu | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/cudaMultistep.cu b/src/cudaMultistep.cu index ed39a205d..2425fd97d 100644 --- a/src/cudaMultistep.cu +++ b/src/cudaMultistep.cu @@ -484,8 +484,21 @@ void cuda_compute_levels() // Make thrust do device copy // - float minT = *(thrust::min_element(c->minDT.begin(), c->minDT.end())); - float maxT = *(thrust::max_element(c->maxDT.begin(), c->maxDT.end())); + thrust::host_vector minDT = c->minDT; + thrust::host_vector maxDT = c->maxDT; + + // Note: could not manage to do this on the device. So resorted + // to copying the block results and doing the reduction on the + // host. I assumed that I would be able to do something like + // this: + // + // float minT = *(thrust::min_element(c->minDT.begin(), c->minDT.end())); + // float maxT = *(thrust::max_element(c->maxDT.begin(), c->maxDT.end())); + // + // It seems to work, but the compute-sanitizer is not happy. + + float minT = *(thrust::min_element(minDT.begin(), minDT.end())); + float maxT = *(thrust::max_element(maxDT.begin(), maxDT.end())); if (minTmaxdt) maxdt = maxT; From 0865d9ec87e8d14803dee9b4017692f00b3ce033 Mon Sep 17 00:00:00 2001 From: mdw Date: Tue, 27 Feb 2024 07:27:36 -0500 Subject: [PATCH 003/167] Added NQDHT flag to allowed options in FlatDisk for experimentation with Bessel order [no ci] --- src/FlatDisk.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/FlatDisk.cc b/src/FlatDisk.cc index 2d2dc7345..ba5ab0ec2 100644 --- a/src/FlatDisk.cc +++ b/src/FlatDisk.cc @@ -15,6 +15,7 @@ FlatDisk::valid_keys = { "mmax", "numx", "numy", + "NQDHT", "knots", "logr", "model", From 9a4472c9f943574706f4f588c334fb6616d48db3 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 27 Feb 2024 08:04:39 -0500 Subject: [PATCH 004/167] Added additional allowed flags for FlatDisk To BasisFactory [no ci] --- coefs/BiorthBasis.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coefs/BiorthBasis.cc b/coefs/BiorthBasis.cc index a9e49f98a..b6622f643 100644 --- a/coefs/BiorthBasis.cc +++ b/coefs/BiorthBasis.cc @@ -1394,6 +1394,7 @@ namespace BasisClasses "numx", "numy", "numr", + "NQDHT", "knots", "logr", "model", @@ -1416,6 +1417,7 @@ namespace BasisClasses "Mmax", "nmax", "mmax", + "mlim", "dof", "subsamp", "samplesz", From 4754b0aa6bc3570c96ad728d67062a4ea2bf4979 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 27 Feb 2024 11:47:04 -0500 Subject: [PATCH 005/167] Make zeropos and zerovel the default options [no ci] --- utils/ICs/ZangICs.cc | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/utils/ICs/ZangICs.cc b/utils/ICs/ZangICs.cc index bece0fce8..638c6c755 100644 --- a/utils/ICs/ZangICs.cc +++ b/utils/ICs/ZangICs.cc @@ -38,7 +38,8 @@ main(int ac, char **av) options.add_options() ("h,help", "Print this help message") - ("z,zerovel", "Zero the mean velocity") + ("V,nozerovel", "Do not zero the mean velocity") + ("P,nozeropos", "Do not zero the center of mass") ("d,debug", "Print debug grid") ("N,number", "Number of particles to generate", cxxopts::value(N)->default_value("100000")) @@ -182,9 +183,9 @@ main(int ac, char **av) // Track number of iteration overflows int over = 0; - // Velocity zeroing + // Position and velocity zeroing // - std::vector> zerovel(nomp); + std::vector> zeropos(nomp), zerovel(nomp); // Generation loop with OpenMP // @@ -233,9 +234,12 @@ main(int ac, char **av) vel[n][1] = vr*sin(phi) + vt*cos(phi); vel[n][2] = 0.0; - // Accumulate mean velocity + // Accumulate mean position and velocity // - for (int k=0; k<3; k++) zerovel[tid][k] += vel[n][k]; + for (int k=0; k<3; k++) { + zeropos[tid][k] += pos[n][k]; + zerovel[tid][k] += vel[n][k]; + } // Print progress bar if (tid==0) ++(*progress); @@ -246,16 +250,29 @@ main(int ac, char **av) // double mass = (model->get_mass(Rmax) - model->get_mass(Rmin))/N; - // Reduce the mean velocity + // Reduce the mean position and velocity // for (int n=1; n Date: Tue, 27 Feb 2024 16:07:33 -0500 Subject: [PATCH 006/167] Fix for EVEN_M in BasisFactory for BirothCyl [no ci] --- coefs/BiorthBasis.cc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/coefs/BiorthBasis.cc b/coefs/BiorthBasis.cc index b6622f643..5e32d51bc 100644 --- a/coefs/BiorthBasis.cc +++ b/coefs/BiorthBasis.cc @@ -1729,18 +1729,20 @@ namespace BasisClasses } // Get the basis fields + // ortho->get_dens (dend[tid], R, z); ortho->get_pot (potd[tid], R, z); ortho->get_rforce (potR[tid], R, z); ortho->get_zforce (potZ[tid], R, z); // m loop + // for (int m=0, moffset=0; m<=mmax; m++) { - if (m==0 and NO_M0) continue; - if (m==1 and NO_M1) continue; - if (EVEN_M and m/2*2 != m) continue; - if (m>0 and M0_only) break; + if (m==0 and NO_M0) { moffset++; continue; } + if (m==1 and NO_M1) { moffset += 2; continue; } + if (EVEN_M and m/2*2 != m) { moffset += 2; continue; } + if (m>0 and M0_only) break; if (m==0) { for (int n=std::max(0, N1); n<=std::min(nmax-1, N2); n++) { From e3b663e6475f692fc8c890f33065445ee3296a73 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 1 Mar 2024 09:49:20 -0500 Subject: [PATCH 007/167] Updates to enforce the cache name requirement [no ci] --- exputil/EmpCylSL.cc | 2 +- include/EmpCylSL.H | 30 +++++++++++++++--------------- utils/ICs/eof_basis.cc | 6 ++++-- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 90840b972..1db3164f2 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -3096,7 +3096,7 @@ void EmpCylSL::make_eof(void) // Cache table for restarts // - if (myid==0) cache_grid(1); + if (myid==0) cache_grid(1, cachefile); // Basis complete but still need to compute coefficients // diff --git a/include/EmpCylSL.H b/include/EmpCylSL.H index 55fa787df..f9da0d718 100644 --- a/include/EmpCylSL.H +++ b/include/EmpCylSL.H @@ -235,7 +235,7 @@ protected: std::string cachefile; // 1=write, 0=read // return: 0=failure - int cache_grid(int, string file=""); + int cache_grid(int, std::string file); double integral(int, int, int, int); void get_pot(Eigen::MatrixXd&, Eigen::MatrixXd&, double, double); double massR(double R); @@ -495,8 +495,8 @@ public: */ EmpCylSL(int numr, int lmax, int mmax, int nord, - double ascale, double hscale, int Nodd=-1, - std::string cachename=""); + double ascale, double hscale, int Nodd, + std::string cachename); //! Destructor ~EmpCylSL(void); @@ -504,16 +504,16 @@ public: //! Reconstruct basis with new parameters void reset(int numr, int lmax, int mmax, int nord, double ascale, double hscale, int Nodd, - std::string cachename=""); + std::string cachename); //! Read EOF basis header from saved file - int read_eof_header(const string& eof_file); + int read_eof_header(const std::string& eof_file); //! Read EOF basis from saved file - int read_eof_file(const string& eof_file); + int read_eof_file(const std::string& eof_file); //! Dump the EOF basis in ascii format - void dump_eof_file(const string& eof_file, const string& dump_file); + void dump_eof_file(const std::string& eof_file, const std::string& dump_file); //! Read basis from cache file int read_cache(void); @@ -694,26 +694,26 @@ public: void dump_coefs_binary(std::ostream& out, double time); //! Plot basis - void dump_basis(const string& name, int step, double Rmax=-1.0); + void dump_basis(const std::string& name, int step, double Rmax=-1.0); //! Plot full fields for debugging - void dump_images(const string& OUTFILE, + void dump_images(const std::string& OUTFILE, double XYOUT, double ZOUT, int OUTR, int OUTZ, bool logscale); //! Plot basis images for debugging - void dump_images_basis(const string& OUTFILE, + void dump_images_basis(const std::string& OUTFILE, double XYOUT, double ZOUT, int OUTR, int OUTZ, bool logscale, int M1, int M2, int N1, int N2); //! Plot PCA basis images for debugging - void dump_images_basis_pca(const string& runtag, + void dump_images_basis_pca(const std::string& runtag, double XYOUT, double ZOUT, int OUTR, int OUTZ, int M, int N, int cnt); //! Plot EOF basis images for debugging - void dump_images_basis_eof(const string& runtag, + void dump_images_basis_eof(const std::string& runtag, double XYOUT, double ZOUT, int OUTR, int OUTZ, int M, int N, int cnt, const Eigen::VectorXd& tp); @@ -760,7 +760,7 @@ public: //! Set file name for EOF analysis and sample size for subsample //! computation - inline void setHall(string file, unsigned tot) + inline void setHall(std::string file, unsigned tot) { hallfile = file; nbodstot = tot; @@ -771,14 +771,14 @@ public: if (PCAVAR) { - const string types[] = + const std::string types[] = { "Hall", "Truncate", "None" }; - const string desc[] = + const std::string desc[] = { "Compute the S/N but do not modify coefficients", "Tapered signal-to-noise power defined by Hall", diff --git a/utils/ICs/eof_basis.cc b/utils/ICs/eof_basis.cc index 685b21b66..de38052c9 100644 --- a/utils/ICs/eof_basis.cc +++ b/utils/ICs/eof_basis.cc @@ -27,7 +27,7 @@ main(int argc, char **argv) std::string eof, tag; double rmax, zmax; - int nout, mmax, norder; + int nout, mmax, norder, nodd; cxxopts::Options options(argv[0], "Dump the entire disk orthgonal function file in ascii"); @@ -45,6 +45,8 @@ main(int argc, char **argv) cxxopts::value(mmax)->default_value("6")) ("n,nmax", "maximum radial order", cxxopts::value(norder)->default_value("18")) + ("nodd", "number of vertically antisymmtric functions", + cxxopts::value(nodd)->default_value("6")) ("N,nout", "number of grid points in each dimension", cxxopts::value(nout)->default_value("40")) ; @@ -83,7 +85,7 @@ main(int argc, char **argv) double acyl = 0.01; double hcyl = 0.001; - EmpCylSL test(nmax, lmax, mmax, nord, acyl, hcyl); + EmpCylSL test(nmax, lmax, mmax, nord, acyl, hcyl, nodd, eof); test.read_eof_file(eof); From cd04ea1d8cd61a993cd8cf61e3f300974406c91e Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 1 Mar 2024 11:26:44 -0500 Subject: [PATCH 008/167] Change default nmaxfid to 128 in EmpCyl2d [no ci] --- exputil/BiorthCyl.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exputil/BiorthCyl.cc b/exputil/BiorthCyl.cc index f4f2abd1b..859dc13d9 100644 --- a/exputil/BiorthCyl.cc +++ b/exputil/BiorthCyl.cc @@ -40,7 +40,7 @@ BiorthCyl::BiorthCyl(const YAML::Node& conf) : conf(conf) mmax = conf["Lmax"].as(); if (conf["nmaxfid"]) nmaxfid = conf["nmaxfid"].as(); - else nmaxfid = 40; + else nmaxfid = 256; if (conf["nmax"]) nmax = conf["nmax"].as(); else nmax = nmaxfid; From 614e98f4312e309b40f424b7056f285828070632 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 1 Mar 2024 11:43:49 -0500 Subject: [PATCH 009/167] Throw exception if yaml-cpp detects an error in parse.cc [no ci] --- src/parse.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/parse.cc b/src/parse.cc index 675a84572..160ac12a3 100644 --- a/src/parse.cc +++ b/src/parse.cc @@ -609,7 +609,9 @@ void YAML_parse_args(int argc, char** argv) if (done) { MPI_Finalize(); - exit(EXIT_SUCCESS); + throw EXPException("YAML configuration error", + "parsing failure, check your YAML config for syntax?", + __FILE__, __LINE__); } } From 9daee1613c36fbb0f4d45486c81b8e66e8327beb Mon Sep 17 00:00:00 2001 From: mdw Date: Fri, 1 Mar 2024 12:22:52 -0500 Subject: [PATCH 010/167] Remove MPI_Finalize() + exit() in favor of std::runtime_error exceptions in parse.cc [no ci] --- src/parse.cc | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/parse.cc b/src/parse.cc index 160ac12a3..3419a68d0 100644 --- a/src/parse.cc +++ b/src/parse.cc @@ -71,8 +71,8 @@ void initialize(void) << std::string(60, '-') << std::endl << parse << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw EXPException("YAML configuration error", "Error parsing Global stanza", + __FILE__, __LINE__); } if (_G) { @@ -290,8 +290,8 @@ void initialize(void) MPI_Bcast(&nOK, 1, MPI_INT, 0, MPI_COMM_WORLD); if (nOK) { - MPI_Finalize(); - exit(10); + throw EXPException("Configuration error", "error opening outdir", + __FILE__, __LINE__); } } @@ -327,8 +327,8 @@ void initialize(void) MPI_Bcast(&iok, 1, MPI_INT, 0, MPI_COMM_WORLD); if (!iok) { - MPI_Finalize(); - exit(11); + throw EXPException("Configuration error", "error creating files in outdir", + __FILE__, __LINE__); } } @@ -382,8 +382,8 @@ void update_parm() catch (YAML::Exception & error) { if (myid==0) std::cout << "Error updating parameters in parse.update_parm: " << error.what() << std::endl; - MPI_Finalize(); - exit(-1); + throw EXPException("Configuration error", "error updating YAML config", + __FILE__, __LINE__); } } @@ -404,8 +404,8 @@ void write_parm(void) MPI_Bcast(&nOK, 1, MPI_INT, 0, MPI_COMM_WORLD); if (nOK) { - MPI_Finalize(); - exit(102); + throw EXPException("Configuration error", "error writing YAML config", + __FILE__, __LINE__); } if (myid!=0) return; @@ -502,8 +502,8 @@ void YAML_parse_args(int argc, char** argv) vm = options.parse(argc, argv); } catch (cxxopts::OptionException& e) { std::cout << "Option error: " << e.what() << std::endl; - MPI_Finalize(); - exit(-1); + throw EXPException("cxxopts error", "error parsing options", + __FILE__, __LINE__); } if (vm.count("help")) { @@ -530,8 +530,7 @@ void YAML_parse_args(int argc, char** argv) MPI_Bcast(&done, 1, MPI_INT, 0, MPI_COMM_WORLD); if (done) { - MPI_Finalize(); - exit(EXIT_SUCCESS); + throw EXPException("Clean exit", "done", __FILE__, __LINE__); } // If config file is not specified, use first trailing, unmatched @@ -573,8 +572,9 @@ void YAML_parse_args(int argc, char** argv) MPI_Bcast(&done, 1, MPI_INT, 0, MPI_COMM_WORLD); if (done) { - MPI_Finalize(); - exit(EXIT_SUCCESS); + throw EXPException("Configuration error", + "error opening YAML configuration file", + __FILE__, __LINE__); } try { @@ -590,6 +590,14 @@ void YAML_parse_args(int argc, char** argv) // MPI_Bcast(&done, 1, MPI_INT, 0, MPI_COMM_WORLD); + // Exit if done + // + if (done) { + throw EXPException("YAML configuration error", + "parsing failure, check your YAML config for syntax?", + __FILE__, __LINE__); + } + // The current YAML structure will now be serialized and broadcast // to all processes // @@ -608,13 +616,12 @@ void YAML_parse_args(int argc, char** argv) MPI_Bcast(&done, 1, MPI_INT, 0, MPI_COMM_WORLD); if (done) { - MPI_Finalize(); - throw EXPException("YAML configuration error", - "parsing failure, check your YAML config for syntax?", + throw EXPException("Configuration error in main process", + "exiting", __FILE__, __LINE__); } } - + // Receive the YAML serialized length // int line_size; From ebab458fafc05c410ebd8903c89a2429625a6a08 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Fri, 1 Mar 2024 21:54:39 +0000 Subject: [PATCH 011/167] Simplify workflow; fix tests/ cmake issue --- .github/workflows/build.yml | 64 +------------- .github/workflows/buildfull.yml | 146 ++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 3 +- 3 files changed, 152 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/buildfull.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83a8547a4..a37377ef8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: "Test Builds" on: push: branches: - - main + - '*' pull_request: branches: - main @@ -11,7 +11,7 @@ jobs: pyexp: strategy: matrix: - os: [macos-latest, ubuntu-latest] + os: [ubuntu-latest] cc: [gcc, mpicc] name: "Test pyEXP Build" @@ -26,19 +26,12 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential libeigen3-dev libfftw3-dev libhdf5-dev libopenmpi-dev - - name: Install core dependencies - mac - if: startsWith(matrix.os, 'mac') - run: | - brew update - brew reinstall gcc - brew install eigen fftw hdf5 open-mpi libomp - - name: Setup submodule and build run: | git submodule update --init --recursive mkdir -p build/install - - name: Compile pyEXP - Linux + - name: Compile pyEXP if: runner.os == 'Linux' env: CC: ${{ matrix.cc }} @@ -53,27 +46,6 @@ jobs: -Wno-dev .. - # Note for future: The homebrew paths are for intel only. Once ARM macs are - # supported in here, we'll need to update to /opt/homebrew/... instead - - name: Compile pyEXP - Mac - if: startsWith(matrix.os, 'mac') - env: - CC: ${{ matrix.cc }} - LDFLAGS: -L/usr/local/opt/libomp/lib - CPPFLAGS: -I/usr/local/opt/libomp/include - working-directory: ./build - run: >- - cmake - -DENABLE_NBODY=NO - -DENABLE_PYEXP=YES - -DCMAKE_BUILD_TYPE=Release - -DEigen3_DIR=/usr/local/share/eigen3/cmake - -DCMAKE_INSTALL_PREFIX=./install - -DOpenMP_CXX_INCLUDE_DIR=/usr/local/opt/libomp/include - -DOpenMP_C_INCLUDE_DIR=/usr/local/opt/libomp/include - -Wno-dev - .. - - name: Make working-directory: ./build run: make -j 2 @@ -83,7 +55,7 @@ jobs: exp: strategy: matrix: - os: [macos-latest, ubuntu-latest] + os: [ubuntu-latest] cc: [gcc, mpicc] name: "Test Full EXP Build" @@ -98,13 +70,6 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential libeigen3-dev libfftw3-dev libhdf5-dev libopenmpi-dev - - name: Install core dependencies - mac - if: startsWith(matrix.os, 'mac') - run: | - brew update - brew reinstall gcc - brew install eigen fftw hdf5 open-mpi libomp - - name: Setup submodule and build run: | git submodule update --init --recursive @@ -125,27 +90,6 @@ jobs: -Wno-dev .. - # Note for future: The homebrew paths are for intel only. Once ARM macs are - # supported in here, we'll need to update to /opt/homebrew/... instead - - name: Compile Full EXP - Mac - if: startsWith(matrix.os, 'mac') - env: - CC: ${{ matrix.cc }} - LDFLAGS: -L/usr/local/opt/libomp/lib - CPPFLAGS: -I/usr/local/opt/libomp/include - working-directory: ./build - run: >- - cmake - -DENABLE_NBODY=YES - -DENABLE_PYEXP=NO - -DCMAKE_BUILD_TYPE=Release - -DEigen3_DIR=/usr/local/share/eigen3/cmake - -DCMAKE_INSTALL_PREFIX=./install - -DOpenMP_CXX_INCLUDE_DIR=/usr/local/opt/libomp/include - -DOpenMP_C_INCLUDE_DIR=/usr/local/opt/libomp/include - -Wno-dev - .. - - name: Make working-directory: ./build run: make -j 2 diff --git a/.github/workflows/buildfull.yml b/.github/workflows/buildfull.yml new file mode 100644 index 000000000..eb9c86552 --- /dev/null +++ b/.github/workflows/buildfull.yml @@ -0,0 +1,146 @@ +name: "Test Builds" + +on: + +jobs: + pyexp: + strategy: + matrix: + os: [macos-latest, ubuntu-latest] + cc: [gcc, mpicc] + + name: "Test pyEXP Build" + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install core dependencies - ubuntu + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential libeigen3-dev libfftw3-dev libhdf5-dev libopenmpi-dev + + - name: Install core dependencies - mac + if: startsWith(matrix.os, 'mac') + run: | + brew update + brew reinstall gcc + brew install eigen fftw hdf5 open-mpi libomp + + - name: Setup submodule and build + run: | + git submodule update --init --recursive + mkdir -p build/install + + - name: Compile pyEXP - Linux + if: runner.os == 'Linux' + env: + CC: ${{ matrix.cc }} + working-directory: ./build + run: >- + cmake + -DENABLE_NBODY=NO + -DENABLE_PYEXP=YES + -DCMAKE_BUILD_TYPE=Release + -DEigen3_DIR=/usr/include/eigen3/share/eigen3/cmake + -DCMAKE_INSTALL_PREFIX=./install + -Wno-dev + .. + + # Note for future: The homebrew paths are for intel only. Once ARM macs are + # supported in here, we'll need to update to /opt/homebrew/... instead + - name: Compile pyEXP - Mac + if: startsWith(matrix.os, 'mac') + env: + CC: ${{ matrix.cc }} + LDFLAGS: -L/usr/local/opt/libomp/lib + CPPFLAGS: -I/usr/local/opt/libomp/include + working-directory: ./build + run: >- + cmake + -DENABLE_NBODY=NO + -DENABLE_PYEXP=YES + -DCMAKE_BUILD_TYPE=Release + -DEigen3_DIR=/usr/local/share/eigen3/cmake + -DCMAKE_INSTALL_PREFIX=./install + -DOpenMP_CXX_INCLUDE_DIR=/usr/local/opt/libomp/include + -DOpenMP_C_INCLUDE_DIR=/usr/local/opt/libomp/include + -Wno-dev + .. + + - name: Make + working-directory: ./build + run: make -j 2 + + # ----------------------------------------------------------------------------------- + + exp: + strategy: + matrix: + os: [macos-latest, ubuntu-latest] + cc: [gcc, mpicc] + + name: "Test Full EXP Build" + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install core dependencies - ubuntu + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential libeigen3-dev libfftw3-dev libhdf5-dev libopenmpi-dev + + - name: Install core dependencies - mac + if: startsWith(matrix.os, 'mac') + run: | + brew update + brew reinstall gcc + brew install eigen fftw hdf5 open-mpi libomp + + - name: Setup submodule and build + run: | + git submodule update --init --recursive + mkdir -p build/install + + - name: Compile Full EXP - Linux + if: runner.os == 'Linux' + env: + CC: ${{ matrix.cc }} + working-directory: ./build + run: >- + cmake + -DENABLE_NBODY=YES + -DENABLE_PYEXP=NO + -DCMAKE_BUILD_TYPE=Release + -DEigen3_DIR=/usr/include/eigen3/share/eigen3/cmake + -DCMAKE_INSTALL_PREFIX=./install + -Wno-dev + .. + + # Note for future: The homebrew paths are for intel only. Once ARM macs are + # supported in here, we'll need to update to /opt/homebrew/... instead + - name: Compile Full EXP - Mac + if: startsWith(matrix.os, 'mac') + env: + CC: ${{ matrix.cc }} + LDFLAGS: -L/usr/local/opt/libomp/lib + CPPFLAGS: -I/usr/local/opt/libomp/include + working-directory: ./build + run: >- + cmake + -DENABLE_NBODY=YES + -DENABLE_PYEXP=NO + -DCMAKE_BUILD_TYPE=Release + -DEigen3_DIR=/usr/local/share/eigen3/cmake + -DCMAKE_INSTALL_PREFIX=./install + -DOpenMP_CXX_INCLUDE_DIR=/usr/local/opt/libomp/include + -DOpenMP_C_INCLUDE_DIR=/usr/local/opt/libomp/include + -Wno-dev + .. + + - name: Make + working-directory: ./build + run: make -j 2 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 958f23379..803800330 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -85,6 +85,7 @@ if(ENABLE_NBODY) ${PYTHON_EXECUTABLE} readCoefs.py WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/Halo") set_tests_properties(pyEXPCoefReadTest PROPERTIES DEPENDS makeNbodyTest) + set_tests_properties(pyEXPCoefReadTest PROPERTIES LABELS "long") endif() # A separate test to remove the generated files if they all exist; @@ -136,7 +137,7 @@ if(ENABLE_NBODY) # Set labels for pyEXP tests set_tests_properties(expExecuteTest PROPERTIES LABELS "quick") set_tests_properties(makeICTest expNbodyTest expNbodyCheck2TW - pyEXPCoefReadTest removeTempFiles expCubeTest removeCubeFiles + removeTempFiles expCubeTest removeCubeFiles PROPERTIES LABELS "long") endif() From 5bede3a97fece07254412174026afbcb210f6c93 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 1 Mar 2024 17:55:58 -0500 Subject: [PATCH 012/167] Missing commit of help message termination fix [no ci] --- src/parse.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/parse.cc b/src/parse.cc index 3419a68d0..b2073a1c8 100644 --- a/src/parse.cc +++ b/src/parse.cc @@ -530,7 +530,8 @@ void YAML_parse_args(int argc, char** argv) MPI_Bcast(&done, 1, MPI_INT, 0, MPI_COMM_WORLD); if (done) { - throw EXPException("Clean exit", "done", __FILE__, __LINE__); + MPI_Finalize(); + exit(0); } // If config file is not specified, use first trailing, unmatched @@ -616,6 +617,10 @@ void YAML_parse_args(int argc, char** argv) MPI_Bcast(&done, 1, MPI_INT, 0, MPI_COMM_WORLD); if (done) { + if (i==0) { + MPI_Finalize(); + exit(0); + } throw EXPException("Configuration error in main process", "exiting", __FILE__, __LINE__); From a338b68c4b1ab878e29a540bf8a8601952264bcd Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 1 Mar 2024 19:17:29 -0500 Subject: [PATCH 013/167] Advertise the EXP online documentation in the help strings [no ci] --- src/parse.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parse.cc b/src/parse.cc index b2073a1c8..976da4627 100644 --- a/src/parse.cc +++ b/src/parse.cc @@ -509,7 +509,7 @@ void YAML_parse_args(int argc, char** argv) if (vm.count("help")) { std::cout << options.help() << std::endl << "* The YAML config may be appended to the command line without flags" << std::endl - << "* See EXP/doc/html/index.html for extensive documentation" << std::endl + << "* See https://exp-docs.readthedocs.io for extensive documentation" << std::endl << std::endl << std::endl; done = 1; } From 461de5f101fcd4c44aa7573610a6c6d028704dbb Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 5 Mar 2024 21:48:55 -0500 Subject: [PATCH 014/167] Add an MPI exclusion to allow 2d basis construction to work with single process pyEXP runs --- exputil/BiorthCyl.cc | 34 ++++++++++++++++++++++------------ exputil/EmpCyl2d.cc | 15 +++++---------- include/BiorthCyl.H | 2 +- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/exputil/BiorthCyl.cc b/exputil/BiorthCyl.cc index 859dc13d9..2eecc84a9 100644 --- a/exputil/BiorthCyl.cc +++ b/exputil/BiorthCyl.cc @@ -30,6 +30,14 @@ using namespace __EXP__; // For reference to n-body globals // Constructor BiorthCyl::BiorthCyl(const YAML::Node& conf) : conf(conf) { + // Check whether MPI is initialized. Use flag to suppress MPI calls + // if MPI is not active. + // + int flag; + MPI_Initialized(&flag); + if (flag) use_mpi = true; + else use_mpi = false; + // Read and assign parameters // try { @@ -110,7 +118,7 @@ BiorthCyl::BiorthCyl(const YAML::Node& conf) : conf(conf) << std::string(60, '-') << std::endl << conf << std::string(60, '-') << std::endl; - MPI_Finalize(); + if (use_mpi) MPI_Finalize(); exit(-1); } @@ -231,16 +239,18 @@ void BiorthCyl::create_tables() if (verbose and myid==0) std::cout << std::endl; - for (int m=0; m<=mmax; m++) { - for (int n=0; n EmpCyl2d::createModel() << std::string(60, '-') << std::endl << Params << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("EmpCyl2d::createModel: error parsing YAML config"); } } diff --git a/include/BiorthCyl.H b/include/BiorthCyl.H index da3cd51f5..e8700e378 100644 --- a/include/BiorthCyl.H +++ b/include/BiorthCyl.H @@ -48,7 +48,7 @@ protected: int mmax, nmax, numr, nmaxfid, mmin, mlim, nmin, nlim, knots, NQDHT; double rcylmin, rcylmax, scale, acyltbl, acylcut, Ninner, Mouter; - bool EVEN_M, verbose, logr; + bool EVEN_M, verbose, logr, use_mpi; //@{ //! Grid parameters From 668b3a49eb74a7ae131008b67053b6d4ad570c88 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 7 Mar 2024 12:35:15 -0500 Subject: [PATCH 015/167] Fix incorrect Basis IDs in coefficient construction; remove Numpy rows/cols mnemonic in favor of an explicit choice of row major vs col major [no ci] --- coefs/BasisFactory.H | 6 ++++-- coefs/BasisFactory.cc | 4 ++-- coefs/BiorthBasis.H | 6 ++++-- coefs/BiorthBasis.cc | 37 +++++++++++++++++++------------------ coefs/FieldBasis.H | 2 +- coefs/FieldBasis.cc | 9 ++++----- pyEXP/BasisWrappers.cc | 32 +++++++++++++++++++++++--------- 7 files changed, 57 insertions(+), 39 deletions(-) diff --git a/coefs/BasisFactory.H b/coefs/BasisFactory.H index 19cdd3516..0c8b6024b 100644 --- a/coefs/BasisFactory.H +++ b/coefs/BasisFactory.H @@ -200,13 +200,15 @@ namespace BasisClasses //! Accumulate coefficient contributions from arrays virtual void - addFromArray(Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin) = 0; + addFromArray(Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin, + bool posvelrows) = 0; //! Generate coeffients from an array and optional center location //! for the expansion CoefClasses::CoefStrPtr createFromArray (Eigen::VectorXd& m, RowMatrixXd& p, double time=0.0, - std::vector center={0.0, 0.0, 0.0}, bool roundrobin=true); + std::vector center={0.0, 0.0, 0.0}, + bool roundrobin=true, bool posvelrows=false); //! Create and the coefficients from the array accumulation with the //! provided time value diff --git a/coefs/BasisFactory.cc b/coefs/BasisFactory.cc index b6ddb2474..40b154465 100644 --- a/coefs/BasisFactory.cc +++ b/coefs/BasisFactory.cc @@ -267,10 +267,10 @@ namespace BasisClasses // CoefClasses::CoefStrPtr Basis::createFromArray (Eigen::VectorXd& m, RowMatrixXd& p, double time, std::vector ctr, - bool roundrobin) + bool roundrobin, bool posvelrows) { initFromArray(ctr); - addFromArray(m, p, roundrobin); + addFromArray(m, p, roundrobin, posvelrows); return makeFromArray(time); } diff --git a/coefs/BiorthBasis.H b/coefs/BiorthBasis.H index 50affc39f..5c5fe28b1 100644 --- a/coefs/BiorthBasis.H +++ b/coefs/BiorthBasis.H @@ -96,7 +96,8 @@ namespace BasisClasses //! for the expansion CoefClasses::CoefStrPtr createFromArray (Eigen::VectorXd& m, RowMatrixXd& p, double time=0.0, - std::vector center={0.0, 0.0, 0.0}, bool roundrobin=true); + std::vector center={0.0, 0.0, 0.0}, + bool roundrobin=true, bool posvelrows=false); //! Generate coeffients from an array and optional center location //! for the expansion using multiple particle partitions @@ -110,7 +111,8 @@ namespace BasisClasses //! Initialize accumulating coefficients from arrays. This is //! called once to initialize the accumulation. void addFromArray - (Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin=true); + (Eigen::VectorXd& m, RowMatrixXd& p, + bool roundrobin=true, bool posvelrows=false); //! Create and the coefficients from the array accumulation with the //! provided time value diff --git a/coefs/BiorthBasis.cc b/coefs/BiorthBasis.cc index 5e32d51bc..952df5e60 100644 --- a/coefs/BiorthBasis.cc +++ b/coefs/BiorthBasis.cc @@ -90,13 +90,13 @@ namespace BasisClasses } SphericalSL::SphericalSL(const YAML::Node& CONF) : - BiorthBasis(CONF, "SphericalSL") + BiorthBasis(CONF, "sphereSL") { initialize(); } SphericalSL::SphericalSL(const std::string& confstr) : - BiorthBasis(confstr, "SphericalSL") + BiorthBasis(confstr, "sphereSL") { initialize(); } @@ -826,13 +826,13 @@ namespace BasisClasses }; Cylindrical::Cylindrical(const YAML::Node& CONF) : - BiorthBasis(CONF, "Cylindrical") + BiorthBasis(CONF, "cylinder") { initialize(); } Cylindrical::Cylindrical(const std::string& confstr) : - BiorthBasis(confstr, "Cylindrical") + BiorthBasis(confstr, "cylinder") { initialize(); } @@ -1428,12 +1428,14 @@ namespace BasisClasses "cachename" }; - FlatDisk::FlatDisk(const YAML::Node& CONF) : BiorthBasis(CONF) + FlatDisk::FlatDisk(const YAML::Node& CONF) : + BiorthBasis(CONF, "flatdisk") { initialize(); } - FlatDisk::FlatDisk(const std::string& confstr) : BiorthBasis(confstr) + FlatDisk::FlatDisk(const std::string& confstr) : + BiorthBasis(confstr, "flatdisk") { initialize(); } @@ -1895,12 +1897,12 @@ namespace BasisClasses "method" }; - Cube::Cube(const YAML::Node& CONF) : BiorthBasis(CONF, "Cube") + Cube::Cube(const YAML::Node& CONF) : BiorthBasis(CONF, "cube") { initialize(); } - Cube::Cube(const std::string& confstr) : BiorthBasis(confstr, "Cube") + Cube::Cube(const std::string& confstr) : BiorthBasis(confstr, "cube") { initialize(); } @@ -2198,6 +2200,8 @@ namespace BasisClasses coef = std::make_shared(); else if (name.compare("flatdisk") == 0) coef = std::make_shared(); + else if (name.compare("cube") == 0) + coef = std::make_shared(); else { std::ostringstream sout; sout << "Basis::createCoefficients: basis <" << name << "> not recognized" @@ -2279,7 +2283,8 @@ namespace BasisClasses } // Accumulate coefficient contributions from arrays - void BiorthBasis::addFromArray(Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin) + void BiorthBasis::addFromArray(Eigen::VectorXd& m, RowMatrixXd& p, + bool RoundRobin, bool PosVelRows) { // Sanity check: is coefficient instance created? This is not // foolproof. It is really up the user to make sure that a call @@ -2294,11 +2299,7 @@ namespace BasisClasses std::vector p1(3), v1(3, 0); - if (p.rows() < 10 and p.cols() > p.rows()) { - std::cout << "Basis::addFromArray: interpreting your " - << p.rows() << "X" << p.cols() << " input array as " - << p.cols() << "X" << p.rows() << "." << std::endl; - + if (PosVelRows) { if (p.rows()<3) { std::ostringstream msg; msg << "Basis::addFromArray: you must pass a position array with at " @@ -2308,7 +2309,7 @@ namespace BasisClasses for (int n=0; n ctr, - bool roundrobin) + bool RoundRobin, bool PosVelRows) { initFromArray(ctr); - addFromArray(m, p, roundrobin); + addFromArray(m, p, RoundRobin, PosVelRows); return makeFromArray(time); } diff --git a/coefs/FieldBasis.H b/coefs/FieldBasis.H index d34e9f64a..d405c3179 100644 --- a/coefs/FieldBasis.H +++ b/coefs/FieldBasis.H @@ -146,7 +146,7 @@ namespace BasisClasses //! Initialize accumulating coefficients from arrays. This is //! called once to initialize the accumulation. void addFromArray - (Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin=true); + (Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin=true, bool posvelrows=false); //! Accumulate new coefficients virtual void accumulate(double mass, diff --git a/coefs/FieldBasis.cc b/coefs/FieldBasis.cc index 5b4ee1436..38d9950a2 100644 --- a/coefs/FieldBasis.cc +++ b/coefs/FieldBasis.cc @@ -540,7 +540,8 @@ namespace BasisClasses } // Accumulate coefficient contributions from arrays - void FieldBasis::addFromArray(Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin) + void FieldBasis::addFromArray(Eigen::VectorXd& m, RowMatrixXd& p, + bool roundrobin, bool posvelrows) { // Sanity check: is coefficient instance created? This is not // foolproof. It is really up the user to make sure that a call @@ -555,12 +556,10 @@ namespace BasisClasses std::vector p1(3), v1(3); - if (p.rows() < 10 and p.cols() > p.rows()) { - std::cout << "Basis::addFromArray: interpreting your " - << p.rows() << "X" << p.cols() << " input array as " - << p.cols() << "X" << p.rows() << "." << std::endl; + if (posvelrows) { if (p.rows()<6) { + std::ostringstream msg; msg << "Basis::addFromArray: you must pass a position array with at " "least three six for x, y, z, u, v, w. Yours has " << p.rows() << "."; diff --git a/pyEXP/BasisWrappers.cc b/pyEXP/BasisWrappers.cc index 402a4e925..b49d894eb 100644 --- a/pyEXP/BasisWrappers.cc +++ b/pyEXP/BasisWrappers.cc @@ -277,8 +277,8 @@ void BasisFactoryClasses(py::module &m) } virtual void - addFromArray(Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin) override { - PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin); + addFromArray(Eigen::VectorXd& m, RowMatrixXd& p, bool roundrobin, bool posvelrows) override { +PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); } }; @@ -676,9 +676,11 @@ void BasisFactoryClasses(py::module &m) ) .def("createFromArray", [](BasisClasses::Basis& A, Eigen::VectorXd& mass, RowMatrixXd& ps, - double time, std::vector center, bool roundrobin) + double time, std::vector center, + bool roundrobin, bool posvelrows) { - return A.createFromArray(mass, ps, time, center, roundrobin); + return A.createFromArray(mass, ps, time, center, + roundrobin, posvelrows); }, R"( Generate the coefficients from a mass and position array or, @@ -694,7 +696,12 @@ void BasisFactoryClasses(py::module &m) roundrobin : bool the particles will be accumulated for each process round-robin style with MPI by default. This may be - disabled with 'roundrobin=false'. + disabled with 'roundrobin=False'. + posvelrows : bool + positions (and optionally velocities) will be packed + in rows instead of columns. This accommodates the numpy + construction [xpos, ypos, zpos] where xpos, ypos, zpos are + arrays. Defaults to True. Returns ------- @@ -709,7 +716,7 @@ void BasisFactoryClasses(py::module &m) )", py::arg("mass"), py::arg("pos"), py::arg("time"), py::arg("center") = std::vector(3, 0.0), - py::arg("roundrobin") = true) + py::arg("roundrobin") = true, py::arg("posvelrows") = true) .def("makeFromArray", [](BasisClasses::Basis& A, double time) { @@ -801,9 +808,11 @@ void BasisFactoryClasses(py::module &m) py::arg("center") = std::vector(3, 0.0)) .def("createFromArray", [](BasisClasses::BiorthBasis& A, Eigen::VectorXd& mass, RowMatrixXd& pos, - double time, std::vector center, bool roundrobin) + double time, std::vector center, + bool roundrobin, bool posvelrows) { - return A.createFromArray(mass, pos, time, center, roundrobin); + return A.createFromArray(mass, pos, time, center, + roundrobin, posvelrows); }, R"( Generate the coefficients from a mass and position array, @@ -819,6 +828,11 @@ void BasisFactoryClasses(py::module &m) the particles will be accumulated for each process round-robin style with MPI by default. This may be disabled with 'roundrobin=false'. + posvelrows : bool + positions (and optionally velocities) will be packed + in rows instead of columns. This accommodates the numpy + construction [xpos, ypos, zpos] where xpos, ypos, zpos are + arrays. Defaults to True. Returns ------- @@ -833,7 +847,7 @@ void BasisFactoryClasses(py::module &m) )", py::arg("mass"), py::arg("pos"), py::arg("time"), py::arg("center") = std::vector(3, 0.0), - py::arg("roundrobin") = true) + py::arg("roundrobin") = true, py::arg("posvelrows") = true) .def("initFromArray", [](BasisClasses::BiorthBasis& A, std::vector center) { From 78b55cfe40bec2d3c545b2cb6e93d8531fa91499 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 12 Mar 2024 13:26:51 -0400 Subject: [PATCH 016/167] Added spherical index helpers to BasisWrapper [no ci] --- pyEXP/BasisWrappers.cc | 46 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/pyEXP/BasisWrappers.cc b/pyEXP/BasisWrappers.cc index b49d894eb..9083bc0b3 100644 --- a/pyEXP/BasisWrappers.cc +++ b/pyEXP/BasisWrappers.cc @@ -1140,8 +1140,52 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); dict({tag: value},...) cache parameters )", - py::arg("cachefile")); + py::arg("cachefile")) + .def_static("I", [](int l, int m) + { + if (l<0) throw std::runtime_error("l must be greater than 0"); + if (m<0) throw std::runtime_error("m must be greater than 0"); + if (abs(m)>l) throw std::runtime_error("m must be less than or equal to l"); + return (l * (l + 1) / 2) + m; + }, + R"( + Calculate the index of a spherical harmonic element given the angular numbers l and m . + + Parameters + ---------- + l : int + spherical harmonic order l + m : int + azimuthal order m + + Returns + ------- + I : int + index array packing index + )", + py::arg("l"), py::arg("m")) + .def_static("invI", [](int I) + { + if (I<0) std::runtime_error("I must be an interger greater than or equal to 0"); + int l = std::floor(0.5*(-1.0 + std::sqrt(1.0 + 8.0 * I))); + int m = I - int(l * (l + 1) / 2); + return std::tuple(l, m); + }, + R"( + Calculate the spherical harmonic indices l and m from the coefficient array packing index I + + Parameters + ---------- + I : int + the spherical coefficient array index + Returns + ------- + (l, m) : tuple + the harmonic indices (l, m). + )", py::arg("I")); + + py::class_, PyCylindrical, BasisClasses::BiorthBasis>(m, "Cylindrical") .def(py::init(), R"( From 07cff24dd511bc66f101f2cc469c0d2fff0b1b53 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 12 Mar 2024 13:28:52 -0400 Subject: [PATCH 017/167] Remove internal error termination using MPI_Finalize() + exit() in favor of throwing std::runtime_error [no ci] --- coefs/BiorthBasis.cc | 6 ++---- coefs/FieldBasis.cc | 3 +-- exputil/BiorthCube.cc | 3 +-- exputil/BiorthCyl.cc | 3 +-- exputil/ParticleReader.cc | 3 +-- src/AxisymmetricBasis.cc | 3 +-- src/CBrockDisk.cc | 12 ++++-------- src/CenterFile.cc | 15 +++++---------- src/Component.cc | 37 ++++++++++++------------------------- src/ComponentContainer.cc | 6 ++---- src/Cube.cc | 3 +-- src/Cylinder.cc | 21 +++++++-------------- src/Direct.cc | 3 +-- src/ExternalCollection.cc | 3 +-- src/FlatDisk.cc | 3 +-- src/HaloBulge.cc | 3 +-- src/OrbTrace.cc | 3 +-- src/OutAscii.cc | 9 +++------ src/OutCHKPT.cc | 9 +++------ src/OutCHKPTQ.cc | 9 +++------ src/OutCalbr.cc | 9 ++------- src/OutCoef.cc | 3 +-- src/OutDiag.cc | 3 +-- src/OutFrac.cc | 9 +++------ src/OutLog.cc | 3 +-- src/OutMulti.cc | 3 +-- src/OutPS.cc | 3 +-- src/OutPSN.cc | 3 +-- src/OutPSP.cc | 3 +-- src/OutPSQ.cc | 3 +-- src/OutPSR.cc | 3 +-- src/OutRelaxation.cc | 3 +-- src/OutVel.cc | 3 +-- src/PeriodicBC.cc | 3 +-- src/PolarBasis.cc | 12 ++++-------- src/ScatterMFP.cc | 3 +-- src/ShearSL.cc | 2 +- src/Shells.cc | 3 +-- src/Slab.cc | 3 +-- src/SlabSL.cc | 3 +-- src/Sphere.cc | 3 +-- src/SphericalBasis.cc | 12 ++++-------- src/TwoCenter.cc | 3 +-- src/TwoDCoefs.cc | 6 ++---- src/externalShock.cc | 3 +-- src/tidalField.cc | 3 +-- 46 files changed, 88 insertions(+), 179 deletions(-) diff --git a/coefs/BiorthBasis.cc b/coefs/BiorthBasis.cc index 952df5e60..316f269bc 100644 --- a/coefs/BiorthBasis.cc +++ b/coefs/BiorthBasis.cc @@ -1147,8 +1147,7 @@ namespace BasisClasses << mtype << ">, valid types are: " << "Exponential, Gaussian, Plummer, Power " << "(not case sensitive)" << std::endl; - if (use_mpi) MPI_Finalize(); - return; + throw std::runtime_error("Cylindrical:initialize: EmpCylSL bad parameter"); } // Convert dtype string to lower case @@ -1172,8 +1171,7 @@ namespace BasisClasses for (auto v : dtlookup) std::cout << v.first << " "; std::cout << std::endl; } - if (use_mpi) MPI_Finalize(); - return; + throw std::runtime_error("Cylindrical::initialize: invalid DiskType"); } // Use these user models to deproject for the EOF spherical basis diff --git a/coefs/FieldBasis.cc b/coefs/FieldBasis.cc index 38d9950a2..eecdcc520 100644 --- a/coefs/FieldBasis.cc +++ b/coefs/FieldBasis.cc @@ -207,8 +207,7 @@ namespace BasisClasses << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - if (use_mpi) MPI_Finalize(); - exit(-1); + throw std::runtime_error("FieldBasis::initialize: error parsing YAML"); } } diff --git a/exputil/BiorthCube.cc b/exputil/BiorthCube.cc index a65ed5ca2..5ac186af4 100644 --- a/exputil/BiorthCube.cc +++ b/exputil/BiorthCube.cc @@ -63,8 +63,7 @@ BiorthCube::BiorthCube(const YAML::Node& conf) : conf(conf) << std::string(60, '-') << std::endl << conf << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("BiorthCube: YAML parsing error"); } geometry = "cube"; diff --git a/exputil/BiorthCyl.cc b/exputil/BiorthCyl.cc index 2eecc84a9..b604ba68f 100644 --- a/exputil/BiorthCyl.cc +++ b/exputil/BiorthCyl.cc @@ -118,8 +118,7 @@ BiorthCyl::BiorthCyl(const YAML::Node& conf) : conf(conf) << std::string(60, '-') << std::endl << conf << std::string(60, '-') << std::endl; - if (use_mpi) MPI_Finalize(); - exit(-1); + throw std::runtime_error("BiorthCyl: YAML parsing error"); } geometry = "cylinder"; diff --git a/exputil/ParticleReader.cc b/exputil/ParticleReader.cc index f3bcf5546..99f09a8c4 100644 --- a/exputil/ParticleReader.cc +++ b/exputil/ParticleReader.cc @@ -62,8 +62,7 @@ namespace PR { if (!file.is_open()) { std::cerr << "Error opening file: " << f << std::endl; - if (use_mpi) MPI_Finalize(); - exit(1); + throw std::runtime_error("GadgetNative::getNumbers: open file error"); } // read in file data diff --git a/src/AxisymmetricBasis.cc b/src/AxisymmetricBasis.cc index be1d15438..98ffeae04 100644 --- a/src/AxisymmetricBasis.cc +++ b/src/AxisymmetricBasis.cc @@ -92,8 +92,7 @@ AxisymmetricBasis:: AxisymmetricBasis(Component* c0, const YAML::Node& conf) : << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("AxisymmetricBasis: error parsing YAML"); } diff --git a/src/CBrockDisk.cc b/src/CBrockDisk.cc index f2c7d29ef..0c2f66ecc 100644 --- a/src/CBrockDisk.cc +++ b/src/CBrockDisk.cc @@ -135,8 +135,7 @@ void CBrockDisk::initialize(void) if (not test) { std::cerr << "CBrockDisk: process " << myid << " cannot open <" << file << "> for reading" << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("CBrockDisk: file open error"); } } @@ -148,8 +147,7 @@ void CBrockDisk::initialize(void) << "] does not match specification [" << nmax << "]" << std::endl; } - MPI_Finalize(); - exit(-1); + throw std::runtime_error("CBrockDisk: parameter mismatch"); } if (playback->Lmax != Lmax) { @@ -158,8 +156,7 @@ void CBrockDisk::initialize(void) << "] does not match specification [" << Lmax << "]" << std::endl; } - MPI_Finalize(); - exit(-1); + throw std::runtime_error("CBrockDisk: parameter mismatch"); } play_back = true; @@ -187,8 +184,7 @@ void CBrockDisk::initialize(void) << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("CBrockDisk: error parsing YAML"); } } diff --git a/src/CenterFile.cc b/src/CenterFile.cc index b7ad954e3..b858949b8 100644 --- a/src/CenterFile.cc +++ b/src/CenterFile.cc @@ -23,8 +23,7 @@ CenterFile::CenterFile(const YAML::Node& conf) << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("CenterFile: missing parameter"); } try { @@ -39,8 +38,7 @@ CenterFile::CenterFile(const YAML::Node& conf) << std::string(60, '-') << std::endl << conf << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-2); + throw std::runtime_error("CenterFile: bad center "); } // Convert type to upper case @@ -58,8 +56,7 @@ CenterFile::CenterFile(const YAML::Node& conf) << "fouund <" << type << "> but expected either " << "'COM' or 'EJ'" << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-3); + throw std::runtime_error("CenterFile: error parsing type"); } // Open input file @@ -98,8 +95,7 @@ CenterFile::CenterFile(const YAML::Node& conf) std::cout << "CenterFile error opening center file <" << name << ">" << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-4); + throw std::runtime_error("CenterFile: error opening file"); } // Some chatty output @@ -123,8 +119,7 @@ std::array CenterFile::operator()(double T) std::cout << "CenterFile range error: T=" << T << " is off grid"<< std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-5); + throw std::runtime_error("CenterFile::operator(): range error"); } // Check limiting cases diff --git a/src/Component.cc b/src/Component.cc index 344ee396d..47fe2901c 100644 --- a/src/Component.cc +++ b/src/Component.cc @@ -123,9 +123,7 @@ Component::Component(YAML::Node& CONF) << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Component: error parsing "); } try { @@ -141,8 +139,7 @@ Component::Component(YAML::Node& CONF) << conf << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-2); + throw std::runtime_error("Component: error parsing "); } pfile = conf["bodyfile"].as(); @@ -161,8 +158,7 @@ Component::Component(YAML::Node& CONF) << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-3); + throw std::runtime_error("Component: error parsing "); } // Check for unmatched keys @@ -186,8 +182,7 @@ Component::Component(YAML::Node& CONF) << force << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-4); + throw std::runtime_error("Component: error parsing force "); } EJ = 0; @@ -646,8 +641,7 @@ Component::Component(YAML::Node& CONF, istream *in, bool SPL) : conf(CONF) << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-5); + throw std::runtime_error("Component: error parsing component "); } try { @@ -663,8 +657,7 @@ Component::Component(YAML::Node& CONF, istream *in, bool SPL) : conf(CONF) << conf << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-6); + throw std::runtime_error("Component: error parsing "); } pfile = conf["bodyfile"].as(); @@ -683,8 +676,7 @@ Component::Component(YAML::Node& CONF, istream *in, bool SPL) : conf(CONF) << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-7); + throw std::runtime_error("Component: error parsing "); } id = cforce["id"].as(); @@ -702,8 +694,7 @@ Component::Component(YAML::Node& CONF, istream *in, bool SPL) : conf(CONF) << cforce << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-8); + throw std::runtime_error("Component: error parsing force "); } // Defaults @@ -887,8 +878,7 @@ void Component::configure(void) << cconf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-9); + throw std::runtime_error("Component: error parsing YAML"); } @@ -1545,8 +1535,7 @@ void Component::read_bodies_and_distribute_binary_out(istream *in) std::cerr << "YAML: error parsing <" << info.get() << "> " << "in " << __FILE__ << ":" << __LINE__ << std::endl << "YAML error: " << error.what() << std::endl; - MPI_Finalize(); - exit(-10); + throw std::runtime_error("Component::read_bodies_and_distribute: error parsing YAML on load"); } try { @@ -1562,8 +1551,7 @@ void Component::read_bodies_and_distribute_binary_out(istream *in) << std::string(60, '-') << std::endl << config << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-11); + throw std::runtime_error("Component::read_bodies_and_distribute: error parsing YAML in PSP"); } YAML::Node force; @@ -1582,8 +1570,7 @@ void Component::read_bodies_and_distribute_binary_out(istream *in) << config << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-12); + throw std::runtime_error("Component::read_bodies_and_distribute: error parsing force stanza"); } // Assign local conf diff --git a/src/ComponentContainer.cc b/src/ComponentContainer.cc index 1bf45de38..72030cddf 100644 --- a/src/ComponentContainer.cc +++ b/src/ComponentContainer.cc @@ -161,8 +161,7 @@ void ComponentContainer::initialize(void) if (myid==0) std::cerr << "I did not find any components; quitting . . ." << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("ComponentContainer::initialize: no components?"); } // Test of reassignment @@ -219,8 +218,7 @@ void ComponentContainer::initialize(void) } if (not interOkay) { - MPI_Finalize(); - exit(-11); + throw std::runtime_error("ComponentContainer::initialize: interaction list error"); } } diff --git a/src/Cube.cc b/src/Cube.cc index c5c935200..fc1ac1b8a 100644 --- a/src/Cube.cc +++ b/src/Cube.cc @@ -123,8 +123,7 @@ void Cube::initialize(void) << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Cube::initialize: error parsing YAML"); } #if HAVE_LIBCUDA==1 diff --git a/src/Cylinder.cc b/src/Cylinder.cc index d68bb447f..8c029ff44 100644 --- a/src/Cylinder.cc +++ b/src/Cylinder.cc @@ -226,8 +226,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << std::string(60, '-') << std::endl << conf << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Cylinder: error in parsing tk_type"); } @@ -265,8 +264,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : MPI_Bcast(&nOK, 1, MPI_UNSIGNED_CHAR, 0, MPI_COMM_WORLD); if (nOK) { - MPI_Finalize(); - exit(12); + throw std::runtime_error("Cylinder: error initializing from parameters"); } // Attempt to read EOF file from cache on restart @@ -291,8 +289,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : if (myid==0) std::cerr << "Cylinder: can not read cache file on restart ... aborting" << std::endl; - MPI_Finalize(); - exit(13); + throw std::runtime_error("Cylinder: error reading cache"); } // Genererate eof if needed @@ -482,8 +479,7 @@ void Cylinder::initialize() if (not test) { std::cerr << "Cylinder: process " << myid << " cannot open <" << file << "> for reading" << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Cylinder: error reading playback file"); } } @@ -510,8 +506,7 @@ void Cylinder::initialize() << "] does not match specification [" << mmax << "]" << std::endl; } - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Cylinder: parameter mismatch"); } P.resize(mmax+1, nmax); @@ -545,8 +540,7 @@ void Cylinder::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Cylinder::initialize: error parsing YAML"); } } @@ -876,8 +870,7 @@ void Cylinder::determine_coefficients_particles(void) if (restart && !cache_ok) { if (myid==0) std::cerr << "Cylinder: can not read cache file on restart" << endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Cylinder: cache read error"); } } diff --git a/src/Direct.cc b/src/Direct.cc index aea55e6ac..89498adba 100644 --- a/src/Direct.cc +++ b/src/Direct.cc @@ -109,8 +109,7 @@ void Direct::initialize(void) << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Direct::initialize: error parsing YAML"); } } diff --git a/src/ExternalCollection.cc b/src/ExternalCollection.cc index d1cc97f60..abc0be485 100644 --- a/src/ExternalCollection.cc +++ b/src/ExternalCollection.cc @@ -55,8 +55,7 @@ void ExternalCollection::initialize() << std::string(60, '-') << std::endl << parse << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("ExternalCollection: error in parsing YAML"); } if (ext.IsSequence()) { diff --git a/src/FlatDisk.cc b/src/FlatDisk.cc index ba5ab0ec2..faf626fa8 100644 --- a/src/FlatDisk.cc +++ b/src/FlatDisk.cc @@ -90,8 +90,7 @@ void FlatDisk::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("FlatDisk::initialize: error in parsing YAML"); } // Create the BiorthCyl instance diff --git a/src/HaloBulge.cc b/src/HaloBulge.cc index 751160f30..04cda5e5e 100644 --- a/src/HaloBulge.cc +++ b/src/HaloBulge.cc @@ -150,7 +150,6 @@ void HaloBulge::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("HaloBulge::initialize: error in parsing YAML"); } } diff --git a/src/OrbTrace.cc b/src/OrbTrace.cc index 2a978a393..df577312b 100644 --- a/src/OrbTrace.cc +++ b/src/OrbTrace.cc @@ -248,8 +248,7 @@ void OrbTrace::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OrbTrace::initialize: error in parsing YAML"); } } diff --git a/src/OutAscii.cc b/src/OutAscii.cc index 272b77c15..627c0b8f7 100644 --- a/src/OutAscii.cc +++ b/src/OutAscii.cc @@ -45,8 +45,7 @@ OutAscii::OutAscii(const YAML::Node& conf) : Output(conf) if (myid==0) std::cerr << "Process " << myid << ": can't find desired component <" << name << ">" << std::endl; - MPI_Finalize(); - exit(35); + throw std::runtime_error("OutAscii: missing component"); } } else @@ -88,8 +87,7 @@ void OutAscii::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutAscii::initialize: error in parsing YAML"); } // Determine last file @@ -155,8 +153,7 @@ void OutAscii::Run(int n, int mstep, bool last) // MPI_Bcast(&nOK, 1, MPI_INT, 0, MPI_COMM_WORLD); if (nOK) { - MPI_Finalize(); - exit(33); + throw std::runtime_error("OutAscii::Run: I/O error"); } // Update total particle number diff --git a/src/OutCHKPT.cc b/src/OutCHKPT.cc index fb24d9dc8..be5d554c0 100644 --- a/src/OutCHKPT.cc +++ b/src/OutCHKPT.cc @@ -85,8 +85,7 @@ void OutCHKPT::initialize() << std::string(60, '-') << std::endl << Output::conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutCHKPT::initialize: error in parsing YAML"); } } @@ -186,8 +185,7 @@ void OutCHKPT::Run(int n, int mstep, bool last) MPI_Allreduce(&nOK, &badCount, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD); if (badCount) { - MPI_Finalize(); - exit(33); + throw std::runtime_error("OutCHKPT::Run: error in I/O"); } MPI_Info_free(&info); @@ -287,8 +285,7 @@ void OutCHKPT::Run(int n, int mstep, bool last) MPI_Bcast(&nOK, 1, MPI_INT, 0, MPI_COMM_WORLD); if (nOK) { - MPI_Finalize(); - exit(33); + throw std::runtime_error("OutCHKPT::Run: error in I/O"); } for (auto c : comp->components) { diff --git a/src/OutCHKPTQ.cc b/src/OutCHKPTQ.cc index 4ae666bc3..95cf78fdb 100644 --- a/src/OutCHKPTQ.cc +++ b/src/OutCHKPTQ.cc @@ -79,8 +79,7 @@ void OutCHKPTQ::initialize() << std::string(60, '-') << std::endl << Output::conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutCHKPTQ::initialize: error in parsing YAML"); } } @@ -236,8 +235,7 @@ void OutCHKPTQ::Run(int n, int mstep, bool last) MPI_Bcast(&nOK, 1, MPI_INT, 0, MPI_COMM_WORLD); if (nOK) { - MPI_Finalize(); - exit(33); + throw std::runtime_error("OutCHKPTQ::Run: error in I/O"); } int count = 0; @@ -285,8 +283,7 @@ void OutCHKPTQ::Run(int n, int mstep, bool last) MPI_Allreduce(&nOK, &sumOK, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD); if (sumOK) { - MPI_Finalize(); - exit(35); + throw std::runtime_error("OutCHKPTQ::Run: error in I/O"); } } diff --git a/src/OutCalbr.cc b/src/OutCalbr.cc index 0ba139dfd..e1cc8f58e 100644 --- a/src/OutCalbr.cc +++ b/src/OutCalbr.cc @@ -27,11 +27,7 @@ OutCalbr::OutCalbr(const YAML::Node& conf) : Output(conf) initialize(); if (!tcomp) { - if (myid==0) { - std::cerr << "OutCalbr: no component to trace\n"; - } - MPI_Finalize(); - exit(112); + throw std::runtime_error("OutCalbr: no component to trace"); } } @@ -176,8 +172,7 @@ void OutCalbr::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutCalbr::initialize: error parsing YAML"); } } diff --git a/src/OutCoef.cc b/src/OutCoef.cc index 4522fabab..2b1ac72ba 100644 --- a/src/OutCoef.cc +++ b/src/OutCoef.cc @@ -80,8 +80,7 @@ void OutCoef::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutCoef::initialize: error parsing YAML"); } } diff --git a/src/OutDiag.cc b/src/OutDiag.cc index 9a941ca22..b716b717f 100644 --- a/src/OutDiag.cc +++ b/src/OutDiag.cc @@ -98,8 +98,7 @@ void OutDiag::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutDiag::initialize: error parsing YAML"); } } diff --git a/src/OutFrac.cc b/src/OutFrac.cc index 83181d57e..28db2d8f5 100644 --- a/src/OutFrac.cc +++ b/src/OutFrac.cc @@ -36,16 +36,14 @@ OutFrac::OutFrac(const YAML::Node& conf) : Output(conf) if (myid==0) { std::cerr << "OutFrac: no component to trace\n"; } - MPI_Finalize(); - exit(112); + throw std::runtime_error("OutFrac: missing component"); } if (numQuant==0) { if (myid==0) { std::cerr << "OutFrac: no quantiles defined!\n"; } - MPI_Finalize(); - exit(113); + throw std::runtime_error("OutFrac: missing quantiles"); } else if (myid==0) std::cout << "OutFrac: using " << numQuant << " quantiles\n"; @@ -183,8 +181,7 @@ void OutFrac::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutFrac::initialize: error parsing YAML"); } } diff --git a/src/OutLog.cc b/src/OutLog.cc index d941d0402..0b31541dd 100644 --- a/src/OutLog.cc +++ b/src/OutLog.cc @@ -117,8 +117,7 @@ void OutLog::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutLog::initialize: error parsing YAML"); } } diff --git a/src/OutMulti.cc b/src/OutMulti.cc index f01e543eb..627994a62 100644 --- a/src/OutMulti.cc +++ b/src/OutMulti.cc @@ -57,8 +57,7 @@ void OutMulti::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutMulti::initialize: error parsing YAML"); } } diff --git a/src/OutPS.cc b/src/OutPS.cc index 7ccfd706e..4083dfe0e 100644 --- a/src/OutPS.cc +++ b/src/OutPS.cc @@ -68,8 +68,7 @@ void OutPS::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutPS::initialize: error parsing YAML"); } } diff --git a/src/OutPSN.cc b/src/OutPSN.cc index 2ef1bbd2b..893d28f01 100644 --- a/src/OutPSN.cc +++ b/src/OutPSN.cc @@ -81,8 +81,7 @@ void OutPSN::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutPSN::initialize: error parsing YAML"); } diff --git a/src/OutPSP.cc b/src/OutPSP.cc index 176b638d6..bc03d340c 100644 --- a/src/OutPSP.cc +++ b/src/OutPSP.cc @@ -88,8 +88,7 @@ void OutPSP::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutPSP::initialize: error parsing YAML"); } // Determine last file diff --git a/src/OutPSQ.cc b/src/OutPSQ.cc index 2582a1f4f..dffa0466d 100644 --- a/src/OutPSQ.cc +++ b/src/OutPSQ.cc @@ -87,8 +87,7 @@ void OutPSQ::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutPSQ::initialize: error parsing YAML"); } diff --git a/src/OutPSR.cc b/src/OutPSR.cc index 7abec4328..19623bf2e 100644 --- a/src/OutPSR.cc +++ b/src/OutPSR.cc @@ -88,8 +88,7 @@ void OutPSR::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutPSR::initialize: error parsing YAML"); } diff --git a/src/OutRelaxation.cc b/src/OutRelaxation.cc index 3e4075809..d9dda3775 100644 --- a/src/OutRelaxation.cc +++ b/src/OutRelaxation.cc @@ -66,8 +66,7 @@ void OutRelaxation::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutRelaxation::initialize: error parsing YAML"); } } diff --git a/src/OutVel.cc b/src/OutVel.cc index 79350fec1..78225f9d2 100644 --- a/src/OutVel.cc +++ b/src/OutVel.cc @@ -127,8 +127,7 @@ void OutVel::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("OutVel::initialize: error parsing YAML"); } } diff --git a/src/PeriodicBC.cc b/src/PeriodicBC.cc index c0ec5007e..58d235df4 100644 --- a/src/PeriodicBC.cc +++ b/src/PeriodicBC.cc @@ -158,8 +158,7 @@ void PeriodicBC::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("PeriodicBC::initialize: error parsing YAML"); } } diff --git a/src/PolarBasis.cc b/src/PolarBasis.cc index 4e7926c4f..67f2e454f 100644 --- a/src/PolarBasis.cc +++ b/src/PolarBasis.cc @@ -136,8 +136,7 @@ PolarBasis::PolarBasis(Component* c0, const YAML::Node& conf, MixtureBasis *m) : if (not test) { std::cerr << "PolarBasis: process " << myid << " cannot open <" << file << "> for reading" << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("PolarBasis: file open error"); } } @@ -159,8 +158,7 @@ PolarBasis::PolarBasis(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << "] does not match specification [" << nmax << "]" << std::endl; } - MPI_Finalize(); - exit(-1); + throw std::runtime_error("PolarBasis: parameter mismatch"); } if (playback->mmax() != Mmax) { @@ -169,8 +167,7 @@ PolarBasis::PolarBasis(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << "] does not match specification [" << Mmax << "]" << std::endl; } - MPI_Finalize(); - exit(-1); + throw std::runtime_error("PolarBasis: parameter mismatch"); } play_back = true; @@ -197,8 +194,7 @@ PolarBasis::PolarBasis(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("PolarBasis: error parsing YAML"); } if (nthrds<1) nthrds=1; diff --git a/src/ScatterMFP.cc b/src/ScatterMFP.cc index 59af31a9c..7dd31e1a9 100644 --- a/src/ScatterMFP.cc +++ b/src/ScatterMFP.cc @@ -103,8 +103,7 @@ void ScatterMFP::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("ScatterMFP::initialze: error parsing YAML"); } } diff --git a/src/ShearSL.cc b/src/ShearSL.cc index aed0da7b8..59a1be64a 100644 --- a/src/ShearSL.cc +++ b/src/ShearSL.cc @@ -113,7 +113,7 @@ void ShearSL::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); + throw std::runtime_error("ShearSL::initialze: error parsing YAML"); exit(-1); } } diff --git a/src/Shells.cc b/src/Shells.cc index 81db00ece..6a3e7d35d 100644 --- a/src/Shells.cc +++ b/src/Shells.cc @@ -72,8 +72,7 @@ void Shells::initialize(void) << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Shells::initialze: error parsing YAML"); } } diff --git a/src/Slab.cc b/src/Slab.cc index 2c643d653..c51f82539 100644 --- a/src/Slab.cc +++ b/src/Slab.cc @@ -88,8 +88,7 @@ void Slab::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Slab::initialize: error parsing YAML"); } } diff --git a/src/SlabSL.cc b/src/SlabSL.cc index 27f0693dc..e6e8c24f9 100644 --- a/src/SlabSL.cc +++ b/src/SlabSL.cc @@ -88,8 +88,7 @@ void SlabSL::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("SlabSL::initialze: error parsing YAML"); } } diff --git a/src/Sphere.cc b/src/Sphere.cc index 95d4783ec..df11809e9 100644 --- a/src/Sphere.cc +++ b/src/Sphere.cc @@ -126,8 +126,7 @@ void Sphere::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("Sphere::initialze: error parsing YAML"); } } diff --git a/src/SphericalBasis.cc b/src/SphericalBasis.cc index d5c66b4e4..6a0480147 100644 --- a/src/SphericalBasis.cc +++ b/src/SphericalBasis.cc @@ -145,8 +145,7 @@ SphericalBasis::SphericalBasis(Component* c0, const YAML::Node& conf, MixtureBas if (not test) { std::cerr << "SphericalBasis: process " << myid << " cannot open <" << file << "> for reading" << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("SphericalBasis: file error"); } } @@ -168,8 +167,7 @@ SphericalBasis::SphericalBasis(Component* c0, const YAML::Node& conf, MixtureBas << "] does not match specification [" << nmax << "]" << std::endl; } - MPI_Finalize(); - exit(-1); + throw std::runtime_error("SphericalBasis: parameter mismatch"); } if (playback->lmax() != Lmax) { @@ -178,8 +176,7 @@ SphericalBasis::SphericalBasis(Component* c0, const YAML::Node& conf, MixtureBas << "] does not match specification [" << Lmax << "]" << std::endl; } - MPI_Finalize(); - exit(-1); + throw std::runtime_error("SphericalBasis: parameter mismatch"); } play_back = true; @@ -210,8 +207,7 @@ SphericalBasis::SphericalBasis(Component* c0, const YAML::Node& conf, MixtureBas << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("SphericalBasis: error parsing YAML"); } if (nthrds<1) nthrds=1; diff --git a/src/TwoCenter.cc b/src/TwoCenter.cc index f8990b3ae..93b0aedc0 100644 --- a/src/TwoCenter.cc +++ b/src/TwoCenter.cc @@ -101,8 +101,7 @@ void TwoCenter::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("TwoCenter::initialze: error parsing YAML"); } } diff --git a/src/TwoDCoefs.cc b/src/TwoDCoefs.cc index 3a7515ae6..ada3820d8 100644 --- a/src/TwoDCoefs.cc +++ b/src/TwoDCoefs.cc @@ -115,16 +115,14 @@ TwoDCoefs::TwoDCoefs(const std::string& file, unsigned stride) std::cout << "TwoDCoefs: [" << myid << "] " << "coefficient stanza rank mismatch: lmax=" << v->Lmax << ", expected " << Lmax << std::endl; - MPI_Finalize(); - exit(-31); + throw std::runtime_error("TwoDCoefs: rank error"); } if (nmax != v->nmax) { std::cout << "TwoDCoefs: [" << myid << "] " << "coefficient stanza rank mismatch: nmax=" << v->nmax << ", expected " << nmax << std::endl; - MPI_Finalize(); - exit(-32); + throw std::runtime_error("TwoDCoefs: rank error"); } } diff --git a/src/externalShock.cc b/src/externalShock.cc index 22cadffc8..d525d12cb 100644 --- a/src/externalShock.cc +++ b/src/externalShock.cc @@ -44,8 +44,7 @@ void externalShock::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("externalShock::initialize: error in parsing YAML"); } } diff --git a/src/tidalField.cc b/src/tidalField.cc index ef89018d1..63f3987b3 100644 --- a/src/tidalField.cc +++ b/src/tidalField.cc @@ -37,8 +37,7 @@ void tidalField::initialize() << std::string(60, '-') << std::endl << conf << std::endl << std::string(60, '-') << std::endl; - MPI_Finalize(); - exit(-1); + throw std::runtime_error("tidalField::initialize: error parsing YAML"); } } From c10bab24b6e6f5c744f41c33dfa62c0e2bef764a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 13 Mar 2024 15:40:51 -0400 Subject: [PATCH 018/167] Zero Cylindrical cos & sin storage to prevent uninitialized values --- coefs/BiorthBasis.cc | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/coefs/BiorthBasis.cc b/coefs/BiorthBasis.cc index 952df5e60..1383b1c2b 100644 --- a/coefs/BiorthBasis.cc +++ b/coefs/BiorthBasis.cc @@ -1147,8 +1147,7 @@ namespace BasisClasses << mtype << ">, valid types are: " << "Exponential, Gaussian, Plummer, Power " << "(not case sensitive)" << std::endl; - if (use_mpi) MPI_Finalize(); - return; + throw std::runtime_error("Cylindrical:initialize: EmpCylSL bad parameter"); } // Convert dtype string to lower case @@ -1172,8 +1171,7 @@ namespace BasisClasses for (auto v : dtlookup) std::cout << v.first << " "; std::cout << std::endl; } - if (use_mpi) MPI_Finalize(); - return; + throw std::runtime_error("Cylindrical::initialize: invalid DiskType"); } // Use these user models to deproject for the EOF spherical basis @@ -1303,7 +1301,14 @@ namespace BasisClasses Eigen::VectorXd cos1(nmax), sin1(nmax); - cf->store((mmax+1)*nmax); + // Initialize the values + cos1.setZero(); + sin1.setZero(); + + // Allocate the coefficient storage + cf->store.resize((mmax+1)*nmax); + + // Create a new instance cf->coefs = std::make_shared (cf->store.data(), mmax+1, nmax); From 6abd285f436aa6a8345ae967f84f9e1c5d014801 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 13 Mar 2024 15:52:20 -0400 Subject: [PATCH 019/167] Fix uninitialized imaginary parts in CylCoef [no ci] --- coefs/Coefficients.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coefs/Coefficients.cc b/coefs/Coefficients.cc index cff29c1ed..b79bfb3a3 100644 --- a/coefs/Coefficients.cc +++ b/coefs/Coefficients.cc @@ -787,6 +787,10 @@ namespace CoefClasses Eigen::MatrixXcd in(Mmax+1, Nmax); stanza.getDataSet("coefficients").read(in); + // Work around for previous unitiaized data bug; enforces real data + // + for (int n=0; n(); From f660fc216bde3be8ddaebfdb40c95dbbf1fe0c24 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 14 Mar 2024 19:14:21 -0400 Subject: [PATCH 020/167] Throw an exception if a Null pointer is passed to Coefs::add() [no ci] --- coefs/Coefficients.cc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/coefs/Coefficients.cc b/coefs/Coefficients.cc index b79bfb3a3..ed93a3cbb 100644 --- a/coefs/Coefficients.cc +++ b/coefs/Coefficients.cc @@ -743,6 +743,8 @@ namespace CoefClasses void SphCoefs::add(CoefStrPtr coef) { auto p = std::dynamic_pointer_cast(coef); + if (not p) throw std::runtime_error("SphCoefs::add: Null coefficient structure, nothing added!"); + Lmax = p->lmax; Nmax = p->nmax; coefs[roundTime(coef->time)] = p; @@ -1953,6 +1955,8 @@ namespace CoefClasses void CylCoefs::add(CoefStrPtr coef) { auto p = std::dynamic_pointer_cast(coef); + if (not p) throw std::runtime_error("CylCoefs::add: Null coefficient structure, nothing added!"); + Mmax = p->mmax; Nmax = p->nmax; coefs[roundTime(coef->time)] = p; @@ -1961,6 +1965,8 @@ namespace CoefClasses void CubeCoefs::add(CoefStrPtr coef) { auto p = std::dynamic_pointer_cast(coef); + if (not p) throw std::runtime_error("CubeCoefs::add: Null coefficient structure, nothing added!"); + NmaxX = p->nmaxx; NmaxY = p->nmaxy; NmaxZ = p->nmaxz; @@ -1969,7 +1975,10 @@ namespace CoefClasses void TableData::add(CoefStrPtr coef) { - coefs[roundTime(coef->time)] = std::dynamic_pointer_cast(coef); + auto p = std::dynamic_pointer_cast(coef); + if (not p) throw std::runtime_error("TableData::add: Null coefficient structure, nothing added!"); + + coefs[roundTime(coef->time)] = p; } @@ -1985,6 +1994,8 @@ namespace CoefClasses void CylFldCoefs::add(CoefStrPtr coef) { auto p = std::dynamic_pointer_cast(coef); + if (not p) throw std::runtime_error("CylFldCoefs::add: Null coefficient structure, nothing added!"); + Nfld = p->nfld; Mmax = p->mmax; Nmax = p->nmax; From 4ad81c60bc5f7aee954d92f1ef81feaa62281554 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 15 Mar 2024 17:51:22 -0400 Subject: [PATCH 021/167] More improvement of exception handling behavior --- src/expand.cc | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/expand.cc b/src/expand.cc index f045a21a3..3f1abd4fd 100644 --- a/src/expand.cc +++ b/src/expand.cc @@ -504,24 +504,18 @@ main(int argc, char** argv) exit(0); } catch (std::runtime_error& e) { - std::cerr << "Process " << myid << ": std exception" << std::endl - << e.what() << std::endl; + std::cerr << "Process " << myid << ": std exception" << std::endl; + if (myid==0) std::cerr << e.what() << std::endl; if (VERBOSE>4) print_trace(std::cerr, 0, 0); sleep(5); std::cerr << std::flush; - - // Try to force all process to exit! - MPI_Abort(MPI_COMM_WORLD, 0); } catch (std::string& msg) { - std::cerr << "Process " << myid << ": str exception" << std::endl - << msg << std::endl; + std::cerr << "Process " << myid << ": str exception" << std::endl; + if (myid==0) std::cerr << msg << std::endl; sleep(5); if (VERBOSE>4) print_trace(std::cerr, 0, 0); std::cerr << std::flush; - - // Try to force all process to exit! - MPI_Abort(MPI_COMM_WORLD, 0); } //============= From 5efc210d999afd02c7f6377f98169b3466020963 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 15 Mar 2024 17:52:18 -0400 Subject: [PATCH 022/167] Replace 'override' with more mnemonic 'create'. The 'override' flag will still work but is now deprecated --- src/Cylinder.H | 4 ++-- src/Cylinder.cc | 28 +++++++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Cylinder.H b/src/Cylinder.H index bda9c8430..17e1ab46a 100644 --- a/src/Cylinder.H +++ b/src/Cylinder.H @@ -62,7 +62,7 @@ class MixtureBasis; @param cachename is the name of the basis cache file - @param override boolean ignores the specificed cache file name and performes a recomputation + @param create boolean ignores the specificed cache file name and performes a recomputation @param samplesz is the default particle number in PCA subsampling partitions (default is 1). The value 0 sets the sample size to sqrt(N). @@ -134,7 +134,7 @@ private: double hcyl, hexp, snr, rem; int nmax, ncylodd, ncylrecomp, npca, npca0, nvtk, cmapR, cmapZ; std::string cachename; - bool self_consistent, logarithmic, pcavar, pcainit, pcavtk, pcadiag, pcaeof, eof_over; + bool self_consistent, logarithmic, pcavar, pcainit, pcavtk, pcadiag, pcaeof, create; bool try_cache, firstime, dump_basis, compute, firstime_coef; // These should be ok for all derived classes, hence declared private diff --git a/src/Cylinder.cc b/src/Cylinder.cc index 8c029ff44..c9502117a 100644 --- a/src/Cylinder.cc +++ b/src/Cylinder.cc @@ -82,6 +82,7 @@ Cylinder::valid_keys = { "cachename", "eof_file", "override", + "create", "samplesz", "rnum", "pnum", @@ -177,7 +178,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : coefMaster = true; lastPlayTime = -std::numeric_limits::max(); EVEN_M = false; - eof_over = false; + create = false; cachename = ""; #if HAVE_LIBCUDA==1 cuda_aware = true; @@ -252,10 +253,10 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : if (myid==0) { // Diagnostic output . . . std::cerr << "Cylinder: can not read explicitly specified EOF file <" << cachename << ">" << std::endl; - if (eof_over) { - std::cerr << "Cylinder: override specified . . ." << std::endl; + if (create) { + std::cerr << "Cylinder: new cache requested . . ." << std::endl; } else { - std::cerr << "Cylinder: shamelessly aborting . . ." << std::endl; + std::cerr << "Cylinder: aborting. Set 'create' to build a new cache." << std::endl; nOK = 1; } } @@ -330,7 +331,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << std::endl << sep << "npca0=" << npca0 << std::endl << sep << "pcadiag=" << pcadiag << std::endl << sep << "cachename=" << cachename - << std::endl << sep << "override=" << std::boolalpha << eof_over + << std::endl << sep << "create=" << std::boolalpha << create << std::endl << sep << "selfgrav=" << std::boolalpha << self_consistent << std::endl << sep << "logarithmic=" << logarithmic << std::endl << sep << "vflag=" << vflag @@ -359,7 +360,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << std::endl << sep << "npca0=" << npca0 << std::endl << sep << "pcadiag=" << pcadiag << std::endl << sep << "cachename=" << cachename - << std::endl << sep << "override=" << std::boolalpha << eof_over + << std::endl << sep << "create=" << std::boolalpha << create << std::endl << sep << "selfgrav=" << std::boolalpha << self_consistent << std::endl << sep << "logarithmic=" << logarithmic << std::endl << sep << "vflag=" << vflag @@ -423,9 +424,10 @@ void Cylinder::initialize() if (conf["npca" ]) npca = conf["npca" ].as(); if (conf["npca0" ]) npca0 = conf["npca0" ].as(); if (conf["nvtk" ]) nvtk = conf["nvtk" ].as(); - if (conf["cachename" ]) cachename = conf["cachename" ].as(); - if (conf["eof_file" ]) cachename = conf["eof_file" ].as(); - if (conf["override" ]) eof_over = conf["override" ].as(); + if (conf["cachename" ]) cachename = conf["cachename" ].as(); + if (conf["eof_file" ]) cachename = conf["eof_file" ].as(); + if (conf["create" ]) create = conf["create" ].as(); + if (conf["override" ]) create = conf["override" ].as(); if (conf["samplesz" ]) defSampT = conf["samplesz" ].as(); if (conf["rnum" ]) rnum = conf["rnum" ].as(); @@ -454,6 +456,14 @@ void Cylinder::initialize() << std::endl; } + // Deprecation warning + if (conf["override"]) { + if (myid==0) + std::cout << "Cylinder: parameter 'override' is deprecated. " + << "Please use 'create' instead." + << std::endl; + } + // Deprecation warning if (conf["eof_file"]) { if (myid==0) From 3de94e26d6b1a3f2bfb358db906d30b8ab49f9b1 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 16 Mar 2024 09:56:49 -0400 Subject: [PATCH 023/167] Added logfile spacers only [no ci] --- exputil/EmpCylSL.cc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 1db3164f2..a10280442 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -146,9 +146,9 @@ EmpCylSL::EmpCylSL(int nmax, int lmax, int mmax, int nord, // Sanity check if (lmax <= mmax) { if (myid==0) { - std::cout << "EmpCylSL: lmax must be greater than mmax for consistency" + std::cout << "---- EmpCylSL: lmax must be greater than mmax for consistency" << std::endl - << "EmpCylSL: setting lmax=" << mmax + 1 + << "---- EmpCylSL: setting lmax=" << mmax + 1 << " but you probably want lmax >> mmax" << std::endl; } @@ -231,9 +231,9 @@ void EmpCylSL::reset(int numr, int lmax, int mmax, int nord, // Option sanity check if (lmax <= mmax) { if (myid==0) { - std::cout << "EmpCylSL: lmax must be greater than mmax for consistency" + std::cout << "---- EmpCylSL: lmax must be greater than mmax for consistency" << std::endl - << "EmpCylSL: setting lmax=" << mmax + 1 + << "---- EmpCylSL: setting lmax=" << mmax + 1 << " but you probably want lmax >> mmax" << std::endl; } @@ -502,7 +502,7 @@ SphModTblPtr EmpCylSL::make_sl() // Debug sanity check // ------------------------------------------ if (myid==0) { - std::cout << "EmpCylSL::make_sl(): making SLGridSph with <" + std::cout << "---- EmpCylSL::make_sl(): making SLGridSph with <" << EmpModelLabs[mtype] << "> model" << std::endl; } @@ -7108,7 +7108,7 @@ bool EmpCylSL::ReadH5Cache() { std::string v; HighFive::Attribute vv = file.getAttribute(name); vv.read(v); if (value.compare(v)==0) return true; - std::cout << "--- EmpCylSL cache parameter " << name << ": wanted " + std::cout << "---- EmpCylSL cache parameter " << name << ": wanted " << value << " found " << v << std::endl; return false; }; From ad4c6b4928b868a5e50d349b209ee15f54acaea2 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 16 Mar 2024 09:57:30 -0400 Subject: [PATCH 024/167] Deprecated the 'override' parameter in favor of backup files [no ci] --- src/Cylinder.H | 6 ++--- src/Cylinder.cc | 72 +++++++++++++++++++++---------------------------- 2 files changed, 32 insertions(+), 46 deletions(-) diff --git a/src/Cylinder.H b/src/Cylinder.H index 17e1ab46a..1d1166227 100644 --- a/src/Cylinder.H +++ b/src/Cylinder.H @@ -62,8 +62,6 @@ class MixtureBasis; @param cachename is the name of the basis cache file - @param create boolean ignores the specificed cache file name and performes a recomputation - @param samplesz is the default particle number in PCA subsampling partitions (default is 1). The value 0 sets the sample size to sqrt(N). @param rnum if the size of the radial quadrature grid for analytic EOF computation @@ -134,7 +132,7 @@ private: double hcyl, hexp, snr, rem; int nmax, ncylodd, ncylrecomp, npca, npca0, nvtk, cmapR, cmapZ; std::string cachename; - bool self_consistent, logarithmic, pcavar, pcainit, pcavtk, pcadiag, pcaeof, create; + bool self_consistent, logarithmic, pcavar, pcainit, pcavtk, pcadiag, pcaeof; bool try_cache, firstime, dump_basis, compute, firstime_coef; // These should be ok for all derived classes, hence declared private @@ -341,7 +339,7 @@ public: //! \param npca0 is the first step for Hall coefficient computaton //! \param nvtk is the number of step VTK output //! \param pcadiag set to true enables PCA output diagnostics (default: false) - //! \param cachename is an override for the default EOF cache file name + //! \param cachename is the file for the EOF basis //! \param vflag sets verbosity (see EmpCylSL.cc) //! \param rnum is the number of Legendre radial knots for numerical basis computation //! \param pnum is the number of azimuthal knots for numerical basis computation diff --git a/src/Cylinder.cc b/src/Cylinder.cc index c9502117a..91701c214 100644 --- a/src/Cylinder.cc +++ b/src/Cylinder.cc @@ -82,7 +82,6 @@ Cylinder::valid_keys = { "cachename", "eof_file", "override", - "create", "samplesz", "rnum", "pnum", @@ -178,7 +177,6 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : coefMaster = true; lastPlayTime = -std::numeric_limits::max(); EVEN_M = false; - create = false; cachename = ""; #if HAVE_LIBCUDA==1 cuda_aware = true; @@ -245,20 +243,31 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : // whether first time or restart. Aborts if overridden cache is // not found. // - if (cachename.size()>0) { + if (std::filesystem::exists(cachename)) { cache_ok = ortho->read_cache(); + // If new cache is requested, backup existing cache if (!cache_ok) { - if (myid==0) { // Diagnostic output . . . - std::cerr << "Cylinder: can not read explicitly specified EOF file <" - << cachename << ">" << std::endl; - if (create) { + + // Backup name for existing file + string backupfile = cachename + ".bak"; + + try { + std::filesystem::rename(cachename, backupfile); + if (myid==0) + std::cout << "---- Cylinder: parameter mismatch. Renaming <" + << cachename << "> to <" << backupfile << "> and " + << "recreating the basis" << std::endl; + } catch (const std::filesystem::filesystem_error& e) { + if (myid==0) { std::cerr << "Cylinder: new cache requested . . ." << std::endl; - } else { - std::cerr << "Cylinder: aborting. Set 'create' to build a new cache." << std::endl; - nOK = 1; + std::cerr << "Cylinder: error creating backup file <" + << backupfile << "> from <" << cachename + << ">, message: " << e.code().message() + << std::endl; } + nOK = 1; } } } @@ -268,35 +277,18 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : throw std::runtime_error("Cylinder: error initializing from parameters"); } - // Attempt to read EOF file from cache on restart + // Genererate eof if needed // - if (try_cache || restart) { - - cache_ok = ortho->read_cache(); - - // Diagnostic output . . . - // - if (!cache_ok and myid==0) - std::cerr << "Cylinder: can not read EOF file <" - << cachename << ">" << std::endl - << "Cylinder: will attempt to generate EOF file, " - << "this will take some time (e.g. hours) . . ." - << std::endl; - } + if (!cache_ok) { - // On restart, abort if the cache is gone - // - if (restart && !cache_ok) { - if (myid==0) - std::cerr << "Cylinder: can not read cache file on restart ... aborting" + if (myid==0) + std::cout << "---- Cylinder: generating the EOF basis file . . . " + << "this step will take many minutes." << std::endl; - throw std::runtime_error("Cylinder: error reading cache"); + + ortho->generate_eof(rnum, pnum, tnum, dcond); } - // Genererate eof if needed - // - if (!cache_ok) ortho->generate_eof(rnum, pnum, tnum, dcond); - firstime = false; } @@ -331,7 +323,6 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << std::endl << sep << "npca0=" << npca0 << std::endl << sep << "pcadiag=" << pcadiag << std::endl << sep << "cachename=" << cachename - << std::endl << sep << "create=" << std::boolalpha << create << std::endl << sep << "selfgrav=" << std::boolalpha << self_consistent << std::endl << sep << "logarithmic=" << logarithmic << std::endl << sep << "vflag=" << vflag @@ -360,7 +351,6 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << std::endl << sep << "npca0=" << npca0 << std::endl << sep << "pcadiag=" << pcadiag << std::endl << sep << "cachename=" << cachename - << std::endl << sep << "create=" << std::boolalpha << create << std::endl << sep << "selfgrav=" << std::boolalpha << self_consistent << std::endl << sep << "logarithmic=" << logarithmic << std::endl << sep << "vflag=" << vflag @@ -426,8 +416,6 @@ void Cylinder::initialize() if (conf["nvtk" ]) nvtk = conf["nvtk" ].as(); if (conf["cachename" ]) cachename = conf["cachename" ].as(); if (conf["eof_file" ]) cachename = conf["eof_file" ].as(); - if (conf["create" ]) create = conf["create" ].as(); - if (conf["override" ]) create = conf["override" ].as(); if (conf["samplesz" ]) defSampT = conf["samplesz" ].as(); if (conf["rnum" ]) rnum = conf["rnum" ].as(); @@ -451,7 +439,7 @@ void Cylinder::initialize() // Deprecation warning if (conf["density"]) { if (myid==0) - std::cout << "Cylinder: parameter 'density' is deprecated. " + std::cout << "---- Cylinder: parameter 'density' is deprecated. " << "The density field will be computed regardless." << std::endl; } @@ -459,15 +447,15 @@ void Cylinder::initialize() // Deprecation warning if (conf["override"]) { if (myid==0) - std::cout << "Cylinder: parameter 'override' is deprecated. " - << "Please use 'create' instead." + std::cout << "---- Cylinder: parameter 'override' is deprecated. " + << "Basis will be recomputed if needed automatically." << std::endl; } // Deprecation warning if (conf["eof_file"]) { if (myid==0) - std::cout << "Cylinder: parameter 'eof_file' is deprecated. " + std::cout << "---- Cylinder: parameter 'eof_file' is deprecated. " << "and will be removed in a future release. Please " << "use 'cachename' instead." << std::endl; From 7df6b9cff623276789a53e143f5aeae65c483e52 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 16 Mar 2024 11:52:05 -0400 Subject: [PATCH 025/167] Additional comments and organization [no CI] --- exputil/EmpCylSL.cc | 3 +-- src/Cylinder.H | 2 +- src/Cylinder.cc | 46 +++++++++++++++++++++++++-------------------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index a10280442..26de64330 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -499,13 +499,12 @@ SphModTblPtr EmpCylSL::make_sl() std::vector pw(number); // ------------------------------------------ - // Debug sanity check + // Log file output // ------------------------------------------ if (myid==0) { std::cout << "---- EmpCylSL::make_sl(): making SLGridSph with <" << EmpModelLabs[mtype] << "> model" << std::endl; } - // ------------------------------------------ // Make radial, density and mass array // ------------------------------------------ diff --git a/src/Cylinder.H b/src/Cylinder.H index 1d1166227..660efaf09 100644 --- a/src/Cylinder.H +++ b/src/Cylinder.H @@ -72,7 +72,7 @@ class MixtureBasis; @param ashift offsets the center of the analytic model for basis function computation - @param expcond boolean true turns on basis function reconditioning + @param expcond boolean true turns on analytic basis function preconditioning (default: true) @param logr boolean turns on logarithmic radial basis gridding in EmpCylSL diff --git a/src/Cylinder.cc b/src/Cylinder.cc index 91701c214..1bc732db5 100644 --- a/src/Cylinder.cc +++ b/src/Cylinder.cc @@ -229,8 +229,12 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : } + // Cache file reading or generation + // if (expcond) { - // Set parameters for external dcond function + + // Set parameters for external dcond function + // EXPSCALE = acyl; HSCALE = hcyl; ASHIFT = ashift; @@ -239,44 +243,46 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : bool cache_ok = false; int nOK = 0; - // Attempt to read EOF file from cache with override. Will work - // whether first time or restart. Aborts if overridden cache is - // not found. + // Attempt to read EOF file from cache // if (std::filesystem::exists(cachename)) { cache_ok = ortho->read_cache(); - // If new cache is requested, backup existing cache + // If new cache is requested, backup existing basis file + // if (!cache_ok) { // Backup name for existing file string backupfile = cachename + ".bak"; - try { - std::filesystem::rename(cachename, backupfile); - if (myid==0) + // Only root process renames + if (myid==0) { + try { + std::filesystem::rename(cachename, backupfile); std::cout << "---- Cylinder: parameter mismatch. Renaming <" << cachename << "> to <" << backupfile << "> and " << "recreating the basis" << std::endl; - } catch (const std::filesystem::filesystem_error& e) { - if (myid==0) { - std::cerr << "Cylinder: new cache requested . . ." << std::endl; - std::cerr << "Cylinder: error creating backup file <" - << backupfile << "> from <" << cachename - << ">, message: " << e.code().message() + } catch (const std::filesystem::filesystem_error& e) { + std::cerr << "Cylinder: new cache requested but " + << "error creating backup file <" << backupfile + << "> from <" << cachename << ">, message: " + << e.code().message() << std::endl; + // Set termination flag + nOK = 1; } - nOK = 1; } + + // Broadcast termination request to all processes + // + MPI_Bcast(&nOK, 1, MPI_UNSIGNED_CHAR, 0, MPI_COMM_WORLD); + if (nOK) { + throw std::runtime_error("Cylinder: error initializing from parameters"); + } } } - MPI_Bcast(&nOK, 1, MPI_UNSIGNED_CHAR, 0, MPI_COMM_WORLD); - if (nOK) { - throw std::runtime_error("Cylinder: error initializing from parameters"); - } - // Genererate eof if needed // if (!cache_ok) { From 6ee27a61a18a0d5236f4bdc1c90fffcfb5efdadd Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 16 Mar 2024 12:11:05 -0400 Subject: [PATCH 026/167] Deprecate 'expcond' in favor of 'precond' [no ci] --- src/Cylinder.H | 6 +++--- src/Cylinder.cc | 34 +++++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Cylinder.H b/src/Cylinder.H index 660efaf09..8dc55b14b 100644 --- a/src/Cylinder.H +++ b/src/Cylinder.H @@ -72,7 +72,7 @@ class MixtureBasis; @param ashift offsets the center of the analytic model for basis function computation - @param expcond boolean true turns on analytic basis function preconditioning (default: true) + @param precond boolean true turns on analytic basis function preconditioning (default: true) @param logr boolean turns on logarithmic radial basis gridding in EmpCylSL @@ -109,7 +109,7 @@ class Cylinder : public Basis { private: - bool expcond, EVEN_M, subsamp; + bool precond, EVEN_M, subsamp; int rnum, pnum, tnum; double ashift; unsigned int vflag; @@ -346,7 +346,7 @@ public: //! \param tnum is the number of Legendre polar knots for numerical basis computation //! \param ashift is the shift applied in the x-axis relative to the original for basis conditioning //! \param self_consistent set to false for fixed potential - //! \param expcond set to true for analytic basis function conditioning (default: true) + //! \param precond set to true for analytic basis function conditioning (default: true) //! \param logr tabulate basis in logarithmic coordinates (default: false) //! \param pcavar set to true for real-time Hall analysis (default: false) //! \param samplesz is the default particle number in PCA subsampling partitions (default is 1). The value 0 sets the sample size to sqrt(N). diff --git a/src/Cylinder.cc b/src/Cylinder.cc index 1bc732db5..e17fd50e1 100644 --- a/src/Cylinder.cc +++ b/src/Cylinder.cc @@ -88,6 +88,7 @@ Cylinder::valid_keys = { "tnum", "ashift", "expcond", + "precond", "logr", "pcavar", "pcaeof", @@ -158,7 +159,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : rem = -1.0; self_consistent = true; firstime = true; - expcond = true; + precond = true; cmapR = 1; cmapZ = 1; logarithmic = false; @@ -231,7 +232,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : // Cache file reading or generation // - if (expcond) { + if (precond) { // Set parameters for external dcond function // @@ -282,13 +283,18 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : } } } + else { + if (myid==0) + std::cout << "---- Cylinder: can't find the EOF basis file <" + << cachename << ">" << std::endl; + } // Genererate eof if needed // if (!cache_ok) { if (myid==0) - std::cout << "---- Cylinder: generating the EOF basis file . . . " + std::cout << "---- Cylinder: generating the EOF basis file; " << "this step will take many minutes." << std::endl; @@ -321,7 +327,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << std::endl << sep << "rcylmax=" << rcylmax << std::endl << sep << "acyl=" << acyl << std::endl << sep << "hcyl=" << hcyl - << std::endl << sep << "expcond=" << expcond + << std::endl << sep << "precond=" << precond << std::endl << sep << "pcavar=" << pcavar << std::endl << sep << "pcaeof=" << pcaeof << std::endl << sep << "nvtk=" << nvtk @@ -349,7 +355,7 @@ Cylinder::Cylinder(Component* c0, const YAML::Node& conf, MixtureBasis *m) : << std::endl << sep << "rcylmax=" << rcylmax << std::endl << sep << "acyl=" << acyl << std::endl << sep << "hcyl=" << hcyl - << std::endl << sep << "expcond=" << expcond + << std::endl << sep << "precond=" << precond << std::endl << sep << "pcavar=" << pcavar << std::endl << sep << "pcaeof=" << pcaeof << std::endl << sep << "nvtk=" << nvtk @@ -428,7 +434,8 @@ void Cylinder::initialize() if (conf["pnum" ]) pnum = conf["pnum" ].as(); if (conf["tnum" ]) tnum = conf["tnum" ].as(); if (conf["ashift" ]) ashift = conf["ashift" ].as(); - if (conf["expcond" ]) expcond = conf["expcond" ].as(); + if (conf["expcond" ]) precond = conf["expcond" ].as(); + if (conf["precond" ]) precond = conf["precond" ].as(); if (conf["logr" ]) logarithmic = conf["logr" ].as(); if (conf["pcavar" ]) pcavar = conf["pcavar" ].as(); if (conf["pcaeof" ]) pcaeof = conf["pcaeof" ].as(); @@ -442,6 +449,15 @@ void Cylinder::initialize() if (conf["cmapz" ]) cmapZ = conf["cmapz" ].as(); if (conf["vflag" ]) vflag = conf["vflag" ].as(); + // Deprecation warning + if (conf["expcond"]) { + if (myid==0) + std::cout << "---- Cylinder: parameter 'expcond' is deprecated. " + << "It has been renamed to 'precond'. The old parameter " + << "will be removed in a later release." + << std::endl; + } + // Deprecation warning if (conf["density"]) { if (myid==0) @@ -600,7 +616,7 @@ void Cylinder::get_acceleration_and_potential(Component* C) //======================= // No recomputation ever if the - if (!expcond) { // basis has been precondtioned + if (!precond) { // basis has been precondtioned // Only do this check only once per // multistep; might as well be at @@ -864,7 +880,7 @@ void Cylinder::determine_coefficients_particles(void) if (!self_consistent && !firstime_coef && !initializing) return; - if (!expcond && firstime) { + if (!precond && firstime) { // Try to read cache bool cache_ok = false; if (try_cache || restart) { @@ -1016,7 +1032,7 @@ void Cylinder::determine_coefficients_particles(void) // Dump basis on first call //========================= - if ( dump_basis and (this_step==0 || (expcond and ncompcyl==0) ) + if ( dump_basis and (this_step==0 || (precond and ncompcyl==0) ) && ortho->coefs_made_all() && !initializing) { if (myid == 0 and multistep==0 || mstep==0) { From 3bd75b651a8f8e46825097e180cddb471721da2c Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 18 Mar 2024 17:30:12 -0400 Subject: [PATCH 027/167] Change debug output defaults for gendisk [no ci] --- exputil/EmpCylSL.cc | 6 +++--- utils/ICs/initial.cc | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 26de64330..274bf85f5 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -2192,7 +2192,7 @@ void EmpCylSL::generate_eof(int numr, int nump, int numt, #endif std::shared_ptr progress; - if (VFLAG & 16 && myid==0) { + if (VFLAG & 8 && myid==0) { std::cout << std::endl << "Quadrature loop progress" << std::endl; progress = std::make_shared(numr); } @@ -2418,7 +2418,7 @@ void EmpCylSL::generate_eof(int numr, int nump, int numt, } // *** r quadrature loop - if (VFLAG & 16) { + if (VFLAG & 8) { auto t = timer.stop(); if (myid==0) { std::cout << std::endl @@ -2451,7 +2451,7 @@ void EmpCylSL::generate_eof(int numr, int nump, int numt, // make_eof(); - if (VFLAG & 16) { + if (VFLAG & 8) { cout << "Process " << setw(4) << myid << ": completed basis in " << timer.stop() << " seconds" << endl; diff --git a/utils/ICs/initial.cc b/utils/ICs/initial.cc index 3287754a3..d5d949c55 100644 --- a/utils/ICs/initial.cc +++ b/utils/ICs/initial.cc @@ -443,15 +443,15 @@ main(int ac, char **av) cxxopts::value(NMAXLIM)->default_value("10000")) ("NUMDF", "Number of knots in Eddington inversion grid", cxxopts::value(NUMDF)->default_value("1000")) - ("VFLAG", "", - cxxopts::value(VFLAG)->default_value("31")) - ("DFLAG", "", - cxxopts::value(DFLAG)->default_value("31")) + ("VFLAG", "Debug bit flags for EmpCylSL. Default is EOF timing only. Set to zero for quiet.", + cxxopts::value(VFLAG)->default_value("8")) + ("DFLAG", "Debug bit flags for DiskHalo. Default is 0=quiet. 1=internal values, 2=model files, 4=dispersion/epicyclic files, 8=oab errors", + cxxopts::value(DFLAG)->default_value("0")) ("expcond", "", - cxxopts::value(expcond)->default_value("false")) + cxxopts::value(expcond)->default_value("true")) ("report", "", cxxopts::value(report)->default_value("false")) - ("ignore", "", + ("ignore", "Ignore the specified EOF cache file even if it exists", cxxopts::value(ignore)->default_value("false")) ("evolved", "", cxxopts::value(evolved)->default_value("false")) @@ -492,7 +492,7 @@ main(int ac, char **av) ("NHT", "", cxxopts::value(NHT)->default_value("800")) ("NDP", "", - cxxopts::value(NDP)->default_value("800")) + cxxopts::value(NDP)->default_value("8")) ("NUMR", "Size of radial grid for Spherical SL", cxxopts::value(NUMR)->default_value("2000")) ("SHFAC", "", @@ -508,7 +508,7 @@ main(int ac, char **av) ("ToomreQ", "Toomre Q parameter for the disk", cxxopts::value(ToomreQ)->default_value("1.4")) ("RMIN", "Minimum halo radius", - cxxopts::value(RMIN)->default_value("0.005")) + cxxopts::value(RMIN)->default_value("0.001")) ("RCYLMIN", "Minimum disk radius", cxxopts::value(RCYLMIN)->default_value("0.001")) ("RCYLMAX", "Maximum disk radius, in units of ASCALE", @@ -569,8 +569,8 @@ main(int ac, char **av) cxxopts::value(centerfile)) ("runtag", "Prefix for output files", cxxopts::value(runtag)->default_value("gendisk")) - ("suffix", "Suffix for output files", - cxxopts::value(suffix)->default_value("diag")) + ("suffix", "Suffix for output files (none by default)", + cxxopts::value(suffix)) ("threads", "Number of threads to run", cxxopts::value(nthrds)->default_value("1")) ("allow", "Allow multimass algorithm to generature negative masses for testing") From a46653ae52c315de27444b226a77ea36e7341a8b Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 18 Mar 2024 17:45:03 -0400 Subject: [PATCH 028/167] One more necessary change to diagnostic output level [no ci] --- exputil/EmpCylSL.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 274bf85f5..df2af62c3 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -2205,9 +2205,7 @@ void EmpCylSL::generate_eof(int numr, int nump, int numt, // Diagnostic timing output for MPI process loop // - if (VFLAG & 16 && myid==0) { - ++(*progress); - } + if (progress) ++(*progress); if (cntr++ % numprocs != myid) continue; From 19c573f8dfeb80ea3e6c937d25a5703bccfb9daf Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 19 Mar 2024 17:13:55 -0400 Subject: [PATCH 029/167] Custom named coefficient files belong in outdir; make it so [no ci] --- src/OutCoef.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OutCoef.cc b/src/OutCoef.cc index 2b1ac72ba..872add11f 100644 --- a/src/OutCoef.cc +++ b/src/OutCoef.cc @@ -64,7 +64,7 @@ void OutCoef::initialize() if (conf["filename"]) { - filename = conf["filename"].as(); + filename = outdir + conf["filename"].as(); } else { From 23d5984e0da41bae4908feb342ea6bf593934db6 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 20 Mar 2024 14:34:52 -0400 Subject: [PATCH 030/167] Remove Boost dependence from CMake config altogether [no ci] --- CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ab0c66397..b09fb9d17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,6 @@ set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC) # Package support find_package(MPI REQUIRED COMPONENTS C CXX) -find_package(Boost COMPONENTS serialization) find_package(OpenMP) find_package(FFTW) find_package(HDF5 COMPONENTS C CXX HL REQUIRED) @@ -151,9 +150,6 @@ if(PNG_FOUND AND ENABLE_PNG) set(HAVE_LIBPNGPP TRUE) endif() if(ENABLE_DSMC) - if(NOT BOOST_FOUND) - message(SEND_ERROR "You need Boost to compile DSMC") - endif() set(DSMC_ENABLED 1) endif() if(ENABLE_CUDA_SINGLE) From b153dc149ab22b86d57fe94125422df6549ee49a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 20 Mar 2024 20:33:01 -0400 Subject: [PATCH 031/167] Renamed coefs directory to expui and libexpcoef to libexpui; changed the cuda config for DSMC for consistency [no ci] --- CMakeLists.txt | 6 +++++- {coefs => expui}/BasisFactory.H | 0 {coefs => expui}/BasisFactory.cc | 0 {coefs => expui}/BiorthBasis.H | 0 {coefs => expui}/BiorthBasis.cc | 0 {coefs => expui}/CMakeLists.txt | 16 ++++++++-------- {coefs => expui}/Centering.H | 0 {coefs => expui}/Centering.cc | 0 {coefs => expui}/CoefContainer.H | 0 {coefs => expui}/CoefContainer.cc | 0 {coefs => expui}/CoefStruct.H | 0 {coefs => expui}/CoefStruct.cc | 0 {coefs => expui}/Coefficients.H | 0 {coefs => expui}/Coefficients.cc | 0 {coefs => expui}/FieldBasis.H | 0 {coefs => expui}/FieldBasis.cc | 0 {coefs => expui}/FieldGenerator.H | 0 {coefs => expui}/FieldGenerator.cc | 0 {coefs => expui}/KMeans.H | 0 {coefs => expui}/KMeans.cc | 0 {coefs => expui}/Koopman.H | 0 {coefs => expui}/Koopman.cc | 0 {coefs => expui}/OrthoBasisFactory.H | 0 {coefs => expui}/ParticleIterator.H | 0 {coefs => expui}/ParticleIterator.cc | 0 {coefs => expui}/README | 0 {coefs => expui}/RedSVD.H | 0 {coefs => expui}/coefstoh5.cc | 0 {coefs => expui}/expMSSA.H | 0 {coefs => expui}/expMSSA.cc | 0 {coefs => expui}/h5compare.cc | 0 {coefs => expui}/h5power.cc | 0 {coefs => expui}/makecoefs.cc | 0 {coefs => expui}/testread.cc | 0 {coefs => expui}/viewcoefs.cc | 0 pyEXP/CMakeLists.txt | 2 +- src/CMakeLists.txt | 4 ++-- utils/PhaseSpace/CMakeLists.txt | 2 +- utils/Test/CMakeLists.txt | 2 +- 39 files changed, 18 insertions(+), 14 deletions(-) rename {coefs => expui}/BasisFactory.H (100%) rename {coefs => expui}/BasisFactory.cc (100%) rename {coefs => expui}/BiorthBasis.H (100%) rename {coefs => expui}/BiorthBasis.cc (100%) rename {coefs => expui}/CMakeLists.txt (79%) rename {coefs => expui}/Centering.H (100%) rename {coefs => expui}/Centering.cc (100%) rename {coefs => expui}/CoefContainer.H (100%) rename {coefs => expui}/CoefContainer.cc (100%) rename {coefs => expui}/CoefStruct.H (100%) rename {coefs => expui}/CoefStruct.cc (100%) rename {coefs => expui}/Coefficients.H (100%) rename {coefs => expui}/Coefficients.cc (100%) rename {coefs => expui}/FieldBasis.H (100%) rename {coefs => expui}/FieldBasis.cc (100%) rename {coefs => expui}/FieldGenerator.H (100%) rename {coefs => expui}/FieldGenerator.cc (100%) rename {coefs => expui}/KMeans.H (100%) rename {coefs => expui}/KMeans.cc (100%) rename {coefs => expui}/Koopman.H (100%) rename {coefs => expui}/Koopman.cc (100%) rename {coefs => expui}/OrthoBasisFactory.H (100%) rename {coefs => expui}/ParticleIterator.H (100%) rename {coefs => expui}/ParticleIterator.cc (100%) rename {coefs => expui}/README (100%) rename {coefs => expui}/RedSVD.H (100%) rename {coefs => expui}/coefstoh5.cc (100%) rename {coefs => expui}/expMSSA.H (100%) rename {coefs => expui}/expMSSA.cc (100%) rename {coefs => expui}/h5compare.cc (100%) rename {coefs => expui}/h5power.cc (100%) rename {coefs => expui}/makecoefs.cc (100%) rename {coefs => expui}/testread.cc (100%) rename {coefs => expui}/viewcoefs.cc (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b09fb9d17..7bb4763ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -226,7 +226,7 @@ add_subdirectory(extern/HighFive) # Configure the remaining native subdirectories add_subdirectory(exputil) -add_subdirectory(coefs) +add_subdirectory(expui) if (ENABLE_NBODY) add_subdirectory(src) endif() @@ -237,6 +237,10 @@ endif() add_subdirectory(extern/user-modules) +if (ENABLE_DSMC) + add_subdirectory(extern/DSMC/src) +endif() + # Build the tests; set ENABLE_TEST=OFF to disable if(ENABLE_TESTS) include(CTest) diff --git a/coefs/BasisFactory.H b/expui/BasisFactory.H similarity index 100% rename from coefs/BasisFactory.H rename to expui/BasisFactory.H diff --git a/coefs/BasisFactory.cc b/expui/BasisFactory.cc similarity index 100% rename from coefs/BasisFactory.cc rename to expui/BasisFactory.cc diff --git a/coefs/BiorthBasis.H b/expui/BiorthBasis.H similarity index 100% rename from coefs/BiorthBasis.H rename to expui/BiorthBasis.H diff --git a/coefs/BiorthBasis.cc b/expui/BiorthBasis.cc similarity index 100% rename from coefs/BiorthBasis.cc rename to expui/BiorthBasis.cc diff --git a/coefs/CMakeLists.txt b/expui/CMakeLists.txt similarity index 79% rename from coefs/CMakeLists.txt rename to expui/CMakeLists.txt index cb05b447f..8193c278c 100644 --- a/coefs/CMakeLists.txt +++ b/expui/CMakeLists.txt @@ -29,18 +29,18 @@ if(ENABLE_XDR AND TIRPC_FOUND) list(APPEND common_LINKLIB ${TIRPC_LIBRARIES}) endif() -# Make the expcoef shared library +# Make the expui shared library # -set(expcoef_SOURCES BasisFactory.cc BiorthBasis.cc FieldBasis.cc +set(expui_SOURCES BasisFactory.cc BiorthBasis.cc FieldBasis.cc CoefContainer.cc CoefStruct.cc FieldGenerator.cc expMSSA.cc Coefficients.cc KMeans.cc Centering.cc ParticleIterator.cc Koopman.cc) -add_library(expcoef ${expcoef_SOURCES}) -set_target_properties(expcoef PROPERTIES OUTPUT_NAME expcoef) -target_include_directories(expcoef PUBLIC ${common_INCLUDE}) -target_link_libraries(expcoef PUBLIC ${common_LINKLIB}) +add_library(expui ${expui_SOURCES}) +set_target_properties(expui PROPERTIES OUTPUT_NAME expui) +target_include_directories(expui PUBLIC ${common_INCLUDE}) +target_link_libraries(expui PUBLIC ${common_LINKLIB}) -install(TARGETS expcoef DESTINATION lib) +install(TARGETS expui DESTINATION lib) # Configure and build the test routines # @@ -52,7 +52,7 @@ add_executable(makecoefs makecoefs.cc) add_executable(testread testread.cc) foreach(program ${bin_PROGRAMS}) - target_link_libraries(${program} expcoef exputil ${common_LINKLIB}) + target_link_libraries(${program} expui exputil ${common_LINKLIB}) target_include_directories(${program} PUBLIC ${common_INCLUDE}) target_compile_options(${program} PUBLIC ${OpenMP_CXX_FLAGS}) install(TARGETS ${program} DESTINATION bin) diff --git a/coefs/Centering.H b/expui/Centering.H similarity index 100% rename from coefs/Centering.H rename to expui/Centering.H diff --git a/coefs/Centering.cc b/expui/Centering.cc similarity index 100% rename from coefs/Centering.cc rename to expui/Centering.cc diff --git a/coefs/CoefContainer.H b/expui/CoefContainer.H similarity index 100% rename from coefs/CoefContainer.H rename to expui/CoefContainer.H diff --git a/coefs/CoefContainer.cc b/expui/CoefContainer.cc similarity index 100% rename from coefs/CoefContainer.cc rename to expui/CoefContainer.cc diff --git a/coefs/CoefStruct.H b/expui/CoefStruct.H similarity index 100% rename from coefs/CoefStruct.H rename to expui/CoefStruct.H diff --git a/coefs/CoefStruct.cc b/expui/CoefStruct.cc similarity index 100% rename from coefs/CoefStruct.cc rename to expui/CoefStruct.cc diff --git a/coefs/Coefficients.H b/expui/Coefficients.H similarity index 100% rename from coefs/Coefficients.H rename to expui/Coefficients.H diff --git a/coefs/Coefficients.cc b/expui/Coefficients.cc similarity index 100% rename from coefs/Coefficients.cc rename to expui/Coefficients.cc diff --git a/coefs/FieldBasis.H b/expui/FieldBasis.H similarity index 100% rename from coefs/FieldBasis.H rename to expui/FieldBasis.H diff --git a/coefs/FieldBasis.cc b/expui/FieldBasis.cc similarity index 100% rename from coefs/FieldBasis.cc rename to expui/FieldBasis.cc diff --git a/coefs/FieldGenerator.H b/expui/FieldGenerator.H similarity index 100% rename from coefs/FieldGenerator.H rename to expui/FieldGenerator.H diff --git a/coefs/FieldGenerator.cc b/expui/FieldGenerator.cc similarity index 100% rename from coefs/FieldGenerator.cc rename to expui/FieldGenerator.cc diff --git a/coefs/KMeans.H b/expui/KMeans.H similarity index 100% rename from coefs/KMeans.H rename to expui/KMeans.H diff --git a/coefs/KMeans.cc b/expui/KMeans.cc similarity index 100% rename from coefs/KMeans.cc rename to expui/KMeans.cc diff --git a/coefs/Koopman.H b/expui/Koopman.H similarity index 100% rename from coefs/Koopman.H rename to expui/Koopman.H diff --git a/coefs/Koopman.cc b/expui/Koopman.cc similarity index 100% rename from coefs/Koopman.cc rename to expui/Koopman.cc diff --git a/coefs/OrthoBasisFactory.H b/expui/OrthoBasisFactory.H similarity index 100% rename from coefs/OrthoBasisFactory.H rename to expui/OrthoBasisFactory.H diff --git a/coefs/ParticleIterator.H b/expui/ParticleIterator.H similarity index 100% rename from coefs/ParticleIterator.H rename to expui/ParticleIterator.H diff --git a/coefs/ParticleIterator.cc b/expui/ParticleIterator.cc similarity index 100% rename from coefs/ParticleIterator.cc rename to expui/ParticleIterator.cc diff --git a/coefs/README b/expui/README similarity index 100% rename from coefs/README rename to expui/README diff --git a/coefs/RedSVD.H b/expui/RedSVD.H similarity index 100% rename from coefs/RedSVD.H rename to expui/RedSVD.H diff --git a/coefs/coefstoh5.cc b/expui/coefstoh5.cc similarity index 100% rename from coefs/coefstoh5.cc rename to expui/coefstoh5.cc diff --git a/coefs/expMSSA.H b/expui/expMSSA.H similarity index 100% rename from coefs/expMSSA.H rename to expui/expMSSA.H diff --git a/coefs/expMSSA.cc b/expui/expMSSA.cc similarity index 100% rename from coefs/expMSSA.cc rename to expui/expMSSA.cc diff --git a/coefs/h5compare.cc b/expui/h5compare.cc similarity index 100% rename from coefs/h5compare.cc rename to expui/h5compare.cc diff --git a/coefs/h5power.cc b/expui/h5power.cc similarity index 100% rename from coefs/h5power.cc rename to expui/h5power.cc diff --git a/coefs/makecoefs.cc b/expui/makecoefs.cc similarity index 100% rename from coefs/makecoefs.cc rename to expui/makecoefs.cc diff --git a/coefs/testread.cc b/expui/testread.cc similarity index 100% rename from coefs/testread.cc rename to expui/testread.cc diff --git a/coefs/viewcoefs.cc b/expui/viewcoefs.cc similarity index 100% rename from coefs/viewcoefs.cc rename to expui/viewcoefs.cc diff --git a/pyEXP/CMakeLists.txt b/pyEXP/CMakeLists.txt index f616377cd..d47080e6e 100644 --- a/pyEXP/CMakeLists.txt +++ b/pyEXP/CMakeLists.txt @@ -33,7 +33,7 @@ endif() pybind11_add_module(pyEXP PyWrappers.cc CoefWrappers.cc UtilWrappers.cc BasisWrappers.cc FieldWrappers.cc ParticleReaderWrappers.cc MSSAWrappers.cc EDMDWrappers.cc) -target_link_libraries(pyEXP PUBLIC expcoef exputil ${common_LINKLIB}) +target_link_libraries(pyEXP PUBLIC expui exputil ${common_LINKLIB}) target_include_directories(pyEXP PUBLIC ${common_INCLUDE}) target_compile_options(pyEXP PUBLIC ${OpenMP_CXX_FLAGS}) get_target_property(cxxflags pyEXP COMPILE_OPTIONS) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ec4f53e7a..a9ebe6cf8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,11 +35,11 @@ set(common_INCLUDE_DIRS set(DSMC_LIBS) if (ENABLE_DSMC) - add_subdirectory(${PROJECT_SOURCE_DIR}/extern/DSMC/src DSMC) + # add_subdirectory(${PROJECT_SOURCE_DIR}/extern/DSMC/src DSMC) list(APPEND DSMC_LIBS expdsmc) endif() -set(common_LINKLIB ${DSMC_LIBS} exputil expcoef OpenMP::OpenMP_CXX +set(common_LINKLIB ${DSMC_LIBS} exputil expui OpenMP::OpenMP_CXX MPI::MPI_CXX yaml-cpp ${VTK_LIBRARIES}) if(PNG_FOUND) diff --git a/utils/PhaseSpace/CMakeLists.txt b/utils/PhaseSpace/CMakeLists.txt index 8bde028a7..3076d0b12 100644 --- a/utils/PhaseSpace/CMakeLists.txt +++ b/utils/PhaseSpace/CMakeLists.txt @@ -13,7 +13,7 @@ if(HAVE_XDR) list(APPEND bin_PROGRAMS tipstd2psp) endif() -set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX yaml-cpp expcoef +set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX yaml-cpp expui exputil ${VTK_LIBRARIES} ${HDF5_LIBRARIES}) if(PNG_FOUND) diff --git a/utils/Test/CMakeLists.txt b/utils/Test/CMakeLists.txt index aa705fee7..e680d1723 100644 --- a/utils/Test/CMakeLists.txt +++ b/utils/Test/CMakeLists.txt @@ -1,7 +1,7 @@ set(bin_PROGRAMS testBarrier expyaml) -set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX expcoef exputil +set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX expui exputil yaml-cpp ${VTK_LIBRARIES}) if(PNG_FOUND) From 948ed04fa75cdc836d96d172d05068da2119babb Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 22 Mar 2024 13:25:39 -0400 Subject: [PATCH 032/167] Add outdir prefix to levels file from CUDA computation --- src/cudaComponent.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cudaComponent.cu b/src/cudaComponent.cu index 799deef71..4c5d3eff6 100644 --- a/src/cudaComponent.cu +++ b/src/cudaComponent.cu @@ -1057,7 +1057,7 @@ void Component::print_level_lists_cuda(double T) if (tot) { std::ostringstream ofil; - ofil << runtag << ".levels"; + ofil << outdir << runtag << ".levels"; std::ofstream out(ofil.str().c_str(), ios::app); int sum=0; From e9c4365b45dc26482867d9d871a691b2ae80d052 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 22 Mar 2024 16:47:47 -0400 Subject: [PATCH 033/167] Added new defaults for RedSVD rank and changed the wrapper documentation to reflect that [no ci] --- expui/expMSSA.H | 2 +- expui/expMSSA.cc | 45 +++++++++++++++++++++++++++++++------------ pyEXP/MSSAWrappers.cc | 12 ++++++++---- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/expui/expMSSA.H b/expui/expMSSA.H index 65361514b..4c2bfa9f7 100644 --- a/expui/expMSSA.H +++ b/expui/expMSSA.H @@ -33,7 +33,7 @@ namespace MSSA //! Primary MSSA analysis void mssa_analysis(); - bool computed, reconstructed; + bool computed, reconstructed, trajectory; //! The reconstructed coefficients for each PC std::map RC; diff --git a/expui/expMSSA.cc b/expui/expMSSA.cc index 77d80985c..309d60f07 100644 --- a/expui/expMSSA.cc +++ b/expui/expMSSA.cc @@ -234,11 +234,24 @@ namespace MSSA { Eigen::MatrixXd cov; double Scale; - int rank; + int rank, srank=0; - // Covariance is the default + // Ensure a reasonable rank for RedSVD // - if (params["Traj"]) { + if (not params["Jacobi"] and not params["BDCSVD"]) { + srank = std::min( + {static_cast(Y.cols()), + static_cast(Y.rows())} + ); + if (params["rank"]) + srank = std::min({srank, params["rank"].as()}); + + npc = std::min(npc, srank); + } + + // Trajectory is the default + // + if (trajectory) { // Deduce the maximum rank of the trajectory matrix Y // @@ -267,7 +280,7 @@ namespace MSSA { // Only write covariance matrix on request // - if (not params["Traj"] and params["writeCov"]) { + if (not trajectory and params["writeCov"]) { std::string filename = prefix + ".cov"; std::ofstream out(filename); out << cov; @@ -278,7 +291,7 @@ namespace MSSA { // if (params["Jacobi"]) { // -->Using Jacobi - if (params["Traj"]) { // Trajectory matrix + if (trajectory) { // Trajectory matrix auto YY = Y/Scale; Eigen::JacobiSVD svd(YY, Eigen::ComputeThinU | Eigen::ComputeThinV); @@ -293,7 +306,7 @@ namespace MSSA { } } else if (params["BDCSVD"]) { // -->Using BDC - if (params["Traj"]) { // Trajectory matrix + if (trajectory) { // Trajectory matrix auto YY = Y/Scale; Eigen::BDCSVD svd(YY, Eigen::ComputeThinU | Eigen::ComputeThinV); @@ -309,19 +322,19 @@ namespace MSSA { } else { // -->Use Random approximation algorithm from Halko, Martinsson, // and Tropp - if (params["Traj"]) { // Trajectory matrix + if (trajectory) { // Trajectory matrix auto YY = Y/Scale; - RedSVD::RedSVD svd(YY, rank); + RedSVD::RedSVD svd(YY, srank); S = svd.singularValues(); U = svd.matrixV(); } else { // Covariance matrix if (params["RedSym"]) { - RedSVD::RedSymEigen eigen(cov, rank); + RedSVD::RedSymEigen eigen(cov, srank); S = eigen.eigenvalues().reverse(); U = eigen.eigenvectors().rowwise().reverse(); } else { - RedSVD::RedSVD svd(cov, rank); + RedSVD::RedSVD svd(cov, srank); S = svd.singularValues(); U = svd.matrixU(); } @@ -331,10 +344,13 @@ namespace MSSA { std::cout << "shape U = " << U.rows() << " x " << U.cols() << std::endl; + std::cout << "shape Y = " << Y.rows() << " x " + << Y.cols() << std::endl; + // Rescale the SVD factorization by the Frobenius norm // S *= Scale; - if (params["Traj"]) { + if (trajectory) { for (int i=0; i(); + // Eigen OpenMP reporting // static bool firstTime = true; diff --git a/pyEXP/MSSAWrappers.cc b/pyEXP/MSSAWrappers.cc index 9d7cc89da..d5cec2bcb 100644 --- a/pyEXP/MSSAWrappers.cc +++ b/pyEXP/MSSAWrappers.cc @@ -74,14 +74,18 @@ void MSSAtoolkitClasses(py::module &m) { " approximation algorithm from Halko, Martinsson,\n" " and Tropp (RedSVD). This is quite accurate but\n" " _very_ slow\n" - " BDCSVD: true Use the Binary Divide and Conquer SVD rather\n" + " BDCSVD: true Use the Bidiagonal Divide and Conquer SVD\n" " rather than RedSVD; this is faster and more\n" " accurate than the default RedSVD but slower\n" " Traj: true Perform the SVD of the trajectory matrix\n" " rather than the more computationally SVD\n" - " of the trajectory matrix. Do not use this\n" - " with the default RedSVD algorithm. Use either\n" - " either Jacobi or BDCSVD for accuracy.\n" + " of the trajectory matrix. In practice, this\n" + " sufficiently accurate for all PC orders that\n" + " are typically found to have dynamical content.\n" + " rank: 100 The default rank for the randomized SVD. The\n" + " default value will give decent accuray with a\n" + " small computational overhead and will be a good\n" + " choice for most applications.\n" " RedSym: true Use the randomized symmetric eigenvalue solver\n" " RedSym rather rather than RedSVD. The main use\n" " for these various SVD algorithm toggles is for\n" From 49d8e47c50e54c4b13007a642ac588509d8a2da8 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 22 Mar 2024 17:04:10 -0400 Subject: [PATCH 034/167] Tell the user whether Trajectory method is used. We may want to remove this chatter in the long run but useful since the default changed [no ci] --- expui/expMSSA.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/expui/expMSSA.cc b/expui/expMSSA.cc index 309d60f07..9ca2602ad 100644 --- a/expui/expMSSA.cc +++ b/expui/expMSSA.cc @@ -1566,6 +1566,9 @@ namespace MSSA { // if (params["Traj"]) trajectory = params["Traj"].as(); + std::cout << "Trajectory is " << std::boolalpha << trajectory + << std::endl; + // Eigen OpenMP reporting // static bool firstTime = true; From 79f4bb47be405664ca6d87b33ca8f183b4ba4ed3 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 22 Mar 2024 18:08:25 -0400 Subject: [PATCH 035/167] Fix a few docstring typos [no ci] --- pyEXP/MSSAWrappers.cc | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/pyEXP/MSSAWrappers.cc b/pyEXP/MSSAWrappers.cc index d5cec2bcb..69fcf0ec1 100644 --- a/pyEXP/MSSAWrappers.cc +++ b/pyEXP/MSSAWrappers.cc @@ -63,34 +63,36 @@ void MSSAtoolkitClasses(py::module &m) { "construct these simple YAML configurations on the fly. A simple example\n" "is also given below. The boolean parameters are listed below by my\n" "guess of their usefulness to most people:\n\n" - " verbose: false Whether there is report or not\n" + " verbose: false Report on internal progress for debugging\n" " noMean: false If true, do not subtract the mean when\n" - " reading in channels. Valid only for totPow\n" + " reading in channels. Used only for totPow\n" " detrending method.\n" " writeCov: true Write the covariance matrix to a file for\n" " diagnostics since this is not directly\n" - " available from the interface\n" + " available from the interface. Used of if\n" + " Traj: false.\n" " Jacobi: true Use the Jacobi SVD rather than the Random\n" " approximation algorithm from Halko, Martinsson,\n" " and Tropp (RedSVD). This is quite accurate but\n" " _very_ slow\n" " BDCSVD: true Use the Bidiagonal Divide and Conquer SVD\n" " rather than RedSVD; this is faster and more\n" - " accurate than the default RedSVD but slower\n" + " accurate than the default RedSVD but slower.\n" " Traj: true Perform the SVD of the trajectory matrix\n" - " rather than the more computationally SVD\n" - " of the trajectory matrix. In practice, this\n" - " sufficiently accurate for all PC orders that\n" - " are typically found to have dynamical content.\n" - " rank: 100 The default rank for the randomized SVD. The\n" - " default value will give decent accuray with a\n" + " rather than the more computationally intensive\n" + " but more stable SVD of the covariance matrix.\n" + " In practice, this method is sufficiently\n" + " accurate for all PC orders that are typically\n" + " found to have dynamical signal.\n" + " rank: 100 The default rank for the randomized matrix SVD.\n" + " The default value will give decent accuracy with\n" " small computational overhead and will be a good\n" " choice for most applications.\n" " RedSym: true Use the randomized symmetric eigenvalue solver\n" - " RedSym rather rather than RedSVD. The main use\n" - " for these various SVD algorithm toggles is for\n" - " checking the accuracy of the default randomized\n" - " matrix methods.\n" + " RedSym rather rather than RedSVD for the co-\n" + " variance matrix SVD (Traj: false). The main use\n" + " for this is checking the accuracy of the default\n" + " randomized matrix methods.\n" " allchan: true Perform k-mean clustering analysis using all\n" " channels simultaneously\n" " distance: true Compute w-correlation matrix PNG images using\n" From b5df7cc6b047448ce900cc6b8022f11ce58ea4bf Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 23 Mar 2024 17:40:28 -0400 Subject: [PATCH 036/167] Some typo fixes in docstrings only [no ci] --- pyEXP/MSSAWrappers.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyEXP/MSSAWrappers.cc b/pyEXP/MSSAWrappers.cc index 69fcf0ec1..9fdc5658f 100644 --- a/pyEXP/MSSAWrappers.cc +++ b/pyEXP/MSSAWrappers.cc @@ -63,13 +63,13 @@ void MSSAtoolkitClasses(py::module &m) { "construct these simple YAML configurations on the fly. A simple example\n" "is also given below. The boolean parameters are listed below by my\n" "guess of their usefulness to most people:\n\n" - " verbose: false Report on internal progress for debugging\n" + " verbose: false Report on internal progress for debugging only\n" " noMean: false If true, do not subtract the mean when\n" " reading in channels. Used only for totPow\n" " detrending method.\n" " writeCov: true Write the covariance matrix to a file for\n" " diagnostics since this is not directly\n" - " available from the interface. Used of if\n" + " available from the interface. Used only if\n" " Traj: false.\n" " Jacobi: true Use the Jacobi SVD rather than the Random\n" " approximation algorithm from Halko, Martinsson,\n" @@ -81,7 +81,8 @@ void MSSAtoolkitClasses(py::module &m) { " Traj: true Perform the SVD of the trajectory matrix\n" " rather than the more computationally intensive\n" " but more stable SVD of the covariance matrix.\n" - " In practice, this method is sufficiently\n" + " Set to false to get standard covariance SVD.\n" + " In practice, 'Traj: true' is sufficiently\n" " accurate for all PC orders that are typically\n" " found to have dynamical signal.\n" " rank: 100 The default rank for the randomized matrix SVD.\n" @@ -93,7 +94,7 @@ void MSSAtoolkitClasses(py::module &m) { " variance matrix SVD (Traj: false). The main use\n" " for this is checking the accuracy of the default\n" " randomized matrix methods.\n" - " allchan: true Perform k-mean clustering analysis using all\n" + " allchan: true Perform k-means clustering analysis using all\n" " channels simultaneously\n" " distance: true Compute w-correlation matrix PNG images using\n" " w-distance rather than correlation\n" From cca3be4649149bcaa6fa9802d866250b20746227 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sun, 24 Mar 2024 19:15:14 -0400 Subject: [PATCH 037/167] Added a midplane estimator to the disk basis for FieldGenerator [no ci] --- expui/BiorthBasis.H | 3 +++ expui/BiorthBasis.cc | 21 ++++++++++++++----- exputil/EmpCylSL.cc | 50 ++++++++++++++++++++++++++++++++++++++++++++ include/EmpCylSL.H | 3 +++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/expui/BiorthBasis.H b/expui/BiorthBasis.H index 5c5fe28b1..da9480d4c 100644 --- a/expui/BiorthBasis.H +++ b/expui/BiorthBasis.H @@ -74,6 +74,9 @@ namespace BasisClasses //! Subspace index virtual const std::string harmonic() = 0; + //! Turn on midplane evaluation + bool midplane = false; + public: //! Constructor from YAML node diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index 1383b1c2b..94d49ecad 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -74,6 +74,7 @@ namespace BasisClasses labels.push_back("rad force"); labels.push_back("ver force"); labels.push_back("azi force"); + if (midplane) labels.push_back("midplane"); } else if (ctype == Coord::Cartesian) { labels.push_back("x force"); labels.push_back("y force"); @@ -822,7 +823,8 @@ namespace BasisClasses "self_consistent", "playback", "coefCompute", - "coefMaster" + "coefMaster", + "midplane" }; Cylindrical::Cylindrical(const YAML::Node& CONF) : @@ -1035,6 +1037,7 @@ namespace BasisClasses if (conf["mtype" ]) mtype = conf["mtype" ].as(); if (conf["dtype" ]) dtype = conf["dtype" ].as(); if (conf["vflag" ]) vflag = conf["vflag" ].as(); + if (conf["midplane" ]) midplane = conf["midplane" ].as(); // Deprecation warning if (conf["density" ]) { @@ -1269,14 +1272,22 @@ namespace BasisClasses // Evaluate in cylindrical coordinates std::vector Cylindrical::cyl_eval(double R, double z, double phi) { - double tdens0, tdens, tpotl0, tpotl, tpotR, tpotz, tpotp; + double tdens0, tdens, tpotl0, tpotl, tpotR, tpotz, tpotp, height; sl->accumulated_eval(R, z, phi, tpotl0, tpotl, tpotR, tpotz, tpotp); tdens = sl->accumulated_dens_eval(R, z, phi, tdens0); - return - {tdens0, tdens - tdens0, tdens, - tpotl0, tpotl - tpotl0, tpotl, tpotR, tpotz, tpotp}; + if (midplane) { + height = sl->accumulated_midplane_eval(R, -4.0*hcyl, 4.0*hcyl, phi); + + return + {tdens0, tdens - tdens0, tdens, + tpotl0, tpotl - tpotl0, tpotl, tpotR, tpotz, tpotp, height}; + } else { + return + {tdens0, tdens - tdens0, tdens, + tpotl0, tpotl - tpotl0, tpotl, tpotR, tpotz, tpotp}; + } } void Cylindrical::accumulate(double x, double y, double z, double mass) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index df2af62c3..22b293dbc 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -5154,6 +5154,56 @@ double EmpCylSL::accumulated_dens_eval(double r, double z, double phi, +double EmpCylSL::accumulated_midplane_eval(double r, double zmin, double zmax, + double phi, int num) +{ + if (!coefs_made_all()) { + if (VFLAG>3) + std::cerr << "Process " << myid << ": in EmpCylSL::accumlated_midplane_eval, " + << "calling make_coefficients()" << std::endl; + make_coefficients(); + } + + std::vector tdens(num); + double dz = (zmax - zmin)/(num - 1), d0; + + // Compute density in a column + // + for (int k=0; k Date: Mon, 25 Mar 2024 11:07:13 -0400 Subject: [PATCH 038/167] Control midplane toggle from the FieldGenerator interface not from the Basis interface [no ci] --- expui/BasisFactory.H | 12 ++++++++++++ expui/BiorthBasis.H | 3 --- expui/BiorthBasis.cc | 6 ++---- expui/FieldGenerator.H | 16 +++++++++++++++- expui/FieldGenerator.cc | 8 ++++++++ exputil/EmpCylSL.cc | 21 +++++++++++---------- pyEXP/FieldWrappers.cc | 20 ++++++++++++++++++++ 7 files changed, 68 insertions(+), 18 deletions(-) diff --git a/expui/BasisFactory.H b/expui/BasisFactory.H index 0c8b6024b..089887934 100644 --- a/expui/BasisFactory.H +++ b/expui/BasisFactory.H @@ -117,6 +117,11 @@ namespace BasisClasses //! Get field labels virtual std::vector getFieldLabels(const Coord ctype) = 0; + //! Turn on midplane evaluation + bool midplane = false; + + //! Midplane escursion parameter + double colh = 4.0; public: @@ -219,6 +224,13 @@ namespace BasisClasses //! Clear the particle selector callback void clrSelector() { ftor = nullptr; } + + //! Turn on/off midplane evaluation (only effective for disk basis) + void setMidplane(bool value) { midplane = value; } + + //! Height above/below the plane for midplane search in disk scale + //! lengths + void setColumnHeight(double value) { colh = value; } }; using BasisPtr = std::shared_ptr; diff --git a/expui/BiorthBasis.H b/expui/BiorthBasis.H index da9480d4c..5c5fe28b1 100644 --- a/expui/BiorthBasis.H +++ b/expui/BiorthBasis.H @@ -74,9 +74,6 @@ namespace BasisClasses //! Subspace index virtual const std::string harmonic() = 0; - //! Turn on midplane evaluation - bool midplane = false; - public: //! Constructor from YAML node diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index 94d49ecad..c5cdd32ea 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -823,8 +823,7 @@ namespace BasisClasses "self_consistent", "playback", "coefCompute", - "coefMaster", - "midplane" + "coefMaster" }; Cylindrical::Cylindrical(const YAML::Node& CONF) : @@ -1037,7 +1036,6 @@ namespace BasisClasses if (conf["mtype" ]) mtype = conf["mtype" ].as(); if (conf["dtype" ]) dtype = conf["dtype" ].as(); if (conf["vflag" ]) vflag = conf["vflag" ].as(); - if (conf["midplane" ]) midplane = conf["midplane" ].as(); // Deprecation warning if (conf["density" ]) { @@ -1278,7 +1276,7 @@ namespace BasisClasses tdens = sl->accumulated_dens_eval(R, z, phi, tdens0); if (midplane) { - height = sl->accumulated_midplane_eval(R, -4.0*hcyl, 4.0*hcyl, phi); + height = sl->accumulated_midplane_eval(R, -colh*hcyl, colh*hcyl, phi); return {tdens0, tdens - tdens0, tdens, diff --git a/expui/FieldGenerator.H b/expui/FieldGenerator.H index f961d4963..3b4873ade 100644 --- a/expui/FieldGenerator.H +++ b/expui/FieldGenerator.H @@ -25,7 +25,13 @@ namespace Field void check_times(CoefClasses::CoefsPtr coefs); //! Using MPI - bool use_mpi; + bool use_mpi = false; + + //! Perform midplane evaluation + bool midplane = false; + + //! Midplane search height + double colheight = 4.0; public: @@ -107,6 +113,14 @@ namespace Field const std::string prefix, const std::string outdir="."); //@} + //! Turn on/off midplane evaluation (only effective for disk basis + //! and slices) + void setMidplane(bool value) { midplane = value; } + + //! Height above/below the plane for midplane search in disk scale + //! lengths + void setColumnHeight(double value) { colheight = value; } + }; } diff --git a/expui/FieldGenerator.cc b/expui/FieldGenerator.cc index 64e3162cd..3f1631a0b 100644 --- a/expui/FieldGenerator.cc +++ b/expui/FieldGenerator.cc @@ -293,6 +293,11 @@ namespace Field FieldGenerator::slices(BasisClasses::BasisPtr basis, CoefClasses::CoefsPtr coefs) { + // Set midplane evaluation parameters + // + basis->setMidplane(midplane); + basis->setColumnHeight(colheight); + // Check // check_times(coefs); @@ -462,6 +467,9 @@ namespace Field } } + // Toggle off midplane evaluation + basis->setMidplane(false); + return ret; } diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 22b293dbc..7eba87867 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -5165,12 +5165,12 @@ double EmpCylSL::accumulated_midplane_eval(double r, double zmin, double zmax, } std::vector tdens(num); - double dz = (zmax - zmin)/(num - 1), d0; + double dz = (zmax - zmin)/(num - 1); // Compute density in a column // for (int k=0; k Date: Mon, 25 Mar 2024 17:31:48 -0400 Subject: [PATCH 039/167] A WIP implementation of SlabSL with pyEXP. It compiles but is completely untested otherwise. --- expui/BiorthBasis.H | 139 ++++++++++++++ expui/BiorthBasis.cc | 412 ++++++++++++++++++++++++++++++++++++++++++ expui/CoefStruct.H | 21 ++- expui/CoefStruct.cc | 39 ++++ expui/Coefficients.H | 131 ++++++++++++++ expui/Coefficients.cc | 379 ++++++++++++++++++++++++++++++++++++++ exputil/SLGridMP2.cc | 40 ++++ include/SLGridMP2.H | 4 + src/SlabSL.H | 20 +- src/SlabSL.cc | 75 ++++++-- 10 files changed, 1233 insertions(+), 27 deletions(-) diff --git a/expui/BiorthBasis.H b/expui/BiorthBasis.H index 5c5fe28b1..5cbc559ae 100644 --- a/expui/BiorthBasis.H +++ b/expui/BiorthBasis.H @@ -2,6 +2,8 @@ #define _BiorthBasis_H #include +#include + #include #include // For 3d rectangular grids #include @@ -573,6 +575,143 @@ namespace BasisClasses } }; + /** + Uses the SLGridSlab basis to evaluate expansion coeffients and + provide potential and density basis fields + */ + class Slab : public BiorthBasis + { + + public: + + using BasisMap = std::map; + using BasisArray = std::vector>; + + private: + + //! Wave function constant + static constexpr std::complex kfac{0.0, 2.0*M_PI}; + + //! Initialization helper + void initialize(); + + //! Orthogonal basis instance + std::shared_ptr ortho; + + //! Minimum expansion order for restriction + int nminx, nminy, nminz; + + //! Maximum expansion order for construction + int nmaxx, nmaxy, nmaxz, imx, imy, imz; + + //! Number of integration knots for orthogonal check + int knots; + + //! SLGridSlab mesh size + int NGRID = 100; + + //! Scale height for slab + double hslab; + + //! Upper vertical bound for slab + double zmax; + + //! Number of particles used to compute grid + unsigned used; + + using coefType = Eigen::Tensor, 3>; + + coefType expcoef; + + //! Notal mass on grid + double totalMass; + + //! Number of particles + int npart; + + protected: + + //! Evaluate basis in Cartesian coordinates + virtual std::vector + crt_eval(double x, double y, double z); + + //! Evaluate basis in spherical coordinates. Conversion from the + //! Cartesian evaluation above. + virtual std::vector + sph_eval(double r, double costh, double phi); + + //! Evaluate basis in cylindrical coordinates + virtual std::vector + cyl_eval(double r, double z, double phi); + + //! Load coefficients into the new CoefStruct + virtual void load_coefs(CoefClasses::CoefStrPtr coefs, double time); + + //! Set coefficients + virtual void set_coefs(CoefClasses::CoefStrPtr coefs); + + //! Valid keys for YAML configurations + static const std::set valid_keys; + + //! Return readable class name + virtual const std::string classname() { return "Slab";} + + //! Readable index name + virtual const std::string harmonic() { return "n";} + + //! Evaluate field + std::tuple + eval(double x, double y, double z); + + public: + + //! Constructor from YAML node + Slab(const YAML::Node& conf); + + //! Constructor from YAML string + Slab(const std::string& confstr); + + //! Destructor + virtual ~Slab(void) {} + + //! Zero out coefficients to prepare for a new expansion + void reset_coefs(void); + + //! Make coefficients after accumulation + void make_coefs(void); + + //! Accumulate new coefficients + virtual void accumulate(double x, double y, double z, double mass); + + //! Return current maximum harmonic order in expansion + Eigen::Vector3i getNmax() { return {nmaxx, nmaxy, nmaxz}; } + + //! Compute the orthogonality of the basis by returning inner + //! produce matrices + std::vector orthoCheck(); + + //! Biorthogonality sanity check + bool orthoTest() + { + auto ret = orthoCheck(); + double worst = 0.0; + for (auto c : ret) { + for (int n1=0; n1(val, worst); + } + } + } + + std::cout << "Slab::orthoTest: worst=" << worst << std::endl; + if (worst > __EXP__::orthoTol) return false; + return true; + } + + }; + /** Uses the BiorthCyl basis to evaluate expansion coeffients and provide potential and density basis fields diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index c5cdd32ea..2715ede55 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -1897,6 +1897,418 @@ namespace BasisClasses return ret; } + const std::set + Slab::valid_keys = { + "nmaxx", + "nmaxy", + "nmaxz", + "nminx", + "nminy", + "hslab", + "zmax", + "knots", + "verbose", + "check", + "method" + }; + + Slab::Slab(const YAML::Node& CONF) : BiorthBasis(CONF, "slab") + { + initialize(); + } + + Slab::Slab(const std::string& confstr) : BiorthBasis(confstr, "slab") + { + initialize(); + } + + void Slab::initialize() + { + nminx = std::numeric_limits::max(); + nminy = std::numeric_limits::max(); + + nmaxx = 6; + nmaxy = 6; + nmaxz = 6; + + knots = 40; + + // Check orthogonality (false by default because of its long + // runtime and very low utility) + // + bool check = false; + + // Check for unmatched keys + // + auto unmatched = YamlCheck(conf, valid_keys); + if (unmatched.size()) + throw YamlConfigError("Basis::Basis::Slab", "parameter", unmatched, __FILE__, __LINE__); + + // Default cachename, empty by default + // + std::string cachename; + + // Assign values from YAML + // + try { + if (conf["nminx"]) nminx = conf["nminx"].as(); + if (conf["nminy"]) nminy = conf["nminy"].as(); + + if (conf["nmaxx"]) nmaxx = conf["nmaxx"].as(); + if (conf["nmaxy"]) nmaxy = conf["nmaxy"].as(); + if (conf["nmaxz"]) nmaxz = conf["nmaxz"].as(); + + if (conf["hslab"]) hslab = conf["hslab"].as(); + if (conf["zmax "]) zmax = conf["zmax" ].as(); + + if (conf["knots"]) knots = conf["knots"].as(); + + if (conf["check"]) check = conf["check"].as(); + } + catch (YAML::Exception & error) { + if (myid==0) std::cout << "Error parsing parameter stanza for <" + << name << ">: " + << error.what() << std::endl + << std::string(60, '-') << std::endl + << conf << std::endl + << std::string(60, '-') << std::endl; + + throw std::runtime_error("Slab: error parsing YAML"); + } + + // Finally, make the basis + // + SLGridSlab::mpi = 1; + SLGridSlab::ZBEG = 0.0; + SLGridSlab::ZEND = 0.1; + SLGridSlab::H = hslab; + + int nnmax = (nmaxx > nmaxy) ? nmaxx : nmaxy; + + ortho = std::make_shared(nnmax, nmaxz, NGRID, zmax); + + // Orthogonality sanity check + // + if (check) orthoTest(); + + // Get max threads + // + int nthrds = omp_get_max_threads(); + + imx = 2*nmaxx + 1; + imy = 2*nmaxy + 1; + imz = nmaxz; + + expcoef.resize(imx, imy, imz); + expcoef.setZero(); + + used = 0; + + // Set cartesian coordindates + // + coordinates = Coord::Cartesian; + } + + void Slab::reset_coefs(void) + { + expcoef.setZero(); + totalMass = 0.0; + used = 0; + } + + + void Slab::load_coefs(CoefClasses::CoefStrPtr coef, double time) + { + auto cf = dynamic_cast(coef.get()); + + cf->nmaxx = nmaxx; + cf->nmaxy = nmaxy; + cf->nmaxz = nmaxz; + cf->time = time; + + cf->allocate(); + + *cf->coefs = expcoef; + } + + void Slab::set_coefs(CoefClasses::CoefStrPtr coef) + { + // Sanity check on derived class type + // + if (typeid(*coef) != typeid(CoefClasses::SlabStruct)) + throw std::runtime_error("Slab::set_coefs: you must pass a CoefClasses::SlabStruct"); + + // Sanity check on dimensionality + // + { + auto cc = dynamic_cast(coef.get()); + auto d = cc->coefs->dimensions(); + if (d[0] != 2*nmaxx+1 or d[1] != 2*nmaxy+1 or d[2] != 2*nmaxz+1) { + std::ostringstream sout; + sout << "Slab::set_coefs: the basis has (2*nmaxx+1, 2*nmaxy+1, 2*nmaxz+1)=(" + << 2*nmaxx+1 << ", " + << 2*nmaxy+1 << ", " + << 2*nmaxz+1 + << "). The coef structure has dimension=(" + << d[0] << ", " << d[1] << ", " << d[2] << ")"; + + throw std::runtime_error(sout.str()); + } + } + + auto cf = dynamic_cast(coef.get()); + expcoef = *cf->coefs; + + coefctr = {0.0, 0.0, 0.0}; + } + + void Slab::accumulate(double x, double y, double z, double mass) + { + // Truncate to slab with sides in [0,1] + if (x<0.0) + x += std::floor(-x) + 1.0; + else + x -= std::floor( x); + + if (y<0.0) + y += std::floor(-y) + 1.0; + else + y -= std::floor( y); + + // Recursion multipliers + Eigen::Vector3cd step + {std::exp(-kfac*x), std::exp(-kfac*y), std::exp(-kfac*z)}; + + // Initial values for recursion + Eigen::Vector3cd init + {std::exp(-kfac*(x*nmaxx)), + std::exp(-kfac*(y*nmaxy)), + std::exp(-kfac*(z*nmaxz))}; + + Eigen::Vector3cd curr(init); + for (int ix=0; ix<=2*nmaxx; ix++, curr(0)*=step(0)) { + curr(1) = init(1); + for (int iy=0; iy<=2*nmaxy; iy++, curr(1)*=step(1)) { + curr(2) = init(2); + for (int iz=0; iz<=2*nmaxz; iz++, curr(2)*=step(2)) { + + // Compute wavenumber; recall that the coefficients are + // stored as: -nmax,-nmax+1,...,0,...,nmax-1,nmax + // + int ii = ix-nmaxx; + int jj = iy-nmaxy; + int kk = iz-nmaxz; + + // Normalization + double norm = 1.0/sqrt(M_PI*(ii*ii + jj*jj + kk*kk));; + + expcoef(ix, iy, iz) += - mass * curr(0)*curr(1)*curr(2) * norm; + } + } + } + + int ix, iy, iz; // Loop indices + + // Recursion multipliers + std::complex stepx = exp(-kfac*x), facx; + std::complex stepy = exp(-kfac*y), facy; + + // Initial values + std::complex startx = exp(static_cast(nmaxx)*kfac*x); + std::complex starty = exp(static_cast(nmaxy)*kfac*y); + + Eigen::VectorXd zpot(nmaxz); + + for (facx=startx, ix=0; ix nmaxx) { + std::cerr << "Out of bounds: iix=" << ii << std::endl; + } + if (iiy > nmaxy) { + std::cerr << "Out of bounds: iiy=" << jj << std::endl; + } + + if (iix>=iiy) + ortho->get_pot(zpot, z, iix, iiy); + else + ortho->get_pot(zpot, z, iiy, iix); + + + for (iz=0; iz + Slab::eval(double x, double y, double z) + { + // Loop indices + // + int ix, iy, iz; + + // Working values + // + std::complex facx, facy, fac, facf, facd; + const std::complex I(0.0, 1.0); + const double dfac = 2.0*M_PI; + + // Return values + // + std::complex accx(0.0), accy(0.0), accz(0.0), potl(0.0), dens(0.0); + + // Recursion multipliers + // + std::complex stepx = exp(kfac*x); + std::complex stepy = exp(kfac*y); + + // Initial values (note sign change) + // + std::complex startx = exp(-static_cast(nmaxx)*kfac*x); + std::complex starty = exp(-static_cast(nmaxy)*kfac*y); + + Eigen::VectorXd vpot(nmaxz), vfrc(nmaxz), vden(nmaxz); + + for (facx=startx, ix=0; ix nmaxx) { + std::cerr << "Out of bounds: ii=" << ii << std::endl; + } + if (iiy > nmaxy) { + std::cerr << "Out of bounds: jj=" << jj << std::endl; + } + + if (iix>=iiy) { + ortho->get_pot (vpot, z, iix, iiy); + ortho->get_force(vfrc, z, iix, iiy); + ortho->get_dens (vden, z, iix, iiy); + } + else { + ortho->get_pot (vpot, z, iiy, iix); + ortho->get_force(vfrc, z, iiy, iix); + ortho->get_dens (vden, z, iiy, iix); + } + + + for (int iz=0; iz Slab::crt_eval(double x, double y, double z) + { + // Get thread id + int tid = omp_get_thread_num(); + + auto [pot, den, frcx, frcy, frcz] = eval(x, y, z); + + return {0, den, 0, pot, frcx, frcy, frcz}; + } + + std::vector Slab::cyl_eval(double R, double z, double phi) + { + // Get thread id + int tid = omp_get_thread_num(); + + // Cartesian from Cylindrical coordinates + double x = R*cos(phi), y = R*sin(phi); + + auto [pot, den, frcx, frcy, frcz] = eval(x, y, z); + + double potR = frcx*cos(phi) + frcy*sin(phi); + double potp = -frcx*sin(phi) + frcy*cos(phi); + double potz = frcz; + + potR *= -1; + potp *= -1; + potz *= -1; + + return {0, den, den, 0, pot, pot, potR, potz, potp}; + } + + std::vector Slab::sph_eval(double r, double costh, double phi) + { + // Get thread id + int tid = omp_get_thread_num(); + + // Spherical from Cylindrical coordinates + double sinth = sqrt(fabs(1.0 - costh*costh)); + double x = r*cos(phi)*sinth, y = r*sin(phi)*sinth, z = r*costh; + + auto [pot, den, frcx, frcy, frcz] = eval(x, y, z); + + double potr = frcx*cos(phi)*sinth + frcy*sin(phi)*sinth + frcz*costh; + double pott = frcx*cos(phi)*costh + frcy*sin(phi)*costh - frcz*sinth; + double potp = -frcx*sin(phi) + frcy*cos(phi); + + potr *= -1; + pott *= -1; + potp *= -1; + + return {0, den, den, 0, pot, pot, potr, pott, potp}; + } + + std::vector Slab::orthoCheck() + { + return ortho->orthoCheck(); + } + const std::set Cube::valid_keys = { "nminx", diff --git a/expui/CoefStruct.H b/expui/CoefStruct.H index 7e96cf28e..12228d7f7 100644 --- a/expui/CoefStruct.H +++ b/expui/CoefStruct.H @@ -166,6 +166,9 @@ namespace CoefClasses //! Angular and radial dimension int nmaxx, nmaxy, nmaxz; + //! Basis dimensions + int nx, ny, nz, dim; + //! Constructor SlabStruct() : nmaxx(0), nmaxy(0), nmaxz(0) { geom = "slab"; } @@ -182,8 +185,11 @@ namespace CoefClasses //! Allocate storage arrays void allocate() { - store.resize(nmaxx*nmaxy*nmaxz); - coefs = std::make_shared(store.data(), nmaxx, nmaxy, nmaxz); + nx = 2*nmaxx + 1; + ny = 2*nmaxy + 1; + nz = nmaxz; + store.resize(nx*ny*nz); + coefs = std::make_shared(store.data(), nx, ny, nz); } @@ -191,9 +197,14 @@ namespace CoefClasses void assign(const Eigen::Tensor, 3>& dat) { const auto& d = dat.dimensions(); - nmaxx = d[0]; - nmaxy = d[1]; - nmaxz = d[2]; + nx = d[0]; + ny = d[1]; + nz = d[2]; + + nmaxx = (nx - 1)/2; + nmaxy = (ny - 1)/2; + nmaxz = nz; + allocate(); *coefs = dat; } diff --git a/expui/CoefStruct.cc b/expui/CoefStruct.cc index 293e4b39d..20cb6b937 100644 --- a/expui/CoefStruct.cc +++ b/expui/CoefStruct.cc @@ -38,6 +38,18 @@ namespace CoefClasses throw std::runtime_error("SphStruct::create: nmax must be >0"); } + void SlabStruct::create() + { + if (nmaxx>=0 and nmaxy>=0 and nmaxz>0) { + nx = 2*nmaxx + 1; + ny = 2*nmaxy + 1; + nz = nmaxz; + dim = nx * ny * nz; + coefs = std::make_shared(store.data(), nx, ny, nz); + } else + throw std::runtime_error("SlabStruct::create: all dimensions must be >=0"); + } + void CubeStruct::create() { if (nmaxx>0 and nmaxy>0 and nmaxz>0) { @@ -109,6 +121,26 @@ namespace CoefClasses return ret; } + std::shared_ptr SlabStruct::deepcopy() + { + auto ret = std::make_shared(); + + copyfields(ret); + + assert(("SlabStruct::deepcopy dimension mismatch", dim == store.size())); + + ret->coefs = std::make_shared(ret->store.data(), nx, ny, nz); + ret->nmaxx = nmaxx; + ret->nmaxy = nmaxy; + ret->nmaxz = nmaxz; + ret->nx = nx; + ret->ny = ny; + ret->nz = nz; + ret->dim = dim; + + return ret; + } + std::shared_ptr CubeStruct::deepcopy() { auto ret = std::make_shared(); @@ -434,6 +466,13 @@ namespace CoefClasses return true; } + bool SlabStruct::read(std::istream& in, bool exp_type, bool verbose) + { + std::cout << "SlabStruct: no native coefficient format for this class" << std::endl; + return false; + } + + bool CubeStruct::read(std::istream& in, bool exp_type, bool verbose) { std::cout << "CubeStruct: no native coefficient format for this class" << std::endl; diff --git a/expui/Coefficients.H b/expui/Coefficients.H index 3f0607584..6cc6c0df8 100644 --- a/expui/Coefficients.H +++ b/expui/Coefficients.H @@ -482,6 +482,137 @@ namespace CoefClasses }; + /** Derived class for slab coefficients */ + class SlabCoefs : public Coefs + { + protected: + //! 3d coefficient data type + typedef + Eigen::Tensor, 3> Eigen3d; + + //! Data + Eigen3d dat; + + //! Parameters + int NmaxX, NmaxY, NmaxZ; + + //! The coefficient DB + std::map coefs; + + //! Read the coefficients + virtual void readNativeCoefs(const std::string& file, + int stride, double tmin, double tmax); + + //! Get the YAML config + virtual std::string getYAML(); + + //! Write parameter attributes + virtual void WriteH5Params(HighFive::File& file); + + //! Write coefficient data in H5 + virtual unsigned WriteH5Times(HighFive::Group& group, unsigned count); + + public: + + //! Constructor + SlabCoefs(bool verbose=false) : Coefs("slab", verbose) {} + + //! H5 constructor + SlabCoefs(HighFive::File& file, int stride=1, + double tmin=-std::numeric_limits::max(), + double tmax= std::numeric_limits::max(), + bool verbose=false); + + //! Copy constructor + SlabCoefs(SlabCoefs& p) : Coefs(p) { coefs = p.coefs; } + + //! Clear coefficient container + virtual void clear() { coefs.clear(); } + + //! Add a coefficient structure to the container + virtual void add(CoefStrPtr coef); + + //! Get coefficient matrix at given time + virtual Eigen::VectorXcd& getData(double time); + + //! Native version + virtual Eigen3d& getTensor(double time); + + //! Set coefficient matrix at given time + virtual void setData(double time, const Eigen::VectorXcd& dat); + + //! Native version + virtual void setTensor(double time, const Eigen3d& dat); + + //! Interpolate coefficient tensor at given time + std::tuple, 3>&, bool> + interpolate(double time) + { + auto ret = Coefs::interpolate(time); + dat = Eigen::TensorMap(std::get<0>(ret).data(), NmaxX, NmaxY, NmaxZ); + return {dat, std::get<1>(ret)}; + } + + //! Get coefficient structure at a given time + virtual std::shared_ptr getCoefStruct(double time) + { return coefs[roundTime(time)]; } + + + //! Dump to ascii list for testing + void dump(int nmaxx, int nmaxy, int nmaxz); + + //! Get list of coefficient times + virtual std::vector Times() + { + times.clear(); + for (auto t : coefs) times.push_back(t.first); + return times; + } + + //! Get all coefficients indexed in time + Eigen::Tensor, 4> getAllCoefs(); + + /** Get power for the coefficient DB as a function of harmonic + index by dimension: x, y, z specified as a quoted char */ + Eigen::MatrixXd& Power + (char d, int min=0, int max=std::numeric_limits::max()); + + /** Get power for the coefficient DB as a function of harmonic + index. Time as rows, harmonics as columns. 'x' dimension by + default. + + @param min is the minimum radial order + @param max is the maximum radial order + */ + Eigen::MatrixXd& Power + (int min=0, int max=std::numeric_limits::max()) + { return Power('x', min, max); } + + //! Make a list of all index keys + virtual std::vector makeKeys(); + virtual std::vector makeKeys(Key k) { return makeKeys(); } + + //! Compare two collections of stanzas (this is for testing only) + virtual bool CompareStanzas(std::shared_ptr check); + + //! Copy all of the data; make new instances of shared pointer + //! objects + virtual std::shared_ptr deepcopy(); + + virtual void zerodata() { + for (auto v : coefs) v.second->zerodata(); + } + + //! Get nmax + double nmax(char d) const { + if (d=='x') return NmaxX; + if (d=='y') return NmaxY; + if (d=='z') return NmaxZ; + throw std::runtime_error("SlabCoefs: error in nmax accessor"); + } + }; + + /** Derived class for cube coefficients */ class CubeCoefs : public Coefs { diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index ed93a3cbb..aabed5c04 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -182,6 +182,26 @@ namespace CoefClasses return ret; } + std::shared_ptr SlabCoefs::deepcopy() + { + auto ret = std::make_shared(); + + // Copy the base-class fields + copyfields(ret); + + // Copy the local structures from the map to the struct pointers + // by copyfing fields, not the pointer + for (auto v : coefs) + ret->coefs[v.first] = + std::dynamic_pointer_cast(v.second->deepcopy()); + + ret->NmaxX = NmaxX; + ret->NmaxY = NmaxY; + ret->NmaxZ = NmaxZ; + + return ret; + } + std::shared_ptr CubeCoefs::deepcopy() { auto ret = std::make_shared(); @@ -1120,6 +1140,354 @@ namespace CoefClasses return {powerE, powerO}; } + SlabCoefs::SlabCoefs(HighFive::File& file, int stride, + double Tmin, double Tmax, bool verbose) : + Coefs("cube", verbose) + { + unsigned count; + std::string config; + + file.getAttribute("name" ).read(name ); + file.getAttribute("nmaxx" ).read(NmaxX ); + file.getAttribute("nmaxy" ).read(NmaxY ); + file.getAttribute("nmaxz" ).read(NmaxZ ); + file.getAttribute("config" ).read(config); + file.getDataSet ("count" ).read(count ); + + // Open the snapshot group + // + auto snaps = file.getGroup("snapshots"); + + for (unsigned n=0; n ctr; + if (stanza.hasAttribute("Center")) { + stanza.getAttribute("Center").read(ctr); + } + + if (Time < Tmin or Time > Tmax) continue; + + Eigen::VectorXcd in; + stanza.getDataSet("coefficients").read(in); + + Eigen::TensorMap dat(in.data(), 2*NmaxX+1, 2*NmaxY+1, 2*NmaxZ+1); + + // Pack the data into the coefficient variable + // + auto coef = std::make_shared(); + + coef->assign(dat); + coef->time = Time; + + coefs[roundTime(Time)] = coef; + } + + times.clear(); + for (auto t : coefs) times.push_back(t.first); + } + + Eigen::VectorXcd& SlabCoefs::getData(double time) + { + auto it = coefs.find(roundTime(time)); + + if (it == coefs.end()) { + arr.resize(0); + } else { + arr = it->second->store; + } + + return arr; + } + + void SlabCoefs::setData(double time, const Eigen::VectorXcd& dat) + { + auto it = coefs.find(roundTime(time)); + + if (it == coefs.end()) { + std::ostringstream str; + str << "CylCoefs::setMatrix: requested time=" << time << " not found"; + throw std::runtime_error(str.str()); + } else { + it->second->store = dat; + it->second->coefs = std::make_shared + (it->second->store.data(), 2*NmaxX+1, 2*NmaxY+1, NmaxZ); + } + } + + void SlabCoefs::setTensor(double time, const Eigen3d& dat) + { + auto it = coefs.find(roundTime(time)); + + if (it == coefs.end()) { + std::ostringstream str; + str << "CylCoefs::setMatrix: requested time=" << time << " not found"; + throw std::runtime_error(str.str()); + } else { + it->second->allocate(); // Assign storage for the flattened tensor + *it->second->coefs = dat; // Populate using the tensor map + } + } + + SlabCoefs::Eigen3d& SlabCoefs::getTensor(double time) + { + auto it = coefs.find(roundTime(time)); + + if (it == coefs.end()) { + arr.resize(0); + } else { + arr = it->second->store; + dat = Eigen::TensorMap(arr.data(), 2*NmaxX+1, 2*NmaxY+1, NmaxZ); + } + + return dat; + } + + Eigen::Tensor, 4> SlabCoefs::getAllCoefs() + { + Eigen::Tensor, 4> ret; + + auto times = Times(); + + int ntim = times.size(); + + // Resize the tensor + ret.resize(2*NmaxX+1, 2*NmaxY+1, NmaxZ, ntim); + + for (int t=0; tcoefs)(ix, iy, iz); + } + } + } + } + + return ret; + } + + std::vector SlabCoefs::makeKeys() + { + std::vector ret; + if (coefs.size()==0) return ret; + + for (unsigned ix=0; ix<=2*NmaxX; ix++) { + for (unsigned iy=0; iy<=2*NmaxY; iy++) { + for (unsigned iz=0; izsecond->id); + + file.createAttribute("nmaxx", HighFive::DataSpace::From(NmaxX)).write(NmaxX); + file.createAttribute("nmaxy", HighFive::DataSpace::From(NmaxY)).write(NmaxY); + file.createAttribute("nmaxz", HighFive::DataSpace::From(NmaxZ)).write(NmaxZ); + file.createAttribute("forceID", HighFive::DataSpace::From(forceID)).write(forceID); + } + + unsigned SlabCoefs::WriteH5Times(HighFive::Group& snaps, unsigned count) + { + for (auto c : coefs) { + auto C = c.second; + + std::ostringstream stim; + stim << std::setw(8) << std::setfill('0') << std::right << count++; + HighFive::Group stanza = snaps.createGroup(stim.str()); + + // Add time attribute + // + stanza.createAttribute("Time", HighFive::DataSpace::From(C->time)).write(C->time); + + // Add coefficient data + // + HighFive::DataSet dataset = stanza.createDataSet("coefficients", C->store); + } + + return count; + } + + std::string SlabCoefs::getYAML() + { + std::string ret; + if (coefs.size()) { + ret = coefs.begin()->second->buf; + } + return ret; + } + + void SlabCoefs::dump(int nmaxx, int nmaxy, int nmaxz) + { + for (auto c : coefs) { + for (int ix=0; ix(nmaxx, c.second->nmaxx); ix++) { + std::cout << std::setw(18) << c.first << std::setw(5) << ix; + for (int iy=0; iy(nmaxy, c.second->nmaxy); iy++) { + for (int iz=0; iz(nmaxz, c.second->nmaxz); iz++) { + std::cout << std::setw(18) << c.first + << std::setw(5) << ix + << std::setw(5) << iy + << std::setw(5) << iz + << std::setw(18) << (*c.second->coefs)(ix, iy, iz).real() + << std::setw(18) << (*c.second->coefs)(ix, iy, iz).imag() + << std::endl; + } + // Z loop + } + // Y loop + } + // X loop + } + // T loop + } + + Eigen::MatrixXd& SlabCoefs::Power(char d, int min, int max) + { + if (coefs.size()) { + + int nmaxX = coefs.begin()->second->nmaxx; + int nmaxY = coefs.begin()->second->nmaxy; + int nmaxZ = coefs.begin()->second->nmaxz; + + int dim = 0; + if (d == 'x') + dim = 2*nmaxX + 1; + else if (d == 'y') + dim = 2*nmaxY + 1; + else + dim = nmaxZ; + + power.resize(coefs.size(), dim); + power.setZero(); + + int T=0; + for (auto v : coefs) { + if (d=='x') { + for (int ix=0; ix<=2*NmaxX; ix++) { + double val(0.0); + for (int iy=0; iy<=2*NmaxY; iy++) { + if (abs(iy - nmaxY) < min) continue; + for (int iz=0; izcoefs)(ix, iy, iz)); + power(T, ix) += val * val; + } + } + } + } else if (d=='y') { + for (int iy=0; iy<=2*NmaxY; iy++) { + double val(0.0); + for (int ix=0; iy<=2*NmaxX; ix++) { + if (abs(ix - nmaxX) < min) continue; + for (int iz=0; izcoefs)(ix, iy, iz)); + power(T, iy) += val * val; + } + } + } + } else { + for (int iz=0; izcoefs)(ix, iy, iz)); + power(T, iz) += val * val; + } + } + } + } + T++; + } + } else { + power.resize(0, 0); + } + + return power; + } + + void SlabCoefs::readNativeCoefs(const std::string& file, + int stride, double tmin, double tmax) + { + std::runtime_error("SlabCoefs: no native coefficients files"); + } + + bool SlabCoefs::CompareStanzas(CoefsPtr check) + { + bool ret = true; + + auto other = std::dynamic_pointer_cast(check); + + // Check that every time in this one is in the other + for (auto v : coefs) { + if (other->coefs.find(roundTime(v.first)) == other->coefs.end()) { + std::cout << "Can't find Time=" << v.first << std::endl; + ret = false; + } + } + + if (not ret) { + std::cout << "Times in other coeffcients are:"; + for (auto v : other->Times()) std::cout << " " << v; + std::cout << std::endl; + } + + if (ret) { + std::cout << "Times are the same, now checking parameters at each time" + << std::endl; + for (auto v : coefs) { + auto it = other->coefs.find(v.first); + if (v.second->nmaxx != it->second->nmaxx) ret = false; + if (v.second->nmaxy != it->second->nmaxy) ret = false; + if (v.second->nmaxz != it->second->nmaxz) ret = false; + if (v.second->time != it->second->time ) ret = false; + } + } + + if (ret) { + std::cout << "Parameters are the same, now checking coefficients" + << std::endl; + for (auto v : coefs) { + auto it = other->coefs.find(v.first); + auto & cv = *(v.second->coefs); + auto & ci = *(it->second->coefs); + auto dim = cv.dimensions(); // This is an Eigen::Tensor map + for (int i=0; itime)] = p; } + void SlabCoefs::add(CoefStrPtr coef) + { + auto p = std::dynamic_pointer_cast(coef); + if (not p) throw std::runtime_error("SlabCoefs::add: Null coefficient structure, nothing added!"); + + NmaxX = p->nmaxx; + NmaxY = p->nmaxy; + NmaxZ = p->nmaxz; + coefs[roundTime(coef->time)] = p; + } + void TableData::add(CoefStrPtr coef) { auto p = std::dynamic_pointer_cast(coef); diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index c8b5e5d88..6d6603e51 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -5223,6 +5223,46 @@ void SLGridSlab::mpi_unpack_table(void) MPI_DOUBLE, MPI_COMM_WORLD); } +std::vector SLGridSlab::orthoCheck(int num) +{ + // Gauss-Legendre knots and weights + LegeQuad lw(num); + + // Get the scaled coordinate limits + double ximin = z_to_xi(-zmax); + double ximax = z_to_xi( zmax); + + // Initialize the return matrices + std::vector ret((numk+1)*(numk+2)/2); + for (auto & v : ret) { + v.resize(numz, numz); + v.setZero(); + } + + + Eigen::VectorXd vpot(numz), vden(numz); + +#pragma omp parallel for + for (int i=0; i orthoCheck(int knots=40); + //@} }; diff --git a/src/SlabSL.H b/src/SlabSL.H index 11e5dcfd9..5888e9fba 100644 --- a/src/SlabSL.H +++ b/src/SlabSL.H @@ -2,6 +2,11 @@ #define _SlabSL_H #include + +#include +#include + +#include #include #include #include @@ -24,10 +29,13 @@ struct SlabSLCoefHeader { private: - SLGridSlab *grid; + std::shared_ptr grid; + + //! Coefficients are a 3-tensor + using coefType = Eigen::Tensor, 3>; - std::vector expccof; - std::vector> trig; + //! Current coefficient tensor + std::vector expccof; int nminx, nminy; int nmaxx, nmaxy, nmaxz; @@ -53,6 +61,9 @@ private: void * determine_coefficients_thread(void * arg); void * determine_acceleration_and_potential_thread(void * arg); + //! Coefficient container instance for writing HDF5 + CoefClasses::SlabCoefs slabCoefs; + // Biorth ID static const int ID=1; @@ -75,6 +86,9 @@ public: //! Destructor virtual ~SlabSL(); + //! Coefficient output + void dump_coefs_h5(const std::string& file); + //! Print coefficients to output stream void dump_coefs(ostream& out); }; diff --git a/src/SlabSL.cc b/src/SlabSL.cc index e6e8c24f9..6881715bf 100644 --- a/src/SlabSL.cc +++ b/src/SlabSL.cc @@ -1,9 +1,8 @@ +#include #include #include "expand.H" -#include - #include const std::set @@ -36,7 +35,7 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) int nnmax = (nmaxx > nmaxy) ? nmaxx : nmaxy; - grid = new SLGridSlab(nnmax, nmaxz, NGRID, zmax); + grid = std::make_shared(nnmax, nmaxz, NGRID, zmax); imx = 1+2*nmaxx; imy = 1+2*nmaxy; @@ -44,7 +43,7 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) jmax = imx*imy*imz; expccof.resize(nthrds); - for (auto & v : expccof) v.resize(jmax); + for (auto & v : expccof) v.resize(imx, imy, imz); dfac = 2.0*M_PI; kfac = std::complex(0.0, dfac); @@ -60,7 +59,7 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) SlabSL::~SlabSL() { - delete grid; + // Nothing } void SlabSL::initialize() @@ -108,11 +107,12 @@ void SlabSL::determine_coefficients(void) exp_thread_fork(true); - int used1 = 0; + int used1 = 0, rank = expccof[0].size(); used = 0; for (int i=1; iMass(i)*adb* + // +--- density in orthogonal series + // | is 4.0*M_PI rho + // v + expccof[id](ix, iy, iz) += -4.0*M_PI*cC->Mass(i)*adb* facx*facy*zpot[id][iz+1]; } } @@ -215,7 +212,7 @@ void SlabSL::get_acceleration_and_potential(Component* C) void * SlabSL::determine_acceleration_and_potential_thread(void * arg) { - int ix, iy, iz, iix, iiy, ii, jj, indx; + int ix, iy, iz, iix, iiy, ii, jj; std::complex fac, startx, starty, facx, facy, potl, facf; std::complex stepx, stepy; std::complex accx, accy, accz; @@ -272,10 +269,8 @@ void * SlabSL::determine_acceleration_and_potential_thread(void * arg) for (iz=0; iz(); + + cur->time = tnow; + cur->geom = geoname[geometry]; + cur->id = id; + cur->time = tnow; + cur->nmaxx = nmaxx; + cur->nmaxy = nmaxy; + cur->nmaxz = nmaxz; + + cur->allocate(); // Set the storage and copy the + // coefficients through the map + *cur->coefs = expccof[0]; + + // Check if file exists + // + if (std::filesystem::exists(file)) { + slabCoefs.clear(); + slabCoefs.add(cur); + slabCoefs.ExtendH5Coefs(file); + } + // Otherwise, extend the existing HDF5 file + // + else { + // Copy the YAML config. We only need this on the first call. + std::ostringstream sout; sout << conf; + cur->buf = sout.str(); // Copy to CoefStruct buffer + + // Add the name attribute. We only need this on the first call. + slabCoefs.setName(component->name); + + // And the new coefficients and write the new HDF5 + slabCoefs.clear(); + slabCoefs.add(cur); + slabCoefs.WriteH5Coefs(file); + } +} + + void SlabSL::dump_coefs(ostream& out) { coefheader.time = tnow; From 66ac76b5a58eb1e0c67e9ac2b8e8bca71c70af8b Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 26 Mar 2024 10:27:39 -0400 Subject: [PATCH 040/167] Missing files from yesterday's commit [no ci] --- pyEXP/BasisWrappers.cc | 143 +++++++++++++++++++++++++++++++++++++---- src/SlabSL.H | 3 +- src/SlabSL.cc | 58 ++++++++--------- 3 files changed, 159 insertions(+), 45 deletions(-) diff --git a/pyEXP/BasisWrappers.cc b/pyEXP/BasisWrappers.cc index 9083bc0b3..926ccec09 100644 --- a/pyEXP/BasisWrappers.cc +++ b/pyEXP/BasisWrappers.cc @@ -23,7 +23,7 @@ void BasisFactoryClasses(py::module &m) forces and, together with the FieldGenerator, surfaces and fields for visualization. - Six bases are currently implemented: + Seven bases are currently implemented: 1. SphericalSL, the Sturm-Liouiville spherical basis; @@ -32,13 +32,17 @@ void BasisFactoryClasses(py::module &m) 3. FlatDisk, an EOF rotation of the finite Bessel basis; and - 4. Cube, a periodic cube basis whose functions are the Cartesian + 4. Slab, a biorthogonal basis for a slab geometry with a finite + finite vertical extent. The basis is constructed from direct + solution of the Sturm-Liouville equation. + + 5. Cube, a periodic cube basis whose functions are the Cartesian eigenfunctions of the Cartesian Laplacian: sines and cosines. - 5. FieldBasis, for computing user-provided quantities from a + 6. FieldBasis, for computing user-provided quantities from a phase-space snapshot. - 6. VelocityBasis, for computing the mean field velocity fields from + 7. VelocityBasis, for computing the mean field velocity fields from a phase-space snapshot. This is a specialized version of FieldBasis. Each of these bases take a YAML configuration file as input. These parameter @@ -152,9 +156,11 @@ void BasisFactoryClasses(py::module &m) 3. FlatDisk uses cylindrical coordinates - 4. Cube uses Cartesian coordinates + 4. Slab uses Cartesian coordinates + + 5. Cube uses Cartesian coordinates - 5. FieldBasis and VelocityBasis provides two natural geometries for + 6. FieldBasis and VelocityBasis provides two natural geometries for field evaluation: a two-dimensional (dof=2) polar disk and a three-dimensional (dof=3) spherical geometry that are chosen using the 'dof' parameter. These use cylindrical and spherical @@ -555,6 +561,74 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); }; + class PySlab : public Slab + { + protected: + + std::vector sph_eval(double r, double costh, double phi) override + { + PYBIND11_OVERRIDE(std::vector, Slab, sph_eval, r, costh, phi); + } + + + std::vector cyl_eval(double R, double z, double phi) override + { + PYBIND11_OVERRIDE(std::vector, Slab, cyl_eval, R, z, phi); + } + + std::vector crt_eval(double x, double y, double z) override + { + PYBIND11_OVERRIDE(std::vector, Slab, crt_eval, x, y, z); + } + + void load_coefs(CoefClasses::CoefStrPtr coefs, double time) override + { + PYBIND11_OVERRIDE(void, Slab, load_coefs, coefs, time); + } + + void set_coefs(CoefClasses::CoefStrPtr coefs) override + { + PYBIND11_OVERRIDE(void, Slab, set_coefs, coefs); + } + + const std::string classname() override + { + PYBIND11_OVERRIDE(std::string, Slab, classname); + } + + const std::string harmonic() override + { + PYBIND11_OVERRIDE(std::string, Slab, harmonic); + } + + public: + + // Inherit the constructors + using Slab::Slab; + + std::vector getFields(double x, double y, double z) override + { + PYBIND11_OVERRIDE(std::vector, Slab, getFields, x, y, z); + } + + void accumulate(double x, double y, double z, double mass) override + { + PYBIND11_OVERRIDE(void, Slab, accumulate, x, y, z, mass); + } + + void reset_coefs(void) override + { + PYBIND11_OVERRIDE(void, Slab, reset_coefs,); + } + + void make_coefs(void) override + { + PYBIND11_OVERRIDE(void, Slab, make_coefs,); + } + + }; + + class PyCube : public Cube { protected: @@ -638,7 +712,7 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); py::class_, PyBasis> (m, "Basis") - .def("factory", &BasisClasses::BiorthBasis::factory_string, + .def("factory", &BasisClasses::BiorthBasis::factory_string, R"( Generate a basis from a YAML configuration supplied as a string @@ -969,9 +1043,10 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); Set the coordinate system for force evaluations. The natural coordinates for the basis class are the default; spherical coordinates for SphericalSL, cylindrical coordinates for - Cylindrical and FlatDisk, and Cartesian coordinates for Cube. - This member function can be used to override the default. The - available coorindates are: 'spherical', 'cylindrical', 'cartesian'. + Cylindrical and FlatDisk, and Cartesian coordinates for the Slab + and Cube. This member function can be used to override the default. + The available coorindates are: 'spherical', 'cylindrical', + 'cartesian'. Parameters ---------- @@ -1371,6 +1446,50 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); )", py::arg("cachefile")); + py::class_, PySlab, BasisClasses::BiorthBasis>(m, "Slab") + .def(py::init(), + R"( + Create a slab basis, periodic on the unit square and finite in vertical extent + + Parameters + ---------- + YAMLstring : str + The YAML configuration for the periodic cube basis. The coordinates + are the unit square in x, y with origin at (0, 0) and maximum extent (1, 1) + and maximum vertical extent of -zZmax to zmax. + The default parameters will wave numbers between [-6,...,6] in each + dimension. + + Returns + ------- + Cube + the new instance + )", py::arg("YAMLstring")) + .def("orthoCheck", [](BasisClasses::Cube& A) + { + return A.orthoCheck(); + }, + R"( + Check orthgonality of basis functions by quadrature + + Inner-product matrix of indexed by flattened wave number (nx, ny, nz) where + each of nx is in [-nmaxx, nmaxx], ny is in [-nmaxy, nmaxy] and nz is in + [0, nmaxz-1]. This is an analytic basis so the orthogonality matrix is not a + check of any numerical computation other than the quadrature itself. It is + included for completeness. + + Parameters + ---------- + None + + Returns + ------- + numpy.ndarray + list of numpy.ndarrays + )" + ); + + py::class_, PyCube, BasisClasses::BiorthBasis>(m, "Cube") .def(py::init(), R"( @@ -1400,8 +1519,8 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); each of nx is in [-nmaxx, nmaxx], and so on for ny and nz. Each dimension has dx=2*nmaxx+1 wave numbers and similarly for dy and dz. The index into the array is index=(nx+nmaxx)*dx*dy + (ny+nmaxy)*dy + (nz+nmaxz). This is an - analyic basis so the orthogonality matrix is not a check of andy numerical - computation other than the quadature itself. It is included for completeness. + analyic basis so the orthogonality matrix is not a check of any numerical + computation other than the quadrature itself. It is included for completeness. Parameters ---------- diff --git a/src/SlabSL.H b/src/SlabSL.H index 5888e9fba..f0902d662 100644 --- a/src/SlabSL.H +++ b/src/SlabSL.H @@ -18,6 +18,7 @@ class SlabSL : public PotAccel { //! Header structure for Sturm-Liouville slab expansion +//! Used for deprecated native coefficient files struct SlabSLCoefHeader { double time; double zmax; @@ -49,7 +50,7 @@ private: SlabSLCoefHeader coefheader; - int NGRID; + int NGRID = 100; // Usual evaluation interface diff --git a/src/SlabSL.cc b/src/SlabSL.cc index 6881715bf..8689c88d1 100644 --- a/src/SlabSL.cc +++ b/src/SlabSL.cc @@ -19,7 +19,6 @@ SlabSL::valid_keys = { SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) { id = "Slab (Sturm-Liouville)"; - NGRID = 100; nminx = nminy = 0; nmaxx = nmaxy = nmaxz = 10; zmax = 10.0; @@ -37,9 +36,9 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) grid = std::make_shared(nnmax, nmaxz, NGRID, zmax); - imx = 1+2*nmaxx; - imy = 1+2*nmaxy; - imz = nmaxz; + imx = 1+2*nmaxx; + imy = 1+2*nmaxy; + imz = nmaxz; jmax = imx*imy*imz; expccof.resize(nthrds); @@ -48,8 +47,6 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) dfac = 2.0*M_PI; kfac = std::complex(0.0, dfac); - nnmax = (nmaxx > nmaxy) ? nmaxx : nmaxy; - zpot.resize(nthrds); zfrc.resize(nthrds); @@ -124,7 +121,7 @@ void SlabSL::determine_coefficients(void) void * SlabSL::determine_coefficients_thread(void * arg) { - int ix, iy, iz, iix, iiy, ii, jj, indx; + int ix, iy, iz, iix, iiy; std::complex startx, starty, facx, facy; std::complex stepx, stepy; @@ -134,7 +131,6 @@ void * SlabSL::determine_coefficients_thread(void * arg) int nbeg = nbodies*id/nthrds; int nend = nbodies*(id+1)/nthrds; double adb = cC->Adiabatic(); - double zz; for (int i=nbeg; i nmaxx) { - cerr << "Out of bounds: iix=" << ii << endl; + std::cerr << "Out of bounds: iix=" << ii << std::endl; } if (iiy > nmaxy) { - cerr << "Out of bounds: iiy=" << jj << endl; + std::cerr << "Out of bounds: iiy=" << jj << std::endl; } - zz = cC->Pos(i, 2, Component::Centered); + double zz = cC->Pos(i, 2, Component::Centered); if (iix>=iiy) grid->get_pot(zpot[id], zz, iix, iiy); @@ -187,7 +183,7 @@ void * SlabSL::determine_coefficients_thread(void * arg) grid->get_pot(zpot[id], zz, iiy, iix); - for (iz=0; iz fac, startx, starty, facx, facy, potl, facf; - std::complex stepx, stepy; + int ix, iy; + std::complex fac, facx, facy, potl, facf; std::complex accx, accy, accz; unsigned nbodies = cC->Number(); int id = *((int*)arg); int nbeg = nbodies*id/nthrds; int nend = nbodies*(id+1)/nthrds; - double zz; for (int i=nbeg; iPos(i, 0)); - stepy = exp(kfac*cC->Pos(i, 1)); + std::complex stepx = exp(kfac*cC->Pos(i, 0)); + std::complex stepy = exp(kfac*cC->Pos(i, 1)); // Initial values (note sign change) - startx = exp(-static_cast(nmaxx)*kfac*cC->Pos(i, 0)); - starty = exp(-static_cast(nmaxy)*kfac*cC->Pos(i, 1)); + std::complex startx = exp(-static_cast(nmaxx)*kfac*cC->Pos(i, 0)); + std::complex starty = exp(-static_cast(nmaxy)*kfac*cC->Pos(i, 1)); for (facx=startx, ix=0; ix nmaxx) { - cerr << "Out of bounds: ii=" << ii << endl; + std::cerr << "Out of bounds: ii=" << ii << std::endl; } if (iiy > nmaxy) { - cerr << "Out of bounds: jj=" << jj << endl; + std::cerr << "Out of bounds: jj=" << jj << std::endl; } - zz = cC->Pos(i, 2, Component::Centered); + double zz = cC->Pos(i, 2, Component::Centered); if (iix>=iiy) { grid->get_pot (zpot[id], zz, iix, iiy); @@ -267,7 +261,7 @@ void * SlabSL::determine_acceleration_and_potential_thread(void * arg) } - for (iz=0; iz Date: Tue, 26 Mar 2024 16:13:22 -0400 Subject: [PATCH 041/167] Spelling corrections --- pyEXP/BasisWrappers.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyEXP/BasisWrappers.cc b/pyEXP/BasisWrappers.cc index 926ccec09..ae339f9a7 100644 --- a/pyEXP/BasisWrappers.cc +++ b/pyEXP/BasisWrappers.cc @@ -1454,11 +1454,11 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); Parameters ---------- YAMLstring : str - The YAML configuration for the periodic cube basis. The coordinates - are the unit square in x, y with origin at (0, 0) and maximum extent (1, 1) - and maximum vertical extent of -zZmax to zmax. - The default parameters will wave numbers between [-6,...,6] in each - dimension. + The YAML configuration for the Slab basis. The coordinates are the + unit square in x, y with origin at (0, 0) and maximum extent (1, 1) + and maximum vertical extent of -zmax to zmax. The default + parameters are wave numbers between [-6,...,6] in x, y, and order 6 + for the vertical basis. Returns ------- From 6439c4207821c818e0a424b9e2610fe7dc567373 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 26 Mar 2024 16:26:57 -0400 Subject: [PATCH 042/167] Replace 'master-slave' pattern names by 'root-worker' [no ci] --- exputil/localmpi.cc | 22 +++++++++++----------- include/localmpi.H | 6 +++--- src/Component.cc | 12 ++++++------ src/Orient.cc | 12 ++++++------ src/end.cc | 4 ++-- src/expand.cc | 28 ++++++++++++++-------------- src/global.H | 4 ++-- utils/SL/cyltest.cc | 2 +- utils/SL/qtest.cc | 2 +- utils/SL/slcheck.cc | 2 +- utils/Test/test_barrier.cc | 2 +- 11 files changed, 48 insertions(+), 48 deletions(-) diff --git a/exputil/localmpi.cc b/exputil/localmpi.cc index 987230175..b0ddc8708 100644 --- a/exputil/localmpi.cc +++ b/exputil/localmpi.cc @@ -11,9 +11,9 @@ // // MPI variables // -MPI_Comm MPI_COMM_SLAVE; -MPI_Group world_group, slave_group; -int numprocs=1, slaves, myid=0, proc_namelen; +MPI_Comm MPI_COMM_WORKER; +MPI_Group world_group, worker_group; +int numprocs=1, workers, myid=0, proc_namelen; char processor_name[MPI_MAX_PROCESSOR_NAME]; std::ofstream mpi_debug; @@ -29,18 +29,18 @@ void local_init_mpi(int argc, char **argv) MPI_Get_processor_name(processor_name, &proc_namelen); //========================= - // Make SLAVE communicator + // Make WORKER communicator //========================= - slaves = numprocs - 1; + workers = numprocs - 1; - if (slaves) { + if (workers) { MPI_Comm_group(MPI_COMM_WORLD, &world_group); - std::vector nslaves (slaves); + std::vector nworkers (workers); - for (int n=1; n #include -extern MPI_Comm MPI_COMM_SLAVE; -extern MPI_Group world_group, slave_group; -extern int numprocs, slaves, myid, proc_namelen; +extern MPI_Comm MPI_COMM_WORKER; +extern MPI_Group world_group, worker_group; +extern int numprocs, workers, myid, proc_namelen; extern char processor_name[MPI_MAX_PROCESSOR_NAME]; extern std::ofstream mpi_debug; diff --git a/src/Component.cc b/src/Component.cc index 47fe2901c..609c1b07e 100644 --- a/src/Component.cc +++ b/src/Component.cc @@ -1768,7 +1768,7 @@ void Component::read_bodies_and_distribute_binary_spl(istream *in) in->read((char*)&number, sizeof(int)); } catch (...) { std::ostringstream sout; - sout << "Error reading magic info and file count from master"; + sout << "Error reading magic info and file count from root"; throw GenericError(sout.str(), __FILE__, __LINE__, 1010, true); } @@ -2116,7 +2116,7 @@ PartPtr * Component::get_particles(int* number) for (auto it=itb; it!=ite; it++) pbuf[icount++] = it->second; #ifdef DEBUG - std::cout << "get_particles: master loaded " + std::cout << "get_particles: root loaded " << icount << " of its own particles" << ", beg=" << beg << ", iend=" << end << ", dist=" << std::distance(itb, ite) @@ -2133,7 +2133,7 @@ PartPtr * Component::get_particles(int* number) while (PartPtr part=pf->RecvParticle()) pbuf[icount++] = part; #ifdef DEBUG std::cout << "Process " << myid - << ": received " << icount << " particles from Slave " << node + << ": received " << icount << " particles from Worker " << node << ", expected " << number << ", total=" << totals[node] << std::endl << std::flush; #endif @@ -2145,7 +2145,7 @@ PartPtr * Component::get_particles(int* number) curcount++; counter++; } - // Nodes send particles to master + // Nodes send particles to root } else if (myid == node) { auto itb = particles.begin(); @@ -2163,7 +2163,7 @@ PartPtr * Component::get_particles(int* number) #ifdef DEBUG std::cout << "Process " << myid - << ": sent " << icount << " particles from Slave " << node + << ": sent " << icount << " particles from Worker " << node << std::endl << std::flush; #endif } @@ -3353,7 +3353,7 @@ int Component::round_up(double dnumb) void Component::setup_distribution(void) { - // Needed for both master and slaves + // Needed for both root and workers nbodies_index = vector(numprocs); nbodies_table = vector(numprocs); diff --git a/src/Orient.cc b/src/Orient.cc index 2fa2f9bb1..676e40792 100644 --- a/src/Orient.cc +++ b/src/Orient.cc @@ -76,7 +76,7 @@ Orient::Orient(int n, int nwant, unsigned Oflg, unsigned Cflg, int in_ok; std::vector in1(4), in2(4); - if (myid==0) { // Master does the reading + if (myid==0) { // Root does the reading ifstream in(logfile.c_str()); @@ -115,7 +115,7 @@ Orient::Orient(int n, int nwant, unsigned Oflg, unsigned Cflg, double time; int tused; - in_ok = 1; // Signal slave: OK + in_ok = 1; // Signal worker: OK MPI_Bcast(&in_ok, 1, MPI_INT, 0, MPI_COMM_WORLD); @@ -176,12 +176,12 @@ Orient::Orient(int n, int nwant, unsigned Oflg, unsigned Cflg, if (restart) cout << " -- Orient: cached time=" << time << " Ecurr= " << Ecurr << endl; - cout << " -- Orient: axis master (cache size=" << sumsA.size() << "): " + cout << " -- Orient: axis root (cache size=" << sumsA.size() << "): " << axis[0] << ", " << axis[1] << ", " << axis[2] << endl; - cout << " -- Orient: center master (cache size=" << sumsC.size() << "): " + cout << " -- Orient: center root (cache size=" << sumsC.size() << "): " << center[0] << ", " << center[1] << ", " << center[2] << endl; @@ -212,7 +212,7 @@ Orient::Orient(int n, int nwant, unsigned Oflg, unsigned Cflg, } else { - in_ok = 0; // Signal slave: NO VALUES + in_ok = 0; // Signal worker: NO VALUES MPI_Bcast(&in_ok, 1, MPI_INT, 0, MPI_COMM_WORLD); @@ -261,7 +261,7 @@ Orient::Orient(int n, int nwant, unsigned Oflg, unsigned Cflg, } else { - // Get state from Master + // Get state from Root MPI_Bcast(&in_ok, 1, MPI_INT, 0, MPI_COMM_WORLD); diff --git a/src/end.cc b/src/end.cc index 5b0577e7e..2f7c0afb9 100644 --- a/src/end.cc +++ b/src/end.cc @@ -26,13 +26,13 @@ void clean_up(void) << "Process " << setw(4) << right << myid << " on " << processor_name << " pid=" << getpid() - << " MASTER NODE\t Exiting EXP\n"; + << " ROOT NODE\t Exiting EXP\n"; MPI_Barrier(MPI_COMM_WORLD); for (int j=1; j\n"; + nworkers = new int [workers]; + if (!nworkers) { + cerr << "main: problem allocating \n"; MPI_Finalize(); exit(10); } - for (n=1; n > nameMap; diff --git a/utils/SL/cyltest.cc b/utils/SL/cyltest.cc index 257082b55..4ee092f0b 100644 --- a/utils/SL/cyltest.cc +++ b/utils/SL/cyltest.cc @@ -223,7 +223,7 @@ int main(int argc, char** argv) // | // Turn on diagnostic output in SL creation---------------------------+ - // Slaves exit + // Workers exit if (use_mpi && myid>0) { MPI_Finalize(); exit(0); diff --git a/utils/SL/qtest.cc b/utils/SL/qtest.cc index 6547c23e4..1a3e1cf40 100644 --- a/utils/SL/qtest.cc +++ b/utils/SL/qtest.cc @@ -104,7 +104,7 @@ int main(int argc, char** argv) // | // Turn on diagnostic output in SL creation---------------------------------+ - // Slaves exit + // Workers exit if (use_mpi && myid>0) { MPI_Finalize(); exit(0); diff --git a/utils/SL/slcheck.cc b/utils/SL/slcheck.cc index 85a1cea9c..4a28596e8 100644 --- a/utils/SL/slcheck.cc +++ b/utils/SL/slcheck.cc @@ -124,7 +124,7 @@ int main(int argc, char** argv) } bad = true; } - // Slaves exit + // Workers exit if (use_mpi && myid>0) { MPI_Finalize(); exit(0); diff --git a/utils/Test/test_barrier.cc b/utils/Test/test_barrier.cc index 6635aa172..932b293ba 100644 --- a/utils/Test/test_barrier.cc +++ b/utils/Test/test_barrier.cc @@ -56,7 +56,7 @@ int main(int argc, char **argv) // MPI preliminaries //-------------------------------------------------- - int numprocs, slaves, myid, proc_namelen; + int numprocs, myid, proc_namelen; char processor_name[MPI_MAX_PROCESSOR_NAME]; MPI_Init(&argc, &argv); From 7d430906ae68ad7ffbfa3137b8ea048aaa4ddfbe Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 26 Mar 2024 22:26:46 -0400 Subject: [PATCH 043/167] Fixed indexing error from pre-Eigen coefficient tensor; added orothCheck [no ci] --- exputil/SLGridMP2.cc | 39 ++++++++++++-------- src/SlabSL.H | 6 ++- src/SlabSL.cc | 87 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 101 insertions(+), 31 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 6d6603e51..dfc2f8673 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -3675,7 +3675,7 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, MPI_Recv(&bad, 1, MPI_INT, MPI_ANY_TAG, 10, MPI_COMM_WORLD, &status); totbad += bad; - // Get sledge comptuation result + // Get sledge computation result int retid = status.MPI_SOURCE; MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, retid, 11, MPI_COMM_WORLD, &status); @@ -4059,8 +4059,11 @@ SLGridSlab::~SLGridSlab() } // Members +// #define TANH_MAP 1 +// #define SECH_MAP 1 + +#if defined(TANH_MAP) -/* double SLGridSlab::z_to_xi(double z) { return tanh(z/H); @@ -4075,10 +4078,9 @@ double SLGridSlab::d_xi_to_z(double xi) { return H/(1.0 - xi*xi); } -*/ +#elif defined (SECH_MAP) -/* double SLGridSlab::z_to_xi(double z) { return z/sqrt(z*z + H*H); @@ -4091,10 +4093,10 @@ double SLGridSlab::xi_to_z(double xi) double SLGridSlab::d_xi_to_z(double xi) { - return pow(1.0 - xi*xi, 1.5)/H; + return H/pow(1.0 - xi*xi, 1.5); } -*/ +#else // Simple cartesian coordinates seem // to work best here; this transformation // is the identity . . . @@ -4114,6 +4116,7 @@ double SLGridSlab::d_xi_to_z(double xi) return 1.0; } +#endif double SLGridSlab::get_pot(double x, int kx, int ky, int n, int which) @@ -5108,11 +5111,11 @@ void SLGridSlab::compute_table_worker(void) // N = nmax - N; - for (int i=0; i SLGridSlab::orthoCheck(int num) // Initialize the return matrices std::vector ret((numk+1)*(numk+2)/2); for (auto & v : ret) { - v.resize(numz, numz); + v.resize(nmax, nmax); v.setZero(); } - - Eigen::VectorXd vpot(numz), vden(numz); + int nthrds = omp_get_max_threads(); + std::vector vpot(nthrds), vden(nthrds); + for (auto & v : vpot) v.resize(nmax); + for (auto & v : vden) v.resize(nmax); #pragma omp parallel for for (int i=0; i nmaxy) ? nmaxx : nmaxy; - grid = std::make_shared(nnmax, nmaxz, NGRID, zmax); + grid = std::make_shared(nnmax, nmaxz, ngrid, zmax, type); + + // Test for basis consistency (will generate an exception if maximum + // error is out of tolerance) + // + double worst = 0.0, diff = 0.0; + int kxw = 0, kyw = 0; + auto test = grid->orthoCheck(10000); + for (int kx=0, indx=0; kx<=nnmax; kx++) { + for (int ky=0; ky<=kx; ky++, indx++) { + for (int n1=0; n1 worst) { + worst = diff; + kxw = kx; + kyw = ky; + } + } + } + } + } + + if (true) { + std::ofstream tmp("SlabSL.ortho"); + for (int kx=0, indx=0; kx<=nnmax; kx++) { + for (int ky=0; ky<=kx; ky++, indx++) { + tmp << "---- kx=" << kx << " ky=" << ky << std::endl + << test[indx] << std::endl; + } + } + } + + if (worst > __EXP__::orthoTol) { + if (myid==0) + std::cout << "SlabSL: orthogonality failure, worst=" << worst + << " at (" << kxw << ", " << kyw << ")" << std::endl; + throw std::runtime_error("SlabSL: biorthogonal sanity check"); + } else { + if (myid==0) + std::cout << "---- SlabSL: biorthogonal check passed, worst=" + << worst << std::endl; + } imx = 1+2*nmaxx; imy = 1+2*nmaxy; @@ -73,8 +118,10 @@ void SlabSL::initialize() if (conf["nmaxz"]) nmaxz = conf["nmaxz"].as(); if (conf["nminx"]) nminx = conf["nminx"].as(); if (conf["nminy"]) nminy = conf["nminy"].as(); + if (conf["ngrid"]) ngrid = conf["ngrid"].as(); if (conf["hslab"]) hslab = conf["hslab"].as(); - if (conf["zmax"]) zmax = conf["zmax"].as(); + if (conf["zmax" ]) zmax = conf["zmax" ].as(); + if (conf["type" ]) type = conf["type" ].as(); } catch (YAML::Exception & error) { if (myid==0) std::cout << "Error parsing parameters in SlabSL: " @@ -132,8 +179,14 @@ void * SlabSL::determine_coefficients_thread(void * arg) int nend = nbodies*(id+1)/nthrds; double adb = cC->Adiabatic(); - for (int i=nbeg; iParticles().begin(); + unsigned long i; + + for (int q=0; qfirst; + it++; // Increment particle counter use[id]++; @@ -188,7 +241,7 @@ void * SlabSL::determine_coefficients_thread(void * arg) // | is 4.0*M_PI rho // v expccof[id](ix, iy, iz) += -4.0*M_PI*cC->Mass(i)*adb* - facx*facy*zpot[id][iz+1]; + facx*facy*zpot[id][iz]; } } } @@ -217,8 +270,14 @@ void * SlabSL::determine_acceleration_and_potential_thread(void * arg) int nbeg = nbodies*id/nthrds; int nend = nbodies*(id+1)/nthrds; - for (int i=nbeg; iParticles().begin(); + unsigned long i; + + for (int q=0; qfirst; it++; + accx = accy = accz = potl = 0.0; // Recursion multipliers @@ -263,8 +322,8 @@ void * SlabSL::determine_acceleration_and_potential_thread(void * arg) for (int iz=0; iz Date: Wed, 27 Mar 2024 22:27:48 -0400 Subject: [PATCH 044/167] More updates for SlabSL --- expui/BasisFactory.cc | 5 +- expui/BiorthBasis.H | 5 +- expui/BiorthBasis.cc | 48 ++++++++-- expui/CoefStruct.H | 3 +- expui/Coefficients.cc | 4 +- pyEXP/CoefWrappers.cc | 208 ++++++++++++++++++++++++++++++++++++++---- src/SlabSL.cc | 14 ++- 7 files changed, 248 insertions(+), 39 deletions(-) diff --git a/expui/BasisFactory.cc b/expui/BasisFactory.cc index 40b154465..599a0eafb 100644 --- a/expui/BasisFactory.cc +++ b/expui/BasisFactory.cc @@ -181,6 +181,9 @@ namespace BasisClasses else if ( !name.compare("flatdisk") ) { basis = std::make_shared(conf); } + else if ( !name.compare("slabSL") ) { + basis = std::make_shared(conf); + } else if ( !name.compare("cube") ) { basis = std::make_shared(conf); } @@ -193,7 +196,7 @@ namespace BasisClasses else { std::string msg("I don't know about the basis named: "); msg += name; - msg += ". Known types are currently 'sphereSL', 'cylinder' and 'flatdisk'"; + msg += ". Known types are currently 'sphereSL', 'cylinder', 'flatdisk', 'slabSL', 'field', and 'velocity'"; throw std::runtime_error(msg); } } diff --git a/expui/BiorthBasis.H b/expui/BiorthBasis.H index 5cbc559ae..cc2727c4a 100644 --- a/expui/BiorthBasis.H +++ b/expui/BiorthBasis.H @@ -608,7 +608,10 @@ namespace BasisClasses int knots; //! SLGridSlab mesh size - int NGRID = 100; + int ngrid = 1000; + + //! Target model type for SLGridSlab + std::string type = "isothermal"; //! Scale height for slab double hslab; diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index 2715ede55..0e9450a60 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -280,7 +280,7 @@ namespace BasisClasses int ldim = (lmax+1)*(lmax+2)/2; // Allocate the coefficient storage - cf->store.resize((lmax+1)*(lmax+2)/2, nmax); + cf->store.resize((lmax+1)*(lmax+2)/2*nmax); // Make the coefficient map cf->coefs = std::make_shared @@ -1906,6 +1906,8 @@ namespace BasisClasses "nminy", "hslab", "zmax", + "ngrid", + "type", "knots", "verbose", "check", @@ -1924,8 +1926,8 @@ namespace BasisClasses void Slab::initialize() { - nminx = std::numeric_limits::max(); - nminy = std::numeric_limits::max(); + nminx = 0; + nminy = 0; nmaxx = 6; nmaxy = 6; @@ -1959,7 +1961,9 @@ namespace BasisClasses if (conf["nmaxz"]) nmaxz = conf["nmaxz"].as(); if (conf["hslab"]) hslab = conf["hslab"].as(); - if (conf["zmax "]) zmax = conf["zmax" ].as(); + if (conf["zmax" ]) zmax = conf["zmax" ].as(); + if (conf["ngrid"]) ngrid = conf["ngrid"].as(); + if (conf["type" ]) type = conf["type" ].as(); if (conf["knots"]) knots = conf["knots"].as(); @@ -1978,14 +1982,14 @@ namespace BasisClasses // Finally, make the basis // - SLGridSlab::mpi = 1; + SLGridSlab::mpi = 0; SLGridSlab::ZBEG = 0.0; SLGridSlab::ZEND = 0.1; SLGridSlab::H = hslab; int nnmax = (nmaxx > nmaxy) ? nmaxx : nmaxy; - ortho = std::make_shared(nnmax, nmaxz, NGRID, zmax); + ortho = std::make_shared(nnmax, nmaxz, ngrid, zmax, type); // Orthogonality sanity check // @@ -2043,12 +2047,12 @@ namespace BasisClasses { auto cc = dynamic_cast(coef.get()); auto d = cc->coefs->dimensions(); - if (d[0] != 2*nmaxx+1 or d[1] != 2*nmaxy+1 or d[2] != 2*nmaxz+1) { + if (d[0] != 2*nmaxx+1 or d[1] != 2*nmaxy+1 or d[2] != nmaxz) { std::ostringstream sout; - sout << "Slab::set_coefs: the basis has (2*nmaxx+1, 2*nmaxy+1, 2*nmaxz+1)=(" + sout << "Slab::set_coefs: the basis has (2*nmaxx+1, 2*nmaxy+1, nmaxz)=(" << 2*nmaxx+1 << ", " << 2*nmaxy+1 << ", " - << 2*nmaxz+1 + << nmaxz << "). The coef structure has dimension=(" << d[0] << ", " << d[1] << ", " << d[2] << ")"; @@ -2723,6 +2727,32 @@ namespace BasisClasses throw std::runtime_error(msg); } + // Assume position arrays in rows by default + // + int rows = p.rows(); + int cols = p.cols(); + + bool ambiguous = false; + + if (cols==3 or cols==6) { + if (rows>cols or rows != 6 or rows != 3) PosVelRows = false; + else ambiguous = true; + } + + if (rows==3 or rows==6) { + if (cols>rows or cols != 6 or cols != 3) PosVelRows = true; + else ambiguous = true; + } + + if (ambiguous and myid==0) { + std::cout << "---- BiorthBasis::addFromArray: dimension deduction " + << "is ambiguous. Assuming that "; + if (PosVelRows) std::cout << "positions are in rows" << std::endl; + else std::cout << "positions are in columns" << std::endl; + std::cout << "---- BiorthBasis::addFromArray: reset 'posvelrows' flag " + << "if this assumption is wrong." << std::endl; + } + std::vector p1(3), v1(3, 0); if (PosVelRows) { diff --git a/expui/CoefStruct.H b/expui/CoefStruct.H index 12228d7f7..b50bb46f9 100644 --- a/expui/CoefStruct.H +++ b/expui/CoefStruct.H @@ -188,7 +188,8 @@ namespace CoefClasses nx = 2*nmaxx + 1; ny = 2*nmaxy + 1; nz = nmaxz; - store.resize(nx*ny*nz); + dim = nx * ny * nz; + store.resize(dim); coefs = std::make_shared(store.data(), nx, ny, nz); } diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index aabed5c04..06a892faa 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -1180,7 +1180,7 @@ namespace CoefClasses Eigen::VectorXcd in; stanza.getDataSet("coefficients").read(in); - Eigen::TensorMap dat(in.data(), 2*NmaxX+1, 2*NmaxY+1, 2*NmaxZ+1); + Eigen::TensorMap dat(in.data(), 2*NmaxX+1, 2*NmaxY+1, NmaxZ); // Pack the data into the coefficient variable // @@ -2094,6 +2094,8 @@ namespace CoefClasses coefs = std::make_shared(h5file, stride, tmin, tmax); } else if (geometry.compare("cylinder")==0) { coefs = std::make_shared(h5file, stride, tmin, tmax); + } else if (geometry.compare("slab")==0) { + coefs = std::make_shared(h5file, stride, tmin, tmax); } else if (geometry.compare("cube")==0) { coefs = std::make_shared(h5file, stride, tmin, tmax); } else if (geometry.compare("table")==0) { diff --git a/pyEXP/CoefWrappers.cc b/pyEXP/CoefWrappers.cc index e5131baee..8ad4f8242 100644 --- a/pyEXP/CoefWrappers.cc +++ b/pyEXP/CoefWrappers.cc @@ -24,9 +24,8 @@ void CoefficientClasses(py::module &m) { and metadata specific to each geometry. There are three groups of CoefStruct derived classes for biorthogonal basis coefficients, field data coeffients, and auxiliary table data. The biorthogonal - classes are spherical (SphStruct), cylindrical (CylStruct), and - cube (CubeStruct). EXP also knows about slabs. These may be added - in a future release if there is a need. The field classes + classes are spherical (SphStruct), cylindrical (CylStruct), slab + (SlabStruct), and cube (CubeStruct). The field classes cylindrical (CylFldStruct), and spherical (SphFldStruct). The table data is stored in TblStruct. @@ -41,25 +40,26 @@ void CoefficientClasses(py::module &m) { array. The dimen- sions are: 1. (lmax, nmax) for SphStruct 2. (mmax, nmax) for a CylStruct - 3. (nmaxx, nmaxy, nmaxz) for a CubeStruct - 4. (nfld, lmax, nmax) for a SphFldStruct - 5. (nfld, mmax, nmax) for a CylFldStruct - 6. (cols) for a TblStruct. + 3. (nmaxx, nmaxy, nmaxz) for a SlabStruct + 4. (nmaxx, nmaxy, nmaxz) for a CubeStruct + 5. (nfld, lmax, nmax) for a SphFldStruct + 6. (nfld, mmax, nmax) for a CylFldStruct + 7. (cols) for a TblStruct. Coefs ----- The base class, 'Coefs', provides a factory reader that will create one of the derived coefficient classes, SphCoefs, CylCoefs, - CubeCoefs, TblCoefs, SphFldCoefs, and CylFldCoefs, deducing the - type from the input file. The input files may be EXP native or - HDF5 cofficient files. Only biorthgonal basis coefficients have a - native EXP type. The Basis factory, Basis::createCoefficients, - will create set of coefficients from phase-space snapshots. See - help(pyEXP.basis). Files which are not recognized as EXP - coefficient files are assumed to be data files and are parsed by - the TblCoefs class. The first column in data tables is interpreted - as time and each successive column is interpreted as a new data - field. + SlabCoefs, CubeCoefs, TblCoefs, SphFldCoefs, and CylFldCoefs, + deducing the type from the input file. The input files may be EXP + native or HDF5 cofficient files. Only biorthgonal basis + coefficients have a native EXP type. The Basis factory, + Basis::createCoefficients, will create set of coefficients from + phase-space snapshots. See help(pyEXP.basis). Files which are not + recognized as EXP coefficient files are assumed to be data files + and are parsed by the TblCoefs class. The first column in data + tables is interpreted as time and each successive column is + interpreted as a new data field. Once created, you may get a list of times, get the total gravitational power from biothogonal basis coefficients and @@ -361,6 +361,85 @@ void CoefficientClasses(py::module &m) { }; + class PySlabCoefs : public SlabCoefs + { + protected: + void readNativeCoefs(const std::string& file, int stride, double tmin, double tmax) override { + PYBIND11_OVERRIDE(void, SlabCoefs, readNativeCoefs, file, stride, tmin, tmax); + } + + std::string getYAML() override { + PYBIND11_OVERRIDE(std::string, SlabCoefs, getYAML,); + } + + void WriteH5Params(HighFive::File& file) override { + PYBIND11_OVERRIDE(void, SlabCoefs, WriteH5Params, file); + } + + unsigned WriteH5Times(HighFive::Group& group, unsigned count) override { + PYBIND11_OVERRIDE(unsigned, SlabCoefs, WriteH5Times, group, count); + } + + public: + // Inherit the constructors + using SlabCoefs::SlabCoefs; + + Eigen::VectorXcd& getData(double time) override { + PYBIND11_OVERRIDE(Eigen::VectorXcd&, SlabCoefs, getData, time); + } + + void setData(double time, const Eigen::VectorXcd& array) override { + PYBIND11_OVERRIDE(void, SlabCoefs, setData, time, array); + } + + std::shared_ptr getCoefStruct(double time) override { + PYBIND11_OVERRIDE(std::shared_ptr, SlabCoefs, getCoefStruct, + time); + } + + std::vector Times() override { + PYBIND11_OVERRIDE(std::vector, SlabCoefs, Times,); + } + + void WriteH5Coefs(const std::string& prefix) override { + PYBIND11_OVERRIDE(void, SlabCoefs, WriteH5Coefs, prefix); + } + + void ExtendH5Coefs(const std::string& prefix) override { + PYBIND11_OVERRIDE(void, SlabCoefs, ExtendH5Coefs, prefix); + } + + Eigen::MatrixXd& Power(int min, int max) override { + PYBIND11_OVERRIDE(Eigen::MatrixXd&, SlabCoefs, Power, min, max); + } + + bool CompareStanzas(std::shared_ptr check) override { + PYBIND11_OVERRIDE(bool, SlabCoefs, CompareStanzas, check); + } + + void clear() override { + PYBIND11_OVERRIDE(void, SlabCoefs, clear,); + } + + void add(CoefStrPtr coef) override { + PYBIND11_OVERRIDE(void, SlabCoefs, add, coef); + } + + std::vector makeKeys(Key k) override { + PYBIND11_OVERRIDE(std::vector, SlabCoefs, makeKeys, k); + } + + std::shared_ptr deepcopy() override { + PYBIND11_OVERRIDE(std::shared_ptr, SlabCoefs, deepcopy,); + } + + void zerodata() override { + PYBIND11_OVERRIDE(void, SlabCoefs, zerodata,); + } + + + }; + class PyCubeCoefs : public CubeCoefs { protected: @@ -635,6 +714,28 @@ void CoefficientClasses(py::module &m) { None )"); + py::class_, CoefStruct> + (m, "SlabStruct") + .def(py::init<>(), "Slab coefficient data structure object") + .def("assign", &SlabStruct::assign, + R"( + Assign a coefficient matrix to CoefStruct. + + Parameters + ---------- + mat : numpy.ndarray, complex + complex-valued NumPy tensor of coefficient values + + Returns + ------- + None + + Notes + ----- + The dimensions are inferred from the 3-dimensional NumPy array + (tensor) + )"); + py::class_, CoefStruct> (m, "CubeStruct") .def(py::init<>(), "Cube coefficient data structure object") @@ -1348,6 +1449,77 @@ void CoefficientClasses(py::module &m) { 4-dimensional numpy array containing the spherical coefficients )"); + + py::class_, PySlabCoefs, CoefClasses::Coefs> + (m, "SlabCoefs", "Container for cube coefficients") + .def(py::init(), + R"( + Construct a null SlabCoefs object + + Parameters + ---------- + verbose : bool + display verbose information. + + Returns + ------- + SlabCoefs instance + )") + .def("__call__", + &CoefClasses::SlabCoefs::getTensor, + R"( + Return the coefficient tensor for the desired time. + + Parameters + ---------- + time : float + the desired time + + Returns + ------- + numpy.ndarray + the coefficient Matrix at the requested time + + Notes + ----- + This operator will return the 0-rank tensor if no + coefficients are found at the requested time + )", + py::arg("time")) + .def("setTensor", + &CoefClasses::SlabCoefs::setTensor, + R"( + Enter and/or rewrite the coefficient tensor at the provided time + + Parameters + ---------- + time : float + snapshot time corresponding to the the coefficient tensor + mat : numpy.ndarray + the new coefficient array. + + Returns + ------- + None + )", + py::arg("time"), py::arg("tensor")) + .def("getAllCoefs", + [](CoefClasses::SlabCoefs& A) + { + auto M = A.getAllCoefs(); // Need a copy here + py::array_t> ret = make_ndarray4>(M); + return ret; + }, + R"( + Provide a 4-dimensional ndarray indexed by nx, ny, nz, and time indices. + + Returns + ------- + numpy.ndarray + 4-dimensional numpy array containing the slab coefficients + )"); + + py::class_, PyCubeCoefs, CoefClasses::Coefs> (m, "CubeCoefs", "Container for cube coefficients") .def(py::init(), @@ -1414,7 +1586,7 @@ void CoefficientClasses(py::module &m) { Returns ------- numpy.ndarray - 4-dimensional numpy array containing the cylindrical coefficients + 4-dimensional numpy array containing the cube coefficients )"); py::class_, PyTableData, CoefClasses::Coefs> diff --git a/src/SlabSL.cc b/src/SlabSL.cc index ea6b47bad..d235f03ce 100644 --- a/src/SlabSL.cc +++ b/src/SlabSL.cc @@ -168,7 +168,7 @@ void SlabSL::determine_coefficients(void) void * SlabSL::determine_coefficients_thread(void * arg) { - int ix, iy, iz, iix, iiy; + int ix, iy; std::complex startx, starty, facx, facy; std::complex stepx, stepy; @@ -180,12 +180,11 @@ void * SlabSL::determine_coefficients_thread(void * arg) double adb = cC->Adiabatic(); PartMapItr it = cC->Particles().begin(); - unsigned long i; for (int q=0; qfirst; + unsigned long i = it->first; it++; // Increment particle counter use[id]++; @@ -264,6 +263,7 @@ void * SlabSL::determine_acceleration_and_potential_thread(void * arg) int ix, iy; std::complex fac, facx, facy, potl, facf; std::complex accx, accy, accz; + const std::complex I(0.0, 1.0); unsigned nbodies = cC->Number(); int id = *((int*)arg); @@ -271,12 +271,11 @@ void * SlabSL::determine_acceleration_and_potential_thread(void * arg) int nend = nbodies*(id+1)/nthrds; PartMapItr it = cC->Particles().begin(); - unsigned long i; for (int q=0; qfirst; it++; + unsigned long i = it->first; it++; accx = accy = accz = potl = 0.0; @@ -331,10 +330,9 @@ void * SlabSL::determine_acceleration_and_potential_thread(void * arg) potl += fac; - accx += -dfac*ii*std::complex(0.0,1.0)*fac; - accy += -dfac*jj*std::complex(0.0,1.0)*fac; + accx += -kfac*static_cast(ii)*fac; + accy += -kfac*static_cast(jj)*fac; accz += -facf; - } } } From c52d1be595cb41ed7e331ebbb17fb4f0c6159ce4 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 27 Mar 2024 22:39:50 -0400 Subject: [PATCH 045/167] Restore dimension deduction in addFromArray with additional checks --- expui/BiorthBasis.cc | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index 1383b1c2b..698dab869 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -279,7 +279,7 @@ namespace BasisClasses int ldim = (lmax+1)*(lmax+2)/2; // Allocate the coefficient storage - cf->store.resize((lmax+1)*(lmax+2)/2, nmax); + cf->store.resize((lmax+1)*(lmax+2)/2*nmax); // Make the coefficient map cf->coefs = std::make_shared @@ -2302,6 +2302,32 @@ namespace BasisClasses throw std::runtime_error(msg); } + // Assume position arrays in rows by default + // + int rows = p.rows(); + int cols = p.cols(); + + bool ambiguous = false; + + if (cols==3 or cols==6) { + if (rows>cols or rows != 6 or rows != 3) PosVelRows = false; + else ambiguous = true; + } + + if (rows==3 or rows==6) { + if (cols>rows or cols != 6 or cols != 3) PosVelRows = true; + else ambiguous = true; + } + + if (ambiguous and myid==0) { + std::cout << "---- BiorthBasis::addFromArray: dimension deduction " + << "is ambiguous. Assuming that "; + if (PosVelRows) std::cout << "positions are in rows" << std::endl; + else std::cout << "positions are in columns" << std::endl; + std::cout << "---- BiorthBasis::addFromArray: reset 'posvelrows' flag " + << "if this assumption is wrong." << std::endl; + } + std::vector p1(3), v1(3, 0); if (PosVelRows) { From 50566b3ff70eb6debf3bf62d814ce9db7694a8c9 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 28 Mar 2024 09:00:10 -0400 Subject: [PATCH 046/167] Remove the cols<>rows check in the 'rows or columns' check [no ci] --- expui/BiorthBasis.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index 698dab869..d52d4f9df 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -2310,12 +2310,12 @@ namespace BasisClasses bool ambiguous = false; if (cols==3 or cols==6) { - if (rows>cols or rows != 6 or rows != 3) PosVelRows = false; + if (rows != 3 and rows != 6) PosVelRows = false; else ambiguous = true; } if (rows==3 or rows==6) { - if (cols>rows or cols != 6 or cols != 3) PosVelRows = true; + if (cols != 3 and cols != 6) PosVelRows = true; else ambiguous = true; } From b50197b4ea5c09107b807bf060d0a7cf3e0e716a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 28 Mar 2024 14:55:14 -0400 Subject: [PATCH 047/167] More updates to fully integrate SlabSL into pyEXP - Current tests suggest that the Slab is mostly working - Removed SLGridCyl code - Removed trig Slab class --- expui/BiorthBasis.cc | 2 +- expui/CoefContainer.H | 9 +- expui/CoefContainer.cc | 108 ++- expui/expMSSA.cc | 4 +- exputil/SLGridMP2.cc | 1812 +-------------------------------------- include/SLGridMP2.H | 146 ---- src/CMakeLists.txt | 2 +- src/Component.cc | 4 - src/Slab.H | 76 -- src/Slab.cc | 339 -------- utils/SL/CMakeLists.txt | 5 +- utils/SL/cyltest.cc | 390 --------- 12 files changed, 152 insertions(+), 2745 deletions(-) delete mode 100644 src/Slab.H delete mode 100644 src/Slab.cc delete mode 100644 utils/SL/cyltest.cc diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index 406681e71..cca714bc3 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -2262,7 +2262,7 @@ namespace BasisClasses auto [pot, den, frcx, frcy, frcz] = eval(x, y, z); - return {0, den, 0, pot, frcx, frcy, frcz}; + return {0, den, den, 0, pot, pot, frcx, frcy, frcz}; } std::vector Slab::cyl_eval(double R, double z, double phi) diff --git a/expui/CoefContainer.H b/expui/CoefContainer.H index 05a909cb4..66a3c9951 100644 --- a/expui/CoefContainer.H +++ b/expui/CoefContainer.H @@ -100,10 +100,15 @@ namespace MSSA //@} //@{ - //! Cylindrical coefficients + //! Slab coefficients + void pack_slab(); + void unpack_slab(); + //@} + + //@{ + //! Cube coefficients void pack_cube(); void unpack_cube(); - void restore_background_cube(); //@} //@{ diff --git a/expui/CoefContainer.cc b/expui/CoefContainer.cc index 8add12d7e..03af71d92 100644 --- a/expui/CoefContainer.cc +++ b/expui/CoefContainer.cc @@ -55,6 +55,9 @@ namespace MSSA else if (dynamic_cast(coefs.get())) { unpack_cylinder(); } + else if (dynamic_cast(coefs.get())) { + unpack_slab(); + } else if (dynamic_cast(coefs.get())) { unpack_cube(); } @@ -62,7 +65,7 @@ namespace MSSA unpack_table(); } else { - throw std::runtime_error("CoefDB::pack_channels(): can not reflect coefficient type"); + throw std::runtime_error("CoefDB::unpack_channels(): can not reflect coefficient type"); } return coefs; @@ -87,6 +90,10 @@ namespace MSSA pack_sphere(); else if (dynamic_cast(coefs.get())) pack_cylinder(); + else if (dynamic_cast(coefs.get())) + pack_slab(); + else if (dynamic_cast(coefs.get())) + pack_cube(); else if (dynamic_cast(coefs.get())) pack_table(); else { @@ -303,6 +310,87 @@ namespace MSSA // END time loop } + void CoefDB::pack_slab() + { + auto cur = dynamic_cast(coefs.get()); + + times = cur->Times(); + complexKey = true; + + auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + + int nmaxx = cf->nmaxx; + int nmaxy = cf->nmaxy; + int nmaxz = cf->nmaxz; + int ntimes = times.size(); + + // Make extended key list + // + keys.clear(); + for (auto k : keys0) { + // Sanity check rank + // + if (k.size() != 3) { + std::ostringstream sout; + sout << "CoefDB::pack_slab: key vector should have rank 3; " + << "found rank " << k.size() << " instead"; + throw std::runtime_error(sout.str()); + } + // Sanity check values + // + else if (k[0] <= 2*nmaxx and k[0] >= 0 and + k[1] <= 2*nmaxy and k[1] >= 0 and + k[2] < nmaxz and k[2] >= 0 ) { + + auto v = k; + v.push_back(0); + keys.push_back(v); + data[v].resize(ntimes); + + v[3] = 1; + keys.push_back(v); + data[v].resize(ntimes); + } + else { + throw std::runtime_error("CoefDB::pack_slab: key is out of bounds"); + } + } + + bkeys.clear(); + for (auto k : bkeys0) { + // Sanity check values + // + if (k[0] <= 2*nmaxx and k[0] >= 0 and + k[1] <= 2*nmaxy and k[1] >= 0 and + k[2] < nmaxz and k[2] >= 0 ) { + + auto v = k; + v.push_back(0); + keys.push_back(v); + data[v].resize(ntimes); + + v[3] = 1; + keys.push_back(v); + data[v].resize(ntimes); + } + } + + for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { + auto c = (*cf->coefs)(k[0], k[1], k[2]); + if (k[3]) data[k][t] = c.imag(); + else data[k][t] = c.real(); + } + + for (auto k : bkeys) { + auto c = (*cf->coefs)(k[0], k[1], k[2]); + if (k[3]) data[k][t] = c.imag(); + else data[k][t] = c.real(); + } + } + } + void CoefDB::pack_cube() { auto cur = dynamic_cast(coefs.get()); @@ -384,6 +472,24 @@ namespace MSSA } } + void CoefDB::unpack_slab() + { + for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + + for (auto k : keys0) { + auto c = k, s = k; + c.push_back(0); + s.push_back(1); + + (*cf->coefs)(k[0], k[1], k[2]) = {data[c][i], data[s][i]}; + } + // END key loop + } + // END time loop + } + void CoefDB::unpack_cube() { for (int i=0; i cyl; - -//! Target model for slab SL -std::shared_ptr slab; - -extern "C" { - int sledge_(logical* job, doublereal* cons, logical* endfin, - integer* invec, doublereal* tol, logical* type, - doublereal* ev, integer* numx, doublereal* xef, doublereal* ef, - doublereal* pdef, doublereal* t, doublereal* rho, - integer* iflag, doublereal* store); -} - - -static -std::string sledge_error(int flag) -{ - if (flag==0) - return "reliable"; - else if (flag==-1) - return "too many levels for eigenvalues"; - else if (flag==-2) - return "too many levels for eigenvectors"; - else if (flag==1) - return "eigenvalue cluster?"; - else if (flag==2) - return "classification uncertainty"; - else if (flag>0) { - std::ostringstream sout; - sout << "unexpected warning: " << flag; - return sout.str(); - } else { - std::ostringstream sout; - sout << "unexpected fatal error: " << flag; - return sout.str(); - } -} - -// Unit density exponential disk with scale length A - -class KuzminCyl : public CylModel -{ -public: - - KuzminCyl() { id = "kuzmin"; } - - double pot(double R) { - double a2 = SLGridCyl::A * SLGridCyl::A; - return -1.0/sqrt(R*R + a2); - } - - double dpot(double R) { - double a2 = SLGridCyl::A * SLGridCyl::A; - return R/pow(R*R + a2, 1.5); - } - - double dens(double R) { - double a2 = SLGridCyl::A * SLGridCyl::A; - return 4.0*M_PI*SLGridCyl::A/pow(R*R + a2, 1.5)/(2.0*M_PI); - // ^ - // | - // This 4pi from Poisson's eqn - } - -}; - -class MestelCyl : public CylModel -{ -public: - - MestelCyl() { id = "mestel"; } - - double pot(double R) { - return M_PI/(2.0*SLGridCyl::A)*log(0.5*R/SLGridCyl::A); - } - - double dpot(double R) { - double a2 = SLGridCyl::A * SLGridCyl::A; - double fac = sqrt(1.0 + R*R/a2); - return M_PI/(2.0*SLGridCyl::A*R); - } - - double dens(double R) { - if (R>SLGridCyl::A) - return 0.0; - else - return 4.0*M_PI/(2.0*M_PI*SLGridCyl::A*R)*acos(R/SLGridCyl::A); - // ^ - // | - // This 4pi from Poisson's eqn - } -}; - -class ExponCyl : public CylModel -{ - -public: - - ExponCyl() { id = "expon"; } - - double pot(double r) { - double y = 0.5 * r / SLGridCyl::A; - return -2.0*M_PI*SLGridCyl::A*y* - (EXPmath::cyl_bessel_i(0, y)*EXPmath::cyl_bessel_k(1, y) - - EXPmath::cyl_bessel_i(1, y)*EXPmath::cyl_bessel_k(0, y)); - } - - double dpot(double r) { - double y = 0.5 * r / SLGridCyl::A; - return 4.0*M_PI*SLGridCyl::A*y*y* - (EXPmath::cyl_bessel_i(0, y)*EXPmath::cyl_bessel_k(0, y) - - EXPmath::cyl_bessel_i(1, y)*EXPmath::cyl_bessel_k(1, y)); - } - - double dens(double r) { - // This 4pi from Poisson's eqn - // | - // | /-- This begins the true projected density profile - // | | - // v v - return 4.0*M_PI * exp(-r/SLGridCyl::A); - } - -}; - -std::shared_ptr CylModel::createModel(const std::string type) -{ - std::string data(type); - std::transform(data.begin(), data.end(), data.begin(), - [](unsigned char c){ return std::tolower(c); }); - - if (data.find("mestel") != std::string::npos) { - return std::make_shared(); - } - - if (data.find("expon") != std::string::npos) { - return std::make_shared(); - } - - // Default - return std::make_shared(); -} - -void SLGridCyl::bomb(std::string oops) -{ - std::ostringstream sout; - sout << "SLGridCyl error [#=" << myid << "]: " << oops; - throw std::runtime_error(sout.str()); -} - // Constructors - -SLGridCyl::SLGridCyl(int MMAX, int NMAX, int NUMR, int NUMK, - double RMIN, double RMAX, double L, - bool CACHE, int CMAP, double RMAP, - const std::string type, bool VERBOSE) -{ - int m, k; - - mmax = MMAX; - nmax = NMAX; - numr = NUMR; - numk = NUMK; - - rmin = RMIN; - rmax = RMAX; - l = L; - - cache = CACHE; - cmap = CMAP; - rmap = RMAP; - - tbdbg = VERBOSE; - - cyl = CylModel::createModel(type); - - kv.resize(NUMK+1); - - // Zero density boundary condition - auto getK = [L](int k) { return 2.0*M_PI/L*(0.5+k); }; - - // Original boundary condition - // auto getK = [L](int k) { return 2.0*M_PI/L*k; }; - - for (k=0; k<=NUMK; k++) kv[k] = getK(k); - - table = 0; - mpi_buf = 0; - - init_table(); - - - if (tbdbg) { - if (mpi) - std::cout << "Process " << myid << ": MPI is on!" << std::endl; - else - std::cout << "Process " << myid << ": MPI is off!" << std::endl; - } - - if (mpi) { - - table = new TableCyl* [mmax+1]; - for (m=0; m<=mmax; m++) table[m] = new TableCyl [numk+1]; - - mpi_setup(); - - int totbad = 0; - - if (mpi_myid) { - - compute_table_worker(); - - // - // - // - - for (m=0; m<=mmax; m++) { - for (k=0; k<=numk; k++) { - MPI_Bcast(mpi_buf, mpi_bufsz, MPI_PACKED, 0, MPI_COMM_WORLD); - - mpi_unpack_table(); - } - } - - } - else { // BEGIN Root - - int worker = 0; - int request_id = 1; - - if (!read_cached_table()) { - - double K; - m=0; k=0; - - while (m<=mmax) { - - if (workernumk) { - k=0; - m++; - } - } - - if (worker == mpi_numprocs-1 && m<=mmax) { - - // - // - // -#ifdef SLEDGE_THROW - int bad; // Get count of sledge failures - MPI_Recv(&bad, 1, MPI_INT, MPI_ANY_SOURCE, 10, - MPI_COMM_WORLD, &status); - totbad += bad; - // Get source node identity - int retid = status.MPI_SOURCE; - - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, retid, 11, - MPI_COMM_WORLD, &status); -#else - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, 11, - MPI_COMM_WORLD, &status); - - int retid = status.MPI_SOURCE; -#endif - - mpi_unpack_table(); - - // - // - // - - K = dk*k; - - MPI_Send(&request_id, 1, MPI_INT, retid, 11, MPI_COMM_WORLD); - MPI_Send(&m, 1, MPI_INT, retid, 11, MPI_COMM_WORLD); - MPI_Send(&k, 1, MPI_INT, retid, 11, MPI_COMM_WORLD); - - if (tbdbg) - std::cout << "Root gave orders to Worker " << retid - << ": (m, k)=(" << m << ", " << k << ")" << std::endl; - - // Increment counters - k++; - if (k>numk) { - k=0; - m++; - } - } - } - - // - // - // - - while (worker) { - -#ifdef SLEDGE_THROW - int bad; // Get count of sledge failures - MPI_Recv(&bad, 1, MPI_INT, MPI_ANY_SOURCE, 10, - MPI_COMM_WORLD, &status); - totbad += bad; - // Get source node identity - int retid = status.MPI_SOURCE; - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, retid, 11, - MPI_COMM_WORLD, &status); -#else - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, - MPI_ANY_TAG, MPI_COMM_WORLD, &status); -#endif - - mpi_unpack_table(); - - worker--; - } - - if (cache) write_cached_table(); - - } - - - // - // - // - - request_id = -1; - for (worker=1; worker < mpi_numprocs; worker++) - MPI_Send(&request_id, 1, MPI_INT, worker, 11, MPI_COMM_WORLD); - - - // - // - // - - for (m=0; m<=mmax; m++) { - for (k=0; k<=numk; k++) { - int position = mpi_pack_table(&table[m][k], m, k); - MPI_Bcast(mpi_buf, position, MPI_PACKED, 0, MPI_COMM_WORLD); - } - } - - } // END Root - -#ifdef SLEDGE_THROW - // Send total number of sledge failures to all nodes - MPI_Bcast(&totbad, 1, MPI_INT, 0, MPI_COMM_WORLD); - - // All nodes throw exception if any errors - if (totbad) { - std::ostringstream sout; - if (myid==0) { - sout << std::endl - << "SLGridCyl found " << totbad - << " tolerance errors in computing SL solutions" << std::endl - << "We suggest checking your model for sufficient smoothness and" - << std::endl - << "sufficiently many grid points that the relative difference" - << std::endl - << "between field quantities is <= 0.3"; - } else { - sout << std::endl << "sledge tolerance failure"; - } - - throw GenericError(sout.str(), __FILE__, __LINE__); - } -#endif - } - else { - - table = new TableCyl* [mmax+1]; - for (m=0; m<=mmax; m++) table[m] = new TableCyl [numk+1]; - - if (!cache || !read_cached_table()) { - for (m=0; m<=mmax; m++) { - for (k=0; k<=numk; k++) { - if (tbdbg) std::cout << "Begin [" << m << ", " << k << "] . . ." << std::endl; - compute_table(&(table[m][k]), m, k); - if (tbdbg) std::cout << ". . . done" << std::endl; - } - } - if (cache) write_cached_table(); - } - } -} - -const string cyl_cache_name = ".slgrid_cyl_cache"; - -int SLGridCyl::read_cached_table(void) -{ - if (!cache) return 0; - - std::ifstream in(cyl_cache_name); - if (!in) return 0; - - int MMAX, NMAX, NUMR, NUMK, i, j, CMAP; - double RMIN, RMAX, L, AA, RMAP; - std::string MODEL; - - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: trying to read cached table . . ." - << std::endl; - - // Attempt to read magic number - // - unsigned int tmagic; - in.read(reinterpret_cast(&tmagic), sizeof(unsigned int)); - - if (tmagic == hmagic) { - - // YAML size - // - unsigned ssize; - in.read(reinterpret_cast(&ssize), sizeof(unsigned int)); - - // Make and read char buffer - // - auto buf = std::make_unique(ssize+1); - in.read(buf.get(), ssize); - buf[ssize] = 0; // Null terminate - - YAML::Node node; - - try { - node = YAML::Load(buf.get()); - } - catch (YAML::Exception& error) { - std::ostringstream sout; - sout << "YAML: error parsing <" << buf.get() << "> " - << "in " << __FILE__ << ":" << __LINE__ << std::endl - << "YAML error: " << error.what(); - throw GenericError(sout.str(), __FILE__, __LINE__, 1042, false); - } - - // Get parameters - // - MMAX = node["mmax" ].as(); - NMAX = node["nmax" ].as(); - NUMK = node["numk" ].as(); - NUMR = node["numr" ].as(); - CMAP = node["cmap" ].as(); - RMIN = node["rmin" ].as(); - RMAX = node["rmax" ].as(); - RMAP = node["rmapping"].as(); - L = node["L" ].as(); - AA = node["A" ].as(); - MODEL = node["model" ].as(); - } else { - std::cout << "---- SLGridCyl: bad magic number in cache file" << std::endl; - return 0; - } - - - if (MMAX!=mmax) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found mmax=" << MMAX - << " wanted " << mmax << std::endl; - return 0; - } - - if (NMAX!=nmax) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found nmax=" << NMAX - << " wanted " << nmax << std::endl; - return 0; - } - - if (NUMK!=numk) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found numk=" << NUMK - << " wanted " << numk << std::endl; - return 0; - } - - if (NUMR!=numr) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found numr=" << NUMR - << " wanted " << numr << std::endl; - return 0; - } - - if (CMAP!=cmap) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found cmap=" << CMAP - << " wanted " << cmap << std::endl; - return 0; - } - - if (RMIN!=rmin) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found rmin=" << RMIN - << " wanted " << rmin << std::endl; - return 0; - } - - if (RMAX!=rmax) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found rmax=" << RMAX - << " wanted " << rmax << std::endl; - return 0; - } - - if (RMAP!=rmap) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found rmapping=" << RMAP - << " wanted " << rmap << std::endl; - return 0; - } - - if (L!=l) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found l=" << L - << " wanted " << l << std::endl; - return 0; - } - - if (AA!=A) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found A=" << AA - << " wanted " << A << std::endl; - return 0; - } - - if (MODEL!=cyl->ID()) { - if (myid==0) - std::cout << "---- SLGridCyl::read_cached_table: found ID=" << MODEL - << " wanted " << cyl->ID() << std::endl; - return 0; - } - - for (int m=0; m<=mmax; m++) { - for (int k=0; k<=numk; k++) { - - in.read((char *)&table[m][k].m, sizeof(int)); - in.read((char *)&table[m][k].k, sizeof(int)); - - // Double check - if (table[m][k].m != m) { - std::cerr << "---- SLGridCyl: error reading <" << cyl_cache_name << ">" << std::endl; - std::cerr << "---- SLGridCyl: m: read value (" << table[m][k].m << ") != internal value (" << m << ")" << std::endl; - return 0; - } - if (table[m][k].k != k) { - std::cerr << "---- SLGridCyl: error reading <" << cyl_cache_name << ">" << std::endl; - std::cerr << "---- SLGridCyl: k: read value (" << table[m][k].k << ") != internal value (" << k << ")" << std::endl; - return 0; - } - - table[m][k].ev.resize(nmax); - table[m][k].ef.resize(nmax, numr); - - for (int j=0; j" << std::endl; - return; - } - - // This is a node of simple {key: value} pairs. More general - // content can be added as needed. - YAML::Node node; - - node["mmax" ] = mmax; - node["nmax" ] = nmax; - node["numk" ] = numk; - node["numr" ] = numr; - node["cmap" ] = cmap; - node["rmin" ] = rmin; - node["rmax" ] = rmax; - node["rmapping"] = rmap; - node["L" ] = l; - node["A" ] = A; - node["model" ] = cyl->ID(); - - // Serialize the node - // - YAML::Emitter y; y << node; - - // Get the size of the string - // - unsigned int hsize = strlen(y.c_str()); - - // Write magic # - // - out.write(reinterpret_cast(&hmagic), sizeof(unsigned int)); - - // Write YAML string size - // - out.write(reinterpret_cast(&hsize), sizeof(unsigned int)); - - // Write YAML string - // - out.write(reinterpret_cast(y.c_str()), hsize); - - // Now, write the tables - // - for (int m=0; m<=mmax; m++) { - for (int k=0; k<=numk; k++) { - - out.write((char *)&table[m][k].m, sizeof(int)); - out.write((char *)&table[m][k].k, sizeof(int)); - - for (int j=0; j=1.0) { - std::ostringstream ostr; - ostr << "xi=" << xi << " >= 1!"; - bomb(ostr.str()); - } - - return (1.0+xi)/(1.0 - xi) * rmap; - } else { - return xi; - } - -} - -double SLGridCyl::d_xi_to_r(double xi) -{ - if (cmap) { - if (xi<-1.0) { - std::ostringstream ostr; - ostr << "xi=" << xi << " < -1!"; - bomb(ostr.str()); - } - - if (xi>=1.0) { - std::ostringstream ostr; - ostr << "xi=" << xi << " >= 1!"; - bomb(ostr.str()); - } - - return 0.5*(1.0-xi)*(1.0-xi)/rmap; - } else { - return 1.0; - } -} - -double SLGridCyl::get_pot(double x, int m, int n, int k, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<0) indx = 0; - if (indx>numr-2) indx = numr - 2; - - double x1 = (xi[indx+1] - x)/dxi; - double x2 = (x - xi[indx])/dxi; - - - if (std::isnan( - (x1*table[m][k].ef(n, indx) + x2*table[m][k].ef(n, indx+1))/ - sqrt(fabs(table[m][k].ev[n])) * (x1*p0[indx] + x2*p0[indx+1]) - ) - ) - { - std::cout << "Ooops" << std::endl; - } - - -#ifdef USE_TABLE - return (x1*table[m][k].ef(n, indx) + x2*table[m][k].ef(n, indx+1))/ - sqrt(fabs(table[m][k].ev[n])) * (x1*p0[indx] + x2*p0[indx+1]); -#else - return (x1*table[m][k].ef(n, indx) + x2*table[m][k].ef(n, indx+1))/ - sqrt(fabs(table[m][k].ev[n])) * cyl->pot(xi_to_r(x)); -#endif -} - - -double SLGridCyl::get_dens(double x, int m, int n, int k, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<0) indx = 0; - if (indx>numr-2) indx = numr - 2; - - double x1 = (xi[indx+1] - x)/dxi; - double x2 = (x - xi[indx])/dxi; - -#ifdef USE_TABLE - return (x1*table[m][k].ef(n, indx) + x2*table[m][k].ef(n, indx+1)) * - sqrt(fabs(table[m][k].ev[n])) * (x1*d0[indx] + x2*d0[indx+1]) - * table[m][k].ev[n]/fabs(table[m][k].ev[n]); -#else - return (x1*table[m][k].ef(n, indx) + x2*table[m][k].ef(n, indx+1)) * - sqrt(fabs(table[m][k].ev[n])) * cyl->dens(xi_to_r(x)) - * table[m][k].ev[n]/fabs(table[m][k].ev[n]); -#endif - -} - -double SLGridCyl::get_force(double x, int m, int n, int k, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<1) indx = 1; - if (indx>numr-2) indx = numr - 2; - - - double p = (x - xi[indx])/dxi; - - // Use three point formula - - // Point -1: indx-1 - // Point 0: indx - // Point 1: indx+1 - - return d_xi_to_r(x)/dxi * ( - (p - 0.5)*table[m][k].ef(n, indx-1)*p0[indx-1] - -2.0*p*table[m][k].ef(n, indx)*p0[indx] - + (p + 0.5)*table[m][k].ef(n, indx+1)*p0[indx+1] - ) / sqrt(fabs(table[m][k].ev[n])); -} - - -void SLGridCyl::get_pot(Eigen::MatrixXd& mat, double x, int m, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - mat.resize(numk+1, nmax); - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<0) indx = 0; - if (indx>numr-2) indx = numr - 2; - - - double x1 = (xi[indx+1] - x)/dxi; - double x2 = (x - xi[indx])/dxi; - - - for (int k=0; k<=numk; k++) { - for (int n=0; npot(xi_to_r(x)); -#endif -#ifdef DEBUG_NAN - if (std::isnan(mat(k, n)) || std::isinf(mat(k, n)) ) { - std::cerr << "SLGridCyl::get_pot: invalid value" << std::endl; - } -#endif - } - } - -} - - -void SLGridCyl::get_dens(Eigen::MatrixXd& mat, double x, int m, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - mat.resize(numk+1, nmax); - - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<0) indx = 0; - if (indx>numr-2) indx = numr - 2; - - - double x1 = (xi[indx+1] - x)/dxi; - double x2 = (x - xi[indx])/dxi; - - - for (int k=0; k<=numk; k++) { - for (int n=0; ndens(xi_to_r(x)) - * table[m][k].ev[n]/fabs(table[m][k].ev[n]); -#endif - } - } - -} - - -void SLGridCyl::get_force(Eigen::MatrixXd& mat, double x, int m, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - mat.resize(numk+1, nmax); - - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<1) indx = 1; - if (indx>numr-2) indx = numr - 2; - - - double p = (x - xi[indx])/dxi; - double fac = d_xi_to_r(x)/dxi; - - for (int k=0; k<=numk; k++) { - for (int n=0; n=1.0) x=1.0-XOFFSET; - } - - vec.resize(nmax); - - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<0) indx = 0; - if (indx>numr-2) indx = numr - 2; - - - double x1 = (xi[indx+1] - x)/dxi; - double x2 = (x - xi[indx])/dxi; - - - for (int n=0; npot(xi_to_r(x)); -#endif - } - -} - - -void SLGridCyl::get_dens(Eigen::VectorXd& vec, double x, int m, int k, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - vec.resize(nmax); - - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<0) indx = 0; - if (indx>numr-2) indx = numr - 2; - - - double x1 = (xi[indx+1] - x)/dxi; - double x2 = (x - xi[indx])/dxi; - - - for (int n=0; ndens(xi_to_r(x)) - * table[m][k].ev[n]/fabs(table[m][k].ev[n]); -#endif - } - -} - - -void SLGridCyl::get_force(Eigen::VectorXd& vec, double x, int m, int k, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - vec.resize(nmax); - - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<1) indx = 1; - if (indx>numr-2) indx = numr - 2; - - - double p = (x - xi[indx])/dxi; - double fac = d_xi_to_r(x)/dxi; - - for (int n=0; n=1.0) x=1.0-XOFFSET; - } - - if (mmax < mMax) mMax = mmax; - - for (int m=mMin; m<=mMax; m++) - mat[m].resize(numk+1, nmax); - - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<0) indx = 0; - if (indx>numr-2) indx = numr - 2; - - - double x1 = (xi[indx+1] - x)/dxi; - double x2 = (x - xi[indx])/dxi; - - - for (int m=mMin; m<=mMax; m++) { - for (int k=0; k<=numk; k++) { - for (int n=0; npot(xi_to_r(x)); -#endif -#ifdef DEBUG_NAN - if (std::isnan(mat[m](k, n)) || std::isinf(mat[m](k, n)) ) { - std::cerr << "SLGridCyl::get_pot: invalid value" << std::endl; - std::cerr << " x1=" << x1 - << "\n x2=" << x2 - << "\n t0=" << table[m][k].ef(n, indx) - << "\n p0=" << p0[indx] - << "\n tp=" << table[m][k].ef(n, indx+1) - << "\n pp=" << p0[indx+1] - << "\n ev=" << fabs(table[m][k].ev[n]) - << "\n val=" << (x1*table[m][k].ef(n, indx) + - x2*table[m][k].ef(n, indx+1))/ - sqrt(fabs(table[m][k].ev[n])) * (x1*p0[indx] + x2*p0[indx+1]) - << "" << std::endl; - } -#endif - } - } - } - -} - - -void SLGridCyl::get_dens(Eigen::MatrixXd* mat, double x, int mMin, int mMax, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - if (mmax < mMax) mMax = mmax; - - for (int m=mMin; m<=mMax; m++) - mat[m].resize(numk+1, nmax); - - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<0) indx = 0; - if (indx>numr-2) indx = numr - 2; - - - double x1 = (xi[indx+1] - x)/dxi; - double x2 = (x - xi[indx])/dxi; - - - for (int m=mMin; m<=mMax; m++) { - for (int k=0; k<=numk; k++) { - for (int n=0; ndens(xi_to_r(x)) - * table[m][k].ev[n]/fabs(table[m][k].ev[n]); -#endif - } - } - } - -} - - -void SLGridCyl::get_force(Eigen::MatrixXd* mat, double x, int mMin, int mMax, int which) -{ - if (which || !cmap) - x = r_to_xi(x); - else { - if (x<-1.0) x=-1.0; - if (x>=1.0) x=1.0-XOFFSET; - } - - if (mmax < mMax) mMax = mmax; - - for (int m=mMin; m<=mMax; m++) - mat[m].resize(numk+1, nmax); - - - // XI grid is same for all k - - int indx = (int)( (x-xmin)/dxi ); - if (indx<1) indx = 1; - if (indx>numr-2) indx = numr - 2; - - - double p = (x - xi[indx])/dxi; - double fac = d_xi_to_r(x)/dxi; - - for (int m=mMin; m<=mMax; m++) { - for (int k=0; k<=numk; k++) { - for (int n=0; npot(cons[6]); - if (M==0) { - cons[0] = cyl->dpot(cons[6])/f; - cons[2] = 1.0/(cons[6]*f*f); - } - else - cons[0] = 1.0; - - // Outer BC - f = cyl->pot(cons[7]); - // cons[4] = (1.0+M)/cons[7] + dpot(cons[7])/f; - // TEST - cons[4] = (1.0+M)/(cons[7]*cons[7]) + cyl->dpot(cons[7])/f/cons[7]; - cons[5] = 1.0/(cons[7]*f*f); - - - // - // Initialize the vector INVEC(*): - // estimates for the eigenvalues/functions specified - // - - invec[0] = VERBOSE; // little printing (1), no printing (0) - invec[1] = 3; // spectrum is ignored - invec[2] = N; // estimates for N eigenvalues/functions - - for (int i=0; i0) { - - if (myid==0) { - - std::cout.precision(6); - std::cout.setf(ios::scientific); - std::cout << std::left; - - std::cout << std::endl - << "Tolerance errors in Sturm-Liouville solver for m=" - << std::endl << std::endl; - - std::cout << std::setw(15) << "order" - << std::setw(15) << "eigenvalue" - << std::setw(40) << "condition" - << std::endl - << std::setw(15) << "-----" - << std::setw(15) << "----------" - << std::setw(40) << "---------" - << std::endl; - - for (int i=0; i -10) { - std::cout << std::setw(14) << "x" - << std::setw(25) << "u(x)" - << std::setw(25) << "(pu`)(x)" - << std::endl; - k = NUM*i; - for (int j=0; jev.resize(N); - for (int i=0; iev[i] = ev[i]; - - table->ef.resize(N, numr); - - for (int i=0; ief(j, i) = ef[j*NUM+i]; - } - - table->m = m; - table->k = k; - - delete [] iflag; - delete [] invec; - delete [] ev; - delete [] store; - delete [] xef; - delete [] ef; - delete [] pdef; - -} - - -void SLGridCyl::init_table(void) -{ - xi.resize(numr); - r .resize(numr); - p0.resize(numr); - d0.resize(numr); - - if (cmap) { - xmin = (rmin/rmap - 1.0)/(rmin/rmap + 1.0); - xmax = (rmax/rmap - 1.0)/(rmax/rmap + 1.0); - dxi = (xmax-xmin)/(numr-1); - } else { - xmin = rmin; - xmax = rmax; - dxi = (xmax-xmin)/(numr-1); - } - - for (int i=0; ipot(r[i]); - d0[i] = cyl->dens(r[i]); - } - -} - - -void SLGridCyl::compute_table_worker(void) -{ - - double cons[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - double tol[6] = {1.0e-4*rmap,1.0e-5, - 1.0e-4*rmap,1.0e-5, - 1.0e-4*rmap,1.0e-5}; - int i, j, VERBOSE=0; - integer NUM; - logical type[8]; - logical endfin[2] = {1, 1}; - - struct TableCyl table; - int M, N, K; - -#ifdef DEBUG_SLEDGE - if (myid==0) VERBOSE = SLEDGE_VERBOSE; -#endif - - if (tbdbg) - std::cout << "Worker " << mpi_myid << " begins . . ." << std::endl; - - // - // - // - - int request_id; - - while(1) { - - MPI_Recv(&request_id, 1, - MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &status); - - if (request_id < 0) break; // Good-bye - - MPI_Recv(&M, 1, - MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &status); - MPI_Recv(&K, 1, - MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &status); - - - if (tbdbg) - std::cout << "Worker " << mpi_myid << ": ordered to compute (m, k)=(" - << M << ", " << K << ")" << std::endl; - - cons[0] = cons[1] = cons[2] = cons[3] = cons[4] = cons[5] = 0.0; - cons[6] = rmin; - cons[7] = rmax; - M2 = M*M; - K2 = kv[K]*kv[K]; - NUM = numr; - N = nmax; - - // integer iflag[nmax], invec[nmax+3]; - integer *iflag = new integer [nmax]; - integer *invec = new integer [nmax+3]; - - - double *t=0, *rho=0; - double *ev = new double [N]; - double *store = new double [26*(NUM+16)]; - double *xef = new double [NUM+16]; - double *ef = new double [NUM*N]; - double *pdef = new double [NUM*N]; - double f; - - f = cyl->pot(cons[6]); - cons[2] = -1.0/(cons[6]*f); - cons[4] = M/cons[7]; - f = cyl->pot(cons[7]); - cons[5] = 1.0/(cons[7]*f*f); - - // - // Initialize the vector INVEC(*): - // estimates for the eigenvalues/functions specified - // - - invec[0] = VERBOSE; // little printing (1), no printing (0) - invec[1] = 3; // spectrum is ignored - invec[2] = N; // estimates for N eigenvalues/functions - - for (i=0; i0) { - std::cout.precision(6); - std::cout.setf(ios::scientific); - std::cout << std::left; - - std::cout << std::endl - << "Tolerance errors in Sturm-Liouville solver for m=" - << M << " K=" << K << std::endl << std::endl; - - std::cout << std::setw(15) << "order" - << std::setw(15) << "eigenvalue" - << std::setw(40) << "condition" - << std::endl - << std::setw(15) << "-----" - << std::setw(15) << "----------" - << std::setw(40) << "---------" - << std::endl; - - for (int i=0; i -10) { - std::cout << std::setw(14) << "x" - << std::setw(25) << "u(x)" - << std::setw(25) << "(pu`)(x)" - << std::endl; - int k = NUM*i; - for (j=0; j slab; - mpi_buf = new char [mpi_bufsz]; +extern "C" { + int sledge_(logical* job, doublereal* cons, logical* endfin, + integer* invec, doublereal* tol, logical* type, + doublereal* ev, integer* numx, doublereal* xef, doublereal* ef, + doublereal* pdef, doublereal* t, doublereal* rho, + integer* iflag, doublereal* store); } -int SLGridCyl::mpi_pack_table(struct TableCyl* table, int m, int k) +static +std::string sledge_error(int flag) { - int position = 0; - - MPI_Pack( &m, 1, MPI_INT, mpi_buf, mpi_bufsz, - &position, MPI_COMM_WORLD); - MPI_Pack( &k, 1, MPI_INT, mpi_buf, mpi_bufsz, - &position, MPI_COMM_WORLD); - - for (int j=0; jev[j], 1, MPI_DOUBLE, mpi_buf, mpi_bufsz, - &position, MPI_COMM_WORLD); - - for (int j=0; jef(j, i), 1, MPI_DOUBLE, mpi_buf, mpi_bufsz, - &position, MPI_COMM_WORLD); - - return position; + if (flag==0) + return "reliable"; + else if (flag==-1) + return "too many levels for eigenvalues"; + else if (flag==-2) + return "too many levels for eigenvectors"; + else if (flag==1) + return "eigenvalue cluster?"; + else if (flag==2) + return "classification uncertainty"; + else if (flag>0) { + std::ostringstream sout; + sout << "unexpected warning: " << flag; + return sout.str(); + } else { + std::ostringstream sout; + sout << "unexpected fatal error: " << flag; + return sout.str(); + } } +static double L2, M2, K2; +static int sl_dim; -void SLGridCyl::mpi_unpack_table(void) -{ - int length, position = 0; - int m, k; - - int retid = status.MPI_SOURCE; - - // MPI_Get_count( &status, MPI_PACKED, &length); - - length = mpi_bufsz; - - - MPI_Unpack( mpi_buf, length, &position, &m, 1, MPI_INT, - MPI_COMM_WORLD); - - MPI_Unpack( mpi_buf, length, &position, &k, 1, MPI_INT, - MPI_COMM_WORLD); - - if (tbdbg) - std::cout << "Process " << mpi_myid << ": unpacking table entry from Process " - << retid << ": (m, k)=(" << m << ", " << k << ")" << std::endl; - - table[m][k].m = m; - table[m][k].k = k; - table[m][k].ev.resize(nmax); - table[m][k].ef.resize(nmax, numr); - - - for (int j=0; jpot(*x); - rho = cyl->dens(*x); - - *px = (*x)*f*f; - *qx = (M2*f/(*x) + K2*f*(*x) - cyl->dens(*x)*(*x))*f; - *rx = -rho*(*x)*f; - } else { // Spherical f = sphpot(*x); diff --git a/include/SLGridMP2.H b/include/SLGridMP2.H index 92de78fa1..391e1b7b3 100644 --- a/include/SLGridMP2.H +++ b/include/SLGridMP2.H @@ -23,152 +23,6 @@ using namespace __EXP__; #include #endif -//! Target density models for thin disks -class CylModel -{ -protected: - //! For cache identification - std::string id; - -public: - - //! Factory constructor - static std::shared_ptr createModel(const std::string type); - - //! Return the potential - virtual double pot(double r) = 0; - - //! Return the derivative of the potential - virtual double dpot(double r) = 0; - - //! Return the surface density - virtual double dens(double r) = 0; - - //! Get model ID - std::string ID() const { return id; } -}; - -//! Cylindrical SL grid class -class SLGridCyl -{ - -private: - - int mmax, nmax, numr, numk; - double rmin, rmax, l; - - int cmap; - double rmap; - - double dk; - - double xmin, xmax, dxi; - - Eigen::VectorXd kv; - Eigen::VectorXd r; - Eigen::VectorXd xi; - Eigen::VectorXd p0; - Eigen::VectorXd d0; - - TableCyl** table; - - void init_table(void); - void compute_table(TableCyl* table, int M, int K); - void compute_table_worker(void); - int read_cached_table(void); - void write_cached_table(void); - - //! Basis magic number - inline static const unsigned int hmagic = 0xc0a56a2; - - // Local MPI stuff - void mpi_setup(void); - void mpi_unpack_table(void); - int mpi_pack_table(TableCyl* table, int m, int k); - - int mpi_myid, mpi_numprocs; - int mpi_bufsz; - char *mpi_buf; - - void bomb(string oops); - - //! Use basis cache - bool cache; - - //! For deep debugging - bool tbdbg; - -public: - - //! Global MPI indicator, default: 0=off - static int mpi; - - //! Exponential scale length, default: 1.0 - static double A; - - //! Constructor - SLGridCyl(int mmax, int nmax, int numr, int numk, double rmin, double rmax, - double l, bool cache, int Cmap, double RMAP, - const std::string type, bool Verbose=false); - - //! Destructor - ~SLGridCyl(); - - // Members - - //! Return eigenvalue for given order - double eigenvalue(int m, int k, int n) {return table[m][k].ev[n];} - - //! Dimensional to dimensionless coordinate mapping - double r_to_xi(double r); - - //! Dimensionless to dimensional coordinate mapping - double xi_to_r(double x); - - //! Jacobian of dimensionless mapping - double d_xi_to_r(double x); - - //! Get potential basis value - double get_pot(double x, int m, int n, int k, int which=1); - - //! Get density basis value - double get_dens(double x, int m, int n, int k, int which=1); - - //! Get force basis value - double get_force(double x, int m, int n, int k, int which=1); - - //! Fill vector with desired potential basis - void get_pot(Eigen::VectorXd& vec, double x, int m, int k, int which=1); - - //! Fill vector with desired density basis - void get_dens(Eigen::VectorXd& vec, double x, int m, int k, int which=1); - - //! Fill vector with desired force basis - void get_force(Eigen::VectorXd& vec, double x, int m, int k, int which=1); - - //! Fill Matrix with desired potential basis - void get_pot(Eigen::MatrixXd& tab, double x, int m, int which=1); - -#if HAVE_LIBCUDA==1 - void get_pot_cuda(float* tab, double x, int m, int which=1); -#endif - - //! Fill Matrix with desired potential basis - void get_dens(Eigen::MatrixXd& tab, double x, int m, int which=1); - - //! Fill Matrix with desired potential basis - void get_force(Eigen::MatrixXd& tab, double x, int m, int which=1); - - //! Fill Matricies with desired potential basis - void get_pot(Eigen::MatrixXd* tab, double x, int mMin, int mMax, int which=1); - - //! Fill Matricies with desired density basis - void get_dens(Eigen::MatrixXd* tab, double x, int mMin, int mMax, int which=1); - //! Fill Matricies with desired force basis - void get_force(Eigen::MatrixXd* tab, double x, int mMin, int mMax, int which=1); - -}; - //!! Spherical SL grid class class SLGridSph diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a9ebe6cf8..b131c659e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,7 +17,7 @@ set(exp_SOURCES Basis.cc Bessel.cc CBrock.cc Component.cc Output.cc externalShock.cc CylEXP.cc generateRelaxation.cc HaloBulge.cc incpos.cc incvel.cc ComponentContainer.cc OutAscii.cc OutMulti.cc OutRelaxation.cc OrbTrace.cc OutDiag.cc OutLog.cc - OutVel.cc OutCoef.cc multistep.cc parse.cc Slab.cc SlabSL.cc step.cc + OutVel.cc OutCoef.cc multistep.cc parse.cc SlabSL.cc step.cc tidalField.cc ultra.cc ultrasphere.cc MPL.cc OutFrac.cc OutCalbr.cc ParticleFerry.cc pCell.cc chkSlurm.c chkTimer.cc pHOT.cc GravKernel.cc ${CUDA_SRC} CenterFile.cc PolarBasis.cc FlatDisk.cc diff --git a/src/Component.cc b/src/Component.cc index 609c1b07e..5ea1a17cf 100644 --- a/src/Component.cc +++ b/src/Component.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -905,9 +904,6 @@ void Component::configure(void) else if ( !id.compare("cube") ) { force = new Cube(this, fconf); } - else if ( !id.compare("slab") ) { - force = new Slab(this, fconf); - } else if ( !id.compare("slabSL") ) { force = new SlabSL(this, fconf); } diff --git a/src/Slab.H b/src/Slab.H deleted file mode 100644 index 67c5c98e8..000000000 --- a/src/Slab.H +++ /dev/null @@ -1,76 +0,0 @@ -#ifndef _Slab_H -#define _Slab_H - -#include -#include - -#include - -/*! This routine computes the potential, acceleration and density - using periodic box expansion in X & Y and slab in Z */ -class Slab : public PotAccel -{ - -//! Header structure for slab expansion -struct SlabCoefHeader { - double time; - double zmax; - double h; - int type; - int nmaxx, nmaxy, nmaxz; - int jmax; -}; - -private: - - std::vector expccof; - std::vector> trig; - - int nmaxx, nmaxy, nmaxz; - int nminx, nminy; - double zmax; - - int imx, imy, imz, jmax, nnmax; - double dfac; - std::complex kfac; - - std::vector zfrc, zpot; - - SlabCoefHeader coefheader; - - void determine_coefficients(void); - void get_acceleration_and_potential(Component*); - - // Threading - - void * determine_coefficients_thread(void * arg); - void * determine_acceleration_and_potential_thread(void * arg); - - // Biorth ID - static const int ID=0; - -protected: - - //! Parse parameters and initialize on first call - void initialize(void); - - //! Valid keys for YAML configurations - static const std::set valid_keys; - -public: - - //! Id string - string id; - - //! Constructor - Slab(Component* c0, const YAML::Node& conf); - - //! Destructor - virtual ~Slab(); - - //! Print coefficients to output stream - void dump_coefs(ostream& out); -}; - - -#endif diff --git a/src/Slab.cc b/src/Slab.cc deleted file mode 100644 index c51f82539..000000000 --- a/src/Slab.cc +++ /dev/null @@ -1,339 +0,0 @@ -#include -#include -#include - -#include "expand.H" - -#include - -#include - -static const double KEPS=1.0e-6; - -const std::set -Slab::valid_keys = { - "nmaxx", - "nmaxy", - "nmaxz", - "nminx", - "nminy", - "zmax" -}; - -Slab::Slab(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) -{ - id = "Slab (trigonometric)"; - nminx = nminy = 0; - nmaxx = nmaxy = nmaxz = 10; - zmax = 10.0; - coef_dump = true; - - initialize(); - - imx = 1+2*nmaxx; - imy = 1+2*nmaxy; - imz = nmaxz; - jmax = imx*imy*imz; - - expccof.resize(nthrds); - for (auto & v : expccof) v.resize(jmax); - - dfac = 2.0*M_PI; - kfac = std::complex(0.0, dfac); - - nnmax = (nmaxx > nmaxy) ? nmaxx : nmaxy; - - // I believe that these are thread safe - trig.resize(nnmax+1); - for (int i=0; i<=nnmax; i++) { - trig[i].resize(i+1); - for (int j=0; j<=i; j++) - trig[i][j].reset(dfac*sqrt((double)(i*i + j*j))+KEPS, zmax); - } - - zpot.resize(nthrds); - zfrc.resize(nthrds); - - for (auto & v : zpot) v.resize(nmaxz); - for (auto & v : zfrc) v.resize(nmaxz); - -} - -Slab::~Slab() -{ - // NADA -} - -void Slab::initialize() -{ - // Remove matched keys - // - for (auto v : valid_keys) current_keys.erase(v); - - // Assign values from YAML - // - try { - if (conf["nmaxx"]) nmaxx = conf["nmaxx"].as(); - if (conf["nmaxy"]) nmaxy = conf["nmaxy"].as(); - if (conf["nmaxz"]) nmaxz = conf["nmaxz"].as(); - if (conf["nminx"]) nminx = conf["nminx"].as(); - if (conf["nminy"]) nminy = conf["nminy"].as(); - if (conf["zmax"]) zmax = conf["zmax"].as(); - } - catch (YAML::Exception & error) { - if (myid==0) std::cout << "Error parsing parameters in Slab: " - << error.what() << std::endl - << std::string(60, '-') << std::endl - << "Config node" << std::endl - << std::string(60, '-') << std::endl - << conf << std::endl - << std::string(60, '-') << std::endl; - throw std::runtime_error("Slab::initialize: error parsing YAML"); - } -} - -void Slab::determine_coefficients(void) -{ - // Coefficients are ordered as follows: - // n=-nmax,-nmax+1,...,0,...,nmax-1,nmax - // in a single array for each dimension - // with z dimension changing most rapidly - - // Clean - - for (int i=0; i startx, starty, facx, facy; - std::complex stepx, stepy; - - unsigned nbodies = cC->Number(); - int id = *((int*)arg); - int nbeg = nbodies*id/nthrds; - int nend = nbodies*(id+1)/nthrds; - double adb = component->Adiabatic(); - double zz; - - PartMapItr it = cC->Particles().begin(); - unsigned long i; - - for (int q=0 ; qfirst; - // Increment particle counter - use[id]++; - - // Truncate to box with sides in [0,1] - - if (cC->Pos(i, 0)<0.0) - cC->AddPos(i, 0, (double)((int)fabs(cC->Pos(i, 0)) + 1.0) ); - else - cC->AddPos(i, 0, -(double)((int)cC->Pos(i, 0))); - - if (cC->Pos(i, 1)<0.0) - cC->AddPos(i, 1, (double)((int)fabs(cC->Pos(i, 1))) + 1.0); - else - cC->AddPos(i, 1, -(double)((int)cC->Pos(i, 1))); - - - // Recursion multipliers - stepx = exp(-kfac*cC->Pos(i, 0)); - stepy = exp(-kfac*cC->Pos(i, 1)); - - // Initial values - startx = exp(static_cast(nmaxx)*kfac*cC->Pos(i, 0)); - starty = exp(static_cast(nmaxy)*kfac*cC->Pos(i, 1)); - - for (facx=startx, ix=0; ix nmaxx) { - cerr << "Out of bounds: iix=" << iix << endl; - } - - for (facy=starty, iy=0; iy nmaxy) { - cerr << "Out of bounds: iiy=" << iiy << endl; - } - - zz = cC->Pos(i, 2) - cC->center[2]; - - if (iix>=iiy) - trig[iix][iiy].potl(dum, dum, zz, zpot[id]); - else - trig[iiy][iix].potl(dum, dum, zz, zpot[id]); - - - for (iz=0; izMass(i)*adb* - facx*facy*zpot[id][iz+1]; - } - } - } - } - - return (NULL); -} - -void Slab::get_acceleration_and_potential(Component* C) -{ - cC = C; - - determine_coefficients(); - - MPL_start_timer(); - exp_thread_fork(false); - MPL_stop_timer(); -} - -void * Slab::determine_acceleration_and_potential_thread(void * arg) -{ - int ix, iy, iz, iix, iiy, ii, jj, indx, dum; - - std::complex fac, startx, starty, facx, facy, potl, facf; - std::complex stepx, stepy; - std::complex accx, accy, accz; - - unsigned nbodies = cC->Number(); - int id = *((int*)arg); - int nbeg = nbodies*id/nthrds; - int nend = nbodies*(id+1)/nthrds; - double zz; - - PartMapItr it = cC->Particles().begin(); - unsigned long i; - - for (int q=0 ; qfirst; - - accx = accy = accz = potl = 0.0; - - // Recursion multipliers - stepx = exp(kfac*cC->Pos(i, 0)); - stepy = exp(kfac*cC->Pos(i, 1)); - - // Initial values (note sign change) - startx = exp(-static_cast(nmaxx)*kfac*cC->Pos(i, 0)); - starty = exp(-static_cast(nmaxy)*kfac*cC->Pos(i, 1)); - - for (facx=startx, ix=0; ix nmaxx) { - cerr << "Out of bounds: iix=" << iix << endl; - } - if (iiy < 0 || iiy > nmaxy) { - cerr << "Out of bounds: iiy=" << iiy << endl; - } - - zz = cC->Pos(i, 2) - cC->center[2]; - - if (iix>=iiy) { - trig[iix][iiy].potl(dum, dum, zz, zpot[id]); - trig[iix][iiy].force(dum, dum, zz, zfrc[id]); - } - else { - trig[iiy][iix].potl(dum, dum, zz, zpot[id]); - trig[iiy][iix].force(dum, dum, zz, zfrc[id]); - } - - - for (iz=0; iz(0.0,1.0)*fac; - accy += -dfac*jj*std::complex(0.0,1.0)*fac; - accz += facf; - - } - } - } - - cC->AddAcc(i, 0, accx.real()); - cC->AddAcc(i, 1, accy.real()); - cC->AddAcc(i, 2, accz.real()); - cC->AddPot(i, potl.real()); - } - - return (NULL); -} - -void Slab::dump_coefs(ostream& out) -{ - coefheader.time = tnow; - coefheader.zmax = zmax; - coefheader.h = 0.0; - coefheader.type = ID; - coefheader.nmaxx = nmaxx; - coefheader.nmaxy = nmaxy; - coefheader.nmaxz = nmaxz; - coefheader.jmax = (1+2*nmaxx)*(1+2*nmaxy)*nmaxz; - - out.write((char *)&coefheader, sizeof(SlabCoefHeader)); - out.write((char *)expccof[0].data(), jmax*sizeof(std::complex)); -} - diff --git a/utils/SL/CMakeLists.txt b/utils/SL/CMakeLists.txt index 506fe145e..8e221a405 100644 --- a/utils/SL/CMakeLists.txt +++ b/utils/SL/CMakeLists.txt @@ -1,6 +1,6 @@ -set(bin_PROGRAMS slcheck slshift orthochk diskpot qtest cyltest - eoftest oftest) +set(bin_PROGRAMS slcheck slshift orthochk diskpot qtest eoftest + oftest) set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX yaml-cpp exputil ${VTK_LIBRARIES}) @@ -27,7 +27,6 @@ add_executable(slshift slshift.cc SLSphere.cc) add_executable(orthochk orthochk.cc) add_executable(diskpot diskpot.cc CylindricalDisk.cc SLSphere.cc) add_executable(qtest qtest.cc) -add_executable(cyltest cyltest.cc) add_executable(eoftest EOF2d.cc) add_executable(oftest oftest.cc) diff --git a/utils/SL/cyltest.cc b/utils/SL/cyltest.cc deleted file mode 100644 index 4ee092f0b..000000000 --- a/utils/SL/cyltest.cc +++ /dev/null @@ -1,390 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - - -//! For comparing CB to 3d pillbox with Kuzmin disk and checking CB -//! orthogonality [Potential] -double CB_get_potl(int M, int N, double r) -{ - double r2 = r*r; - double fac = 1.0/(1.0 + r2); - double fac1 = (r2 - 1.0)*fac; - double cur0 = sqrt(fac), rcum = 1.0; - - Eigen::MatrixXd p(M+1, N+1); - - for (int l=0; l<=M; l++) { - double cur = cur0; - - p(l, 0) = cur*rcum; - double curl1 = 0.0; - - for (int nn=0; nn0) { - d(l, 1) = w(l+1, 1)*rcum; - for (int nn=1; nn(cmap)->default_value("0")) - ("scale", "scaling from real coordinates to table", - cxxopts::value(scale)->default_value("1.0")) - ("A,length", "characteristic disk scale length", - cxxopts::value(A)->default_value("1.0")) - ("L,thick", "pillbox size", - cxxopts::value(L)->default_value("1.0")) - ("mmax", "maximum number of angular harmonics in the expansion", - cxxopts::value(mmax)->default_value("4")) - ("nmax", "maximum number of radial harmonics in the expansion", - cxxopts::value(nmax)->default_value("10")) - ("kmax", "maximum number of vertical wave numbers in the expansion", - cxxopts::value(kmax)->default_value("10")) - ("numr", "radial knots for the SL grid", - cxxopts::value(numr)->default_value("1000")) - ("r,rmin", "minimum radius for the SL grid", - cxxopts::value(rmin)->default_value("0.0001")) - ("R,rmax", "maximum radius for the SL grid", - cxxopts::value(rmax)->default_value("20.0")) - ("num", "number of output grid points", - cxxopts::value(num)->default_value("1000")) - ("knots", "Number of Legendre integration knots", - cxxopts::value(knots)->default_value("40")) - ("cache", "cache file", - cxxopts::value(cachefile)->default_value(".slgrid_sph_cache")) - ("model", "SL model target type", - cxxopts::value(model)->default_value("expon")) - ; - - - //=================== - // Parse options - //=================== - - cxxopts::ParseResult vm; - - try { - vm = options.parse(argc, argv); - } catch (cxxopts::OptionException& e) { - if (myid==0) std::cout << "Option error: " << e.what() << std::endl; - if (use_mpi) MPI_Finalize(); - return 2; - } - - // Print help message and exit - // - if (vm.count("help")) { - if (myid == 0) { - std::cout << options.help() << std::endl << std::endl; - } - if (use_mpi) MPI_Finalize(); - return 1; - } - - // Orthogonality? - // - if (vm.count("ortho")) ortho_check = true; - - // Log spacing? - // - if (vm.count("logr")) logr = true; - - // Debugging output? - // - if (vm.count("debug")) dbg = true; - - if (use_mpi) { - SLGridCyl::mpi = 1; // Turn on MPI - } else { - SLGridCyl::mpi = 0; // Turn off MPI - } - - SLGridCyl::A = A; // Set default scale length - - if (vm.count("CB")) { - CB = true; - SLGridCyl::A = 1; - model = "kuzmin"; - } - - // Generate Sturm-Liouville grid - // - auto ortho = std::make_shared(mmax, nmax, numr, kmax, rmin, rmax, - L, true, cmap, scale, model, dbg); - // ^ ^ ^ ^ ^ ^ - // | | | | | | - // Slab height --------------------------+ | | | | | - // | | | | | - // Use model cache -------------------------+ | | | | - // | | | | - // Coordinate mapping type -----------------------+ | | | - // | | | - // Radial scale size -----------------------------------+ | | - // | | - // Target density-potential for SL creation--------------------+ | - // | - // Turn on diagnostic output in SL creation---------------------------+ - - // Workers exit - if (use_mpi && myid>0) { - MPI_Finalize(); - exit(0); - } - - std::cout << "Filename? "; - std::cin >> filename; - std::ofstream out (filename.c_str()); - if (!out) { - std::cout << "Can't open <" << filename << "> for output" << endl; - exit(-1); - } - - cout << "M, N, K? "; - int M, K, N; - std::cin >> M; - std::cin >> N; - std::cin >> K; - - M = std::max(M, 0); - M = std::min(M, mmax); - - N = std::max(N, 0); - N = std::min(N, nmax-1); - - K = std::max(K, 0); - K = std::min(K, kmax); - - double ximin = ortho->r_to_xi(rmin); - double ximax = ortho->r_to_xi(rmax); - - // For the radial output grid - // - double x, r, lrmin, lrmax; - - // BEGIN: file header - // - out << "# M=" << M << " N=" << N << " K=" << K << std::endl; - - out << "# " - << std::setw(13) << " x |" - << std::setw(15) << " r |" - << std::setw(15) << " P (SL) |" - << std::setw(15) << " D (SL) |"; - if (CB) - out << "# " - << std::setw(13) << " p (CB) |" - << std::setw(15) << " d (CB) |"; - out << std::endl; - - out << "# " - << std::setw(13) << " [1] |" - << std::setw(15) << " [2] |" - << std::setw(15) << " [3] |" - << std::setw(15) << " [4] |"; - if (CB) - out << "# " - << std::setw(15) << " [5] |" - << std::setw(15) << " [6] |"; - out << std::endl; - - out << "# " - << std::setw(13) << std::setfill('-') << "+" - << std::setw(15) << std::setfill('-') << "+" - << std::setw(15) << std::setfill('-') << "+" - << std::setw(15) << std::setfill('-') << "+"; - if (CB) - out << std::setw(15) << std::setfill('-') << "+" - << std::setw(15) << std::setfill('-') << "+"; - out << std::endl << std::setfill(' '); - - // - // END: file header - - // Choosing the radial spacing - // - double delr; - if (logr and rmin>0.0) { - lrmin = log(rmin); - lrmax = log(rmax); - delr = (lrmax - lrmin)/(num - 1); - } else { - logr = false; - } - - // Print potential density pairs - // - for (int k=0; kr_to_xi(r); - } else { - x = ximin + (ximax - ximin)*k/(num-1); - r = ortho->xi_to_r(x); - } - - out << std::setw(15) << x << std::setw(15) << r - << std::setw(15) << ortho->get_pot (r, M, N, K) - << std::setw(15) << ortho->get_dens(r, M, N, K); - if (CB) - out << std::setw(15) << CB_get_potl(M, N, r) - << std::setw(15) << CB_get_dens(M, N, r); - out << std::endl; - } - - // Compute the inner product of the pairs - // - if (ortho_check) { - - std::ofstream out("cyltest.ortho"); - - LegeQuad lw(knots); - - Eigen::MatrixXd orth(nmax, nmax), orthCB; - Eigen::VectorXd normCB; - orth.setZero(); - - if (CB) { - orthCB.resize(nmax, nmax); - orthCB.setZero(); - normCB.resize(nmax); - for (int n=0; nxi_to_r(xx); - double fac = lw.weight(k) * rr / ortho->d_xi_to_r(xx) * (ximax - ximin); - - for (int j=0; jget_pot (rr, M, j, K) * ortho->get_dens(rr, M, l, K); - - if (std::isnan(ortho->get_pot (rr, M, j, K))) { - std::cout << "pot R=" << rr << std::endl; - } - - if (std::isnan(ortho->get_dens (rr, M, l, K))) { - std::cout << "dens R=" << rr << std::endl; - } - - if (CB) - orthCB(j, l) += fac * CB_get_potl(M, j, rr) * - CB_get_dens(M, l, rr) / sqrt(normCB(j)*normCB(l)); - } - } - } - - out << "SL" << std::endl << -orth << std::endl; - if (CB) out << std::endl << "CB" << std::endl - << orthCB*2.0*M_PI << std::endl; - // ^ - // | - // Angular norm -------+ - } - - if (use_mpi) MPI_Finalize(); - - return 0; -} From 3ef9a66069905fd345e5eddaf6472f2e225b083d Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 28 Mar 2024 17:30:06 -0400 Subject: [PATCH 048/167] Improvements and fixes for the SLGridSlab coordinate mapping [no ci] --- exputil/SLGridMP2.cc | 162 ++++++++++++++++++------------------------- include/SLGridMP2.H | 71 ++++++++++++++++++- 2 files changed, 136 insertions(+), 97 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index a7302cf94..c997b62a3 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -1850,8 +1850,10 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, tbdbg = VERBOSE; - init_table(); + // This should be controlled by a parameter... + mM = CoordMap::factory(CoordMapTypes::Sech, H); + init_table(); if (tbdbg) { if (mpi) @@ -2317,66 +2319,20 @@ SLGridSlab::~SLGridSlab() delete [] table; } - // Members -// #define TANH_MAP 1 -// #define SECH_MAP 1 - -#if defined(TANH_MAP) - -double SLGridSlab::z_to_xi(double z) -{ - return tanh(z/H); -} - -double SLGridSlab::xi_to_z(double xi) -{ - return H*atanh(xi); -} - -double SLGridSlab::d_xi_to_z(double xi) -{ - return H/(1.0 - xi*xi); -} - -#elif defined (SECH_MAP) - -double SLGridSlab::z_to_xi(double z) -{ - return z/sqrt(z*z + H*H); -} - -double SLGridSlab::xi_to_z(double xi) -{ - return xi*H/sqrt(1.0 - xi*xi); -} - -double SLGridSlab::d_xi_to_z(double xi) -{ - return H/pow(1.0 - xi*xi, 1.5); -} - -#else - // Simple cartesian coordinates seem - // to work best here; this transformation - // is the identity . . . - -double SLGridSlab::z_to_xi(double z) -{ - return z; -} - -double SLGridSlab::xi_to_z(double xi) -{ - return xi; -} - -double SLGridSlab::d_xi_to_z(double xi) -{ - return 1.0; -} +// Coordinate transformation member functions for tanh map +double SLGridSlab::TanhMap::z_to_xi (double z) { return tanh(z/H); } +double SLGridSlab::TanhMap::xi_to_z (double xi) { return H*atanh(xi); } +double SLGridSlab::TanhMap::d_xi_to_z(double xi) { return (1.0 - xi*xi)/H; } -#endif +// Coordinate transformation member functions for sech map +double SLGridSlab::SechMap::z_to_xi (double z) { return z/sqrt(z*z + H*H); } +double SLGridSlab::SechMap::xi_to_z (double xi) { return xi*H/sqrt(1.0 - xi*xi); } +double SLGridSlab::SechMap::d_xi_to_z(double xi) { return pow(1.0 - xi*xi, 1.5)/H; } +// Coordinate transformation member functions for linear map +double SLGridSlab::LinearMap::z_to_xi(double z) { return z; } +double SLGridSlab::LinearMap::xi_to_z(double xi) { return xi; } +double SLGridSlab::LinearMap::d_xi_to_z(double xi) { return 1.0; } double SLGridSlab::get_pot(double x, int kx, int ky, int n, int which) { @@ -2387,8 +2343,8 @@ double SLGridSlab::get_pot(double x, int kx, int ky, int n, int which) if (x<0 && 2*(n/2)!=n) sign=-1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); if (ky > kx) { hold = ky; @@ -2410,7 +2366,7 @@ double SLGridSlab::get_pot(double x, int kx, int ky, int n, int which) sqrt(table[kx][ky].ev[n]) * (x1*p0[indx] + x2*p0[indx+1]) * sign; #else return (x1*table[kx][ky].ef(n, indx) + x2*table[kx][ky].ef(n, indx+1))/ - sqrt(table[kx][ky].ev[n]) * slab->pot(xi_to_z(x)) * sign; + sqrt(table[kx][ky].ev[n]) * slab->pot(mM->xi_to_z(x)) * sign; #endif } @@ -2423,8 +2379,8 @@ double SLGridSlab::get_dens(double x, int kx, int ky, int n, int which) if (x<0 && 2*(n/2)!=n) sign=-1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); if (ky > kx) { hold = ky; @@ -2445,7 +2401,7 @@ double SLGridSlab::get_dens(double x, int kx, int ky, int n, int which) sqrt(table[kx][ky].ev[n]) * (x1*d0[indx] + x2*d0[indx+1]) * sign; #else return (x1*table[kx][ky].ef(n, indx) + x2*table[kx][ky].ef(n, indx+1)) * - sqrt(table[kx][ky].ev[n]) * slab->dens(xi_to_z(x)) * sign; + sqrt(table[kx][ky].ev[n]) * slab->dens(mM->xi_to_z(x)) * sign; #endif } @@ -2458,8 +2414,8 @@ double SLGridSlab::get_force(double x, int kx, int ky, int n, int which) if (x<0 && 2*(n/2)==n) sign = -1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); if (ky > kx) { hold = ky; @@ -2480,7 +2436,7 @@ double SLGridSlab::get_force(double x, int kx, int ky, int n, int which) // Point 0: indx // Point 1: indx+1 - return d_xi_to_z(x)/dxi * ( + return mM->d_xi_to_z(x)/dxi * ( (p - 0.5)*table[kx][ky].ef(n, indx-1)*p0[indx-1] -2.0*p*table[kx][ky].ef(n, indx)*p0[indx] + (p + 0.5)*table[kx][ky].ef(n, indx+1)*p0[indx+1] @@ -2494,8 +2450,8 @@ void SLGridSlab::get_pot(Eigen::MatrixXd& mat, double x, int which) if (x<0) sign = -1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); int ktot = (numk+1)*(numk+2)/2; mat.resize(ktot+1, nmax); @@ -2519,7 +2475,7 @@ void SLGridSlab::get_pot(Eigen::MatrixXd& mat, double x, int which) sqrt(table[kx][ky].ev[n]) * (x1*p0[indx] + x2*p0[indx+1]) * sign2; #else mat(l, n) = (x1*table[kx][ky].ef(n, indx) + x2*table[kx][ky].ef(n, indx+1))/ - sqrt(table[kx][ky].ev[n]) * slab->pot(xi_to_z(x)) * sign2; + sqrt(table[kx][ky].ev[n]) * slab->pot(mM->xi_to_z(x)) * sign2; #endif sign2 *= sign; } @@ -2536,8 +2492,8 @@ void SLGridSlab::get_dens(Eigen::MatrixXd& mat, double x, int which) if (x<0) sign = -1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); int ktot = (numk+1)*(numk+2)/2; mat.resize(ktot+1, nmax); @@ -2559,7 +2515,7 @@ void SLGridSlab::get_dens(Eigen::MatrixXd& mat, double x, int which) sqrt(table[kx][ky].ev[n]) * (x1*d0[indx] + x2*d0[indx+1]) * sign2; #else mat(l, n) = (x1*table[kx][ky].ef(n, indx) + x2*table[kx][ky].ef(n, indx+1))* - sqrt(table[kx][ky].ev[n]) * slab->dens(xi_to_z(x)) * sign2; + sqrt(table[kx][ky].ev[n]) * slab->dens(mM->xi_to_z(x)) * sign2; #endif sign2 *= sign; } @@ -2576,8 +2532,8 @@ void SLGridSlab::get_force(Eigen::MatrixXd& mat, double x, int which) if (x<0) sign = -1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); int ktot = (numk+1)*(numk+2)/2; mat.resize(ktot+1, nmax); @@ -2588,7 +2544,7 @@ void SLGridSlab::get_force(Eigen::MatrixXd& mat, double x, int which) double p = (x - xi[indx])/dxi; - double fac = d_xi_to_z(x)/dxi; + double fac = mM->d_xi_to_z(x)/dxi; int l=0; for (int kx=0; kx<=numk; kx++) { @@ -2617,8 +2573,8 @@ void SLGridSlab::get_pot(Eigen::VectorXd& vec, double x, int kx, int ky, int whi if (x<0) sign = -1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); if (ky > kx) { hold = ky; @@ -2643,7 +2599,7 @@ void SLGridSlab::get_pot(Eigen::VectorXd& vec, double x, int kx, int ky, int whi sqrt(table[kx][ky].ev[n]) * (x1*p0[indx] + x2*p0[indx+1]) * sign2; #else vec[n] = (x1*table[kx][ky].ef(n, indx) + x2*table[kx][ky].ef(n, indx+1))/ - sqrt(table[kx][ky].ev[n]) * slab->pot(xi_to_z(x)) * sign2; + sqrt(table[kx][ky].ev[n]) * slab->pot(mM->xi_to_z(x)) * sign2; #endif sign2 *= sign; } @@ -2657,8 +2613,8 @@ void SLGridSlab::get_dens(Eigen::VectorXd& vec, double x, int kx, int ky, int wh if (x<0) sign = -1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); vec.resize(nmax); @@ -2677,7 +2633,7 @@ void SLGridSlab::get_dens(Eigen::VectorXd& vec, double x, int kx, int ky, int wh sqrt(table[kx][ky].ev[n]) * (x1*d0[indx] + x2*d0[indx+1]) * sign2; #else vec[n] = (x1*table[kx][ky].ef(n, indx) + x2*table[kx][ky].ef(n, indx+1))* - sqrt(table[kx][ky].ev[n]) * slab->dens(xi_to_z(x)) * sign2; + sqrt(table[kx][ky].ev[n]) * slab->dens(mM->xi_to_z(x)) * sign2; #endif sign2 *= sign; } @@ -2693,8 +2649,8 @@ void SLGridSlab::get_force(Eigen::VectorXd& vec, double x, int kx, int ky, int w if (x<0) sign = -1; x = fabs(x); - if (which) - x = z_to_xi(x); + if (which) // Convert from z to x + x = mM->z_to_xi(x); if (ky > kx) { hold = ky; @@ -2710,7 +2666,7 @@ void SLGridSlab::get_force(Eigen::VectorXd& vec, double x, int kx, int ky, int w double p = (x - xi[indx])/dxi; - double fac = d_xi_to_z(x)/dxi; + double fac = mM->d_xi_to_z(x)/dxi; sign2 = sign; for (int n=0; nz_to_xi( ZBEG); + xmax = mM->z_to_xi( zmax); dxi = (xmax-xmin)/(numz-1); for (int i=0; ixi_to_z(xi[i]); p0[i] = slab->pot(z[i]); d0[i] = slab->dens(z[i]); } @@ -3485,14 +3441,32 @@ void SLGridSlab::mpi_unpack_table(void) MPI_DOUBLE, MPI_COMM_WORLD); } +std::unique_ptr SLGridSlab::CoordMap::factory +(CoordMapTypes type, double H) +{ + if (type == CoordMapTypes::Tanh) { + return std::make_unique(H); + } + else if (type == CoordMapTypes::Sech) { + return std::make_unique(H); + } + else if (type == CoordMapTypes::Linear) { + return std::make_unique(H); + } + else { + throw std::runtime_error("CoordMap::factory: invalid map type"); + } +} + + std::vector SLGridSlab::orthoCheck(int num) { // Gauss-Legendre knots and weights LegeQuad lw(num); // Get the scaled coordinate limits - double ximin = z_to_xi(-zmax); - double ximax = z_to_xi( zmax); + double ximin = mM->z_to_xi(-zmax); + double ximax = mM->z_to_xi( zmax); // Initialize the return matrices std::vector ret((numk+1)*(numk+2)/2); @@ -3515,13 +3489,13 @@ std::vector SLGridSlab::orthoCheck(int num) for (int kx=0; kx<=numk; kx++) { for (int ky=0; ky<=kx; ky++, indx++) { - get_pot (vpot[tid], x, kx, ky); - get_dens(vden[tid], x, kx, ky); + get_pot (vpot[tid], x, kx, ky, 0); + get_dens(vden[tid], x, kx, ky, 0); for (int n1=0; n1d_xi_to_z(x) *(ximax - ximin)*lw.weight(i); } } diff --git a/include/SLGridMP2.H b/include/SLGridMP2.H index 391e1b7b3..a71964765 100644 --- a/include/SLGridMP2.H +++ b/include/SLGridMP2.H @@ -284,6 +284,71 @@ private: //! For deep debugging bool tbdbg; + //@{ + + //! This defines a class providing selectable maps from the infinite + //! to finite interval + enum CoordMapTypes {Tanh, Sech, Linear}; + + //! The base class for all coordinate maps + class CoordMap + { + protected: + //! Scale parameter + double H; + + public: + //! Constructor + CoordMap(double H) : H(H) {} + + //! Convert from vertical to mapped coordinate + virtual double z_to_xi (double z) = 0; + + //! Convert from mapped coordinate back to vertical + virtual double xi_to_z (double z) = 0; + + //! Jacobian of the transformation + virtual double d_xi_to_z(double z) = 0; + + //! Coordinate map factory + static std::unique_ptr factory + (CoordMapTypes type, double H); + }; + + + //! x = tanh(z/H) + class TanhMap : public CoordMap + { + public: + TanhMap(double H) : CoordMap(H) {} + virtual double z_to_xi (double z); + virtual double xi_to_z (double z); + virtual double d_xi_to_z(double z); + }; + + //! x = z/sqrt(z^2 + H^2) + class SechMap : public CoordMap + { + public: + SechMap(double H) : CoordMap(H) {} + virtual double z_to_xi (double z); + virtual double xi_to_z (double z); + virtual double d_xi_to_z(double z); + }; + + //! x = z + class LinearMap : public CoordMap + { + public: + LinearMap(double H) : CoordMap(H) {} + virtual double z_to_xi (double z); + virtual double xi_to_z (double z); + virtual double d_xi_to_z(double z); + }; + + //! The current map created by the SLGridSlab constructor + std::unique_ptr mM; + public: //! Global MPI flag, default: 0=off @@ -320,13 +385,13 @@ public: double eigenvalue(int kx, int ky, int n) {return table[kx][ky].ev[n];} //! Map from vertical coordinate to dimensionless coordinate - double z_to_xi(double z); + double z_to_xi(double z) { return mM->z_to_xi(z); } //! Map from dimensionless coordinate to vertical coordinate - double xi_to_z(double x); + double xi_to_z(double x) { return mM->xi_to_z(x); } //! Jacobian of coordinate mapping - double d_xi_to_z(double x); + double d_xi_to_z(double x) { return mM->d_xi_to_z(x); } //! Get potential for dimensionless coord with given wave numbers and index double get_pot(double x, int kx, int ky, int n, int which=1); From 8b7192c70de54c916d7d59541689377b5c2f9313 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 29 Mar 2024 11:52:22 -0400 Subject: [PATCH 049/167] Version bump; added per dimension power to CubeCoefs and SlabCoefs [no ci] --- CMakeLists.txt | 2 +- doc/exp.cfg | 2 +- exputil/SLGridMP2.cc | 3 ++- pyEXP/CoefWrappers.cc | 52 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bb4763ad..8c7155119 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21) # Needed for CUDA, MPI, and CTest features project( EXP - VERSION "7.7.28" + VERSION "7.7.29" HOMEPAGE_URL https://github.com/EXP-code/EXP LANGUAGES C CXX Fortran) diff --git a/doc/exp.cfg b/doc/exp.cfg index db58900a8..b098166f9 100644 --- a/doc/exp.cfg +++ b/doc/exp.cfg @@ -38,7 +38,7 @@ PROJECT_NAME = "EXP" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 7.7.28 +PROJECT_NUMBER = 7.7.29 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index c997b62a3..94cc341d3 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -1850,7 +1850,8 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, tbdbg = VERBOSE; - // This should be controlled by a parameter... + // This could be controlled by a parameter...at this point is a + // fixed tuning. mM = CoordMap::factory(CoordMapTypes::Sech, H); init_table(); diff --git a/pyEXP/CoefWrappers.cc b/pyEXP/CoefWrappers.cc index 8ad4f8242..477718c79 100644 --- a/pyEXP/CoefWrappers.cc +++ b/pyEXP/CoefWrappers.cc @@ -1517,7 +1517,31 @@ void CoefficientClasses(py::module &m) { ------- numpy.ndarray 4-dimensional numpy array containing the slab coefficients - )"); + )") + .def("PowerDim", + [](CoefClasses::SlabCoefs& A, std::string d, int min, int max) + { + return A.Power(d[0], min, max); + }, + R"( + Get power for the coefficient DB as a function of harmonic index for a + given dimension. This Power() member is equivalent to PowerDim('x'). + + Parameters + ---------- + d : char + dimension for power summary; one of 'x', 'y', or 'z' + min : int + minimum index along requested dimension (default=0) + max : int + maximum index along requested dimension (default=max int) + + Returns + ------- + numpy.ndarray + 2-dimensional numpy array containing the power + )", py::arg("d"), py::arg("min")=0, + py::arg("max")=std::numeric_limits::max()); py::class_, PyCubeCoefs, CoefClasses::Coefs> @@ -1587,7 +1611,31 @@ void CoefficientClasses(py::module &m) { ------- numpy.ndarray 4-dimensional numpy array containing the cube coefficients - )"); + )") + .def("PowerDim", + [](CoefClasses::CubeCoefs& A, std::string d, int min, int max) + { + return A.Power(d[0], min, max); + }, + R"( + Get power for the coefficient DB as a function of harmonic index for a + given dimension. This Power() member is equivalent to PowerDim('x'). + + Parameters + ---------- + d : char + dimension for power summary; one of 'x', 'y', or 'z' + min : int + minimum index along requested dimension (default=0) + max : int + maximum index along requested dimension (default=max int) + + Returns + ------- + numpy.ndarray + 2-dimensional numpy array containing the power + )", py::arg("d"), py::arg("min")=0, + py::arg("max")=std::numeric_limits::max()); py::class_, PyTableData, CoefClasses::Coefs> (m, "TableData", "Container for simple data tables with multiple columns") From a701c703044410215f9b2d11a248a2b76eb446e5 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 29 Mar 2024 17:47:43 -0400 Subject: [PATCH 050/167] Added CUDA implementation for SlabSL; untested [no ci] --- exputil/cudaSLGridMP2.cu | 228 ++++++- include/SLGridMP2.H | 25 + src/CMakeLists.txt | 2 +- src/SlabSL.H | 54 ++ src/SlabSL.cc | 67 +- src/cudaSlabSL.cu | 1294 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 1658 insertions(+), 12 deletions(-) create mode 100644 src/cudaSlabSL.cu diff --git a/exputil/cudaSLGridMP2.cu b/exputil/cudaSLGridMP2.cu index 8d51ac338..18e55be41 100644 --- a/exputil/cudaSLGridMP2.cu +++ b/exputil/cudaSLGridMP2.cu @@ -49,12 +49,6 @@ thrust::host_vector returnTestSph return f_d; } -struct Element { - int l; - double a; - double b; -}; - void SLGridSph::initialize_cuda(std::vector& cuArray, thrust::host_vector& tex) { @@ -151,6 +145,12 @@ void SLGridSph::initialize_cuda(std::vector& cuArray, if (false) { const cuFP_t tol = 10.0*std::numeric_limits::epsilon(); + struct Element { + int l; + double a; + double b; + }; + std::multimap compare; thrust::host_vector ret(numr); @@ -219,3 +219,219 @@ void SLGridSph::initialize_cuda(std::vector& cuArray, } } + +__global__ +void testFetchSlab(dArray T, dArray f, + int l, int j, int nmax, int numz) +{ + const int n = blockDim.x * blockIdx.x + threadIdx.x; + const int k = l*nmax + 1; +#if cuREAL == 4 + if (n < numz) f._v[n] = tex1D(T._v[k+j], n); +#else + if (n < numz) f._v[n] = int2_as_double(tex1D(T._v[k+j], n)); +#endif +} + + +thrust::host_vector returnTestSlab +(thrust::host_vector& tex, + int kx, int ky, int j, int numk, int nmax, int numz) +{ + thrust::device_vector t_d = tex; + + unsigned int gridSize = numz/BLOCK_SIZE; + if (numz > gridSize*BLOCK_SIZE) gridSize++; + + thrust::device_vector f_d(numz); + + int l = kx*(kx+1)/2*numk + ky; + + testFetchSlab<<>>(toKernel(t_d), toKernel(f_d), + l, j, nmax, numz); + + cudaDeviceSynchronize(); + + return f_d; +} + +void SLGridSlab::initialize_cuda(std::vector& cuArray, + thrust::host_vector& tex) +{ + // Number of texture arrays + // + int ndim = (numk+1)*(numk+2)/2*nmax + 1; + + // Allocate CUDA array in device memory (a one-dimension 'channel') + // +#if cuREAL == 4 + cudaChannelFormatDesc channelDesc = cudaCreateChannelDesc(); +#else + cudaChannelFormatDesc channelDesc = cudaCreateChannelDesc(); +#endif + + // Interpolation data array + // + cuArray.resize(ndim); + + // Size of interpolation array + // + size_t tsize = numz*sizeof(cuFP_t); + + // Create texture objects + // + tex.resize(ndim); + thrust::fill(tex.begin(), tex.end(), 0); + + // std::vector resDesc; + // std::vector texDesc; + + + cudaTextureDesc texDesc; + + memset(&texDesc, 0, sizeof(cudaTextureDesc)); + texDesc.addressMode[0] = cudaAddressModeClamp; + texDesc.filterMode = cudaFilterModePoint; + texDesc.readMode = cudaReadModeElementType; + texDesc.normalizedCoords = 0; + + thrust::host_vector tt(numz); + + cuda_safe_call(cudaMallocArray(&cuArray[0], &channelDesc, numz), __FILE__, __LINE__, "malloc cuArray"); + + // Copy to device memory some data located at address h_data + // in host memory + for (int j=0; j::epsilon(); + + struct Element { + int kx; + int ky; + double a; + double b; + }; + + std::multimap compare; + + thrust::host_vector ret(numz); + std::cout << "**HOST** Texture compare" << std::endl << std::scientific; + unsigned tot = 0, bad = 0; + + for (int kx=0; kx<=numk; kx++) { + + for (int ky=0; ky<=kx; ky++) { + + for (int j=0; j1.0e-18) { + + Element comp = {kx, ky, a, b}; + compare.insert(std::make_pair(fabs((a - b)/a), comp)); + + if ( fabs((a - b)/a ) > tol) { + std::cout << std::setw( 5) << kx << std::setw( 5) << ky + << std::setw( 5) << j << std::setw( 5) << i + << std::setw(20) << a + << std::setw(20) << (a-b)/a << std::endl; + bad++; + } + } + tot++; + } + } + } + } + + std::multimap::iterator beg = compare.begin(); + std::multimap::iterator end = compare.end(); + std::multimap::iterator + lo1=beg, lo9=beg, mid=beg, hi9=end, hi1=end; + + std::advance(lo9, 9); + std::advance(mid, compare.size()/2); + std::advance(hi1, -1); + std::advance(hi9, -10); + + std::cout << "**Found " << bad << "/" << tot << " values > eps" << std::endl + << "**Low[1] : " << lo1->first << " (" << lo1->second.kx << ", " << lo1->second.ky << ", " << lo1->second.a << ", " << lo1->second.b << ", " << lo1->second.a - lo1->second.b << ")" << std::endl + << "**Low[9] : " << lo9->first << " (" << lo9->second.kx << ", " << lo9->second.ky << ", " << lo9->second.a << ", " << lo9->second.b << ", " << lo9->second.a - lo9->second.b << ")" << std::endl + << "**Middle : " << mid->first << " (" << mid->second.kx << ", " << mid->second.ky << ", " << mid->second.a << ", " << mid->second.b << ", " << mid->second.a - mid->second.b << ")" << std::endl + << "**Hi [9] : " << hi9->first << " (" << hi9->second.kx << ", " << hi9->second.ky << ", " << hi9->second.a << ", " << hi9->second.b << ", " << hi9->second.a - hi9->second.b << ")" << std::endl + << "**Hi [1] : " << hi1->first << " (" << hi1->second.kx << ", " << hi1->second.ky << ", " << hi1->second.a << ", " << hi1->second.b << ", " << hi1->second.a - hi1->second.b << ")" << std::endl + << "**" << std::endl; + } + + if (false) { + std::cout << "cuInterpArray size = " << cuArray.size() << std::endl; + unsigned cnt = 0; + for (size_t i=0; i orthoCheck(int knots=40); //@} + +#if HAVE_LIBCUDA==1 + void initialize_cuda(std::vector& cuArray, + thrust::host_vector& tex); + + virtual cudaMappingConstants getCudaMappingConstants() + { + cudaMappingConstants ret; + + ret.hscale = H; + ret.xmin = xmin; + ret.xmax = xmax; + ret.ymin = 0.0; + ret.ymax = 0.0; + ret.numr = numz; + ret.numx = 0; + ret.numy = 0; + ret.dxi = dxi; + ret.dyi = 0.0; + + return ret; + } + +#endif + }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b131c659e..e916be5cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,7 @@ if (ENABLE_CUDA) list(APPEND CUDA_SRC cudaPolarBasis.cu cudaSphericalBasis.cu cudaCylinder.cu cudaEmpCylSL.cu cudaComponent.cu NVTX.cc cudaHOT.cu cudaIncpos.cu cudaIncvel.cu cudaMultistep.cu - cudaOrient.cu cudaBiorthCyl.cu cudaCube.cu) + cudaOrient.cu cudaBiorthCyl.cu cudaCube.cu cudaSlabSL.cu) endif() set(exp_SOURCES Basis.cc Bessel.cc CBrock.cc Component.cc diff --git a/src/SlabSL.H b/src/SlabSL.H index 79e5c83e6..16c4b6f4a 100644 --- a/src/SlabSL.H +++ b/src/SlabSL.H @@ -11,6 +11,12 @@ #include #include +#if HAVE_LIBCUDA==1 +#include +#include +#include +#endif + /*! This routine computes the potential, acceleration and density using expansion periodic in X & Y and outgoing vacuum boundary condtions in Z */ @@ -50,6 +56,54 @@ private: SlabSLCoefHeader coefheader; +#if HAVE_LIBCUDA==1 + virtual void determine_coefficients_cuda(); + virtual void determine_acceleration_cuda(); + virtual void multistep_update_cuda(); + + thrust::host_vector> host_coefs; + thrust::device_vector> dev_coefs; + + //! Move coefficients from host to device + virtual void HtoD_coefs(); + + //! Move coefficients from device to host + virtual void DtoH_coefs(unsigned); + + //! Assign constants on the device + virtual void initialize_constants(); + + //! Deallocate storage + virtual void destroy_cuda(); + + //@{ + + //! Helper struct to hold device data + struct cudaStorage + { + thrust::device_vector> dN_coef; + thrust::device_vector> dc_coef; + thrust::device_vector> dw_coef; + thrust::device_vector> df_coef; + + void resize_coefs(int N, int osize, int gridSize, int stride); + }; + + //! A storage instance + cudaStorage cuS; + + //! Only initialize once + bool initialize_cuda_cube; + + //! Initialize the cuda streams + void cuda_initialize(); + + //! Zero the coefficient output vectors + void cuda_zero_coefs(); + //@} + +#endif + //! Default number of grid points for SLGridSlab int ngrid = 1000; diff --git a/src/SlabSL.cc b/src/SlabSL.cc index d235f03ce..0e96beee6 100644 --- a/src/SlabSL.cc +++ b/src/SlabSL.cc @@ -18,6 +18,12 @@ SlabSL::valid_keys = { "type" }; +//@{ +//! These are for testing exclusively (should be set false for production) +static bool cudaAccumOverride = false; +static bool cudaAccelOverride = false; +//@} + SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) { id = "Slab (Sturm-Liouville)"; @@ -27,6 +33,10 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) hslab = 0.2; coef_dump = true; +#if HAVE_LIBCUDA==1 + cuda_aware = true; +#endif + initialize(); SLGridSlab::mpi = 1; @@ -101,7 +111,9 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) SlabSL::~SlabSL() { - // Nothing +#if HAVE_LIBCUDA==1 + if (component->cudaDevice>=0) destroy_cuda(); +#endif } void SlabSL::initialize() @@ -137,10 +149,9 @@ void SlabSL::initialize() void SlabSL::determine_coefficients(void) { - // Coefficients are ordered as follows: - // n=-nmax,-nmax+1,...,0,...,nmax-1,nmax - // in a single array for each dimension - // with z dimension changing most rapidly + // Coefficients are ordered as follows: + // n=-nmax,-nmax+1,...,0,...,nmax-1,nmax in a single array for each + // dimension with z dimension changing most rapidly // Clean @@ -149,7 +160,23 @@ void SlabSL::determine_coefficients(void) expccof[i].setZero(); } +#if HAVE_LIBCUDA==1 + (*barrier)("SlabSL::entering cuda coefficients", __FILE__, __LINE__); + if (component->cudaDevice>=0 and use_cuda) { + if (cudaAccumOverride) { + component->CudaToParticles(); + exp_thread_fork(true); + } else { + determine_coefficients_cuda(); + DtoH_coefs(mlevel); + } + } else { + exp_thread_fork(true); + } + (*barrier)("SlabSL::exiting cuda coefficients", __FILE__, __LINE__); +#else exp_thread_fork(true); +#endif int used1 = 0, rank = expccof[0].size(); used = 0; @@ -164,6 +191,9 @@ void SlabSL::determine_coefficients(void) MPI_Allreduce( MPI_IN_PLACE, expccof[0].data(), expccof[0].size(), MPI_CXX_DOUBLE_COMPLEX, MPI_SUM, MPI_COMM_WORLD); +#if HAVE_LIBCUDA==1 + cuda_initialize(); +#endif } void * SlabSL::determine_coefficients_thread(void * arg) @@ -253,8 +283,35 @@ void SlabSL::get_acceleration_and_potential(Component* C) { cC = C; + MPL_start_timer(); + +#if HAVE_LIBCUDA==1 + if (use_cuda and cC->cudaDevice>=0 and cC->force->cudaAware()) { + if (cudaAccelOverride) { + cC->CudaToParticles(); + exp_thread_fork(false); + cC->ParticlesToCuda(); + } else { + // Copy coefficients from this component to device + // + HtoD_coefs(); + // + // Do the force computation + // + determine_acceleration_cuda(); + } + } else { + + exp_thread_fork(false); + + } +#else + exp_thread_fork(false); + +#endif + MPL_stop_timer(); } diff --git a/src/cudaSlabSL.cu b/src/cudaSlabSL.cu new file mode 100644 index 000000000..ab6ee6e72 --- /dev/null +++ b/src/cudaSlabSL.cu @@ -0,0 +1,1294 @@ +// -*- C++ -*- + +#include + +#include +#include +#include +#if CUDART_VERSION < 12000 +#include +#endif + +#include +#include +#include +#include + +#include "expand.H" + +// Define for debugging +// +// #define BOUNDS_CHECK +// #define VERBOSE_RPT +// #define VERBOSE_DBG + +// Global symbols for slab construction +// +__device__ __constant__ +int slabNumX, slabNumY, slabNumZ, slabNX, slabNY, slabNZ, slabNdim; + +__device__ __constant__ +int slabCmap; + +__device__ __constant__ +cuFP_t slabDfac, SlabHscl, slabXmin, slabXmax, slabDxi; + +// Alias for Thrust complex type to make this code more readable +// +using CmplxT = thrust::complex; + +// Index functions for coefficients based on Eigen Tensor packing order +// +__device__ +int Index(int i, int j, int k) +{ + i += slabNumX; + j += slabNumY; + k += slabNumZ; + return k*slabNX*slabNY + j*slabNX + i; +} + +// Index function for modulus coefficients +// +__device__ +thrust::tuple TensorIndices(int indx) +{ + int k = indx/(slabNX*slabNY); + int j = (indx - k*slabNX*slabNY)/slabNX; + int i = indx - (j + k*slabNY)*slabNX; + + return {i, j, k}; +} + +__device__ +thrust::tuple WaveNumbers(int indx) +{ + int k = indx/(slabNX*slabNY); + int j = (indx - k*slabNX*slabNY)/slabNX; + int i = indx - (j + k*slabNY)*slabNX; + + return {i-slabNumX, j-slabNumY, k}; +} + + +__global__ +void testConstantsSlab() +{ + printf("-------------------------\n"); + printf("---SlabBasis constants---\n"); + printf("-------------------------\n"); + printf(" Ndim = %d\n", slabNdim ); + printf(" Numx = %d\n", slabNumX ); + printf(" Numy = %d\n", slabNumY ); + printf(" Numy = %d\n", slabNumZ ); + printf(" Nx = %d\n", slabNX ); + printf(" Ny = %d\n", slabNY ); + printf(" Nz = %d\n", slabNZ ); + printf(" Dfac = %e\n", slabDfac ); + printf(" Hscl = %e\n", slabHscl ); + printf(" Cmap = %d\n", slabCmap ); + printf("-------------------------\n"); +} + +__global__ +void testFetchSlab(dArray T, dArray f, + int l, int j, int nmax, int numr) +{ + const int n = blockDim.x * blockIdx.x + threadIdx.x; + const int k = l*nmax + 1; +#if cuREAL == 4 + if (n < numr) f._v[n] = tex1D(T._v[k+j], n); +#else + if (n < numr) f._v[n] = int2_as_double(tex1D(T._v[k+j], n)); +#endif +} + + +thrust::host_vector returnTestSlab +(thrust::host_vector& tex, + int l, int j, int nmax, int numr) +{ + thrust::device_vector t_d = tex; + + unsigned int gridSize = numr/BLOCK_SIZE; + if (numr > gridSize*BLOCK_SIZE) gridSize++; + + thrust::device_vector f_d(numr); + + testFetchSph<<>>(toKernel(t_d), toKernel(f_d), + l, j, nmax, numr); + + cudaDeviceSynchronize(); + + return f_d; +} + +__device__ +cuFP_t cu_z_to_xi(cuFP_t z) +{ + cuFP_t ret; + + if (sphCmap==0) { + ret = tanh(z/slabHscl); + } else if (sphCmap==1) { + ret = z/sqrt(z*z + slabHscl*slabHscl); + } else { + ret = z; + } + + return ret; +} + +__device__ +cuFP_t cu_xi_to_z(cuFP_t xi) +{ + cuFP_t ret; + + if (sphCmap==0) { + ret = slabHscl*atanh(xi); + } else if (sphCmap==1) { + ret = xi*slabHscl/sqrt(1.0 - xi*xi); + } else { + ret = xi; + } + + return ret; +} + +__device__ +cuFP_t cu_d_xi_to_r(cuFP_t xi) +{ + cuFP_t ret; + + if (sphCmap==0) { + ret = (1.0 - xi*xi)/SlabHscl; + } else if (sphCmap==1) { + ret = pow(1.0 - xi*xi, 1.5)/SlabHscl; + } else { + ret = 1.0; + } + + return ret; +} + +// Initialize anything cuda specific +// +void SlabSL::cuda_initialize() +{ + // Default + // + byPlanes = true; + + // Make method string lower case + // + std::transform(cuMethod.begin(), cuMethod.end(), cuMethod.begin(), + [](unsigned char c){ return std::tolower(c); }); + + // Parse cuMethods variable + // + // All dimensions at once + // + if (cuMethod.find("all") != std::string::npos) byPlanes = false; + if (cuMethod.find("full") != std::string::npos) byPlanes = false; + if (cuMethod.find("2d") != std::string::npos) byPlanes = false; + // + // Only one dimension at a time + // + if (cuMethod.find("axes") != std::string::npos) byPlanes = true; + if (cuMethod.find("1d") != std::string::npos) byPlanes = true; + + std::cout << "---- SlabSL::cuda_initialize: byPlanes=" + << std::boolalpha << byPlanes << std::endl; +} + +// Copy constants to device +// +void SlabSL::initialize_constants() +{ + cuda_safe_call(cudaMemcpyToSymbol(slabNumX, &nmaxx, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabNumX"); + + cuda_safe_call(cudaMemcpyToSymbol(slabNumY, &nmaxy, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabNumY"); + + cuda_safe_call(cudaMemcpyToSymbol(slabNumZ, &nmaxz, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabNumZ"); + + cuda_safe_call(cudaMemcpyToSymbol(slabNX, &imx, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabNX"); + + cuda_safe_call(cudaMemcpyToSymbol(slabNY, &imy, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabNY"); + + cuda_safe_call(cudaMemcpyToSymbol(slabNZ, &imz, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabNZ"); + + cuda_safe_call(cudaMemcpyToSymbol(slabNdim, &osize, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabNdim"); + + int Cmap = 0; + + cuda_safe_call(cudaMemcpyToSymbol(slabCmap, &Cmap, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabCmap"); + + cuFP_t dfac = 2.0*M_PI; + + cuda_safe_call(cudaMemcpyToSymbol(slabDfac, &dfac, sizeof(cuFP_t), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabDfac"); + + dfac = H; + + cuda_safe_call(cudaMemcpyToSymbol(slabHscl, &dfac, sizeof(cuFP_t), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabHscl"); +} + +__global__ void coefKernelSlab +(dArray P, dArray I, dArray coef, + int stride, PII lohi) +{ + // Thread ID + // + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + const int N = lohi.second - lohi.first; + + for (int n=0; n=P._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); +#endif + cudaParticle & p = P._v[I._v[npart]]; + + cuFP_t pos[3] = {p.pos[0], p.pos[1], p.pos[2]}; + cuFP_t mm = p.mass; + + + // Restore particles to the unit slab + // + for (int k=0; k<2; k++) { + if (pos[k]<0.0) + pos[k] += floor(-pos[k]) + 1.0; + else + pos[k] += -floor(pos[key_functor]); + } + + // Wave number loop + // + const auto xx = CmplxT(0.0, slabDfac*pos[0]); // Phase values + const auto yy = CmplxT(0.0, slabDfac*pos[1]); + + // Recursion increments and initial values + // + const auto sx = thrust::exp(-xx), cx = thrust::exp(xx*slabNumX); + const auto sy = thrust::exp(-yy), cy = thrust::exp(yy*slabNumY); + + // Vertical interpolation + // + cuFP_t x = cu_z_to_zi(pos[2]); + cuFP_t xi = (x - slabXmin)/slabDxi; + cuFP_t dx = dx = cu_d_xi_to_z(xi)/slabDxi; + + int ind = floor(xi); + int in0 = ind; + + if (in0 < 0) in0 = 0; + if (in0 > slabNumz-2) in0 = slabNumz - 2; + + if (ind < 1) ind = 1; + if (ind > slabNumz-2) ind = slabNumz - 2; + + cuFP_t a = (cuFP_t)(in0+1) - xi; + cuFP_t b = 1.0 - a; + + // Flip sign for antisymmetric basis functions + int sign=1; + if (x<0 && 2*(n/2)!=n) sign=-1; + + + // Will contain the incremented basis + // + CmplxT X, Y; + + X = cx; // Assign the min X wavenumber conjugate + + for (int ii=-slabNumX; ii<=slabNumX; ii++, X*=sx) { + + Y = cy; // Assign the min Y wavenumber conjugate + + int kx = abs(ii); + + for (int jj=-slabNumY; jj<=slabNumY; jj++, Y*=sy) { + + int ky = abs(jj); + + for (int n=0; n(tex._v[0], ind ) + + b*tex1D(tex._v[0], ind+1) ; +#else + a*int2_as_double(tex1D(tex._v[0], ind )) + + b*int2_as_double(tex1D(tex._v[0], ind+1)) ; +#endif + int k = 1 + kx*(kx+1)/2*(slabNumY+1) + n; + +#ifdef BOUNDS_CHECK + if (k>=tex._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); +#endif + cuFP_t v = ( +#if cuREAL == 4 + a*tex1D(tex._v[k], ind ) + + b*tex1D(tex._v[k], ind+1) +#else + a*int2_as_double(tex1D(tex._v[k], ind )) + + b*int2_as_double(tex1D(tex._v[k], ind+1)) +#endif + ) * p0 * sign; + + coef._v[Index(ii, jj, n)] = -2.0*slabDfac * X * Y * v * mass; + + } + } + } + // END: wave number loop +#endif + } + // END: particle index limit + } + // END: stride loop +} + + +__global__ void +forceKernelSlab(dArray P, dArray I, + dArray coef, int stride, PII lohi) +{ + // Thread ID + // + const int tid = blockDim.x * blockIdx.x + threadIdx.x; + + for (int n=0; n=P._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); +#endif + cudaParticle & p = P._v[I._v[npart]]; + + CmplxT acc[3] = {0.0, 0.0, 0.0}, pot = 0.0, fac, facf; + cuFP_t pos[3] = {p.pos[0], p.pos[1], p.pos[2]}; + cuFP_t mm = p.mass; + int ind[3]; + + // Wave number loop + // + const auto xx = CmplxT(0.0, slabDfac*pos[0]); // Phase values + const auto yy = CmplxT(0.0, slabDfac*pos[1]); + + // Recursion increments and initial values + const auto sx = thrust::exp(xx), cx = thrust::exp(-xx*slabNumX); + const auto sy = thrust::exp(yy), cy = thrust::exp(-yy*slabNumY); + + // Vertical interpolation + // + cuFP_t x = cu_z_to_zi(pos[2]); + cuFP_t xi = (x - slabXmin)/slabDxi; + cuFP_t dx = dx = cu_d_xi_to_z(xi)/slabDxi; + + int ind = floor(xi); + int in0 = ind; + + if (in0 < 0) in0 = 0; + if (in0 > slabNumz-2) in0 = slabNumz - 2; + + if (ind < 1) ind = 1; + if (ind > slabNumz-2) ind = slabNumz - 2; + + cuFP_t a = (cuFP_t)(in0+1) - xi; + cuFP_t b = 1.0 - a; + + + // For 3-pt formula + + int jn0 = floor(xi); + if (jn0 < 1) jn0 = 1; + if (jn0 > slabNumz-2) jn0 = slabNumz - 2; + + double p = (x - xi[jn0])/dxi; + + // Flip sign for antisymmetric basis functions + int sign=1; + if (pos[2]<0 && 2*(n/2)!=n) sign=-1; + + CmplxT X, Y; // Will contain the incremented basis + CmplxT fac, facf; + + X = cx; // Assign the min X wavenumber + for (int ii=-slabNumX; ii<=slabNumX; ii++, X*=sx) { + Y = cy; // Assign the min Y wavenumber + for (int jj=-slabNumY; jj<=slabNumY; jj++, Y*=sy) { + + for (int n=0; n(tex._v[0], ind ) + + b*tex1D(tex._v[0], ind+1) ; +#else + a*int2_as_double(tex1D(tex._v[0], ind )) + + b*int2_as_double(tex1D(tex._v[0], ind+1)) ; +#endif + int k = 1 + kx*(kx+1)/2*(slabNumY+1) + n; + +#ifdef BOUNDS_CHECK + if (k>=tex._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); +#endif + cuFP_t v = ( +#if cuREAL == 4 + a*tex1D(tex._v[k], ind ) + + b*tex1D(tex._v[k], ind+1) +#else + a*int2_as_double(tex1D(tex._v[k], ind )) + + b*int2_as_double(tex1D(tex._v[k], ind+1)) +#endif + ) * p0 * sign; + + cuFP_t f = ( +#if cuREAL == 4 + (p - 0.5)*tex1D(tex._v[k], jnd-1)*tex1D(tex._v[0], jnd-1) + -2.0*tex1D(tex._v[k], jnd)*tex1D(tex._v[0], jnd) + + (p + 0.5)**tex1D(tex._v[k], jnd+1)*tex1D(tex._v[0], jnd+1) +#else + (p - 0.5)*int2_as_double(tex1D(tex._v[k], jnd-1))* + int2_as_double(tex1D(tex._v[0], jnd-1)) + -2.0*int2_as_double(tex1D(tex._v[k], jnd))* + int2_as_double(tex1D(tex._v[0], jnd)) + + (p + 0.5)*int2_as_double(tex1D(tex._v[k], jnd+1))* + int2_as_double(tex1D(tex._v[0], jnd+1)) +#endif + ) * sign; + + fac = X * Y * v * coef._v[Index(ii, jj, n)]; + facf = X * Y * f * coef._v[Index(ii, jj, n)]; + + acc[0] += CmplxT(0.0, -slabDfac*ii) * fac; + acc[1] += CmplxT(0.0, -slabDfac*jj) * fac; + acc[2] += -facf; + } + // END: z wavenumber loop + } + // END: y wave number loop + } + // END: x wavenumber loop +#endif + // Particle assignment + // + p.pot = pot.real(); + for (int k=0; k<3; k++) p.acc[k] = acc[k].real(); + } + // END: particle index limit + } + // END: stride loop +} + +template +class LessAbs : public std::binary_function +{ +public: + bool operator()( const T &a, const T &b ) const + { + return (thrust::abs(a) < thrust::abs(b)); + } +}; + +void SlabSL::cudaStorage::resize_coefs(int N, int osize, int gridSize, int stride) +{ + // Reserve space for coefficient reduction + // + if (dN_coef.capacity() < osize*N) + dN_coef.reserve(osize*N); + + if (dc_coef.capacity() < osize*gridSize) + dc_coef.reserve(osize*gridSize); + + // Set space for current step + // + dN_coef.resize(osize*N); + dc_coef.resize(osize*gridSize); + dw_coef.resize(osize); // This will stay fixed +} + + +void SlabSL::cuda_zero_coefs() +{ + auto cr = component->cuStream; + + cuS.df_coef.resize(osize); + + // Zero output array + // + thrust::fill(thrust::cuda::par.on(cr->stream), + cuS.df_coef.begin(), cuS.df_coef.end(), 0.0); +} + +void SlabSL::determine_coefficients_cuda() +{ + // Only do this once but copying mapping coefficients and textures + // must be done every time + // + if (initialize_cuda_slab) { + initialize_cuda(); + initialize_cuda_slab = false; + } + + // Copy coordinate mapping + // + initialize_constants(); + + + std::cout << std::scientific; + + cudaDeviceProp deviceProp; + cudaGetDeviceProperties(&deviceProp, component->cudaDevice); + cuda_check_last_error_mpi("cudaGetDeviceProperties", __FILE__, __LINE__, myid); + + // This will stay fixed for the entire run + // + host_coefs.resize(osize); + + // Get the stream for this component + // + auto cs = component->cuStream; + + // VERBOSE diagnostic output on first call + // + static bool firstime = true; + + if (firstime and myid==0 and VERBOSE>4) { + testConstantsSlab<<<1, 1, 0, cs->stream>>>(); + cudaDeviceSynchronize(); + cuda_check_last_error_mpi("cudaDeviceSynchronize", __FILE__, __LINE__, myid); + firstime = false; + } + + // Zero counter and coefficients + // + thrust::fill(host_coefs.begin(), host_coefs.end(), 0.0); + + // Zero out coefficient storage + // + cuda_zero_coefs(); + + // Get sorted particle range for mlevel + // + PII lohi = component->CudaGetLevelRange(mlevel, mlevel), cur; + + if (false) { + for (int n=0; nbunchSize + 1; + + // Loop over bunches + // + for (int n=0; nbunchSize*n; + cur.second = lohi.first + component->bunchSize*(n+1); + cur.second = std::min(cur.second, lohi.second); + + if (cur.second <= cur.first) break; + + // Compute grid + // + unsigned int N = cur.second - cur.first; + unsigned int stride = N/BLOCK_SIZE/deviceProp.maxGridSize[0] + 1; + unsigned int gridSize = N/BLOCK_SIZE/stride; + + if (N > gridSize*BLOCK_SIZE*stride) gridSize++; + +#ifdef VERBOSE_RPT + static unsigned debug_max_count = 100; + static unsigned debug_cur_count = 0; + if (debug_cur_count++ < debug_max_count) { + std::cout << std::endl + << "** -------------------------" << std::endl + << "** cudaSlab coefficients" << std::endl + << "** -------------------------" << std::endl + << "** N = " << N << std::endl + << "** Npacks = " << Npacks << std::endl + << "** I low = " << cur.first << std::endl + << "** I high = " << cur.second << std::endl + << "** Stride = " << stride << std::endl + << "** Block = " << BLOCK_SIZE << std::endl + << "** Grid = " << gridSize << std::endl + << "** Level = " << mlevel << std::endl + << "** lo = " << lohi.first << std::endl + << "** hi = " << lohi.second << std::endl + << "**" << std::endl; + } +#endif + + // Shared memory size for the reduction + // + int sMemSize = BLOCK_SIZE * sizeof(CmplxT); + + if (byPlanes) { + + // Adjust cached storage, if necessary + // + cuS.resize_coefs(N, imx, gridSize, stride); + + // Compute the coefficient contribution for each order + // + auto beg = cuS.df_coef.begin(); + + for (int kk=-nmaxz; kk<=nmaxz; kk++) { + + for (int jj=-nmaxy; jj<=nmaxy; jj++) { + + coefKernelSlabX<<stream>>> + (toKernel(cs->cuda_particles), toKernel(cs->indx1), + toKernel(cuS.dN_coef), jj, kk, stride, cur); + + // Begin the reduction by blocks [perhaps this should use a + // stride?] + // + unsigned int gridSize1 = N/BLOCK_SIZE; + if (N > gridSize1*BLOCK_SIZE) gridSize1++; + + reduceSum + <<stream>>> + (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), imx, N); + + // Finish the reduction for this order in parallel + // + thrust::counting_iterator index_begin(0); + thrust::counting_iterator index_end(gridSize1*imx); + + // The key_functor indexes the sum reduced series by array index + // + thrust::reduce_by_key + ( + thrust::cuda::par.on(cs->stream), + thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), + thrust::make_transform_iterator(index_end, key_functor(gridSize1)), + cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() + ); + + thrust::transform(thrust::cuda::par.on(cs->stream), + cuS.dw_coef.begin(), cuS.dw_coef.end(), + beg, beg, thrust::plus()); + + thrust::advance(beg, imx); + } + // END: y wave number loop + } + // END: z wave number loop + + } else { + + // Adjust cached storage, if necessary + // + cuS.resize_coefs(N, osize, gridSize, stride); + + // Compute the coefficient contribution for each order + // + auto beg = cuS.df_coef.begin(); + + coefKernelSlab<<stream>>> + (toKernel(cs->cuda_particles), toKernel(cs->indx1), + toKernel(cuS.dN_coef), stride, cur); + + // Begin the reduction by blocks [perhaps this should use a + // stride?] + // + unsigned int gridSize1 = N/BLOCK_SIZE; + if (N > gridSize1*BLOCK_SIZE) gridSize1++; + + reduceSum + <<stream>>> + (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), osize, N); + + // Finish the reduction for this order in parallel + // + thrust::counting_iterator index_begin(0); + thrust::counting_iterator index_end(gridSize1*osize); + + // The key_functor indexes the sum reduced series by array index + // + thrust::reduce_by_key + ( + thrust::cuda::par.on(cs->stream), + thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), + thrust::make_transform_iterator(index_end, key_functor(gridSize1)), + cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() + ); + + thrust::transform(thrust::cuda::par.on(cs->stream), + cuS.dw_coef.begin(), cuS.dw_coef.end(), + beg, beg, thrust::plus()); + + thrust::advance(beg, osize); + } + + use1 += N; // Increment particle count + } + + // Accumulate the coefficients from the device to the host + // + host_coefs = cuS.df_coef; + + // Create a wavenumber tuple from a flattened index + // + auto indices = [&](int indx) + { + int NX = 2*this->nmaxx+1, NY = 2*this->nmaxy+1; + int k = indx/(NX*NY); + int j = (indx - k*NX*NY)/NX; + int i = indx - (j + k*NY)*NX; + + return std::tuple{i, j, k}; + }; + + // DEBUG + // + if (false) { + std::cout << std::string(3*4+4*20, '-') << std::endl + << "---- Slab, T=" << tnow << std::endl + << std::string(3*4+4*20, '-') << std::endl + << std::setprecision(10); + + std::cout << std::setw(4) << "i" + << std::setw(4) << "j" + << std::setw(4) << "k" + << std::setw(20) << "GPU" + << std::setw(20) << "CPU" + << std::setw(20) << "diff" + << std::setw(20) << "rel diff" + << std::endl; + + auto cmax = std::max_element(host_coefs.begin(), host_coefs.begin()+osize, + LessAbs()); + + for (int n=0; n>(host_coefs[n]); + auto b = expcoef[0](i, j, k); + auto c = std::abs(a - b); + std::cout << std::setw(4) << i-nmaxx + << std::setw(4) << j-nmaxy + << std::setw(4) << k-nmaxz + << std::setw(20) << a + << std::setw(20) << b + << std::setw(20) << c + << std::setw(20) << c/thrust::abs(*cmax) + << std::endl; + } + + std::cout << std::string(3*4+4*20, '-') << std::endl; + } + + + // Dump the coefficients for sanity checking (set to false for + // production) + // + if (false) { + + coefType test; + if (myid==0) test.resize(2*nmaxx+1, 2*nmaxy+1, 2*nmaxz+1); + + MPI_Reduce (thrust::raw_pointer_cast(&host_coefs[0]), test.data(), osize, + MPI_DOUBLE_COMPLEX, MPI_SUM, 0, MPI_COMM_WORLD); + + if (myid==0) { + + std::string ofile = "slab_dump." + runtag + ".dat"; + std::ofstream out(ofile, ios::app | ios::out); + + if (out) { + out << std::string(3*4+3*20, '-') << std::endl + << "---- Slab, T=" << tnow << std::endl + << std::string(3*4+3*20, '-') << std::endl + << std::setprecision(10); + + out << std::setw(4) << "i" + << std::setw(4) << "j" + << std::setw(4) << "k" + << std::setw(20) << "Real" + << std::setw(20) << "Imag" + << std::setw(20) << "Abs" + << std::endl; + + + for (int n=0; n" << std::endl; + } + } + } + + // Deep debug for checking a single wave number from slabics + // + if (false) { + + coefType test; + if (myid==0) test.resize(2*nmaxx+1, 2*nmaxy+1, 2*nmaxz+1); + + MPI_Reduce (thrust::raw_pointer_cast(&host_coefs[0]), test.data(), osize, + MPI_DOUBLE_COMPLEX, MPI_SUM, 0, MPI_COMM_WORLD); + + if (myid==0) { + + std::string ofile = "slab_test." + runtag + ".dat"; + std::ofstream out(ofile, ios::app | ios::out); + + if (out) { + std::multimap biggest; + + for (int n=0; nsecond); + auto a = test(i, j, k); + + out << std::setw(4) << i-nmaxx + << std::setw(4) << j-nmaxy + << std::setw(4) << k-nmaxz + << std::setw(20) << std::real(a) + << std::setw(20) << std::imag(a) + << std::setw(20) << std::abs(a) + << std::endl; + } + out << std::string(3*4+4*20, '-') << std::endl; + } else { + std::cout << "Error opening <" << ofile << ">" << std::endl; + } + } + } + + + // + // TEST comparison of coefficients for debugging + // + if (false and myid==0) { + + struct Element + { + std::complex d; + std::complex f; + + int i; + int j; + int k; + } + elem; + + std::multimap compare; + + std::string ofile = "test_slab." + runtag + ".dat"; + std::ofstream out(ofile); + + if (out) { + + // m loop + for (int n=0; n>(host_coefs[n]); + + double test = std::abs(elem.d - elem.f); + if (fabs(elem.d)>1.0e-12) test /= fabs(elem.d); + + compare.insert(std::make_pair(test, elem)); + + out << std::setw( 5) << elem.i - nmaxx + << std::setw( 5) << elem.j - nmaxy + << std::setw( 5) << elem.k - nmaxz + << std::setw( 5) << n + << std::setw(20) << elem.d + << std::setw(20) << elem.f + << std::endl; + } + + std::map::iterator best = compare.begin(); + std::map::iterator midl = best; + std::advance(midl, compare.size()/2); + std::map::reverse_iterator last = compare.rbegin(); + + std::cout << std::string(3*3 + 3*20 + 20, '-') << std::endl + << "---- Slab coefficients" << std::endl + << std::string(3*3 + 3*20 + 20, '-') << std::endl; + + std::cout << "Best case: [" + << std::setw( 3) << best->second.i << ", " + << std::setw( 3) << best->second.j << ", " + << std::setw( 3) << best->second.k << "] = " + << std::setw(20) << best->second.d + << std::setw(20) << best->second.f + << std::setw(20) << fabs(best->second.d - best->second.f) + << std::endl; + + std::cout << "Mid case: [" + << std::setw( 3) << midl->second.i << ", " + << std::setw( 3) << midl->second.j << ", " + << std::setw( 3) << midl->second.k << "] = " + << std::setw(20) << midl->second.d + << std::setw(20) << midl->second.f + << std::setw(20) << fabs(midl->second.d - midl->second.f) + << std::endl; + + std::cout << "Last case: [" + << std::setw( 3) << last->second.i << ", " + << std::setw( 3) << last->second.j << ", " + << std::setw( 3) << last->second.k << "] = " + << std::setw(20) << last->second.d + << std::setw(20) << last->second.f + << std::setw(20) << fabs(last->second.d - last->second.f) + << std::endl; + } + } + +} + + +void SlabSL::determine_acceleration_cuda() +{ + // Only do this once but copying mapping coefficients and textures + // must be done every time + // + if (initialize_cuda_slab) { + initialize_cuda(); + initialize_cuda_slab = false; + } + + // Copy coordinate mapping + // + initialize_constants(); + + std::cout << std::scientific; + + cudaDeviceProp deviceProp; + cudaGetDeviceProperties(&deviceProp, cC->cudaDevice); + cuda_check_last_error_mpi("cudaGetDeviceProperties", __FILE__, __LINE__, myid); + + auto cs = cC->cuStream; + + // Get particle index range for levels [mlevel, multistep] + // + PII lohi = cC->CudaGetLevelRange(mlevel, multistep); + + // Compute grid + // + unsigned int N = lohi.second - lohi.first; + unsigned int stride = N/BLOCK_SIZE/deviceProp.maxGridSize[0] + 1; + unsigned int gridSize = N/BLOCK_SIZE/stride; + + if (N>0) { + + if (N > gridSize*BLOCK_SIZE*stride) gridSize++; + +#ifdef VERBOSE_RPT + static unsigned debug_max_count = 100; + static unsigned debug_cur_count = 0; + if (debug_cur_count++ < debug_max_count) { + std::cout << std::endl + << "** -------------------------" << std::endl + << "** cudaSlab acceleration" << std::endl + << "** -------------------------" << std::endl + << "** N = " << N << std::endl + << "** Stride = " << stride << std::endl + << "** Block = " << BLOCK_SIZE << std::endl + << "** Grid = " << gridSize << std::endl + << "** Level = " << mlevel << std::endl + << "** lo = " << lohi.first << std::endl + << "** hi = " << lohi.second << std::endl + << "**" << std::endl; + } +#endif + + // Shared memory size for the reduction + // + int sMemSize = BLOCK_SIZE * sizeof(CmplxT); + + forceKernelSlab<<stream>>> + (toKernel(cs->cuda_particles), toKernel(cs->indx1), + toKernel(dev_coefs), stride, lohi); + } +} + +void SlabSL::HtoD_coefs() +{ + // Check size + host_coefs.resize(osize); + + // Copy from Slab + for (int i=0; iCudaSortLevelChanges(); + + cudaDeviceProp deviceProp; + cudaGetDeviceProperties(&deviceProp, component->cudaDevice); + cuda_check_last_error_mpi("cudaGetDeviceProperties", __FILE__, __LINE__, myid); + auto cs = component->cuStream; + + // Step through all levels + // + for (int olev=mfirst[mstep]; olev<=multistep; olev++) { + + for (int nlev=0; nlev<=multistep; nlev++) { + + if (olev == nlev) continue; + + // Get range of update block in particle index + // + unsigned int Ntotal = chg[olev][nlev].second - chg[olev][nlev].first; + + if (Ntotal==0) continue; // No particles [from, to]=[olev, nlev] + + unsigned int Npacks = Ntotal/component->bunchSize + 1; + + // Zero out coefficient storage + // + cuda_zero_coefs(); + +#ifdef VERBOSE_DBG + std::cout << "[" << myid << ", " << tnow + << "] Adjust slab: Ntotal=" << Ntotal << " Npacks=" << Npacks + << " for (m, d)=(" << olev << ", " << nlev << ")" << std::endl; +#endif + + // Loop over bunches + // + for (int n=0; nbunchSize*n; + cur.second = chg[olev][nlev].first + component->bunchSize*(n+1); + cur.second = std::min(cur.second, chg[olev][nlev].second); + + if (cur.second <= cur.first) break; + + // Compute grid + // + unsigned int N = cur.second - cur.first; + unsigned int stride = N/BLOCK_SIZE/deviceProp.maxGridSize[0] + 1; + unsigned int gridSize = N/BLOCK_SIZE/stride; + + if (N > gridSize*BLOCK_SIZE*stride) gridSize++; + + // Shared memory size for the reduction + // + int sMemSize = BLOCK_SIZE * sizeof(CmplxT); + + if (byPlanes) { + // Adjust cached storage, if necessary + // + cuS.resize_coefs(N, imx, gridSize, stride); + + // Compute the coefficient contribution for each order + // + auto beg = cuS.df_coef.begin(); + + for (int kk=-nmaxz; kk<=nmaxz; kk++) { + + for (int jj=-nmaxy; jj<=nmaxy; jj++) { + + // Do the work! + // + coefKernelSlabX<<stream>>> + (toKernel(cs->cuda_particles), toKernel(cs->indx1), + toKernel(cuS.dN_coef), jj, kk, stride, cur); + + unsigned int gridSize1 = N/BLOCK_SIZE; + if (N > gridSize1*BLOCK_SIZE) gridSize1++; + + reduceSum + <<stream>>> + (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), imx, N); + + // Finish the reduction for this order in parallel + // + thrust::counting_iterator index_begin(0); + thrust::counting_iterator index_end(gridSize1*imx); + + // The key_functor indexes the sum reduced series by array index + // + thrust::reduce_by_key + ( + thrust::cuda::par.on(cs->stream), + thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), + thrust::make_transform_iterator(index_end, key_functor(gridSize1)), + cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() + ); + + thrust::transform(thrust::cuda::par.on(cs->stream), + cuS.dw_coef.begin(), cuS.dw_coef.end(), + beg, beg, thrust::plus()); + + thrust::advance(beg, imx); + } + // END: z wave numbers + } + // END: y wave numbers + } + // END: bunch by planes + else { + // Adjust cached storage, if necessary + // + cuS.resize_coefs(N, osize, gridSize, stride); + + // Shared memory size for the reduction + // + int sMemSize = BLOCK_SIZE * sizeof(CmplxT); + + // Compute the coefficient contribution for each order + // + auto beg = cuS.df_coef.begin(); + + // Do the work! + // + coefKernelSlab<<stream>>> + (toKernel(cs->cuda_particles), toKernel(cs->indx1), + toKernel(cuS.dN_coef), stride, cur); + + unsigned int gridSize1 = N/BLOCK_SIZE; + if (N > gridSize1*BLOCK_SIZE) gridSize1++; + + reduceSum + <<stream>>> + (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), osize, N); + + // Finish the reduction for this order in parallel + // + thrust::counting_iterator index_begin(0); + thrust::counting_iterator index_end(gridSize1*osize); + + // The key_functor indexes the sum reduced series by array index + // + thrust::reduce_by_key + ( + thrust::cuda::par.on(cs->stream), + thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), + thrust::make_transform_iterator(index_end, key_functor(gridSize1)), + cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() + ); + + thrust::transform(thrust::cuda::par.on(cs->stream), + cuS.dw_coef.begin(), cuS.dw_coef.end(), + beg, beg, thrust::plus()); + + thrust::advance(beg, osize); + } + // END: all wave numbers at once + } + // END: bunches + + // Accumulate the coefficients from the device to the host + // + thrust::host_vector ret = cuS.df_coef; + + // Decrement current level and increment new level using the + // Slab update matricies + // + for (int i=0; i val = ret[i]; + differ1[0][olev].data()[i] -= val; + differ1[0][nlev].data()[i] += val; + } + } + // DONE: Inner loop + } + // DONE: Outer loop +} + + +void SlabSL::destroy_cuda() +{ + // Nothing +} + + From 4e7cb2c53d62a0b3875f05d3bfdbe1f6dbf7496e Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 29 Mar 2024 18:51:03 -0400 Subject: [PATCH 051/167] Some additional updates for multistepping in SlabSL [no ci] --- src/SlabSL.H | 34 +++- src/SlabSL.cc | 209 +++++++++++++++++++- src/cudaCube.cu | 15 +- src/cudaSlabSL.cu | 483 +++++++++++++++++----------------------------- 4 files changed, 429 insertions(+), 312 deletions(-) diff --git a/src/SlabSL.H b/src/SlabSL.H index 16c4b6f4a..c07b2fbc4 100644 --- a/src/SlabSL.H +++ b/src/SlabSL.H @@ -78,6 +78,9 @@ private: //@{ + //! Working device vectors + thrust::device_vector t_d; + //! Helper struct to hold device data struct cudaStorage { @@ -93,7 +96,7 @@ private: cudaStorage cuS; //! Only initialize once - bool initialize_cuda_cube; + bool initialize_cuda_slab = false; //! Initialize the cuda streams void cuda_initialize(); @@ -120,6 +123,35 @@ private: void * determine_coefficients_thread(void * arg); void * determine_acceleration_and_potential_thread(void * arg); + /** Extrapolate and sum coefficents per multistep level to get + a complete set of coefficients for force evaluation at an + intermediate time step + */ + void compute_multistep_coefficients(); + + //! For updating levels + //@{ + std::vector< std::vector > differ1; + std::vector< std::complex > pack, unpack; + //@} + + //@{ + //! Interpolation arrays + using coefTypePtr = std::shared_ptr; + std::vector expcoefN; + std::vector expcoefL; + //@} + + /** Update the multi time step coefficient table when moving particle + i from level cur to level + next + */ + //@{ + virtual void multistep_update_begin(); + virtual void multistep_update(int cur, int next, Component* c, int i, int id); + virtual void multistep_update_finish(); + //@} + //! Coefficient container instance for writing HDF5 CoefClasses::SlabCoefs slabCoefs; diff --git a/src/SlabSL.cc b/src/SlabSL.cc index 0e96beee6..c6abeee50 100644 --- a/src/SlabSL.cc +++ b/src/SlabSL.cc @@ -107,6 +107,34 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) for (auto & v : zpot) v.resize(nmaxz); for (auto & v : zfrc) v.resize(nmaxz); + + // Allocate coefficient matrix (one for each multistep level) + // and zero-out contents + // + differ1 = std::vector< std::vector >(nthrds); + for (int n=0; n(imx, imy, imz); + expcoefL[i] = std::make_shared(imx, imy, imz); + expcoefN[i] -> setZero(); + expcoefL[i] -> setZero(); + } + } SlabSL::~SlabSL() @@ -160,6 +188,20 @@ void SlabSL::determine_coefficients(void) expccof[i].setZero(); } + // Swap interpolation arrays + // + if (multistep) { + + auto p = expcoefL[mlevel]; + + expcoefL[mlevel] = expcoefN[mlevel]; + expcoefN[mlevel] = p; + + // Clean arrays for current level + // + expcoefN[mlevel]->setZero(); + } + #if HAVE_LIBCUDA==1 (*barrier)("SlabSL::entering cuda coefficients", __FILE__, __LINE__); if (component->cudaDevice>=0 and use_cuda) { @@ -188,8 +230,22 @@ void SlabSL::determine_coefficients(void) MPI_Allreduce ( &used1, &used, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD); - MPI_Allreduce( MPI_IN_PLACE, expccof[0].data(), expccof[0].size(), - MPI_CXX_DOUBLE_COMPLEX, MPI_SUM, MPI_COMM_WORLD); + if (multistep) { + + MPI_Allreduce( expccof[0].data(), expcoefN[mlevel]->data(), + expccof[0].size(), + MPI_CXX_DOUBLE_COMPLEX, MPI_SUM, MPI_COMM_WORLD); + } else { + + MPI_Allreduce( MPI_IN_PLACE, expccof[0].data(), expccof[0].size(), + MPI_CXX_DOUBLE_COMPLEX, MPI_SUM, MPI_COMM_WORLD); + } + + // Last level? + // + if (multistep and mlevel==multistep) { + compute_multistep_coefficients(); + } #if HAVE_LIBCUDA==1 cuda_initialize(); @@ -283,7 +339,6 @@ void SlabSL::get_acceleration_and_potential(Component* C) { cC = C; - MPL_start_timer(); #if HAVE_LIBCUDA==1 @@ -461,3 +516,151 @@ void SlabSL::dump_coefs(ostream& out) expccof[0].size()*sizeof(std::complex)); } +void SlabSL::multistep_update_begin() +{ + if (play_back and not play_cnew) return; + + // Clear the update matricies + for (int n=0; ndata()[i] += unpack[offset+i]; + } +} + +void SlabSL::multistep_update(int from, int to, Component *c, int i, int id) +{ + if (play_back and not play_cnew) return; + if (c->freeze(i)) return; + + double mass = c->Mass(i) * component->Adiabatic(); + + double x = c->Pos(i, 0); + double y = c->Pos(i, 1); + double z = c->Pos(i, 2); + + // Only compute for points inside the unit cube + // + if (x<0.0 or x>1.0) return; + if (y<0.0 or y>1.0) return; + if (z<0.0 or z>1.0) return; + + // Recursion multipliers + std::complex stepx = std::exp(-kfac*x); + std::complex stepy = std::exp(-kfac*y); + std::complex stepz = std::exp(-kfac*z); + + // Initial values for recursion + std::complex startx = std::exp(kfac*(x*nmaxx)); + std::complex starty = std::exp(kfac*(y*nmaxy)); + std::complex startz = std::exp(kfac*(z*nmaxz)); + + std::complex facx, facy, facz; + int ix, iy, iz; + + for (facx=startx, ix=0; ix val = -mass*facx*facy*facz*norm; + + differ1[id][from](ix, iy, iz) -= val; + differ1[id][ to](ix, iy, iz) += val; + } + } + } +} + +void SlabSL::compute_multistep_coefficients() +{ + if (play_back and not play_cnew) return; + + // Clean coefficient matrix + // + expccof[0].setZero(); + + // Interpolate to get coefficients above + // + for (int M=0; M(mdrft - dstepL[M][mdrft]); + double denom = static_cast(dstepN[M][mdrft] - dstepL[M][mdrft]); + + double b = numer/denom; // Interpolation weights + double a = 1.0 - b; + + for (int i=0; idata()[i] + b*expcoefN[M]->data()[i] ; + } + + // Sanity debug check + // + if (a<0.0 && a>1.0) { + std::cout << "Process " << myid + << ": interpolation error in multistep [a]" + << std::endl; + } + if (b<0.0 && b>1.0) { + std::cout << "Process " << myid + << ": interpolation error in multistep [b]" + << std::endl; + } + } + + // Add coefficients at or below this level + // + for (int M=mfirst[mdrft]; M<=multistep; M++) { + + for (int i=0; idata()[i]; + } + } +} + diff --git a/src/cudaCube.cu b/src/cudaCube.cu index 1dee6eebe..36a1d8a8b 100644 --- a/src/cudaCube.cu +++ b/src/cudaCube.cu @@ -30,7 +30,7 @@ using CmplxT = thrust::complex; // Index functions for coefficients based on Eigen Tensor packing order // __device__ -int Index(int i, int j, int k) +int cubeIndex(int i, int j, int k) { i += cubeNumX; j += cubeNumY; @@ -38,6 +38,7 @@ int Index(int i, int j, int k) return k*cubeNX*cubeNY + j*cubeNX + i; } +/* // Index function for modulus coefficients // __device__ @@ -49,9 +50,10 @@ thrust::tuple TensorIndices(int indx) return {i, j, k}; } +*/ __device__ -thrust::tuple WaveNumbers(int indx) +thrust::tuple cubeWaveNumbers(int indx) { int k = indx/(cubeNX*cubeNY); int j = (indx - k*cubeNX*cubeNY)/cubeNX; @@ -60,7 +62,6 @@ thrust::tuple WaveNumbers(int indx) return {i-cubeNumX, j-cubeNumY, k-cubeNumZ}; } - __global__ void testConstantsCube() { @@ -192,7 +193,7 @@ __global__ void coefKernelCube // Get the wave numbers int ii, jj, kk; - thrust::tie(ii, jj, kk) = WaveNumbers(s); + thrust::tie(ii, jj, kk) = cubeWaveNumbers(s); // Skip the constant term and the divide by zero // @@ -227,7 +228,7 @@ __global__ void coefKernelCube int l2 = ii*ii + jj*jj + kk*kk; if (l2) { // Only compute for non-zero l-index cuFP_t norm = -mm/sqrt(M_PI*l2); - coef._v[Index(ii, jj, kk)*N + i] = X*Y*Z*norm; + coef._v[cubeIndex(ii, jj, kk)*N + i] = X*Y*Z*norm; } } } @@ -361,7 +362,7 @@ forceKernelCube(dArray P, dArray I, // for (int s=0; s P, dArray I, int l2 = ii*ii + jj*jj + kk*kk; if (l2) { // Only compute the non-constant terms cuFP_t norm = 1.0/sqrt(M_PI*l2); - auto pfac = coef._v[Index(ii, jj, kk)] * X*Y*Z*norm; + auto pfac = coef._v[cubeIndex(ii, jj, kk)] * X*Y*Z*norm; pot += pfac; // Potential and force vector acc[0] += CmplxT(0.0, -cubeDfac*ii) * pfac; diff --git a/src/cudaSlabSL.cu b/src/cudaSlabSL.cu index ab6ee6e72..c61034a67 100644 --- a/src/cudaSlabSL.cu +++ b/src/cudaSlabSL.cu @@ -25,13 +25,14 @@ // Global symbols for slab construction // __device__ __constant__ -int slabNumX, slabNumY, slabNumZ, slabNX, slabNY, slabNZ, slabNdim; +int slabNumX, slabNumY, slabNumZ, slabNX, slabNY, slabNZ, slabNdim, slabNum; + __device__ __constant__ int slabCmap; __device__ __constant__ -cuFP_t slabDfac, SlabHscl, slabXmin, slabXmax, slabDxi; +cuFP_t slabDfac, slabHscl, slabXmin, slabXmax, slabDxi; // Alias for Thrust complex type to make this code more readable // @@ -40,18 +41,18 @@ using CmplxT = thrust::complex; // Index functions for coefficients based on Eigen Tensor packing order // __device__ -int Index(int i, int j, int k) +int slabIndex(int i, int j, int k) { i += slabNumX; j += slabNumY; - k += slabNumZ; return k*slabNX*slabNY + j*slabNX + i; } +/* // Index function for modulus coefficients // __device__ -thrust::tuple TensorIndices(int indx) +thrust::tuple slabTensorIndices(int indx) { int k = indx/(slabNX*slabNY); int j = (indx - k*slabNX*slabNY)/slabNX; @@ -61,7 +62,7 @@ thrust::tuple TensorIndices(int indx) } __device__ -thrust::tuple WaveNumbers(int indx) +thrust::tuple slabWaveNumbers(int indx) { int k = indx/(slabNX*slabNY); int j = (indx - k*slabNX*slabNY)/slabNX; @@ -69,7 +70,7 @@ thrust::tuple WaveNumbers(int indx) return {i-slabNumX, j-slabNumY, k}; } - +*/ __global__ void testConstantsSlab() @@ -87,6 +88,9 @@ void testConstantsSlab() printf(" Dfac = %e\n", slabDfac ); printf(" Hscl = %e\n", slabHscl ); printf(" Cmap = %d\n", slabCmap ); + printf(" Xmin = %e\n", slabXmin ); + printf(" Xmax = %e\n", slabXmax ); + printf(" Dxi = %e\n", slabDxi ); printf("-------------------------\n"); } @@ -106,17 +110,17 @@ void testFetchSlab(dArray T, dArray f, thrust::host_vector returnTestSlab (thrust::host_vector& tex, - int l, int j, int nmax, int numr) + int l, int j, int nmax, int numz) { thrust::device_vector t_d = tex; - unsigned int gridSize = numr/BLOCK_SIZE; - if (numr > gridSize*BLOCK_SIZE) gridSize++; + unsigned int gridSize = numz/BLOCK_SIZE; + if (numz > gridSize*BLOCK_SIZE) gridSize++; - thrust::device_vector f_d(numr); + thrust::device_vector f_d(numz); - testFetchSph<<>>(toKernel(t_d), toKernel(f_d), - l, j, nmax, numr); + testFetchSlab<<>>(toKernel(t_d), toKernel(f_d), + l, j, nmax, numz); cudaDeviceSynchronize(); @@ -128,9 +132,9 @@ cuFP_t cu_z_to_xi(cuFP_t z) { cuFP_t ret; - if (sphCmap==0) { + if (slabCmap==0) { ret = tanh(z/slabHscl); - } else if (sphCmap==1) { + } else if (slabCmap==1) { ret = z/sqrt(z*z + slabHscl*slabHscl); } else { ret = z; @@ -144,9 +148,9 @@ cuFP_t cu_xi_to_z(cuFP_t xi) { cuFP_t ret; - if (sphCmap==0) { + if (slabCmap==0) { ret = slabHscl*atanh(xi); - } else if (sphCmap==1) { + } else if (slabCmap==1) { ret = xi*slabHscl/sqrt(1.0 - xi*xi); } else { ret = xi; @@ -156,14 +160,14 @@ cuFP_t cu_xi_to_z(cuFP_t xi) } __device__ -cuFP_t cu_d_xi_to_r(cuFP_t xi) +cuFP_t cu_d_xi_to_z(cuFP_t xi) { cuFP_t ret; - if (sphCmap==0) { - ret = (1.0 - xi*xi)/SlabHscl; - } else if (sphCmap==1) { - ret = pow(1.0 - xi*xi, 1.5)/SlabHscl; + if (slabCmap==0) { + ret = (1.0 - xi*xi)/slabHscl; + } else if (slabCmap==1) { + ret = pow(1.0 - xi*xi, 1.5)/slabHscl; } else { ret = 1.0; } @@ -175,36 +179,16 @@ cuFP_t cu_d_xi_to_r(cuFP_t xi) // void SlabSL::cuda_initialize() { - // Default - // - byPlanes = true; - - // Make method string lower case - // - std::transform(cuMethod.begin(), cuMethod.end(), cuMethod.begin(), - [](unsigned char c){ return std::tolower(c); }); - - // Parse cuMethods variable - // - // All dimensions at once - // - if (cuMethod.find("all") != std::string::npos) byPlanes = false; - if (cuMethod.find("full") != std::string::npos) byPlanes = false; - if (cuMethod.find("2d") != std::string::npos) byPlanes = false; - // - // Only one dimension at a time - // - if (cuMethod.find("axes") != std::string::npos) byPlanes = true; - if (cuMethod.find("1d") != std::string::npos) byPlanes = true; - - std::cout << "---- SlabSL::cuda_initialize: byPlanes=" - << std::boolalpha << byPlanes << std::endl; + // Nothing } // Copy constants to device // void SlabSL::initialize_constants() { + auto f = grid->getCudaMappingConstants(); + cuFP_t z; + cuda_safe_call(cudaMemcpyToSymbol(slabNumX, &nmaxx, sizeof(int), size_t(0), cudaMemcpyHostToDevice), __FILE__, __LINE__, "Error copying slabNumX"); @@ -229,32 +213,45 @@ void SlabSL::initialize_constants() size_t(0), cudaMemcpyHostToDevice), __FILE__, __LINE__, "Error copying slabNZ"); - cuda_safe_call(cudaMemcpyToSymbol(slabNdim, &osize, sizeof(int), + cuda_safe_call(cudaMemcpyToSymbol(slabNdim, &jmax, sizeof(int), size_t(0), cudaMemcpyHostToDevice), __FILE__, __LINE__, "Error copying slabNdim"); + cuda_safe_call(cudaMemcpyToSymbol(slabNum, &f.numr, sizeof(int), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabNum"); + int Cmap = 0; cuda_safe_call(cudaMemcpyToSymbol(slabCmap, &Cmap, sizeof(int), size_t(0), cudaMemcpyHostToDevice), __FILE__, __LINE__, "Error copying slabCmap"); - cuFP_t dfac = 2.0*M_PI; - - cuda_safe_call(cudaMemcpyToSymbol(slabDfac, &dfac, sizeof(cuFP_t), + cuda_safe_call(cudaMemcpyToSymbol(slabDfac, &(z=2.0*M_PI), sizeof(cuFP_t), size_t(0), cudaMemcpyHostToDevice), __FILE__, __LINE__, "Error copying slabDfac"); - dfac = H; - - cuda_safe_call(cudaMemcpyToSymbol(slabHscl, &dfac, sizeof(cuFP_t), + cuda_safe_call(cudaMemcpyToSymbol(slabHscl, &(z=SLGridSlab::H), sizeof(cuFP_t), size_t(0), cudaMemcpyHostToDevice), __FILE__, __LINE__, "Error copying slabHscl"); + + cuda_safe_call(cudaMemcpyToSymbol(slabXmin, &(z=f.xmin), sizeof(cuFP_t), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabXmin"); + + cuda_safe_call(cudaMemcpyToSymbol(slabXmax, &(z=f.xmax), sizeof(cuFP_t), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabXmax"); + + cuda_safe_call(cudaMemcpyToSymbol(slabDxi, &(z=f.dxi), sizeof(cuFP_t), + size_t(0), cudaMemcpyHostToDevice), + __FILE__, __LINE__, "Error copying slabDxi"); } + __global__ void coefKernelSlab (dArray P, dArray I, dArray coef, - int stride, PII lohi) + dArray tex, int stride, PII lohi) { // Thread ID // @@ -287,7 +284,7 @@ __global__ void coefKernelSlab if (pos[k]<0.0) pos[k] += floor(-pos[k]) + 1.0; else - pos[k] += -floor(pos[key_functor]); + pos[k] += -floor(pos[k]); } // Wave number loop @@ -302,25 +299,24 @@ __global__ void coefKernelSlab // Vertical interpolation // - cuFP_t x = cu_z_to_zi(pos[2]); + cuFP_t x = cu_z_to_xi(pos[2]); cuFP_t xi = (x - slabXmin)/slabDxi; - cuFP_t dx = dx = cu_d_xi_to_z(xi)/slabDxi; int ind = floor(xi); int in0 = ind; if (in0 < 0) in0 = 0; - if (in0 > slabNumz-2) in0 = slabNumz - 2; + if (in0 > slabNum-2) in0 = slabNum - 2; if (ind < 1) ind = 1; - if (ind > slabNumz-2) ind = slabNumz - 2; + if (ind > slabNum-2) ind = slabNum - 2; cuFP_t a = (cuFP_t)(in0+1) - xi; cuFP_t b = 1.0 - a; // Flip sign for antisymmetric basis functions - int sign=1; - if (x<0 && 2*(n/2)!=n) sign=-1; + int sign = 1; + if (x<0 && 2*(n/2)!=n) sign = -1; // Will contain the incremented basis @@ -343,11 +339,11 @@ __global__ void coefKernelSlab cuFP_t p0 = #if cuREAL == 4 - a*tex1D(tex._v[0], ind ) + - b*tex1D(tex._v[0], ind+1) ; + a*tex1D(tex._v[0], ind ) + + b*tex1D(tex._v[0], ind+1) ; #else - a*int2_as_double(tex1D(tex._v[0], ind )) + - b*int2_as_double(tex1D(tex._v[0], ind+1)) ; + a*int2_as_double(tex1D(tex._v[0], ind )) + + b*int2_as_double(tex1D(tex._v[0], ind+1)) ; #endif int k = 1 + kx*(kx+1)/2*(slabNumY+1) + n; @@ -364,13 +360,12 @@ __global__ void coefKernelSlab #endif ) * p0 * sign; - coef._v[Index(ii, jj, n)] = -2.0*slabDfac * X * Y * v * mass; + coef._v[slabIndex(ii, jj, n)] = -2.0*slabDfac * X * Y * v * mm; } } } // END: wave number loop -#endif } // END: particle index limit } @@ -380,7 +375,7 @@ __global__ void coefKernelSlab __global__ void forceKernelSlab(dArray P, dArray I, - dArray coef, int stride, PII lohi) + dArray coef, dArray tex, int stride, PII lohi) { // Thread ID // @@ -401,7 +396,6 @@ forceKernelSlab(dArray P, dArray I, CmplxT acc[3] = {0.0, 0.0, 0.0}, pot = 0.0, fac, facf; cuFP_t pos[3] = {p.pos[0], p.pos[1], p.pos[2]}; cuFP_t mm = p.mass; - int ind[3]; // Wave number loop // @@ -414,18 +408,18 @@ forceKernelSlab(dArray P, dArray I, // Vertical interpolation // - cuFP_t x = cu_z_to_zi(pos[2]); + cuFP_t x = cu_z_to_xi(pos[2]); cuFP_t xi = (x - slabXmin)/slabDxi; - cuFP_t dx = dx = cu_d_xi_to_z(xi)/slabDxi; + cuFP_t dx = cu_d_xi_to_z(xi)/slabDxi; int ind = floor(xi); int in0 = ind; if (in0 < 0) in0 = 0; - if (in0 > slabNumz-2) in0 = slabNumz - 2; + if (in0 > slabNum-2) in0 = slabNum - 2; if (ind < 1) ind = 1; - if (ind > slabNumz-2) ind = slabNumz - 2; + if (ind > slabNum-2) ind = slabNum - 2; cuFP_t a = (cuFP_t)(in0+1) - xi; cuFP_t b = 1.0 - a; @@ -435,16 +429,15 @@ forceKernelSlab(dArray P, dArray I, int jn0 = floor(xi); if (jn0 < 1) jn0 = 1; - if (jn0 > slabNumz-2) jn0 = slabNumz - 2; + if (jn0 > slabNum-2) jn0 = slabNum - 2; - double p = (x - xi[jn0])/dxi; + cuFP_t s = (x - slabXmin - slabDxi*jn0)/slabDxi; // Flip sign for antisymmetric basis functions - int sign=1; - if (pos[2]<0 && 2*(n/2)!=n) sign=-1; + int sign = 1; + if (pos[2]<0 && 2*(n/2)!=n) sign = -1; CmplxT X, Y; // Will contain the incremented basis - CmplxT fac, facf; X = cx; // Assign the min X wavenumber for (int ii=-slabNumX; ii<=slabNumX; ii++, X*=sx) { @@ -461,7 +454,9 @@ forceKernelSlab(dArray P, dArray I, a*int2_as_double(tex1D(tex._v[0], ind )) + b*int2_as_double(tex1D(tex._v[0], ind+1)) ; #endif - int k = 1 + kx*(kx+1)/2*(slabNumY+1) + n; + int kx = ii + slabNumX; + int ky = jj + slabNumY; + int k = 1 + kx*(kx+1)/2*(slabNumY+1) + n; #ifdef BOUNDS_CHECK if (k>=tex._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); @@ -478,21 +473,21 @@ forceKernelSlab(dArray P, dArray I, cuFP_t f = ( #if cuREAL == 4 - (p - 0.5)*tex1D(tex._v[k], jnd-1)*tex1D(tex._v[0], jnd-1) - -2.0*tex1D(tex._v[k], jnd)*tex1D(tex._v[0], jnd) + - (p + 0.5)**tex1D(tex._v[k], jnd+1)*tex1D(tex._v[0], jnd+1) + (s - 0.5)*tex1D(tex._v[k], jn0-1)*tex1D(tex._v[0], jn0-1) + -2.0*tex1D(tex._v[k], jnd)*tex1D(tex._v[0], jn0) + + (s + 0.5)*tex1D(tex._v[k], jn0+1)*tex1D(tex._v[0], jn0+1) #else - (p - 0.5)*int2_as_double(tex1D(tex._v[k], jnd-1))* - int2_as_double(tex1D(tex._v[0], jnd-1)) - -2.0*int2_as_double(tex1D(tex._v[k], jnd))* - int2_as_double(tex1D(tex._v[0], jnd)) + - (p + 0.5)*int2_as_double(tex1D(tex._v[k], jnd+1))* - int2_as_double(tex1D(tex._v[0], jnd+1)) + (s - 0.5)*int2_as_double(tex1D(tex._v[k], jn0-1))* + int2_as_double(tex1D(tex._v[0], jn0-1)) + -2.0*int2_as_double(tex1D(tex._v[k], jn0))* + int2_as_double(tex1D(tex._v[0], jn0)) + + (s + 0.5)*int2_as_double(tex1D(tex._v[k], jn0+1))* + int2_as_double(tex1D(tex._v[0], jn0+1)) #endif - ) * sign; + ) * sign * dx; - fac = X * Y * v * coef._v[Index(ii, jj, n)]; - facf = X * Y * f * coef._v[Index(ii, jj, n)]; + fac = X * Y * v * coef._v[slabIndex(ii, jj, n)]; + facf = X * Y * f * coef._v[slabIndex(ii, jj, n)]; acc[0] += CmplxT(0.0, -slabDfac*ii) * fac; acc[1] += CmplxT(0.0, -slabDfac*jj) * fac; @@ -503,7 +498,7 @@ forceKernelSlab(dArray P, dArray I, // END: y wave number loop } // END: x wavenumber loop -#endif + // Particle assignment // p.pot = pot.real(); @@ -546,7 +541,7 @@ void SlabSL::cuda_zero_coefs() { auto cr = component->cuStream; - cuS.df_coef.resize(osize); + cuS.df_coef.resize(jmax); // Zero output array // @@ -577,7 +572,7 @@ void SlabSL::determine_coefficients_cuda() // This will stay fixed for the entire run // - host_coefs.resize(osize); + host_coefs.resize(jmax); // Get the stream for this component // @@ -665,108 +660,53 @@ void SlabSL::determine_coefficients_cuda() // int sMemSize = BLOCK_SIZE * sizeof(CmplxT); - if (byPlanes) { - - // Adjust cached storage, if necessary - // - cuS.resize_coefs(N, imx, gridSize, stride); + // Adjust cached storage, if necessary + // + cuS.resize_coefs(N, jmax, gridSize, stride); - // Compute the coefficient contribution for each order - // - auto beg = cuS.df_coef.begin(); - - for (int kk=-nmaxz; kk<=nmaxz; kk++) { - - for (int jj=-nmaxy; jj<=nmaxy; jj++) { - - coefKernelSlabX<<stream>>> - (toKernel(cs->cuda_particles), toKernel(cs->indx1), - toKernel(cuS.dN_coef), jj, kk, stride, cur); - - // Begin the reduction by blocks [perhaps this should use a - // stride?] - // - unsigned int gridSize1 = N/BLOCK_SIZE; - if (N > gridSize1*BLOCK_SIZE) gridSize1++; - - reduceSum - <<stream>>> - (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), imx, N); - - // Finish the reduction for this order in parallel - // - thrust::counting_iterator index_begin(0); - thrust::counting_iterator index_end(gridSize1*imx); - - // The key_functor indexes the sum reduced series by array index - // - thrust::reduce_by_key - ( - thrust::cuda::par.on(cs->stream), - thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), - thrust::make_transform_iterator(index_end, key_functor(gridSize1)), - cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() - ); - - thrust::transform(thrust::cuda::par.on(cs->stream), - cuS.dw_coef.begin(), cuS.dw_coef.end(), - beg, beg, thrust::plus()); - - thrust::advance(beg, imx); - } - // END: y wave number loop - } - // END: z wave number loop - - } else { - - // Adjust cached storage, if necessary - // - cuS.resize_coefs(N, osize, gridSize, stride); + // Compute the coefficient contribution for each order + // + auto beg = cuS.df_coef.begin(); - // Compute the coefficient contribution for each order - // - auto beg = cuS.df_coef.begin(); - - coefKernelSlab<<stream>>> - (toKernel(cs->cuda_particles), toKernel(cs->indx1), - toKernel(cuS.dN_coef), stride, cur); + coefKernelSlab<<stream>>> + (toKernel(cs->cuda_particles), toKernel(cs->indx1), + toKernel(cuS.dN_coef), toKernel(t_d) ,stride, cur); - // Begin the reduction by blocks [perhaps this should use a - // stride?] - // - unsigned int gridSize1 = N/BLOCK_SIZE; - if (N > gridSize1*BLOCK_SIZE) gridSize1++; + // Begin the reduction by blocks [perhaps this should use a + // stride?] + // + unsigned int gridSize1 = N/BLOCK_SIZE; + if (N > gridSize1*BLOCK_SIZE) gridSize1++; - reduceSum - <<stream>>> - (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), osize, N); - - // Finish the reduction for this order in parallel - // - thrust::counting_iterator index_begin(0); - thrust::counting_iterator index_end(gridSize1*osize); - - // The key_functor indexes the sum reduced series by array index - // - thrust::reduce_by_key - ( - thrust::cuda::par.on(cs->stream), - thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), - thrust::make_transform_iterator(index_end, key_functor(gridSize1)), - cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() + reduceSum + <<stream>>> + (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), jmax, N); + + // Finish the reduction for this order in parallel + // + thrust::counting_iterator index_begin(0); + thrust::counting_iterator index_end(gridSize1*jmax); + + // The key_functor indexes the sum reduced series by array index + // + thrust::reduce_by_key + ( + thrust::cuda::par.on(cs->stream), + thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), + thrust::make_transform_iterator(index_end, key_functor(gridSize1)), + cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() ); - thrust::transform(thrust::cuda::par.on(cs->stream), - cuS.dw_coef.begin(), cuS.dw_coef.end(), - beg, beg, thrust::plus()); - - thrust::advance(beg, osize); - } - - use1 += N; // Increment particle count + thrust::transform(thrust::cuda::par.on(cs->stream), + cuS.dw_coef.begin(), cuS.dw_coef.end(), + beg, beg, thrust::plus()); + + thrust::advance(beg, jmax); } + // use1 += N; // Increment particle count + + // Accumulate the coefficients from the device to the host // host_coefs = cuS.df_coef; @@ -800,13 +740,13 @@ void SlabSL::determine_coefficients_cuda() << std::setw(20) << "rel diff" << std::endl; - auto cmax = std::max_element(host_coefs.begin(), host_coefs.begin()+osize, + auto cmax = std::max_element(host_coefs.begin(), host_coefs.begin()+jmax, LessAbs()); - for (int n=0; n>(host_coefs[n]); - auto b = expcoef[0](i, j, k); + auto b = expccof[0](i, j, k); auto c = std::abs(a - b); std::cout << std::setw(4) << i-nmaxx << std::setw(4) << j-nmaxy @@ -830,7 +770,7 @@ void SlabSL::determine_coefficients_cuda() coefType test; if (myid==0) test.resize(2*nmaxx+1, 2*nmaxy+1, 2*nmaxz+1); - MPI_Reduce (thrust::raw_pointer_cast(&host_coefs[0]), test.data(), osize, + MPI_Reduce (thrust::raw_pointer_cast(&host_coefs[0]), test.data(), jmax, MPI_DOUBLE_COMPLEX, MPI_SUM, 0, MPI_COMM_WORLD); if (myid==0) { @@ -853,7 +793,7 @@ void SlabSL::determine_coefficients_cuda() << std::endl; - for (int n=0; n biggest; - for (int n=0; n>(host_coefs[n]); double test = std::abs(elem.d - elem.f); @@ -1073,18 +1013,18 @@ void SlabSL::determine_acceleration_cuda() forceKernelSlab<<stream>>> (toKernel(cs->cuda_particles), toKernel(cs->indx1), - toKernel(dev_coefs), stride, lohi); + toKernel(dev_coefs), toKernel(t_d), stride, lohi); } } void SlabSL::HtoD_coefs() { // Check size - host_coefs.resize(osize); + host_coefs.resize(jmax); // Copy from Slab for (int i=0; istream>>> - (toKernel(cs->cuda_particles), toKernel(cs->indx1), - toKernel(cuS.dN_coef), jj, kk, stride, cur); - - unsigned int gridSize1 = N/BLOCK_SIZE; - if (N > gridSize1*BLOCK_SIZE) gridSize1++; - - reduceSum - <<stream>>> - (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), imx, N); - - // Finish the reduction for this order in parallel - // - thrust::counting_iterator index_begin(0); - thrust::counting_iterator index_end(gridSize1*imx); - - // The key_functor indexes the sum reduced series by array index - // - thrust::reduce_by_key - ( - thrust::cuda::par.on(cs->stream), - thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), - thrust::make_transform_iterator(index_end, key_functor(gridSize1)), - cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() - ); - - thrust::transform(thrust::cuda::par.on(cs->stream), - cuS.dw_coef.begin(), cuS.dw_coef.end(), - beg, beg, thrust::plus()); - - thrust::advance(beg, imx); - } - // END: z wave numbers - } - // END: y wave numbers - } - // END: bunch by planes - else { - // Adjust cached storage, if necessary - // - cuS.resize_coefs(N, osize, gridSize, stride); + // Compute the coefficient contribution for each order + // + auto beg = cuS.df_coef.begin(); - // Shared memory size for the reduction - // - int sMemSize = BLOCK_SIZE * sizeof(CmplxT); - - // Compute the coefficient contribution for each order - // - auto beg = cuS.df_coef.begin(); - - // Do the work! - // - coefKernelSlab<<stream>>> - (toKernel(cs->cuda_particles), toKernel(cs->indx1), - toKernel(cuS.dN_coef), stride, cur); - - unsigned int gridSize1 = N/BLOCK_SIZE; - if (N > gridSize1*BLOCK_SIZE) gridSize1++; - - reduceSum - <<stream>>> - (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), osize, N); - - // Finish the reduction for this order in parallel - // - thrust::counting_iterator index_begin(0); - thrust::counting_iterator index_end(gridSize1*osize); - - // The key_functor indexes the sum reduced series by array index - // - thrust::reduce_by_key - ( - thrust::cuda::par.on(cs->stream), - thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), - thrust::make_transform_iterator(index_end, key_functor(gridSize1)), - cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() - ); - - thrust::transform(thrust::cuda::par.on(cs->stream), - cuS.dw_coef.begin(), cuS.dw_coef.end(), - beg, beg, thrust::plus()); - - thrust::advance(beg, osize); - } - // END: all wave numbers at once + // Do the work! + // + coefKernelSlab<<stream>>> + (toKernel(cs->cuda_particles), toKernel(cs->indx1), + toKernel(cuS.dN_coef), toKernel(t_d), stride, cur); + + unsigned int gridSize1 = N/BLOCK_SIZE; + if (N > gridSize1*BLOCK_SIZE) gridSize1++; + + reduceSum + <<stream>>> + (toKernel(cuS.dc_coef), toKernel(cuS.dN_coef), jmax, N); + + // Finish the reduction for this order in parallel + // + thrust::counting_iterator index_begin(0); + thrust::counting_iterator index_end(gridSize1*jmax); + + // The key_functor indexes the sum reduced series by array index + // + thrust::reduce_by_key + ( + thrust::cuda::par.on(cs->stream), + thrust::make_transform_iterator(index_begin, key_functor(gridSize1)), + thrust::make_transform_iterator(index_end, key_functor(gridSize1)), + cuS.dc_coef.begin(), thrust::make_discard_iterator(), cuS.dw_coef.begin() + ); + + thrust::transform(thrust::cuda::par.on(cs->stream), + cuS.dw_coef.begin(), cuS.dw_coef.end(), + beg, beg, thrust::plus()); + + thrust::advance(beg, jmax); } // END: bunches @@ -1274,7 +1155,7 @@ void SlabSL::multistep_update_cuda() // Decrement current level and increment new level using the // Slab update matricies // - for (int i=0; i val = ret[i]; differ1[0][olev].data()[i] -= val; differ1[0][nlev].data()[i] += val; From 688370714654fc5b67648ed5e1f54e6090849f33 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 1 Apr 2024 10:30:11 -0400 Subject: [PATCH 052/167] Multistep fixes on CPU side to SlabSL and Cube [no ci] --- expui/Coefficients.cc | 7 +- exputil/Cheby1d.cc | 5 + exputil/SLGridMP2.cc | 454 ++++++++++++++++++---------------------- include/SLGridMP2.H | 28 +-- include/interp.H | 90 +++++++- src/Cube.cc | 124 +++++------ src/SlabSL.H | 34 ++- src/SlabSL.cc | 274 ++++++++++++------------ src/SphericalBasis.H | 4 +- src/cudaSlabSL.cu | 63 +++--- utils/SL/CMakeLists.txt | 3 +- utils/SL/Model1d.H | 434 ++++++++++++++++++++++++++++++++++++++ utils/SL/Model1d.cc | 333 +++++++++++++++++++++++++++++ utils/SL/RK4.H | 45 ++++ utils/SL/slabchk.cc | 210 +++++++++++++++++++ 15 files changed, 1603 insertions(+), 505 deletions(-) create mode 100644 utils/SL/Model1d.H create mode 100644 utils/SL/Model1d.cc create mode 100644 utils/SL/RK4.H create mode 100644 utils/SL/slabchk.cc diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 06a892faa..452ebecba 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -1365,6 +1365,11 @@ namespace CoefClasses int nmaxY = coefs.begin()->second->nmaxy; int nmaxZ = coefs.begin()->second->nmaxz; + // Internal sanity check + assert(nmaxX == NmaxX && "nmaxX <==> NmaxX mismatch"); + assert(nmaxY == NmaxY && "nmaxY <==> NmaxY mismatch"); + assert(nmaxZ == NmaxZ && "nmaxZ <==> NmaxZ mismatch"); + int dim = 0; if (d == 'x') dim = 2*nmaxX + 1; @@ -1393,7 +1398,7 @@ namespace CoefClasses } else if (d=='y') { for (int iy=0; iy<=2*NmaxY; iy++) { double val(0.0); - for (int ix=0; iy<=2*NmaxX; ix++) { + for (int ix=0; ix<=2*NmaxX; ix++) { if (abs(ix - nmaxX) < min) continue; for (int iz=0; iz& X, std::vector& Y, int N) { double y, sum, fac, bpa, bma; + xmin = X.front(); + xmax = X.back(); + std::vector f(n); c = std::vector(n); diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 94cc341d3..44b681702 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -145,7 +145,6 @@ SLGridSph::SLGridSph(std::string modelname, if (cachename.size()) sph_cache_name = cachename; else throw std::runtime_error("SLGridSph: you must specify a cachename"); - mpi_buf = 0; model = SphModTblPtr(new SphericalModelTable(model_file_name, DIVERGE, DFAC)); tbdbg = VERBOSE; diverge = DIVERGE; @@ -159,7 +158,6 @@ SLGridSph::SLGridSph(std::shared_ptr mod, bool CACHE, int CMAP, double RMAP, std::string cachename, bool VERBOSE) { - mpi_buf = 0; model = mod; tbdbg = VERBOSE; diverge = 0; @@ -231,7 +229,7 @@ void SLGridSph::initialize(int LMAX, int NMAX, int NUMR, // if (mpi) { - table = new TableSph [lmax+1]; + table = table_ptr_1D(new TableSph [lmax+1]); mpi_setup(); @@ -246,7 +244,7 @@ void SLGridSph::initialize(int LMAX, int NMAX, int NUMR, for (l=0; l<=lmax; l++) { - MPI_Bcast(mpi_buf, mpi_bufsz, MPI_PACKED, 0, MPI_COMM_WORLD); + MPI_Bcast(&mpi_buf[0], mpi_bufsz, MPI_PACKED, 0, MPI_COMM_WORLD); mpi_unpack_table(); } @@ -293,10 +291,10 @@ void SLGridSph::initialize(int LMAX, int NMAX, int NUMR, MPI_COMM_WORLD, &status); totbad += bad; int retid = status.MPI_SOURCE; - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, retid, 11, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, retid, 11, MPI_COMM_WORLD, &status); #else - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); int retid = status.MPI_SOURCE; @@ -333,10 +331,10 @@ void SLGridSph::initialize(int LMAX, int NMAX, int NUMR, totbad += bad; int retid = status.MPI_SOURCE; - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, retid, 11, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, retid, 11, MPI_COMM_WORLD, &status); #else - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, 11, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, 11, MPI_COMM_WORLD, &status); #endif @@ -361,7 +359,7 @@ void SLGridSph::initialize(int LMAX, int NMAX, int NUMR, for (l=0; l<=lmax; l++) { int position = mpi_pack_table(&table[l], l); - MPI_Bcast(mpi_buf, position, MPI_PACKED, 0, MPI_COMM_WORLD); + MPI_Bcast(&mpi_buf[0], position, MPI_PACKED, 0, MPI_COMM_WORLD); } } @@ -395,7 +393,7 @@ void SLGridSph::initialize(int LMAX, int NMAX, int NUMR, // END MPI stanza, BEGIN single-process stanza else { - table = new TableSph [lmax+1]; + table = table_ptr_1D(new TableSph [lmax+1]); for (l=0; l<=lmax; l++) { if (tbdbg) std::cerr << "Begin [" << l << "] . . ." << std::endl; @@ -534,7 +532,7 @@ bool SLGridSph::ReadH5Cache(void) // Create table instances // - table = new TableSph [lmax+1]; + table = table_ptr_1D(new TableSph [lmax+1]); for (int l=0; l<=lmax; l++) { std::ostringstream sout; @@ -646,8 +644,7 @@ void SLGridSph::WriteH5Cache(void) SLGridSph::~SLGridSph() { - delete [] table; - delete [] mpi_buf; + // Nothing } // Members @@ -1488,7 +1485,7 @@ void SLGridSph::compute_table_worker(void) #endif // Send sledge comptuation to root int position = mpi_pack_table(&table, L); - MPI_Send(mpi_buf, position, MPI_PACKED, 0, 11, MPI_COMM_WORLD); + MPI_Send(&mpi_buf[0], position, MPI_PACKED, 0, 11, MPI_COMM_WORLD); if (tbdbg) std::cout << "Worker " << mpi_myid << ": send to root l = " << L << "" << std::endl; @@ -1524,7 +1521,7 @@ void SLGridSph::mpi_setup(void) nmax*buf2 + // ev nmax*numr*buf2 ; // ef - mpi_buf = new char [mpi_bufsz]; + mpi_buf = std::shared_ptr(new char [mpi_bufsz]); } @@ -1532,16 +1529,16 @@ int SLGridSph::mpi_pack_table(struct TableSph* table, int l) { int position = 0; - MPI_Pack( &l, 1, MPI_INT, mpi_buf, mpi_bufsz, + MPI_Pack( &l, 1, MPI_INT, &mpi_buf[0], mpi_bufsz, &position, MPI_COMM_WORLD); for (int j=0; jev[j], 1, MPI_DOUBLE, mpi_buf, mpi_bufsz, + MPI_Pack( &table->ev[j], 1, MPI_DOUBLE, &mpi_buf[0], mpi_bufsz, &position, MPI_COMM_WORLD); for (int j=0; jef(j, i), 1, MPI_DOUBLE, mpi_buf, mpi_bufsz, + MPI_Pack( &table->ef(j, i), 1, MPI_DOUBLE, &mpi_buf[0], mpi_bufsz, &position, MPI_COMM_WORLD); return position; @@ -1560,7 +1557,7 @@ void SLGridSph::mpi_unpack_table(void) int retid = status.MPI_SOURCE; - MPI_Unpack( mpi_buf, length, &position, &l, 1, MPI_INT, + MPI_Unpack( &mpi_buf[0], length, &position, &l, 1, MPI_INT, MPI_COMM_WORLD); if (tbdbg) @@ -1573,12 +1570,12 @@ void SLGridSph::mpi_unpack_table(void) table[l].ef.resize(nmax, numr); for (int j=0; jpot((1.0+ZEND)*zmax); tbdbg = VERBOSE; - // This could be controlled by a parameter...at this point is a - // fixed tuning. + // This could be controlled by a parameter...but at this point, this + // is a fixed tuning. mM = CoordMap::factory(CoordMapTypes::Sech, H); init_table(); @@ -1863,9 +1860,9 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, std::cout << "Process " << myid << ": MPI is off!" << std::endl; } - table = new TableSlab* [numk+1]; + table = table_ptr_2D(new table_ptr_1D [numk+1]); for (kx=0; kx<=numk; kx++) - table[kx] = new TableSlab [kx+1]; + table[kx] = table_ptr_1D(new TableSlab [kx+1]); if (mpi) { @@ -1883,7 +1880,7 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, for (kx=0; kx<=numk; kx++) { for (ky=0; ky<=kx; ky++) { - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, 0, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &status); mpi_unpack_table(); @@ -1896,7 +1893,7 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, int worker = 0; int request_id = 1; - if (!read_cached_table()) { + if (!ReadH5Cache()) { kx = 0; ky = 0; @@ -1939,10 +1936,10 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, totbad += bad; // Get sledge computation result int retid = status.MPI_SOURCE; - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, retid, 11, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, retid, 11, MPI_COMM_WORLD, &status); #else - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); int retid = status.MPI_SOURCE; @@ -1986,10 +1983,10 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, totbad += bad; // Get sledge computation result int retid = status.MPI_SOURCE; - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, retid, 11, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, retid, 11, MPI_COMM_WORLD, &status); #else - MPI_Recv(mpi_buf, mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, 11, + MPI_Recv(&mpi_buf[0], mpi_bufsz, MPI_PACKED, MPI_ANY_SOURCE, 11, MPI_COMM_WORLD, &status); #endif mpi_unpack_table(); @@ -1997,7 +1994,7 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, worker--; } - if (cache) write_cached_table(); + if (cache) WriteH5Cache(); } @@ -2018,7 +2015,7 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, for (ky=0; ky<=kx; ky++) { int position = mpi_pack_table(&table[kx][ky], kx, ky); for (worker=1; worker < mpi_numprocs; worker++) - MPI_Send(mpi_buf, position, MPI_PACKED, worker, 11, MPI_COMM_WORLD); + MPI_Send(&mpi_buf[0], position, MPI_PACKED, worker, 11, MPI_COMM_WORLD); } } @@ -2050,7 +2047,7 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, } else { - if (!read_cached_table()) { + if (!ReadH5Cache()) { for (kx=0; kx<=numk; kx++) { for (ky=0; ky<=kx; ky++) { @@ -2061,7 +2058,7 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, } } - if (cache) write_cached_table(); + if (cache) WriteH5Cache(); } } @@ -2072,252 +2069,201 @@ SLGridSlab::SLGridSlab(int NUMK, int NMAX, int NUMZ, double ZMAX, const string slab_cache_name = ".slgrid_slab_cache"; -int SLGridSlab::read_cached_table(void) -{ - if (!cache) return 0; - - std::ifstream in(slab_cache_name); - if (!in) return 0; - - int NUMK, NMAX, NUMZ; - double ZMAX, HH, LL, zbeg, zend; - std::string MODEL; - - if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: trying to read cached table . . ." - << std::endl; +bool SLGridSlab::ReadH5Cache(void) +{ + if (!cache) return false; - // Attempt to read magic number + // First attempt to read the file // - unsigned int tmagic; - in.read(reinterpret_cast(&tmagic), sizeof(unsigned int)); + try { + // Silence the HDF5 error stack + // + HighFive::SilenceHDF5 quiet; + + // Try opening the file as HDF5 + // + HighFive::File h5file(slab_cache_name, HighFive::File::ReadOnly); + + // Try checking the rest of the parameters before reading arrays + // + auto checkInt = [&h5file](int value, std::string name) + { + int v; HighFive::Attribute vv = h5file.getAttribute(name); vv.read(v); + if (value == v) return true; + if (myid==0) + std::cout << "---- SLGridSlab::ReadH5Cache: " + << "parameter " << name << ": wanted " << value + << " found " << v << std::endl; + return false; + }; - if (tmagic == hmagic) { + auto checkDbl = [&h5file](double value, std::string name) + { + double v; HighFive::Attribute vv = h5file.getAttribute(name); vv.read(v); + if (fabs(value - v) < 1.0e-16) return true; + if (myid==0) + std::cout << "---- SLGridSlab::ReadH5Cache: " + << "parameter " << name << ": wanted " << value + << " found " << v << std::endl; + return false; + }; - // YAML size - // - unsigned ssize; - in.read(reinterpret_cast(&ssize), sizeof(unsigned int)); + auto checkStr = [&h5file](std::string value, std::string name) + { + std::string v; HighFive::Attribute vv = h5file.getAttribute(name); vv.read(v); + if (value.compare(v)==0) return true; + if (myid==0) + std::cout << "---- SLGridSlab::ReadH5Cache: " + << "parameter " << name << ": wanted " << value + << " found " << v << std::endl; + return false; + }; - // Make and read char buffer + // For cache ID // - auto buf = std::make_unique(ssize+1); - in.read(buf.get(), ssize); - buf[ssize] = 0; // Null terminate - - YAML::Node node; - - try { - node = YAML::Load(buf.get()); - } - catch (YAML::Exception& error) { - std::ostringstream sout; - sout << "YAML: error parsing <" << buf.get() << "> " - << "in " << __FILE__ << ":" << __LINE__ << std::endl - << "YAML error: " << error.what(); - throw GenericError(sout.str(), __FILE__, __LINE__, 1042, false); - } - - // Get parameters + std::string geometry("slab"), forceID("SLGridSlab"); + + // ID check // - NUMK = node["numk" ].as(); - NMAX = node["nmax" ].as(); - NUMZ = node["numz" ].as(); - ZMAX = node["zmax" ].as(); - HH = node["H" ].as(); - LL = node["L" ].as(); - zbeg = node["ZBEG" ].as(); - zend = node["ZEND" ].as(); - MODEL = node["model" ].as(); + if (not checkStr(geometry, "geometry")) return false; + if (not checkStr(forceID, "forceID")) return false; - } else { - std::cout << "---- SLGridSlab: bad magic number in cache file" << std::endl; - return 0; - } - + // Parameter check + // + if (not checkStr(type, "type")) return false; + if (not checkInt(numk, "numk")) return false; + if (not checkInt(nmax, "nmax")) return false; + if (not checkInt(numz, "numz")) return false; + if (not checkDbl(H, "H")) return false; + if (not checkDbl(L, "L")) return false; + if (not checkDbl(zmax, "zmax")) return false; + if (not checkDbl(ZBEG, "ZBEG")) return false; + if (not checkDbl(ZEND, "ZEND")) return false; - if (NUMK!=numk) { - if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found numk=" << NUMK - << " wanted " << numk << std::endl; - return 0; - } + // Harmonic order + // + auto harmonic = h5file.getGroup("Harmonic"); - if (NMAX!=nmax) { - if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found nmax=" << NMAX - << " wanted " << nmax << std::endl; - return 0; - } + // Create table instances + // - if (NUMZ!=numz) { - if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found numz=" << NUMZ - << " wanted " << numz << std::endl; - return 0; - } + table = table_ptr_2D(new table_ptr_1D [numk+1]); + for (int kx=0; kx<=numk; kx++) + table[kx] = table_ptr_1D(new TableSlab [kx+1]); - if (ZMAX!=zmax) { + for (int kx=0; kx<=numk; kx++) { + for (int ky=0; ky<=kx; ky++) { + std::ostringstream sout; + sout << kx << " " << ky; + auto arrays = harmonic.getGroup(sout.str()); + + arrays.getDataSet("ev").read(table[kx][ky].ev); + arrays.getDataSet("ef").read(table[kx][ky].ef); + } + } + if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found zmax=" << ZMAX - << " wanted " << zmax << std::endl; - return 0; - } + std::cout << "---- SLGridSlab::ReadH5Cache: " + << "successfully read basis cache <" << slab_cache_name + << ">" << std::endl; - if (HH!=H) { + return true; + + } catch (HighFive::Exception& err) { if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found H=" << HH - << " wanted " << H << std::endl; - return 0; + std::cerr << "---- SLGridSlab::ReadH5Cache: " + << "error reading <" << slab_cache_name << ">" << std::endl + << "---- SLGridSlab::ReadH5Cache: HDF5 error is <" << err.what() + << ">" << std::endl; } - if (LL!=L) { - if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found L=" << LL - << " wanted " << L << std::endl; - return 0; - } + return false; +} - if (zbeg!=ZBEG) { - if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found ZBEG=" << ZBEG - << " wanted " << zbeg << std::endl; - return 0; - } - if (zend!=ZEND) { - if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found ZEND=" << ZEND - << " wanted " << zend << std::endl; - return 0; - } - if (MODEL!=slab->ID()) { - if (myid==0) - std::cout << "---- SLGridSlab::read_cached_table: found ID=" << MODEL - << " wanted " << slab->ID() << std::endl; - return 0; - } +void SLGridSlab::WriteH5Cache(void) +{ + if (myid) return; - for (int kx=0; kx<=numk; kx++) { - for (int ky=0; ky<=kx; ky++) { + try { - in.read((char *)&table[kx][ky].kx, sizeof(int)); - in.read((char *)&table[kx][ky].ky, sizeof(int)); - - // Double check - if (table[kx][ky].kx != kx) { - if (myid==0) - std::cerr << "SLGridSlab: error reading <" << slab_cache_name << ">" - << std::endl - << "SLGridSlab: kx: read value (" << table[kx][ky].kx - << ") != internal value (" << kx << ")" << std::endl; - return 0; + // Check for new HDF5 file + if (std::filesystem::exists(slab_cache_name)) { + if (myid==0) + std::cout << "---- SLGridSlab::WriteH5Cache cache file <" + << slab_cache_name << "> exists" << std::endl; + try { + std::filesystem::rename(slab_cache_name, slab_cache_name + ".bak"); } - if (table[kx][ky].ky != ky) { - if (myid==0) - std::cerr << "SLGridSlab: error reading <" << slab_cache_name << ">" - << std::endl - << "SLGridSlab: ky: read value (" << table[kx][ky].ky - << ") != internal value (" << ky << ")" << std::endl; - return 0; + catch(std::filesystem::filesystem_error const& ex) { + std::ostringstream sout; + sout << "---- SLGridSlab::WriteH5Cache write error: " + << "what(): " << ex.what() << std::endl + << "path1(): " << ex.path1() << std::endl + << "path2(): " << ex.path2(); + throw GenericError(sout.str(), __FILE__, __LINE__, 12, true); } + + if (myid==0) + std::cout << "---- SLGridSlab::WriteH5Cache: existing file backed up to <" + << slab_cache_name + ".bak>" << std::endl; + } + + // Create a new hdf5 file + // + HighFive::File file(slab_cache_name, + HighFive::File::ReadWrite | HighFive::File::Create); + + // For cache ID + // + std::string geometry("slab"), forceID("SLGridSlab"); - table[kx][ky].ev.resize(nmax); - table[kx][ky].ef.resize(nmax, numz); - - for (int j=0; j("geometry", HighFive::DataSpace::From(geometry)).write(geometry); + file.createAttribute("forceID", HighFive::DataSpace::From(forceID)).write(forceID); -#ifdef DEBUG_NAN - check_vector_values_SL(table[kx][ky].ev); -#endif + // Write parameters + // + file.createAttribute ("type", HighFive::DataSpace::From(type)).write(type); + file.createAttribute ("numk", HighFive::DataSpace::From(numk)).write(numk); + file.createAttribute ("nmax", HighFive::DataSpace::From(nmax)).write(nmax); + file.createAttribute ("numz", HighFive::DataSpace::From(numz)).write(numz); + file.createAttribute ("H", HighFive::DataSpace::From(H)).write(H); + file.createAttribute ("L", HighFive::DataSpace::From(L)).write(L); + file.createAttribute ("zmax", HighFive::DataSpace::From(ZBEG)).write(zmax); + file.createAttribute ("ZBEG", HighFive::DataSpace::From(ZBEG)).write(ZBEG); + file.createAttribute ("ZEND", HighFive::DataSpace::From(ZEND)).write(ZEND); + + // Harmonic order (for h5dump readability) + // + auto harmonic = file.createGroup("Harmonic"); - for (int j=0; j" << std::endl; - return; - } - - // This is a node of simple {key: value} pairs. More general - // content can be added as needed. - YAML::Node node; - - node["numk" ] = numk; - node["nmax" ] = nmax; - node["numz" ] = numz; - node["zmax" ] = zmax; - node["H" ] = H; - node["L" ] = L; - node["ZBEG" ] = ZBEG; - node["ZEND" ] = ZEND; - node["model" ] = slab->ID(); - // Serialize the node - // - YAML::Emitter y; y << node; - - // Get the size of the string - // - unsigned int hsize = strlen(y.c_str()); + std::cout << "---- SLGridSlab::WriteH5Cache: " + << "wrote <" << slab_cache_name << ">" << std::endl; - // Write magic # - // - out.write(reinterpret_cast(&hmagic), sizeof(unsigned int)); - - // Write YAML string size - // - out.write(reinterpret_cast(&hsize), sizeof(unsigned int)); - - // Write YAML string - // - out.write(reinterpret_cast(y.c_str()), hsize); - - - for (int kx=0; kx<=numk; kx++) { - for (int ky=0; ky<=kx; ky++) { - - out.write((char *)&table[kx][ky].kx, sizeof(int)); - out.write((char *)&table[kx][ky].ky, sizeof(int)); - - for (int j=0; j func, double A, double B, int N); + + //! Update the data void new_data(std::vector &X, std::vector &Y, int N); + //! Evaluate the interpolant and its first derivative void eval(const double& x, double& val, double& deriv) { if (defined) { val = chebev(x, c); @@ -121,9 +175,18 @@ public: } } + //! Evaluate the second derivative double deriv2(const double& x) {if (defined) return chebev(x, c1); else bomb("no data!"); return 0.0;} + + //! Handle error void bomb(const char *, ...); + + //@{ + //! Data limits + double xlo() { return xmin; } + double xhi() { return xmax; } + //@} }; @@ -136,17 +199,36 @@ private: public: + //! Null constructor Linear1d(); + + //! Construct from Eigen input Linear1d(const Eigen::VectorXd &x, const Eigen::VectorXd &y); + + //! Construct from std::vector input Linear1d(const std::vector &x, const std::vector &y); + + //! Copy constructor Linear1d &operator=(const Linear1d &); + + //! Destructor ~Linear1d(); + //! Evaluate the interpolant double eval(const double& x); + + //! Evaluate first derivative double deriv(const double& x); + + //@{ + //! Data limits + double xlo() { return x(0); } + double xhi() { return x(x.size()-1); } + //@} }; +//! Two dimensional spline interpolator class Spline2d { private: @@ -158,10 +240,16 @@ private: public: static double DERIV; + //! Null constructor Spline2d(void); + + //! Construct from Eigen input Spline2d(const Eigen::VectorXd &x, const Eigen::VectorXd &y, const Eigen::MatrixXd &mat); + + //! Copy constructor Spline2d &operator=(const Spline2d &); + //! Evaluate the interpolant double eval(const double& x, const double& y); }; diff --git a/src/Cube.cc b/src/Cube.cc index fc1ac1b8a..ab5d0f17c 100644 --- a/src/Cube.cc +++ b/src/Cube.cc @@ -48,10 +48,10 @@ Cube::Cube(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) // Cache computed paramters // - imx = 1+2*nmaxx; - imy = 1+2*nmaxy; - imz = 1+2*nmaxz; - osize = imx * imy * imz; + imx = 1 + 2*nmaxx; // number of x wave numbers + imy = 1 + 2*nmaxy; // number of x wave numbers + imz = 1 + 2*nmaxz; // number of x wave numbers + osize = imx * imy * imz; // total number of coefficients // Allocate storage // @@ -133,74 +133,80 @@ void Cube::initialize(void) void * Cube::determine_coefficients_thread(void * arg) { - unsigned nbodies = cC->Number(); int id = *((int*)arg); - int nbeg = nbodies*id/nthrds; - int nend = nbodies*(id+1)/nthrds; double adb = component->Adiabatic(); use[id] = 0; - PartMapItr it = cC->Particles().begin(); - unsigned long i; + // If we are multistepping, compute accel only at or above + // + for (int lev=mlevel; lev<=multistep; lev++) { - for (int q=0; qlevlist[lev].size(); - i = it->first; - it++; + if (nbodies==0) continue; - use[id]++; - double mass = cC->Mass(i) * adb; + int nbeg = nbodies*(id )/nthrds; + int nend = nbodies*(id+1)/nthrds; - // Get position - // - double x = cC->Pos(i, 0); - double y = cC->Pos(i, 1); - double z = cC->Pos(i, 2); - - // Only compute for points inside the unit cube - // - if (x<0.0 or x>1.0) continue; - if (y<0.0 or y>1.0) continue; - if (z<0.0 or z>1.0) continue; - - // Recursion multipliers - // - std::complex stepx = std::exp(-kfac*x); - std::complex stepy = std::exp(-kfac*y); - std::complex stepz = std::exp(-kfac*z); - - // Initial values for recursion - // - std::complex startx = std::exp(kfac*(x*nmaxx)); - std::complex starty = std::exp(kfac*(y*nmaxy)); - std::complex startz = std::exp(kfac*(z*nmaxz)); - - std::complex facx, facy, facz; - int ix, iy, iz; + for (int q=nbeg; qlevlist[lev][q]; - if (ii==0 and jj==0 and kk==0) continue; + use[id]++; + double mass = cC->Mass(i) * adb; - // Normalization - double norm = 1.0/sqrt(M_PI*(ii*ii + jj*jj + kk*kk)); + // Get position + // + double x = cC->Pos(i, 0); + double y = cC->Pos(i, 1); + double z = cC->Pos(i, 2); - expcoef[id](ix, iy, iz) += - mass * facx * facy * facz * norm; + // Only compute for points inside the unit cube + // + if (x<0.0 or x>1.0) continue; + if (y<0.0 or y>1.0) continue; + if (z<0.0 or z>1.0) continue; + + // Recursion multipliers + // + std::complex stepx = std::exp(-kfac*x); + std::complex stepy = std::exp(-kfac*y); + std::complex stepz = std::exp(-kfac*z); + + // Initial values for recursion + // + std::complex startx = std::exp(kfac*(x*nmaxx)); + std::complex starty = std::exp(kfac*(y*nmaxy)); + std::complex startz = std::exp(kfac*(z*nmaxz)); + + std::complex facx, facy, facz; + int ix, iy, iz; + + for (facx=startx, ix=0; ix val = -mass*facx*facy*facz*norm; diff --git a/src/SlabSL.H b/src/SlabSL.H index c07b2fbc4..ca165b387 100644 --- a/src/SlabSL.H +++ b/src/SlabSL.H @@ -42,7 +42,7 @@ private: using coefType = Eigen::Tensor, 3>; //! Current coefficient tensor - std::vector expccof; + std::vector expccof, expccofP; int nminx, nminy; int nmaxx, nmaxy, nmaxz; @@ -113,15 +113,32 @@ private: //! Default slab type (must be "isothermal", "parabolic", or "constant") std::string type = "isothermal"; - // Usual evaluation interface - + //@{ + //! Usual evaluation interface void determine_coefficients(void); void get_acceleration_and_potential(Component*); + //@} - // Threading + //! Swap coefficients + void swap_coefs(std::vector& from, std::vector& to) + { + if (from.size() != to.size()) { + std::ostringstream sout; + sout << "swap_coefs: size(from)=" << from.size() << " != " + << "size(to)=" << to.size(); + throw std::runtime_error(sout.str()); + } + + std::vector tmp(from); + from = to; + to = tmp; + } + //@{ + //! Threading void * determine_coefficients_thread(void * arg); void * determine_acceleration_and_potential_thread(void * arg); + //@} /** Extrapolate and sum coefficents per multistep level to get a complete set of coefficients for force evaluation at an @@ -138,8 +155,8 @@ private: //@{ //! Interpolation arrays using coefTypePtr = std::shared_ptr; - std::vector expcoefN; - std::vector expcoefL; + std::vector expccofN; + std::vector expccofL; //@} /** Update the multi time step coefficient table when moving particle @@ -169,7 +186,7 @@ protected: public: //! Id string - string id; + std::string id; //! Constructor SlabSL(Component* c0, const YAML::Node& conf); @@ -179,9 +196,6 @@ public: //! Coefficient output void dump_coefs_h5(const std::string& file); - - //! Print coefficients to output stream - void dump_coefs(ostream& out); }; diff --git a/src/SlabSL.cc b/src/SlabSL.cc index c6abeee50..6a69cdb37 100644 --- a/src/SlabSL.cc +++ b/src/SlabSL.cc @@ -91,10 +91,10 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) << worst << std::endl; } - imx = 1+2*nmaxx; - imy = 1+2*nmaxy; - imz = nmaxz; - jmax = imx*imy*imz; + imx = 1 + 2*nmaxx; // Number of x wavenumber + imy = 1 + 2*nmaxy; // Number of y wavenumbers + imz = nmaxz; // Number of vertical functions + jmax = imx * imy * imz; // Total storage in tensor expccof.resize(nthrds); for (auto & v : expccof) v.resize(imx, imy, imz); @@ -108,8 +108,8 @@ SlabSL::SlabSL(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) for (auto & v : zpot) v.resize(nmaxz); for (auto & v : zfrc) v.resize(nmaxz); - // Allocate coefficient matrix (one for each multistep level) - // and zero-out contents + // Allocate coefficient tensor (one for each multistep level) and + // zero-out contents // differ1 = std::vector< std::vector >(nthrds); for (int n=0; n(imx, imy, imz); - expcoefL[i] = std::make_shared(imx, imy, imz); - expcoefN[i] -> setZero(); - expcoefL[i] -> setZero(); + expccofN[i] = std::make_shared(imx, imy, imz); + expccofL[i] = std::make_shared(imx, imy, imz); + expccofN[i] -> setZero(); + expccofL[i] -> setZero(); } } @@ -173,6 +173,10 @@ void SlabSL::initialize() << std::string(60, '-') << std::endl; throw std::runtime_error("SlabSL::initialze: error parsing YAML"); } + +#if HAVE_LIBCUDA==1 + cuda_initialize(); +#endif } void SlabSL::determine_coefficients(void) @@ -192,14 +196,14 @@ void SlabSL::determine_coefficients(void) // if (multistep) { - auto p = expcoefL[mlevel]; + auto p = expccofL[mlevel]; - expcoefL[mlevel] = expcoefN[mlevel]; - expcoefN[mlevel] = p; + expccofL[mlevel] = expccofN[mlevel]; + expccofN[mlevel] = p; // Clean arrays for current level // - expcoefN[mlevel]->setZero(); + expccofN[mlevel]->setZero(); } #if HAVE_LIBCUDA==1 @@ -232,7 +236,7 @@ void SlabSL::determine_coefficients(void) if (multistep) { - MPI_Allreduce( expccof[0].data(), expcoefN[mlevel]->data(), + MPI_Allreduce( expccof[0].data(), expccofN[mlevel]->data(), expccof[0].size(), MPI_CXX_DOUBLE_COMPLEX, MPI_SUM, MPI_COMM_WORLD); } else { @@ -247,9 +251,6 @@ void SlabSL::determine_coefficients(void) compute_multistep_coefficients(); } -#if HAVE_LIBCUDA==1 - cuda_initialize(); -#endif } void * SlabSL::determine_coefficients_thread(void * arg) @@ -259,33 +260,30 @@ void * SlabSL::determine_coefficients_thread(void * arg) std::complex startx, starty, facx, facy; std::complex stepx, stepy; - unsigned nbodies = cC->Number(); + unsigned nbodies = component->levlist[mlevel].size(); int id = *((int*)arg); int nbeg = nbodies*id/nthrds; int nend = nbodies*(id+1)/nthrds; double adb = cC->Adiabatic(); - PartMapItr it = cC->Particles().begin(); - - for (int q=0; qfirst; - it++; + int i = component->levlist[mlevel][q]; + // Increment particle counter use[id]++; // Truncate to box with sides in [0,1] if (cC->Pos(i, 0)<0.0) - cC->AddPos(i, 0, (double)((int)fabs(cC->Pos(i, 0))) + 1.0 ); + cC->AddPos(i, 0, floor(-cC->Pos(i, 0)) + 1.0 ); else - cC->AddPos(i, 0, -(double)((int)cC->Pos(i, 0)) ); + cC->AddPos(i, 0, -floor(cC->Pos(i, 0)) ); if (cC->Pos(i, 1)<0.0) - cC->AddPos(i, 1, (double)((int)fabs(cC->Pos(i, 1))) + 1.0 ); + cC->AddPos(i, 1, floor(-cC->Pos(i, 1)) + 1.0 ); else - cC->AddPos(i, 1, -(double)((int)cC->Pos(i, 1)) ); + cC->AddPos(i, 1, -floor(cC->Pos(i, 1)) ); // Recursion multipliers @@ -296,14 +294,16 @@ void * SlabSL::determine_coefficients_thread(void * arg) startx = exp(static_cast(nmaxx)*kfac*cC->Pos(i, 0)); starty = exp(static_cast(nmaxy)*kfac*cC->Pos(i, 1)); + double zz = cC->Pos(i, 2), mm = -4.0*M_PI * cC->Mass(i) * adb; + for (facx=startx, ix=0; ix nmaxx) { @@ -313,21 +313,14 @@ void * SlabSL::determine_coefficients_thread(void * arg) std::cerr << "Out of bounds: iiy=" << jj << std::endl; } - double zz = cC->Pos(i, 2, Component::Centered); - if (iix>=iiy) grid->get_pot(zpot[id], zz, iix, iiy); else grid->get_pot(zpot[id], zz, iiy, iix); + for (int iz=0; izMass(i)*adb* - facx*facy*zpot[id][iz]; - } } } } @@ -337,10 +330,23 @@ void * SlabSL::determine_coefficients_thread(void * arg) void SlabSL::get_acceleration_and_potential(Component* C) { - cC = C; + cC = C; // "Register" component + nbodies = cC->Number(); // And compute number of bodies MPL_start_timer(); + if (play_back) { + swap_coefs(expccofP, expccof); + } + + if (use_external == false) { + + if (multistep && initializing) { + compute_multistep_coefficients(); + } + + } + #if HAVE_LIBCUDA==1 if (use_cuda and cC->cudaDevice>=0 and cC->force->cudaAware()) { if (cudaAccelOverride) { @@ -367,6 +373,10 @@ void SlabSL::get_acceleration_and_potential(Component* C) #endif + if (play_back) { + swap_coefs(expccof, expccofP); + } + MPL_stop_timer(); } @@ -382,77 +392,88 @@ void * SlabSL::determine_acceleration_and_potential_thread(void * arg) int nbeg = nbodies*id/nthrds; int nend = nbodies*(id+1)/nthrds; - PartMapItr it = cC->Particles().begin(); + // If we are multistepping, compute accel only at or above + // + for (int lev=mlevel; lev<=multistep; lev++) { - for (int q=0; qfirst; it++; + unsigned nbodies = cC->levlist[lev].size(); + + if (nbodies==0) continue; + + int nbeg = nbodies*(id )/nthrds; + int nend = nbodies*(id+1)/nthrds; - accx = accy = accz = potl = 0.0; + for (int q=nbeg; qlevlist[lev][q]; + + accx = accy = accz = potl = 0.0; - // Recursion multipliers - std::complex stepx = exp(kfac*cC->Pos(i, 0)); - std::complex stepy = exp(kfac*cC->Pos(i, 1)); + // Recursion multipliers + // + std::complex stepx = exp(kfac*cC->Pos(i, 0)); + std::complex stepy = exp(kfac*cC->Pos(i, 1)); - // Initial values (note sign change) - std::complex startx = exp(-static_cast(nmaxx)*kfac*cC->Pos(i, 0)); - std::complex starty = exp(-static_cast(nmaxy)*kfac*cC->Pos(i, 1)); + // Initial values (note sign change) + // + std::complex startx = exp(-static_cast(nmaxx)*kfac*cC->Pos(i, 0)); + std::complex starty = exp(-static_cast(nmaxy)*kfac*cC->Pos(i, 1)); - for (facx=startx, ix=0; ix nmaxx) { - std::cerr << "Out of bounds: ii=" << ii << std::endl; - } - if (iiy > nmaxy) { - std::cerr << "Out of bounds: jj=" << jj << std::endl; - } + int jj = iy - nmaxy; + int iiy = abs(jj); - double zz = cC->Pos(i, 2, Component::Centered); - - if (iix>=iiy) { - grid->get_pot (zpot[id], zz, iix, iiy); - grid->get_force(zfrc[id], zz, iix, iiy); - } - else { - grid->get_pot (zpot[id], zz, iiy, iix); - grid->get_force(zfrc[id], zz, iiy, iix); - } + if (iix > nmaxx) { + std::cerr << "Out of bounds: ii=" << ii << std::endl; + } + if (iiy > nmaxy) { + std::cerr << "Out of bounds: jj=" << jj << std::endl; + } + + double zz = cC->Pos(i, 2); + + if (iix>=iiy) { + grid->get_pot (zpot[id], zz, iix, iiy); + grid->get_force(zfrc[id], zz, iix, iiy); + } + else { + grid->get_pot (zpot[id], zz, iiy, iix); + grid->get_force(zfrc[id], zz, iiy, iix); + } - for (int iz=0; iz(ii)*fac; - accy += -kfac*static_cast(jj)*fac; - accz += -facf; + accx += -kfac*static_cast(ii)*fac; + accy += -kfac*static_cast(jj)*fac; + accz += -facf; + } } } + + cC->AddAcc(i, 0, accx.real()); + cC->AddAcc(i, 1, accy.real()); + cC->AddAcc(i, 2, accz.real()); + cC->AddPot(i, potl.real()); } - - cC->AddAcc(i, 0, accx.real()); - cC->AddAcc(i, 1, accy.real()); - cC->AddAcc(i, 2, accz.real()); - cC->AddPot(i, potl.real()); } return (NULL); @@ -499,23 +520,6 @@ void SlabSL::dump_coefs_h5(const std::string& file) } } - -void SlabSL::dump_coefs(ostream& out) -{ - coefheader.time = tnow; - coefheader.zmax = zmax; - coefheader.h = hslab; - coefheader.type = ID; - coefheader.nmaxx = nmaxx; - coefheader.nmaxy = nmaxy; - coefheader.nmaxz = nmaxz; - coefheader.jmax = (1+2*nmaxx)*(1+2*nmaxy)*nmaxz; - - out.write((char *)&coefheader, sizeof(SlabSLCoefHeader)); - out.write((char *)expccof[0].data(), - expccof[0].size()*sizeof(std::complex)); -} - void SlabSL::multistep_update_begin() { if (play_back and not play_cnew) return; @@ -560,7 +564,7 @@ void SlabSL::multistep_update_finish() unsigned offset = (M - mfirst[mdrft])*jmax; for (int i=0; idata()[i] += unpack[offset+i]; + expccofN[M]->data()[i] += unpack[offset+i]; } } @@ -569,52 +573,53 @@ void SlabSL::multistep_update(int from, int to, Component *c, int i, int id) if (play_back and not play_cnew) return; if (c->freeze(i)) return; - double mass = c->Mass(i) * component->Adiabatic(); + double mass = -4.0 * M_PI * c->Mass(i) * component->Adiabatic(); double x = c->Pos(i, 0); double y = c->Pos(i, 1); double z = c->Pos(i, 2); - // Only compute for points inside the unit cube - // - if (x<0.0 or x>1.0) return; - if (y<0.0 or y>1.0) return; - if (z<0.0 or z>1.0) return; + if (x<0.0) x += floor(x) + 1.0; + else x -= floor(x); + + if (y<0.0) y += floor(y) + 1.0; + else y -= floor(y); // Recursion multipliers std::complex stepx = std::exp(-kfac*x); std::complex stepy = std::exp(-kfac*y); - std::complex stepz = std::exp(-kfac*z); // Initial values for recursion std::complex startx = std::exp(kfac*(x*nmaxx)); std::complex starty = std::exp(kfac*(y*nmaxy)); - std::complex startz = std::exp(kfac*(z*nmaxz)); std::complex facx, facy, facz; - int ix, iy, iz; + int ix, iy; for (facx=startx, ix=0; ix val = -mass*facx*facy*facz*norm; + if (iix>=iiy) grid->get_pot(zpot[id], z, iix, iiy); + else grid->get_pot(zpot[id], z, iiy, iix); + for (int iz = 0; iz val = mass*facx*facy*zpot[id][iz]; + differ1[id][from](ix, iy, iz) -= val; differ1[id][ to](ix, iy, iz) += val; } + // END: vertical loop } + // END: y horizontal loop } + // END: x horizontal loop } void SlabSL::compute_multistep_coefficients() @@ -637,7 +642,7 @@ void SlabSL::compute_multistep_coefficients() for (int i=0; idata()[i] + b*expcoefN[M]->data()[i] ; + a*expccofL[M]->data()[i] + b*expccofN[M]->data()[i] ; } // Sanity debug check @@ -659,8 +664,7 @@ void SlabSL::compute_multistep_coefficients() for (int M=mfirst[mdrft]; M<=multistep; M++) { for (int i=0; idata()[i]; + expccof[0].data()[i] += expccofN[M]->data()[i]; } } } - diff --git a/src/SphericalBasis.H b/src/SphericalBasis.H index 679cb8166..cd949a96e 100644 --- a/src/SphericalBasis.H +++ b/src/SphericalBasis.H @@ -283,8 +283,8 @@ protected: //! For updating levels //@{ - vector< vector > differ1; - vector< double > pack, unpack; + std::vector< std::vector > differ1; + std::vector< double > pack, unpack; //@} /** Dump current coefficients (all multistep levels) diff --git a/src/cudaSlabSL.cu b/src/cudaSlabSL.cu index c61034a67..13329ffd8 100644 --- a/src/cudaSlabSL.cu +++ b/src/cudaSlabSL.cu @@ -48,7 +48,6 @@ int slabIndex(int i, int j, int k) return k*slabNX*slabNY + j*slabNX + i; } -/* // Index function for modulus coefficients // __device__ @@ -70,7 +69,6 @@ thrust::tuple slabWaveNumbers(int indx) return {i-slabNumX, j-slabNumY, k}; } -*/ __global__ void testConstantsSlab() @@ -96,21 +94,22 @@ void testConstantsSlab() __global__ void testFetchSlab(dArray T, dArray f, - int l, int j, int nmax, int numr) + int kx, int ky, int j, int nmax, int numz) { const int n = blockDim.x * blockIdx.x + threadIdx.x; - const int k = l*nmax + 1; + const int l = 1 + kx*(kx+1)/2*(slabNumY+1) + j; + #if cuREAL == 4 - if (n < numr) f._v[n] = tex1D(T._v[k+j], n); + if (n < numz) f._v[n] = tex1D(T._v[l], n); #else - if (n < numr) f._v[n] = int2_as_double(tex1D(T._v[k+j], n)); + if (n < numz) f._v[n] = int2_as_double(tex1D(T._v[l], n)); #endif } thrust::host_vector returnTestSlab (thrust::host_vector& tex, - int l, int j, int nmax, int numz) + int kx, int ky, int j, int nmax, int numz) { thrust::device_vector t_d = tex; @@ -120,7 +119,7 @@ thrust::host_vector returnTestSlab thrust::device_vector f_d(numz); testFetchSlab<<>>(toKernel(t_d), toKernel(f_d), - l, j, nmax, numz); + kx, ky, j, nmax, numz); cudaDeviceSynchronize(); @@ -315,10 +314,20 @@ __global__ void coefKernelSlab cuFP_t b = 1.0 - a; // Flip sign for antisymmetric basis functions + // int sign = 1; if (x<0 && 2*(n/2)!=n) sign = -1; + cuFP_t p0 = +#if cuREAL == 4 + a*tex1D(tex._v[0], ind ) + + b*tex1D(tex._v[0], ind+1) ; +#else + a*int2_as_double(tex1D(tex._v[0], ind )) + + b*int2_as_double(tex1D(tex._v[0], ind+1)) ; +#endif + // Will contain the incremented basis // CmplxT X, Y; @@ -335,16 +344,9 @@ __global__ void coefKernelSlab int ky = abs(jj); + // The vertical basis iteration for (int n=0; n(tex._v[0], ind ) + - b*tex1D(tex._v[0], ind+1) ; -#else - a*int2_as_double(tex1D(tex._v[0], ind )) + - b*int2_as_double(tex1D(tex._v[0], ind+1)) ; -#endif int k = 1 + kx*(kx+1)/2*(slabNumY+1) + n; #ifdef BOUNDS_CHECK @@ -433,6 +435,15 @@ forceKernelSlab(dArray P, dArray I, cuFP_t s = (x - slabXmin - slabDxi*jn0)/slabDxi; + cuFP_t p0 = +#if cuREAL == 4 + a*tex1D(tex._v[0], ind ) + + b*tex1D(tex._v[0], ind+1) ; +#else + a*int2_as_double(tex1D(tex._v[0], ind )) + + b*int2_as_double(tex1D(tex._v[0], ind+1)) ; +#endif + // Flip sign for antisymmetric basis functions int sign = 1; if (pos[2]<0 && 2*(n/2)!=n) sign = -1; @@ -446,14 +457,6 @@ forceKernelSlab(dArray P, dArray I, for (int n=0; n(tex._v[0], ind ) + - b*tex1D(tex._v[0], ind+1) ; -#else - a*int2_as_double(tex1D(tex._v[0], ind )) + - b*int2_as_double(tex1D(tex._v[0], ind+1)) ; -#endif int kx = ii + slabNumX; int ky = jj + slabNumY; int k = 1 + kx*(kx+1)/2*(slabNumY+1) + n; @@ -750,7 +753,7 @@ void SlabSL::determine_coefficients_cuda() auto c = std::abs(a - b); std::cout << std::setw(4) << i-nmaxx << std::setw(4) << j-nmaxy - << std::setw(4) << k-nmaxz + << std::setw(4) << k << std::setw(20) << a << std::setw(20) << b << std::setw(20) << c @@ -768,7 +771,7 @@ void SlabSL::determine_coefficients_cuda() if (false) { coefType test; - if (myid==0) test.resize(2*nmaxx+1, 2*nmaxy+1, 2*nmaxz+1); + if (myid==0) test.resize(2*nmaxx+1, 2*nmaxy+1, nmaxz); MPI_Reduce (thrust::raw_pointer_cast(&host_coefs[0]), test.data(), jmax, MPI_DOUBLE_COMPLEX, MPI_SUM, 0, MPI_COMM_WORLD); @@ -798,7 +801,7 @@ void SlabSL::determine_coefficients_cuda() auto a = test(i, j, k); out << std::setw(4) << i-nmaxx << std::setw(4) << j-nmaxy - << std::setw(4) << k-nmaxz + << std::setw(4) << k << std::setw(20) << std::real(a) << std::setw(20) << std::imag(a) << std::setw(20) << std::abs(a) @@ -816,7 +819,7 @@ void SlabSL::determine_coefficients_cuda() if (false) { coefType test; - if (myid==0) test.resize(2*nmaxx+1, 2*nmaxy+1, 2*nmaxz+1); + if (myid==0) test.resize(2*nmaxx+1, 2*nmaxy+1, nmaxz); MPI_Reduce (thrust::raw_pointer_cast(&host_coefs[0]), test.data(), jmax, MPI_DOUBLE_COMPLEX, MPI_SUM, 0, MPI_COMM_WORLD); @@ -852,7 +855,7 @@ void SlabSL::determine_coefficients_cuda() out << std::setw(4) << i-nmaxx << std::setw(4) << j-nmaxy - << std::setw(4) << k-nmaxz + << std::setw(4) << k << std::setw(20) << std::real(a) << std::setw(20) << std::imag(a) << std::setw(20) << std::abs(a) @@ -904,7 +907,7 @@ void SlabSL::determine_coefficients_cuda() out << std::setw( 5) << elem.i - nmaxx << std::setw( 5) << elem.j - nmaxy - << std::setw( 5) << elem.k - nmaxz + << std::setw( 5) << elem.k << std::setw( 5) << n << std::setw(20) << elem.d << std::setw(20) << elem.f diff --git a/utils/SL/CMakeLists.txt b/utils/SL/CMakeLists.txt index 8e221a405..55fe30615 100644 --- a/utils/SL/CMakeLists.txt +++ b/utils/SL/CMakeLists.txt @@ -1,6 +1,6 @@ set(bin_PROGRAMS slcheck slshift orthochk diskpot qtest eoftest - oftest) + oftest slabchk) set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX yaml-cpp exputil ${VTK_LIBRARIES}) @@ -23,6 +23,7 @@ set(common_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/..) add_executable(slcheck slcheck.cc) +add_executable(slabchk slabchk.cc Model1d.cc) add_executable(slshift slshift.cc SLSphere.cc) add_executable(orthochk orthochk.cc) add_executable(diskpot diskpot.cc CylindricalDisk.cc SLSphere.cc) diff --git a/utils/SL/Model1d.H b/utils/SL/Model1d.H new file mode 100644 index 000000000..cfd29a54e --- /dev/null +++ b/utils/SL/Model1d.H @@ -0,0 +1,434 @@ +#ifndef _Model1d_H_ +#define _Model1d_H_ + +#include + +#include + +class OneDModel +{ + +public: + bool dist_defined; + + virtual double get_mass(const double) = 0; + virtual double get_density(const double) = 0; + virtual double get_pot(const double) = 0; + virtual double get_dpot(const double) = 0; + virtual double get_dpot2(const double) = 0; + virtual void get_pot_dpot(const double, double&, double&) = 0; + + // Required members of mass model + + double get_mass(const double x, const double y, const double z) { + return get_mass(z);} + + double get_density(const double x, const double y, const double z) { + return get_density(z);} + + double get_pot(const double x, const double y, const double z) { + return get_pot(z);} + + + // Addiional member functions + + virtual double get_min_radius(void) = 0; + virtual double get_max_radius(void) = 0; + virtual double get_scale_height(void) = 0; + virtual double distf(const double, const double V=0.0) = 0; + virtual double dfde (const double, const double V=0.0) = 0; + virtual double dfdv (const double, const double V=0.0) = 0; +}; + +class OneDModelTable : public OneDModel +{ +protected: + + Spline1d mass, dens, pot; + + int even; + int num; + int numdf; + double half_height; + std::vector params; + +public: + + OneDModelTable() {}; + + OneDModelTable(string filename, int PARM=0); + + OneDModelTable(int num, double *r, double *d, + double *m, double *p, string ID = "" ); + + // Required member functions + + double get_mass (const double); + double get_density(const double); + double get_pot (const double); + double get_dpot (const double); + double get_dpot2 (const double); + void get_pot_dpot (const double, double&, double&); + + // Additional member functions + + const int get_num_param(void) { return params.size(); } + const double get_param(int i) { return params[i-1]; } + double get_scale_height(void) { return half_height; } + + double get_min_radius(void) { return mass.xlo(); } + double get_max_radius(void) { return mass.xhi(); } + int grid_size(void) { return num; } + +// double distf(double E, double V); +// double dfde(double E, double V); +// double dfdv(double E, double V); +}; + +class LowIso : public OneDModelTable +{ +private: + double w0, Bfac, betak, gammak; + double dispx, normx; + + void setup_model(void); + +public: + LowIso(string filename, double DISPX=0.159154943091895335768) : + OneDModelTable(filename, 1) { + dispx = DISPX; + setup_model(); + } + + double get_pot(const double); + double get_dpot(const double); + double get_dpot2(const double); + void get_pot_dpot(const double, double&, double&); + + double distf(const double E, const double V=0.0); + double dfde(const double E, const double V=0.0); + double dfdv(const double E, const double V=0.0); +}; + + +class Sech2 : public OneDModel +{ +private: + double h; + double dispz, dispx; + double norm; + + static double HMAX; + +public: + + Sech2(void) + { + dispz = 1.0; + dispx = 1.0; + // + // Units: G = rho_o = 1 + // + h = sqrt(dispz/(2.0*M_PI)); + norm = 1.0/( sqrt(2.0*M_PI*dispz) ); + dist_defined = true; + } + + Sech2(const double DISPZ, const double DISPX=1.0) + { + dispz = DISPZ; + dispx = DISPX; + // + // Units: G = rho_o = 1 + // + h = sqrt(dispz/(2.0*M_PI)); + norm = 1.0/( sqrt(2.0*M_PI*dispz) ); + dist_defined = true; + } + + double get_mass(const double z) + { + return 2.0*h/(1.0 + exp(-2.0*z/h)); + } + + double get_density(const double z) + { + double zz = fabs(z); + double ret = 2.0*exp(-zz/h)/(1.0 + exp(-2.0*zz/h)); + return ret*ret; + } + + double get_pot(const double z) + { + double zz = fabs(z); + return 4.0*M_PI*h*(zz + h*log(1.0 + exp(-2.0*zz/h)) - h*M_LN2); + } + + double get_dpot(const double z) + { + double zz = fabs(z); + double ret = (1.0 - exp(-2.0*zz/h))/(1.0 + exp(-2.0*zz/h)); + return 4.0*M_PI*h* ret * z/(zz+1.0e-18); + } + + double get_dpot2(const double z) + { + double zz = fabs(z); + double ret = 2.0*exp(-zz/h)/(1.0 + exp(-2*zz/h)); + return 4.0*M_PI*ret*ret; + } + + void get_pot_dpot(const double z, double& p, double& dp) + { + double zz = fabs(z); + p = 4.0*M_PI*h*(zz + h*log(1.0 + exp(-2.0*zz/h)) - h*M_LN2); + double ret = (1.0 - exp(-2.0*zz/h))/(1.0 + exp(-2.0*zz/h)); + dp = 4.0*M_PI*h* ret * z/(zz+1.0e-18); + } + + double get_mass(const double x, const double y, const double z) + { + return get_mass(z); + } + + double get_density(const double x, const double y, const double z) + { + return get_density(z); + } + + double get_pot(const double x, const double y, const double z) + { + return get_pot(z); + } + + double get_min_radius(void) { return 0.0; } + double get_max_radius(void) { return HMAX*h; } + double get_scale_height(void) { return h; } + + static void set_hmax(double hmax) { HMAX = hmax; } + + double distf(const double E, const double p) + { + return exp(-E/dispz - 0.5*p*p/dispx) * norm; + } + + double dfde(const double E, const double p) + { + return -exp(-E/dispz - 0.5*p*p/dispx)/dispz * norm; + } + + double dfdv(const double E, const double p) + { + return -exp(-E/dispz - 0.5*p*p/dispx)*p/dispx * norm; + } + +}; + + + +class Sech2mu : public OneDModel +{ +private: + double mu, h; + double dispz, dispx; + double dnorm, knorm, fnorm; + + static double HMAX; + +public: + + Sech2mu(void) + { + mu = 1.0; + dispz = 1.0; + dispx = 1.0; + h = 1.0; + // + // Units: G = mu = 1 + // + dnorm = 0.25*mu/h; + knorm = 2.0*M_PI*mu*h/dispz; + fnorm = mu/(4.0*h*knorm*sqrt(2.0*M_PI*dispz)); + dist_defined = true; + } + + Sech2mu(const double DISPZ, const double H, const double DISPX=1.0) + { + mu = 1.0; + dispz = DISPZ; + dispx = DISPX; + h = H; + // + // Units: G = mu = 1 + // + dnorm = 0.25*mu/h; + knorm = 2.0*M_PI*mu*h/dispz; + fnorm = mu/(4.0*h*knorm*sqrt(2.0*M_PI*dispz)); + dist_defined = true; + } + + void setMu(double Sigma0) + { + mu = Sigma0; + // + // Units: G = 1, mu = Sigma0 + // + dnorm = 0.25*mu/h; + knorm = 2.0*M_PI*mu*h/dispz; + fnorm = mu/(4.0*h*knorm*sqrt(2.0*M_PI*dispz)); + dist_defined = true; + } + + double get_mass(const double z) { + return mu*exp(z/h)/(1.0 + exp(z/h)); + } + + double get_density(const double z) { + double zz = fabs(z); + double fac = exp(zz/h); + return 4.0*dnorm/(fac + 1.0/fac + 2.0); + } + + double get_pot(const double z) { + double zz = fabs(z); + return 2.0*dispz*(-M_LN2 + 0.5*zz/h + log(1.0 + exp(-zz/h)) ) - dispz*log(knorm); + } + + double get_dpot(const double z) { + double zz = fabs(z); + double ret = (1.0 - exp(-zz/h))/(1.0 + exp(-zz/h)); + return 0.5*dispz*ret/h * z/(zz+1.0e-18); + } + + double get_dpot2(const double z) { + double zz = fabs(z); + double ret = 2.0*exp(-0.5*zz/h)/(1.0 + exp(-zz/h)); + return 0.5*dispz*ret*ret/h/h; + } + + void get_pot_dpot(const double z, double& p, double& dp) { + double zz = fabs(z); + p = 2.0*dispz*(-M_LN2 + 0.5*zz/h + log(1.0 + exp(-zz/h)) ) - dispz*log(knorm); + double ret = (1.0 - exp(-zz/h))/(1.0 + exp(-zz/h)); + dp = 0.5*dispz*ret/h * z/(zz+1.0e-18); + } + + double get_mass(const double x, const double y, const double z) { + return get_mass(z); + } + + double get_density(const double x, const double y, const double z) { + return get_density(z); + } + + double get_pot(const double x, const double y, const double z) { + return get_pot(z); + } + + double get_min_radius(void) { return 0.0; } + double get_max_radius(void) { return HMAX*h; } + double get_scale_height(void) { return h; } + + static void set_hmax(double hmax) { HMAX = hmax; } + + double distf(const double E, const double p) { + return exp(-E/dispz - 0.5*p*p/dispx) * fnorm; + } + + double dfde(const double E, const double p) { + return -exp(-E/dispz - 0.5*p*p/dispx)/dispz * fnorm; + } + + double dfdv(const double E, const double p) { + return -exp(-E/dispz - 0.5*p*p/dispx)*p/dispx * fnorm; + } + +}; + + +class Sech2Halo : public OneDModelTable +{ +private: + double h, rho0; + double dispz, dispx; + double dratio, hratio; + double hh, rho0h; + double norm, hmax; + + bool model_computed; + + static double HMAX; + + void reset(); + +public: + + static int NTABLE; + static double OFFSET; + static bool MU; + + Sech2Halo(void) { + dispz = 1.0; + dispx = 1.0; + dratio = 0.0; + hratio = 1.0; + + dist_defined = false; + model_computed = false; + } + + Sech2Halo(const double DISPZ, const double DRATIO, const double HRATIO, + const double DISPX=1.0); + + double get_pot(const double z) { + if (!model_computed) reset(); + return OneDModelTable::get_pot(z) + 4.0*M_PI*hh*hh*rho0h*log(cosh(z/hh)); + } + + double get_dpot(const double z) { + if (!model_computed) reset(); + return OneDModelTable::get_dpot(z) + 4.0*M_PI*hh*rho0h*tanh(z/hh); + } + + double get_dpot2(const double z) { + if (!model_computed) reset(); + double sech = 1.0/cosh(z/hh); + return OneDModelTable::get_dpot2(z) + 4.0*M_PI*rho0h*sech*sech; + } + + void get_pot_dpot(const double z, double& p, double& dp) { + if (!model_computed) reset(); + OneDModelTable::get_pot_dpot(z, p, dp); + p += 4.0*M_PI*hh*hh*rho0h*log(cosh(z/hh)); + dp += 4.0*M_PI*hh*rho0h*tanh(z/hh); + } + + + double get_min_radius(void) { return 0.0; } + double get_max_radius(void) { return hmax*h; } + double get_scale_height(void) { return h; } + double get_scale_height_halo(void) { return hh; } + double get_rho0(void) { return rho0; } + double get_rho0_halo(void) { return rho0h; } + + static void set_hmax(double hmax) { HMAX = hmax; } + + double distf(const double E, const double p=0.0) { + if (!model_computed) reset(); + return exp(-E/dispz - 0.5*p*p/dispx) * norm; + } + + double dfde(const double E, const double p=0.0) { + if (!model_computed) reset(); + return -exp(-E/dispz - 0.5*p*p/dispx)/dispz * norm; + } + + double dfdv(const double E, const double p=0.0) { + if (!model_computed) reset(); + return -exp(-E/dispz - 0.5*p*p/dispx)*p/dispx * norm; + } + +}; + +#endif + diff --git a/utils/SL/Model1d.cc b/utils/SL/Model1d.cc new file mode 100644 index 000000000..ab036d12c --- /dev/null +++ b/utils/SL/Model1d.cc @@ -0,0 +1,333 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +double Sech2::HMAX = 1.0e6; +double Sech2mu::HMAX = 1.0e6; +double Sech2Halo::HMAX = 3.0e1; + +OneDModelTable::OneDModelTable(std::string filename, int PARM) +{ + int i, imodel; + char line[144]; + double radius; + + std::istringstream iline(line); + + std::ifstream in(filename); + if (!in) { + std::cerr << "Error opening: " << filename << " . . . quitting" + << std::endl; + exit(-1); + } + + // Read header + + in.getline((char *)line, 144); + while (string(line).find_first_of("!#") != string::npos) + in.getline((char *)line, 144); + + + // Assign space for model + iline >> imodel; + + std::vector z(imodel), d(imodel), m(imodel), p(imodel); + + for (int i=0; i> z[i]; + iline >> d[i]; + iline >> m[i]; + iline >> p[i]; + } + + if (PARM) { + in.getline(line, 144); + + istringstream ins(line); + string word; + while (1) { + ins >> word; + if (ins.good()) params.push_back(atof(word.c_str())); + } + } + + // Compute splines + dens = Spline1d(z, d, 0.0, 0.0); + mass = Spline1d(z, m, 0.0, 0.0); + pot = Spline1d(z, p, 0.0, 2.0*M_PI*m[imodel-1]); + + even = 0; + if (fabs((z[1]-z[0]) - (z[2] - z[1])) < 1.0e-6) even = 1; + +} + +double OneDModelTable::get_mass(const double z) +{ + double zz = fabs(z); + if (zz>mass.xhi()) return mass.eval(mass.xhi()); + + return mass.eval(zz); +} + +double OneDModelTable::get_density(const double z) +{ + double zz = fabs(z); + if (z>dens.xhi()) return 0.0; + + return dens.eval(zz); +} + +double OneDModelTable::get_pot(const double z) +{ + double zz = fabs(z); + + if (zz>pot.xhi()) { + double z0 = pot.xlo(), z1 = pot.xhi(); + double dz = (z1 - z0)*0.01; + double p1 = pot.eval(z1), pm = pot.eval(z1-dz); + return p1 + (p1 - pm)/dz * (zz - z1); + } + + return pot.eval(zz); +} + +double OneDModelTable::get_dpot(const double z) +{ + double ans, dum; + double zz = fabs(z); + + if (zz>pot.xhi()) { + double z0 = pot.xlo(), z1 = pot.xhi(); + double dz = (z1 - z0)*0.01; + double p1 = pot.eval(z1), pm = pot.eval(z1-dz); + ans = (p1 - pm)/dz; + } else { + ans = pot.deriv(zz); + } + + return ans*z/(zz+1.0e-18); +} + +void OneDModelTable::get_pot_dpot(const double z, double& ur, double &dur) +{ + double zz = fabs(z); + + if (zz>pot.xhi()) { + double z0 = pot.xlo(), z1 = pot.xhi(); + double dz = (z1 - z0)*0.01; + double p1 = pot.eval(z1), pm = pot.eval(z1-dz); + ur = p1 + (p1 - pm)/dz * (zz - z1); + dur = (p1 - pm)/dz; + } else { + ur = pot.eval(zz); + dur = pot.deriv(zz); + } + + dur *= z/(zz+1.0e-18); +} + +double OneDModelTable::get_dpot2(const double z) +{ + double ans; + double zz = fabs(z); + + if (zz>pot.xhi()) + ans = 0.0; + else + ans = dens.eval(zz); + + return 4.0*M_PI*ans; +} + +void LowIso::setup_model(void) +{ + w0 = params[0]; + Bfac = params[1]; + betak = 1.0/params[2]; + gammak = params[3] / (8.0*sqrt(2.0)*M_PI); + + normx = 1.0/sqrt(2.0*M_PI*dispx); +} + +double LowIso::distf(const double E, const double V) +{ + if (E>0.0) + return 0.0; + else + return gammak*(exp(-betak*E) - 1.0) * normx * exp(-0.5*V*V/dispx); +} + +double LowIso::dfde(const double E, const double V) +{ + if (E>0.0) + return 0.0; + else + return -betak*gammak*exp(-betak*E) * normx * exp(-0.5*V*V/dispx);; +} + +double LowIso::dfdv(const double E, const double V) +{ + if (E>0.0) + return 0.0; + else + return -normx*exp(-0.5*V*V)*V/dispx * gammak*(exp(-betak*E) - 1.0); +} + + +double LowIso::get_pot(const double z) +{ + double zmax = get_max_radius(); + + if (fabs(z) > zmax) + return OneDModelTable::get_pot(zmax) + 2.0*M_PI*get_mass(zmax)*(z-zmax) + + Bfac*(z*z - zmax*zmax); + else + return OneDModelTable::get_pot(z); +} + +double LowIso::get_dpot(const double z) +{ + double zmax = get_max_radius(); + + if (fabs(z) > zmax) + return 2.0*M_PI*get_mass(zmax)*z/(fabs(z)+1.0e-16) + 2.0*Bfac*z; + else + return OneDModelTable::get_dpot(z); +} + +double LowIso::get_dpot2(const double z) +{ + return OneDModelTable::get_dpot2(z) + 2.0*Bfac; +} + +void LowIso::get_pot_dpot(const double z, double& ur, double& dur) +{ + double zmax = get_max_radius(); + + if (fabs(z) > zmax) { + ur = OneDModelTable::get_pot(zmax) + 2.0*M_PI*get_mass(zmax)*(z-zmax) + + Bfac*(z*z - zmax*zmax); + dur = 2.0*M_PI*get_mass(zmax)*z/(fabs(z)+1.0e-16) + 2.0*Bfac*z; + } + else + OneDModelTable::get_pot_dpot(z, ur, dur); +} + +static double halo_fac, halo_height; + +bool Sech2Halo::MU = true; +int Sech2Halo::NTABLE = 400; +double Sech2Halo::OFFSET = 1.0e-4; + +Sech2Halo::Sech2Halo(const double DISPZ, const double DRATIO, + const double HRATIO, const double DISPX) +{ + dispz = DISPZ; + dispx = DISPX; + dratio = DRATIO; + hratio = HRATIO; + + h = 1.0; + rho0 = 1.0; + hh = hratio * h; + rho0h = dratio * rho0; + + dist_defined = false; + + reset(); +} + + +void Sech2Halo::reset() +{ + even = 1; + hmax = HMAX*h; + halo_height = hh/h; + halo_fac = halo_height*halo_height * rho0h/rho0; + + std::vector x(NTABLE), d(NTABLE), m(NTABLE), p(NTABLE); + + double w1, dz = hmax/(NTABLE-1); + double step = dz/100.0; + + x[0] = 0.0; + d[0] = rho0; + m[0] = p[0] = 0.0; + + double z, lastz=OFFSET, tol=1.0e-6; + + w1 = 2.0*halo_fac*log(cosh(lastz/halo_height)); + Eigen::VectorXd y(2); + y(0) = exp(-w1)*lastz*lastz*(1.0 - exp(-w1)*lastz*lastz/6.0); + y(1) = 2.0*exp(-w1)*lastz*(1.0 - exp(-w1)*lastz*lastz/2.0); + + Eigen::VectorXd dy(2); + + auto halo_derivs = [&](double x, ODE::RK4::Vref y) + { + dy(0) = y(1); + double w1 = 2.0*halo_fac*log(cosh(x/halo_height)); + dy(1) = 2.0*exp(-w1-y(0)); + return ODE::RK4::Vref(dy); + }; + + ODE::RK4 rk4(halo_derivs); + + for (int i=1; i= z) break; + } + + p[i] = y[0] * dispz; + w1 = 2.0*halo_fac*log(cosh(z/halo_height)); + d[i] = rho0*exp(-w1-y[0]); + m[i] = y[1]; + + lastz = z; + } + + // Scaling + if (MU) { + double mfac = m[NTABLE-1]; + for (auto & v : d) v *= 2.0*M_PI/(dispz*mfac*mfac)/rho0; + for (auto & m : d) m /= mfac; + rho0 = 2.0*M_PI/(dispz*mfac*mfac); + } + else { + for (auto & v : d) v /= rho0; + for (auto & v : m) v *= sqrt(0.5*dispz/M_PI); + + rho0 = 1.0; + } + + h = sqrt(dispz/(2.0*M_PI*rho0)); + rho0h = rho0 * dratio; + hh = h * hratio; + + for (auto & v : x) v *= h; + + dens = Spline1d(x, d, 0.0, 0.0); + mass = Spline1d(x, m, 0.0, 0.0); + pot = Spline1d(x, p, 0.0, p[NTABLE-1]/x[NTABLE-1]); + + norm = d[0]/( sqrt(2.0*M_PI*dispz) ); + model_computed = true; + dist_defined = true; +} diff --git a/utils/SL/RK4.H b/utils/SL/RK4.H new file mode 100644 index 000000000..c2c273fa3 --- /dev/null +++ b/utils/SL/RK4.H @@ -0,0 +1,45 @@ +#ifndef _RK4_H_ +#define _RK4_H_ + +#include + +namespace ODE +{ + //! RK4 takes one 4th-order Runge-Kutta step + class RK4 + { + + public: + + //! Reference to an Eigen vector + using Vref = Eigen::Ref; + + //! Functoid defintion for the force + using Force = std::function; + + private: + + //! Force instance + Force force; + + public: + + //! Constructor defining the force function + RK4(Force force) : force(force) {} + + //! Take a single RK4 step + double step(Vref x, double t, double dt) + { + Eigen::VectorXd f0 = force(t, x), xt; + Eigen::VectorXd f1 = force(t + 0.5*dt, (xt=x + 0.5*dt*f0)); + Eigen::VectorXd f2 = force(t + 0.5*dt, (xt=x + 0.5*dt*f1)); + Eigen::VectorXd f3 = force(t + dt, (xt=x + dt*f2)); + + x += dt * ( f0 + 2.0*f1 + 2.0*f2 + f3 ) / 6.0; + + return t + dt; + } + }; +} + +#endif // _RK4_H_ diff --git a/utils/SL/slabchk.cc b/utils/SL/slabchk.cc new file mode 100644 index 000000000..de3c045ff --- /dev/null +++ b/utils/SL/slabchk.cc @@ -0,0 +1,210 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include "Model1d.H" + +int +main(int argc, char** argv) +{ + double H, zmax, zend; + int kmax, nmax, knots, numz; + std::string filename; + + // Parse command line + // + cxxopts::Options options(argv[0], "Check the consistency a spherical SL basis"); + + options.add_options() + ("h,help", "Print this help message") + ("ortho", "Compute orthogonality matrix") + ("H,height", "Slab scale height", + cxxopts::value(H)->default_value("1.0")) + ("K,kmax", "maximum order of in-plane harmonics", + cxxopts::value(kmax)->default_value("4")) + ("N,nmax", "maximum number of vertical harmonics", + cxxopts::value(nmax)->default_value("10")) + ("n,numz", "size of vertical grid", + cxxopts::value(numz)->default_value("1000")) + ("Z,zmax", "maximum extent of vertical grid", + cxxopts::value(zmax)->default_value("10.0")) + ("zend", "potential offset", + cxxopts::value(zend)->default_value("0.0")) + ("knots", "Number of Legendre integration knots", + cxxopts::value(knots)->default_value("40")) + ("p,prefix", "Output filename prefix", + cxxopts::value(filename)->default_value("slabchk_test")) + ; + + + //=================== + // Parse options + //=================== + + cxxopts::ParseResult vm; + + try { + vm = options.parse(argc, argv); + } catch (cxxopts::OptionException& e) { + std::cout << "Option error: " << e.what() << std::endl; + return 2; + } + + // Print help message and exit + // + if (vm.count("help")) { + std::cout << options.help() << std::endl << std::endl; + return 1; + } + + // Generate Sech2 disk grid + // + double dispz = 2.0*M_PI*H*H; + + Sech2 sech2(dispz); + double h = sech2.get_scale_height(); + + SLGridSlab::H = h; + SLGridSlab::ZEND = zend; + std::cout << "Check...scale height is: " << SLGridSlab::H + << std::endl << std::endl; + + // Particle position generator + // + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> dis(0.0, 1.0); + + auto sample = [&dis, &gen, &h]() + { + double m = dis(gen); + return 0.5*h*log(m/(1.0 - m)); + }; + + // Generate Sturm-Liouville grid + // + auto ortho = std::make_shared(kmax, nmax, numz, zmax, "isothermal"); + + LegeQuad lw(knots); + + double ximin = ortho->z_to_xi(-zmax); + double ximax = ortho->z_to_xi( zmax); + + std::vector ans1(nmax, 0.0), ans2(nmax, 0.0), ans3(nmax, 0.0); + + for (int i=0; ixi_to_z(x); + + for (int n=0; nget_pot(x, 0, 0, n, 0)* + ortho->get_dens(x, 0, 0, 1, 0) / + ortho->d_xi_to_z(x) * (ximax - ximin)*lw.weight(i); + + ans2[n] += -ortho->get_pot(x, 0, 0, n, 0)* + 4.0*M_PI*sech2.get_density(z) / + ortho->d_xi_to_z(x) * (ximax - ximin)*lw.weight(i); + } + } + + // Monte Carlo version + // + int Number = 100000; + double fac = 4.0*M_PI*2.0*h/Number; + + for (int i=0; iz_to_xi(z); + + for (int n=0; nget_pot(x, 0, 0, n, 0) * fac; + } + } + + for (int n=0; nget_dens(0.0, 0, 0, n)/(4.0*M_PI) + << std::endl; + } + + std::cout << std::endl; + + int NUM = 20; + double dz = 3.0*H/(NUM-1); + + for (int i=0; iz_to_xi(z); + double s = 0.0, t = 0.0, v = 0.0; + + for (int n=0; nget_dens (x, 0, 0, n, 0); + t += ans2[n]*ortho->get_pot (x, 0, 0, n, 0); + v += ans2[n]*ortho->get_force(x, 0, 0, n, 0); + } + + std::cout << std::setw(16) << z + << std::setw(16) << s/(4.0*M_PI) + << std::setw(16) << sech2.get_density(z) + << std::setw(16) << t + << std::setw(16) << sech2.get_pot(z) + << std::setw(16) << v + << std::setw(16) << sech2.get_dpot(z) + << std::endl; + } + + std::ofstream out(filename + ".basis"); + if (out) { + + int NUM = 200; + double zmin = -3.0*H, zmax = 3.0*H; + double dz = (zmax - zmin)/(NUM-1); + + for (int i=0; iz_to_xi(z); + double s = 0.0; + + out << std::setw(16) << z; + for (int n=0; nget_pot (z, 0, 0, n); + out << std::setw(16) << ortho->get_dens (z, 0, 0, n); + out << std::setw(16) << ortho->get_force(z, 0, 0, n); + } + out << std::endl; + } + } else { + throw std::runtime_error("Error opening filename <" + filename + ".basis>"); + } + + out.close(); + out.open(filename + ".ortho"); + if (out) { + auto test = ortho->orthoCheck(); + int cnt = 0; + for ( auto & v : test) { + out << "==== " << cnt++ << std::endl << v << std::endl; + } + + } else { + throw std::runtime_error("Error opening filename <" + filename + ".ortho>"); + } + + + return 0; +} + From 6fd045761ed7d5d58c9fe0dea1bc742452d0db6b Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 1 Apr 2024 12:51:21 -0400 Subject: [PATCH 053/167] Remove override specifier [no ci] --- include/massmodel.H | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/massmodel.H b/include/massmodel.H index 66c789b1c..18ebc10f9 100644 --- a/include/massmodel.H +++ b/include/massmodel.H @@ -388,12 +388,12 @@ public: //@{ //! Evaluate the profile at a particular radius - virtual double get_mass(const double) override; - virtual double get_density(const double) override; - virtual double get_pot(const double) override; - virtual double get_dpot(const double) override; - virtual double get_dpot2(const double) override; - virtual void get_pot_dpot(const double, double&, double&) override; + virtual double get_mass(const double); + virtual double get_density(const double); + virtual double get_pot(const double); + virtual double get_dpot(const double); + virtual double get_dpot2(const double); + virtual void get_pot_dpot(const double, double&, double&); //@} //@{ From 0e2b9439a244c514a197dffc22f06c443c1d23dd Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 1 Apr 2024 12:52:36 -0400 Subject: [PATCH 054/167] Missing initializers for CUDA [no ci] --- src/SlabSL.H | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/SlabSL.H b/src/SlabSL.H index ca165b387..bad380c12 100644 --- a/src/SlabSL.H +++ b/src/SlabSL.H @@ -81,6 +81,10 @@ private: //! Working device vectors thrust::device_vector t_d; + //! Host vectors + std::vector cuInterpArray; + thrust::host_vector tex; + //! Helper struct to hold device data struct cudaStorage { @@ -96,11 +100,17 @@ private: cudaStorage cuS; //! Only initialize once - bool initialize_cuda_slab = false; + bool initialize_cuda_slab = true; - //! Initialize the cuda streams + //! Initialize cuda extra void cuda_initialize(); + //! Initialize the cuda streams + void initialize_cuda() + { + grid->initialize_cuda(cuInterpArray, tex); + } + //! Zero the coefficient output vectors void cuda_zero_coefs(); //@} From 6bdbef324fa1450a187332ddd54ff339b1893164 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 1 Apr 2024 15:37:23 -0400 Subject: [PATCH 055/167] Correct the overloads in MassModel [no ci] --- include/massmodel.H | 60 +++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/include/massmodel.H b/include/massmodel.H index 18ebc10f9..a91af5a8e 100644 --- a/include/massmodel.H +++ b/include/massmodel.H @@ -394,6 +394,15 @@ public: virtual double get_dpot(const double); virtual double get_dpot2(const double); virtual void get_pot_dpot(const double, double&, double&); + + double get_mass(const double x1, const double x2, const double x3) + { return get_mass(sqrt(x1*x1 + x2*x2 + x3*x3)); } + + double get_density(const double x1, const double x2, const double x3) + { return get_density(sqrt(x1*x1 + x2*x2 + x3*x3)); } + + double get_pot(const double x1, const double x2, const double x3) + { return get_pot(sqrt(x1*x1 + x2*x2 + x3*x3)); } //@} //@{ @@ -401,8 +410,8 @@ public: int get_num_param(void) { return num_params; } double get_param(int i) { return params[i]; } - double get_min_radius(void) override { return mass.x[0]; } - double get_max_radius(void) override { return mass.x[mass.num-1]; } + double get_min_radius(void) { return mass.x[0]; } + double get_max_radius(void) { return mass.x[mass.num-1]; } int grid_size(void) { return num; } void print_model(const std::string& name); void print_model_eval(const std::string& name, int number); @@ -417,10 +426,10 @@ public: //@{ //! Evaluate the distribution function - double distf(double E, double L) override; - double dfde(double E, double L) override; - double dfdl(double E, double L) override; - double d2fde2(double E, double L) override; + double distf(double E, double L); + double dfde(double E, double L); + double dfdl(double E, double L); + double d2fde2(double E, double L); //@} }; @@ -448,44 +457,53 @@ public: //@{ //! Evaluate the profile at a radial point - virtual double get_mass(const double r) override + virtual double get_mass(const double r) { return real->get_mass(r); } - virtual double get_density(const double r) override + virtual double get_density(const double r) { return real->get_density(r); } - virtual double get_pot(const double r) override + virtual double get_pot(const double r) { return real->get_pot(r); } - virtual double get_dpot(const double r) override + virtual double get_dpot(const double r) { return real->get_dpot(r); } - virtual double get_dpot2(const double r) override + virtual double get_dpot2(const double r) { return real->get_dpot2(r); } - virtual void get_pot_dpot(const double r, double& p, double& dp) override + virtual void get_pot_dpot(const double r, double& p, double& dp) { real->get_pot_dpot(r, p, dp); } + + double get_mass(const double x1, const double x2, const double x3) + { return get_mass(sqrt(x1*x1 + x2*x2 + x3*x3)); } + + double get_density(const double x1, const double x2, const double x3) + { return get_density(sqrt(x1*x1 + x2*x2 + x3*x3)); } + + double get_pot(const double x1, const double x2, const double x3) + { return get_pot(sqrt(x1*x1 + x2*x2 + x3*x3)); } //@} //@{ //! Additional member functions - double get_min_radius(void) override { return real->get_min_radius(); } - double get_max_radius(void) override { return real->get_max_radius(); } + double get_min_radius(void) { return real->get_min_radius(); } + double get_max_radius(void) { return real->get_max_radius(); } //@} //@{ //! Evaluate distribution function and its partial derivatives - double distf(double E, double L) override { return real->distf(E, L); } - double dfde (double E, double L) override { return real->dfde(E, L); } - double dfdl (double E, double L) override { return real->dfdl(E, L); } - double d2fde2(double E, double L) override { return real->d2fde2(E, L); } + double distf(double E, double L) { return real->distf(E, L); } + double dfde (double E, double L) { return real->dfde(E, L); } + double dfdl (double E, double L) { return real->dfdl(E, L); } + double d2fde2(double E, double L) { return real->d2fde2(E, L); } //@} //@{ //! Overloaded to provide mass distribution from Real and Number distribution from Fake - Eigen::VectorXd gen_point(int& ierr) override; - Eigen::VectorXd gen_point(double r, int& ierr) override; - Eigen::VectorXd gen_point(double Emin, double Emax, double Kmin, double Kmax, int& ierr) override; + Eigen::VectorXd gen_point(int& ierr); + Eigen::VectorXd gen_point(double r, int& ierr); + Eigen::VectorXd gen_point(double Emin, double Emax, double Kmin, double Kmax, int& ierr); //@} From 11955c75370687ff3801baf1262728cb98a8949e Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 1 Apr 2024 15:38:08 -0400 Subject: [PATCH 056/167] Fix texture array indexing [no ci] --- src/cudaSlabSL.cu | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/cudaSlabSL.cu b/src/cudaSlabSL.cu index 13329ffd8..98e9e864e 100644 --- a/src/cudaSlabSL.cu +++ b/src/cudaSlabSL.cu @@ -2,14 +2,6 @@ #include -#include -#include -#include -#if CUDART_VERSION < 12000 -#include -#endif - -#include #include #include #include @@ -79,7 +71,7 @@ void testConstantsSlab() printf(" Ndim = %d\n", slabNdim ); printf(" Numx = %d\n", slabNumX ); printf(" Numy = %d\n", slabNumY ); - printf(" Numy = %d\n", slabNumZ ); + printf(" Numz = %d\n", slabNumZ ); printf(" Nx = %d\n", slabNX ); printf(" Ny = %d\n", slabNY ); printf(" Nz = %d\n", slabNZ ); @@ -338,16 +330,16 @@ __global__ void coefKernelSlab Y = cy; // Assign the min Y wavenumber conjugate - int kx = abs(ii); + int kx = abs(ii); // X index into texture array for (int jj=-slabNumY; jj<=slabNumY; jj++, Y*=sy) { - int ky = abs(jj); + int ky = abs(jj); // Y index into texture array // The vertical basis iteration for (int n=0; n=tex._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); @@ -451,15 +443,22 @@ forceKernelSlab(dArray P, dArray I, CmplxT X, Y; // Will contain the incremented basis X = cx; // Assign the min X wavenumber + for (int ii=-slabNumX; ii<=slabNumX; ii++, X*=sx) { + Y = cy; // Assign the min Y wavenumber + + int kx = abs(ii); // X index into texture array + for (int jj=-slabNumY; jj<=slabNumY; jj++, Y*=sy) { + int ky = abs(jj); // Y index into texture array + + int offset = 1 + (kx*(kx+1)/2 + ky)*slabNumZ; + for (int n=0; n=tex._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); @@ -560,12 +559,15 @@ void SlabSL::determine_coefficients_cuda() if (initialize_cuda_slab) { initialize_cuda(); initialize_cuda_slab = false; - } - - // Copy coordinate mapping - // - initialize_constants(); + // Copy coordinate mapping + // + initialize_constants(); + + // Copy texture objects to device + // + t_d = tex; + } std::cout << std::scientific; From 24b37fcb115b5f8fc452a2aa9eca975f2157342a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 1 Apr 2024 15:38:48 -0400 Subject: [PATCH 057/167] Correct the overloads in DiskWithHalo [no ci] --- include/DiskWithHalo.H | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/include/DiskWithHalo.H b/include/DiskWithHalo.H index 36c8913aa..79e9038f7 100644 --- a/include/DiskWithHalo.H +++ b/include/DiskWithHalo.H @@ -66,6 +66,15 @@ public: dur = dur1 + dur2; } + double get_mass(const double x1, const double x2, const double x3) + { return get_mass(sqrt(x1*x1 + x2*x2 + x3*x3)); } + + double get_density(const double x1, const double x2, const double x3) + { return get_density(sqrt(x1*x1 + x2*x2 + x3*x3)); } + + double get_pot(const double x1, const double x2, const double x3) + { return get_pot(sqrt(x1*x1 + x2*x2 + x3*x3)); } + // Addiional member functions double get_min_radius(void) override From 711f67b63ffe1a1d0a040d6a25e61292d2aac4dc Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 1 Apr 2024 15:39:39 -0400 Subject: [PATCH 058/167] Update texture tests (passing) [no ci] --- exputil/cudaSLGridMP2.cu | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/exputil/cudaSLGridMP2.cu b/exputil/cudaSLGridMP2.cu index 18e55be41..061e06d13 100644 --- a/exputil/cudaSLGridMP2.cu +++ b/exputil/cudaSLGridMP2.cu @@ -245,7 +245,7 @@ thrust::host_vector returnTestSlab thrust::device_vector f_d(numz); - int l = kx*(kx+1)/2*numk + ky; + int l = kx*(kx+1)/2 + ky; testFetchSlab<<>>(toKernel(t_d), toKernel(f_d), l, j, nmax, numz); @@ -344,8 +344,11 @@ void SLGridSlab::initialize_cuda(std::vector& cuArray, cuda_safe_call(cudaCreateTextureObject(&tex[i], &resDesc, &texDesc, NULL), __FILE__, __LINE__, "create texture object"); } + // vertical loop } + // y axis loop } + // x axis loop // This is for debugging: compare texture table fetches to original // tables @@ -356,6 +359,7 @@ void SLGridSlab::initialize_cuda(std::vector& cuArray, struct Element { int kx; int ky; + int j; double a; double b; }; @@ -378,14 +382,16 @@ void SLGridSlab::initialize_cuda(std::vector& cuArray, cuFP_t b = ret[i]; if (a>1.0e-18) { - Element comp = {kx, ky, a, b}; + Element comp = {kx, ky, j, a, b}; compare.insert(std::make_pair(fabs((a - b)/a), comp)); - if ( fabs((a - b)/a ) > tol) { + double test = (a - b)/a; + + if ( fabs(test) > tol) { std::cout << std::setw( 5) << kx << std::setw( 5) << ky << std::setw( 5) << j << std::setw( 5) << i << std::setw(20) << a - << std::setw(20) << (a-b)/a << std::endl; + << std::setw(20) << test << std::endl; bad++; } } @@ -406,11 +412,11 @@ void SLGridSlab::initialize_cuda(std::vector& cuArray, std::advance(hi9, -10); std::cout << "**Found " << bad << "/" << tot << " values > eps" << std::endl - << "**Low[1] : " << lo1->first << " (" << lo1->second.kx << ", " << lo1->second.ky << ", " << lo1->second.a << ", " << lo1->second.b << ", " << lo1->second.a - lo1->second.b << ")" << std::endl - << "**Low[9] : " << lo9->first << " (" << lo9->second.kx << ", " << lo9->second.ky << ", " << lo9->second.a << ", " << lo9->second.b << ", " << lo9->second.a - lo9->second.b << ")" << std::endl - << "**Middle : " << mid->first << " (" << mid->second.kx << ", " << mid->second.ky << ", " << mid->second.a << ", " << mid->second.b << ", " << mid->second.a - mid->second.b << ")" << std::endl - << "**Hi [9] : " << hi9->first << " (" << hi9->second.kx << ", " << hi9->second.ky << ", " << hi9->second.a << ", " << hi9->second.b << ", " << hi9->second.a - hi9->second.b << ")" << std::endl - << "**Hi [1] : " << hi1->first << " (" << hi1->second.kx << ", " << hi1->second.ky << ", " << hi1->second.a << ", " << hi1->second.b << ", " << hi1->second.a - hi1->second.b << ")" << std::endl + << "**Low[1] : " << lo1->first << " (" << lo1->second.kx << ", " << lo1->second.ky << ", " << lo1->second.j << ", " << lo1->second.a << ", " << lo1->second.b << ", " << lo1->second.a - lo1->second.b << ")" << std::endl + << "**Low[9] : " << lo9->first << " (" << lo9->second.kx << ", " << lo9->second.ky << ", " << lo9->second.j << ", " << lo9->second.a << ", " << lo9->second.b << ", " << lo9->second.a - lo9->second.b << ")" << std::endl + << "**Middle : " << mid->first << " (" << mid->second.kx << ", " << mid->second.ky << ", " << mid->second.j << ", " << mid->second.a << ", " << mid->second.b << ", " << mid->second.a - mid->second.b << ")" << std::endl + << "**Hi [9] : " << hi9->first << " (" << hi9->second.kx << ", " << hi9->second.ky << ", " << hi9->second.j << ", " << hi9->second.a << ", " << hi9->second.b << ", " << hi9->second.a - hi9->second.b << ")" << std::endl + << "**Hi [1] : " << hi1->first << " (" << hi1->second.kx << ", " << hi1->second.ky << ", " << hi1->second.j << ", " << hi1->second.a << ", " << hi1->second.b << ", " << hi1->second.a - hi1->second.b << ")" << std::endl << "**" << std::endl; } From 31a5bb58cddef559122f932e6a80f9cb1a4bb9cf Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 1 Apr 2024 22:48:44 -0400 Subject: [PATCH 059/167] Clean up; fixed typo in force computation [no ci] --- src/cudaSlabSL.cu | 151 ++++++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 66 deletions(-) diff --git a/src/cudaSlabSL.cu b/src/cudaSlabSL.cu index 98e9e864e..cd00f09d4 100644 --- a/src/cudaSlabSL.cu +++ b/src/cudaSlabSL.cu @@ -19,7 +19,6 @@ __device__ __constant__ int slabNumX, slabNumY, slabNumZ, slabNX, slabNY, slabNZ, slabNdim, slabNum; - __device__ __constant__ int slabCmap; @@ -40,7 +39,8 @@ int slabIndex(int i, int j, int k) return k*slabNX*slabNY + j*slabNX + i; } -// Index function for modulus coefficients +// Index function for modulus coefficients. This handles the +// eigen::tensor packing order // __device__ thrust::tuple slabTensorIndices(int indx) @@ -92,9 +92,10 @@ void testFetchSlab(dArray T, dArray f, const int l = 1 + kx*(kx+1)/2*(slabNumY+1) + j; #if cuREAL == 4 - if (n < numz) f._v[n] = tex1D(T._v[l], n); + if (n < numz) f._v[n] = tex1D(T._v[l], n)*tex1D(T._v[l], n)); + if (n < numz) f._v[n] = int2_as_double(tex1D(T._v[l], n)) * + int2_as_double(tex1D(T._v[0], n)); #endif } @@ -212,7 +213,8 @@ void SlabSL::initialize_constants() size_t(0), cudaMemcpyHostToDevice), __FILE__, __LINE__, "Error copying slabNum"); - int Cmap = 0; + // Sech map + int Cmap = 1; cuda_safe_call(cudaMemcpyToSymbol(slabCmap, &Cmap, sizeof(int), size_t(0), cudaMemcpyHostToDevice), @@ -259,15 +261,13 @@ __global__ void coefKernelSlab if (npart < lohi.second) { // Check that particle index is in // range for consistency with other // kernels - #ifdef BOUNDS_CHECK if (npart>=P._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); #endif cudaParticle & p = P._v[I._v[npart]]; cuFP_t pos[3] = {p.pos[0], p.pos[1], p.pos[2]}; - cuFP_t mm = p.mass; - + cuFP_t mm = - p.mass * 2.0*slabDfac; // Restore particles to the unit slab // @@ -290,34 +290,26 @@ __global__ void coefKernelSlab // Vertical interpolation // - cuFP_t x = cu_z_to_xi(pos[2]); + cuFP_t x = cu_z_to_xi(fabs(pos[2])); cuFP_t xi = (x - slabXmin)/slabDxi; - int ind = floor(xi); - int in0 = ind; + int in0 = floor(xi); + // For linear interpolation + // if (in0 < 0) in0 = 0; if (in0 > slabNum-2) in0 = slabNum - 2; - if (ind < 1) ind = 1; - if (ind > slabNum-2) ind = slabNum - 2; - cuFP_t a = (cuFP_t)(in0+1) - xi; cuFP_t b = 1.0 - a; - // Flip sign for antisymmetric basis functions - // - int sign = 1; - if (x<0 && 2*(n/2)!=n) sign = -1; - - cuFP_t p0 = #if cuREAL == 4 - a*tex1D(tex._v[0], ind ) + - b*tex1D(tex._v[0], ind+1) ; + a*tex1D(tex._v[0], in0 ) + + b*tex1D(tex._v[0], in0+1) ; #else - a*int2_as_double(tex1D(tex._v[0], ind )) + - b*int2_as_double(tex1D(tex._v[0], ind+1)) ; + a*int2_as_double(tex1D(tex._v[0], in0 )) + + b*int2_as_double(tex1D(tex._v[0], in0+1)) ; #endif // Will contain the incremented basis @@ -336,26 +328,32 @@ __global__ void coefKernelSlab int ky = abs(jj); // Y index into texture array + int offset = 1 + (kx*(kx+1)/2 + ky)*slabNumZ; + // The vertical basis iteration for (int n=0; n=tex._s) printf("out of bounds: %s:%d\n", __FILE__, __LINE__); #endif cuFP_t v = ( #if cuREAL == 4 - a*tex1D(tex._v[k], ind ) + - b*tex1D(tex._v[k], ind+1) + a*tex1D(tex._v[k], in0 ) + + b*tex1D(tex._v[k], in0+1) #else - a*int2_as_double(tex1D(tex._v[k], ind )) + - b*int2_as_double(tex1D(tex._v[k], ind+1)) + a*int2_as_double(tex1D(tex._v[k], in0 )) + + b*int2_as_double(tex1D(tex._v[k], in0+1)) #endif ) * p0 * sign; - coef._v[slabIndex(ii, jj, n)] = -2.0*slabDfac * X * Y * v * mm; - + coef._v[slabIndex(ii, jj, n)*N+i] = X * Y * v * mm; } } } @@ -397,30 +395,28 @@ forceKernelSlab(dArray P, dArray I, const auto yy = CmplxT(0.0, slabDfac*pos[1]); // Recursion increments and initial values + // const auto sx = thrust::exp(xx), cx = thrust::exp(-xx*slabNumX); const auto sy = thrust::exp(yy), cy = thrust::exp(-yy*slabNumY); // Vertical interpolation // - cuFP_t x = cu_z_to_xi(pos[2]); + cuFP_t x = cu_z_to_xi(fabs(pos[2])); + cuFP_t dx = cu_d_xi_to_z(x)/slabDxi; cuFP_t xi = (x - slabXmin)/slabDxi; - cuFP_t dx = cu_d_xi_to_z(xi)/slabDxi; - int ind = floor(xi); - int in0 = ind; + int in0 = floor(xi); + // For linear interpolation + // if (in0 < 0) in0 = 0; if (in0 > slabNum-2) in0 = slabNum - 2; - if (ind < 1) ind = 1; - if (ind > slabNum-2) ind = slabNum - 2; - cuFP_t a = (cuFP_t)(in0+1) - xi; cuFP_t b = 1.0 - a; - // For 3-pt formula - + // int jn0 = floor(xi); if (jn0 < 1) jn0 = 1; if (jn0 > slabNum-2) jn0 = slabNum - 2; @@ -429,17 +425,13 @@ forceKernelSlab(dArray P, dArray I, cuFP_t p0 = #if cuREAL == 4 - a*tex1D(tex._v[0], ind ) + - b*tex1D(tex._v[0], ind+1) ; + a*tex1D(tex._v[0], in0 ) + + b*tex1D(tex._v[0], in0+1) ; #else - a*int2_as_double(tex1D(tex._v[0], ind )) + - b*int2_as_double(tex1D(tex._v[0], ind+1)) ; + a*int2_as_double(tex1D(tex._v[0], in0 )) + + b*int2_as_double(tex1D(tex._v[0], in0+1)) ; #endif - // Flip sign for antisymmetric basis functions - int sign = 1; - if (pos[2]<0 && 2*(n/2)!=n) sign = -1; - CmplxT X, Y; // Will contain the incremented basis X = cx; // Assign the min X wavenumber @@ -458,6 +450,11 @@ forceKernelSlab(dArray P, dArray I, for (int n=0; n P, dArray I, #endif cuFP_t v = ( #if cuREAL == 4 - a*tex1D(tex._v[k], ind ) + - b*tex1D(tex._v[k], ind+1) + a*tex1D(tex._v[k], in0 ) + + b*tex1D(tex._v[k], in0+1) #else - a*int2_as_double(tex1D(tex._v[k], ind )) + - b*int2_as_double(tex1D(tex._v[k], ind+1)) + a*int2_as_double(tex1D(tex._v[k], in0 )) + + b*int2_as_double(tex1D(tex._v[k], in0+1)) #endif ) * p0 * sign; cuFP_t f = ( #if cuREAL == 4 - (s - 0.5)*tex1D(tex._v[k], jn0-1)*tex1D(tex._v[0], jn0-1) - -2.0*tex1D(tex._v[k], jnd)*tex1D(tex._v[0], jn0) + - (s + 0.5)*tex1D(tex._v[k], jn0+1)*tex1D(tex._v[0], jn0+1) + (s - 0.5)*tex1D(tex._v[k], jn0-1) * tex1D(tex._v[0], jn0-1) + -2.0*s*tex1D(tex._v[k], jn0) * tex1D(tex._v[0], jn0) + + (s + 0.5)*tex1D(tex._v[k], jn0+1) * tex1D(tex._v[0], jn0+1) #else - (s - 0.5)*int2_as_double(tex1D(tex._v[k], jn0-1))* - int2_as_double(tex1D(tex._v[0], jn0-1)) - -2.0*int2_as_double(tex1D(tex._v[k], jn0))* - int2_as_double(tex1D(tex._v[0], jn0)) + - (s + 0.5)*int2_as_double(tex1D(tex._v[k], jn0+1))* - int2_as_double(tex1D(tex._v[0], jn0+1)) + (s - 0.5)*int2_as_double(tex1D(tex._v[k], jn0-1)) * int2_as_double(tex1D(tex._v[0], jn0-1)) + -2.0*s*int2_as_double(tex1D(tex._v[k], jn0)) * int2_as_double(tex1D(tex._v[0], jn0)) + + (s + 0.5)*int2_as_double(tex1D(tex._v[k], jn0+1)) * int2_as_double(tex1D(tex._v[0], jn0+1)) #endif ) * sign * dx; fac = X * Y * v * coef._v[slabIndex(ii, jj, n)]; facf = X * Y * f * coef._v[slabIndex(ii, jj, n)]; + pot += fac; acc[0] += CmplxT(0.0, -slabDfac*ii) * fac; acc[1] += CmplxT(0.0, -slabDfac*jj) * fac; acc[2] += -facf; @@ -503,8 +498,8 @@ forceKernelSlab(dArray P, dArray I, // Particle assignment // - p.pot = pot.real(); - for (int k=0; k<3; k++) p.acc[k] = acc[k].real(); + p.pot += pot.real(); + for (int k=0; k<3; k++) p.acc[k] += acc[k].real(); } // END: particle index limit } @@ -592,8 +587,32 @@ void SlabSL::determine_coefficients_cuda() cudaDeviceSynchronize(); cuda_check_last_error_mpi("cudaDeviceSynchronize", __FILE__, __LINE__, myid); firstime = false; + + // Test the texture retrieval for the potential basis + if (false) { + std::vector> test; + + for (int n=0; ngetCudaMappingConstants(); + auto h = SLGridSlab::H; + auto x_to_z = [h](double x) + { + return x*h/sqrt(1.0 - x*x); + }; + + std::ofstream out("test.me"); + if (out) { + for (int i=0; istream>>> (toKernel(cs->cuda_particles), toKernel(cs->indx1), - toKernel(cuS.dN_coef), toKernel(t_d) ,stride, cur); + toKernel(cuS.dN_coef), toKernel(t_d), stride, cur); // Begin the reduction by blocks [perhaps this should use a // stride?] @@ -705,7 +724,7 @@ void SlabSL::determine_coefficients_cuda() thrust::transform(thrust::cuda::par.on(cs->stream), cuS.dw_coef.begin(), cuS.dw_coef.end(), beg, beg, thrust::plus()); - + thrust::advance(beg, jmax); } From 296c57ba0a0ac0450a545ba79b085f6bddcb3ad6 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 2 Apr 2024 10:06:32 -0400 Subject: [PATCH 060/167] Minor clean up only [no ci] --- expui/BiorthBasis.cc | 77 ++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 52 deletions(-) diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index cca714bc3..64a9f9366 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -1982,10 +1982,10 @@ namespace BasisClasses // Finally, make the basis // - SLGridSlab::mpi = 0; + SLGridSlab::mpi = 0; SLGridSlab::ZBEG = 0.0; SLGridSlab::ZEND = 0.1; - SLGridSlab::H = hslab; + SLGridSlab::H = hslab; int nnmax = (nmaxx > nmaxy) ? nmaxx : nmaxy; @@ -1999,10 +1999,12 @@ namespace BasisClasses // int nthrds = omp_get_max_threads(); - imx = 2*nmaxx + 1; - imy = 2*nmaxy + 1; - imz = nmaxz; + imx = 2*nmaxx + 1; // x wave numbers + imy = 2*nmaxy + 1; // y wave numbers + imz = nmaxz; // z basis count + // Coefficient tensor + // expcoef.resize(imx, imy, imz); expcoef.setZero(); @@ -2079,57 +2081,31 @@ namespace BasisClasses else y -= std::floor( y); - // Recursion multipliers - Eigen::Vector3cd step - {std::exp(-kfac*x), std::exp(-kfac*y), std::exp(-kfac*z)}; - - // Initial values for recursion - Eigen::Vector3cd init - {std::exp(-kfac*(x*nmaxx)), - std::exp(-kfac*(y*nmaxy)), - std::exp(-kfac*(z*nmaxz))}; - - Eigen::Vector3cd curr(init); - for (int ix=0; ix<=2*nmaxx; ix++, curr(0)*=step(0)) { - curr(1) = init(1); - for (int iy=0; iy<=2*nmaxy; iy++, curr(1)*=step(1)) { - curr(2) = init(2); - for (int iz=0; iz<=2*nmaxz; iz++, curr(2)*=step(2)) { - - // Compute wavenumber; recall that the coefficients are - // stored as: -nmax,-nmax+1,...,0,...,nmax-1,nmax - // - int ii = ix-nmaxx; - int jj = iy-nmaxy; - int kk = iz-nmaxz; - - // Normalization - double norm = 1.0/sqrt(M_PI*(ii*ii + jj*jj + kk*kk));; + used++; - expcoef(ix, iy, iz) += - mass * curr(0)*curr(1)*curr(2) * norm; - } - } - } + // Storage for basis evaluation + Eigen::VectorXd zpot(nmaxz); - int ix, iy, iz; // Loop indices + // Loop indices + int ix, iy; - // Recursion multipliers + // Recursion multipliers std::complex stepx = exp(-kfac*x), facx; std::complex stepy = exp(-kfac*y), facy; - // Initial values + // Initial values std::complex startx = exp(static_cast(nmaxx)*kfac*x); std::complex starty = exp(static_cast(nmaxy)*kfac*y); - Eigen::VectorXd zpot(nmaxz); - for (facx=startx, ix=0; ix Date: Tue, 2 Apr 2024 11:51:42 -0400 Subject: [PATCH 061/167] Added 'slabics': slab IC generator for SlabSL tests [no ci] --- {utils/SL => include}/RK4.H | 0 utils/ICs/CMakeLists.txt | 4 +- utils/ICs/genslab.cc | 218 ++++++++++++++++++ utils/ICs/massmodel1d.H | 442 ++++++++++++++++++++++++++++++++++++ utils/ICs/massmodel1d.cc | 367 ++++++++++++++++++++++++++++++ 5 files changed, 1030 insertions(+), 1 deletion(-) rename {utils/SL => include}/RK4.H (100%) create mode 100755 utils/ICs/genslab.cc create mode 100755 utils/ICs/massmodel1d.H create mode 100755 utils/ICs/massmodel1d.cc diff --git a/utils/SL/RK4.H b/include/RK4.H similarity index 100% rename from utils/SL/RK4.H rename to include/RK4.H diff --git a/utils/ICs/CMakeLists.txt b/utils/ICs/CMakeLists.txt index f10de0787..deff4307a 100644 --- a/utils/ICs/CMakeLists.txt +++ b/utils/ICs/CMakeLists.txt @@ -2,7 +2,7 @@ set(bin_PROGRAMS shrinkics gensph gendisk gendisk2d gsphere pstmod empinfo empdump eofbasis eofcomp testcoefs testcoefs2 testdeval forcetest hdf52accel forcetest2 cylcache modelfit test2d addsphmod - cubeics zangics) + cubeics zangics slabics) set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX yaml-cpp exputil ${VTK_LIBRARIES} ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES}) @@ -90,6 +90,8 @@ add_executable(cubeics cubeICs.cc) add_executable(zangics ZangICs.cc) +add_executable(slabics genslab.cc massmodel1d.cc) + foreach(program ${bin_PROGRAMS}) target_link_libraries(${program} ${common_LINKLIB}) target_include_directories(${program} PUBLIC ${common_INCLUDE}) diff --git a/utils/ICs/genslab.cc b/utils/ICs/genslab.cc new file mode 100755 index 000000000..3cf773070 --- /dev/null +++ b/utils/ICs/genslab.cc @@ -0,0 +1,218 @@ +/***************************************************************************** + * Description: + * ----------- + * + * Generate sech^2 slab in a unit sqaure + * + * + * Call sequence: + * ------------- + * + * Parameters: + * ---------- + * + * + * Returns: + * ------- + * + * + * Notes: + * ----- + * + * + * By: + * -- + * + * MDW 11/20/91 + * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +int +main(int argc, char **argv) +{ + unsigned int seed; + int Ntable, Number; + double Dratio, Hratio, R, Hmax, DispX, DispZ, fJ; + std::string outfile, config, modfile, modelType; + bool Mu; + + + // Parse command line + // + std::string message = "Generate unit-box slab initial conditions\n"; + + cxxopts::Options options(argv[0], message); + + options.add_options() + ("h,help", "Print this help message") + ("N,number", "Number of bodies", + cxxopts::value(Number)->default_value("10000")) + ("n,ntable", "Number of points in model table", + cxxopts::value(Ntable)->default_value("400")) + ("t,model", "Model type (LowIso, Sech2, Sech2Halo)", + cxxopts::value(modelType)->default_value("Sech2")) + ("d,dratio", "Ratio of disk to halo density", + cxxopts::value(Dratio)->default_value("3.0")) + ("r,hratio", "Ratio of halo to disk scale height", + cxxopts::value(Hratio)->default_value("10.0")) + ("R,vratio", "Ratio of vertical to horizontal velocity dispersion", + cxxopts::value(R)->default_value("1.0")) + ("H,hmax", "Maximum vertical size in scale heights", + cxxopts::value(Hmax)->default_value("10.0")) + ("X,DispX", "In-plane velocity variance", + cxxopts::value(DispX)->default_value("1.0")) + ("F,fJ", "Ratio of Jeans length to box scale", + cxxopts::value(fJ)->default_value("1.0")) + ("M,Mu", "Surface density norm for Sech2Halo", + cxxopts::value(Mu)->default_value("true")) + ("c,config", "Config file", + cxxopts::value(config)) + ("s,seed", "Random # seed", + cxxopts::value(seed)) + ("i,modfile", "Slab model file for LowIso", + cxxopts::value(modfile)->default_value("slab.model")) + ("o,outfile", "output file prefix", + cxxopts::value(outfile)->default_value("slab.bods")) + ; + + cxxopts::ParseResult vm; + + try { + vm = options.parse(argc, argv); + } catch (cxxopts::OptionException& e) { + std::cout << "Option error: " << e.what() << std::endl; + return -1; + } + + // Get help + // + if (vm.count("help")) { + std::cout << std::endl << options.help() << std::endl; + return 0; + } + + std::ofstream out(outfile); + if (!out) { + std::cerr << "Can't open <" << outfile << ">" << std::endl; + exit(-1); + } + out.precision(6); + out.setf(ios::scientific); + + // + // Define model + // + double h = 1.0; + DispZ = DispX*R*R; + std::shared_ptr model; + + if (modelType.compare("LowIso") == 0) { + model = std::make_shared(modfile); + } + else if (modelType.compare("Sech2") == 0) { + DispZ = 2.0/(M_PI*fJ*fJ); + DispX = DispZ/(R*R); + auto tmp = std::make_shared(DispZ); + h = tmp->get_scale_height(); + if (Hmax>0) tmp->set_hmax(Hmax); + model = tmp; + } + else if (modelType.compare("Sech2Halo") == 0) { + Sech2Halo::MU = false; + if (Mu) Sech2Halo::MU = true; + auto tmp = std::make_shared(DispZ, Dratio, Hratio); + h = tmp->get_scale_height(); + if (Hmax>0) tmp->set_hmax(Hmax); + model = tmp; + } + else { + std::cerr << "non-existent model: " << modelType << std::endl; + exit(-1); + } + + // + // Jeans' length for selecting scale height + // + + double maxZ = model->get_max_radius(); + double mu = model->get_mass(maxZ); + double KJ = 2.0*M_PI*mu/DispX; + double LJ = 2.0*M_PI/KJ; + + std::cout.setf(ios::left); + char prev = cout.fill('.'); + + std::cout << std::setw(40) << "Model type" << modelType << std::endl; + std::cout << std::setw(40) << "Surface mass density" << mu << std::endl; + std::cout << std::setw(40) << "Jeans' length" << LJ << std::endl; + std::cout << std::setw(40) << "Scale height" << h << std::endl; + std::cout << std::setw(40) << "Maximum thickness" << maxZ << std::endl; + + cout.fill(prev); + + // + // Make mass table + // + + std::vector Z(Ntable); + std::vector M(Ntable); + double z, dz = 2.0*maxZ/(Ntable-1.0); + for (int i=0; iget_mass(Z[i]); + } + + std::random_device rd; + if (vm.count("seed")==0) seed = rd(); + std::mt19937 gen(seed); + + std::uniform_real_distribution<> Unit(0.0, 1.0); + std::normal_distribution Vv{0.0, sqrt(DispZ)}; + std::normal_distribution Vh{0.0, sqrt(DispX)}; + + // Header line + out << std::setw(10) << Number << std::setw(15) << 0 << std::setw(15) << 0 << std::endl; + + double KE = 0.0; + double VC = 0.0; + double mass = mu/Number; + + for (int n=0; nget_dpot(pos[2]); + + out << std::setw(15) << mass + << std::setw(15) << pos[0] + << std::setw(15) << pos[1] + << std::setw(15) << pos[2] + << std::setw(15) << vel[0] + << std::setw(15) << vel[1] + << std::setw(15) << vel[2] + << std::endl; + } + + std::cout << std::endl + << "Virial parameters: KE=" << 0.5*mass*KE + << " VC=" << mass*VC + << " 2T/VC=" << KE/VC << std::endl; + +} + diff --git a/utils/ICs/massmodel1d.H b/utils/ICs/massmodel1d.H new file mode 100755 index 000000000..7c91ed0d3 --- /dev/null +++ b/utils/ICs/massmodel1d.H @@ -0,0 +1,442 @@ +// -*- C++ -*- + +#ifndef _massmodel1d_H +#define _massmodel1d_H + +#include +#include + +#include + +class OneDModel : public MassModel +{ + +public: + bool dist_defined; + +// AxiSymModel(void) {}; + + virtual double get_mass(const double) = 0; + virtual double get_density(const double) = 0; + virtual double get_pot(const double) = 0; + virtual double get_dpot(const double) = 0; + virtual double get_dpot2 (const double) = 0; + virtual tuple get_pot_dpot (const double) = 0; + + // Required members of mass model + + double get_mass(const double x, const double y, const double z) { + return get_mass(z);} + + double get_density(const double x, const double y, const double z) { + return get_density(z);} + + double get_pot(const double x, const double y, const double z) { + return get_pot(z);} + + + // Addiional member functions + + virtual double get_min_radius(void) = 0; + virtual double get_max_radius(void) = 0; + virtual double get_scale_height(void) = 0; + virtual double distf(const double, const double V=0.0) = 0; + virtual double dfde (const double, const double V=0.0) = 0; + virtual double dfdv (const double, const double V=0.0) = 0; +}; + +class OneDModelTable : public OneDModel +{ +protected: + + Spline1d mass, dens, pot; + + int even; + int num; + int numdf; + double half_height; + std::vector params; + +public: + + OneDModelTable() {}; + + OneDModelTable(string filename, int PARM=0); + + OneDModelTable(int num, double *r, double *d, + double *m, double *p, string ID = "" ); + + // Required member functions + + double get_mass (const double); + double get_density(const double); + double get_pot (const double); + double get_dpot (const double); + double get_dpot2 (const double); + tuple get_pot_dpot (const double); + + // Additional member functions + + const int get_num_param(void) { return params.size(); } + const double get_param(int i) { return params[i-1]; } + double get_scale_height(void) { return half_height; } + + double get_min_radius(void) { return mass.xlo(); } + double get_max_radius(void) { return mass.xhi(); } + int grid_size(void) { return num; } + +// double distf(double E, double V); +// double dfde(double E, double V); +// double dfdv(double E, double V); +}; + +class LowIso : public OneDModelTable +{ +private: + double w0, Bfac, betak, gammak; + double dispx, normx; + + void setup_model(void); + +public: + LowIso(string filename, double DISPX=0.159154943091895335768) : + OneDModelTable(filename, 1) { + dispx = DISPX; + setup_model(); + } + + double get_pot(const double); + double get_dpot(const double); + double get_dpot2(const double); + tuple get_pot_dpot(const double); + + double distf(const double E, const double V=0.0); + double dfde(const double E, const double V=0.0); + double dfdv(const double E, const double V=0.0); +}; + + +class Sech2 : public OneDModel +{ +private: + double h; + double dispz, dispx; + double norm; + + static double HMAX; + +public: + + Sech2(void) + { + dispz = 1.0; + dispx = 1.0; + // + // Units: G = rho_o = 1 + // + h = sqrt(dispz/(2.0*M_PI)); + norm = 1.0/( sqrt(2.0*M_PI*dispz) ); + dist_defined = true; + } + + Sech2(const double DISPZ, const double DISPX=1.0) + { + dispz = DISPZ; + dispx = DISPX; + // + // Units: G = rho_o = 1 + // + h = sqrt(dispz/(2.0*M_PI)); + norm = 1.0/( sqrt(2.0*M_PI*dispz) ); + dist_defined = true; + } + + double get_mass(const double z) + { + return 2.0*h/(1.0 + exp(-2.0*z/h)); + } + + double get_density(const double z) + { + double zz = fabs(z); + double ret = 2.0*exp(-zz/h)/(1.0 + exp(-2.0*zz/h)); + return ret*ret; + } + + double get_pot(const double z) + { + double zz = fabs(z); + return 4.0*M_PI*h*(zz + h*log(1.0 + exp(-2.0*zz/h)) - h*M_LN2); + } + + double get_dpot(const double z) + { + double zz = fabs(z); + double ret = (1.0 - exp(-2.0*zz/h))/(1.0 + exp(-2.0*zz/h)); + return 4.0*M_PI*h* ret * z/(zz+1.0e-18); + } + + double get_dpot2(const double z) + { + double zz = fabs(z); + double ret = 2.0*exp(-zz/h)/(1.0 + exp(-2*zz/h)); + return 4.0*M_PI*ret*ret; + } + + tuple get_pot_dpot(const double z) + { + double zz = fabs(z); + double p = 4.0*M_PI*h*(zz + h*log(1.0 + exp(-2.0*zz/h)) - h*M_LN2); + double ret = (1.0 - exp(-2.0*zz/h))/(1.0 + exp(-2.0*zz/h)); + double dp = 4.0*M_PI*h* ret * z/(zz+1.0e-18); + return {p, dp}; + } + + double get_mass(const double x, const double y, const double z) + { + return get_mass(z); + } + + double get_density(const double x, const double y, const double z) + { + return get_density(z); + } + + double get_pot(const double x, const double y, const double z) + { + return get_pot(z); + } + + double get_min_radius(void) { return 0.0; } + double get_max_radius(void) { return HMAX*h; } + double get_scale_height(void) { return h; } + + static void set_hmax(double hmax) { HMAX = hmax; } + + double distf(const double E, const double p) + { + return exp(-E/dispz - 0.5*p*p/dispx) * norm; + } + + double dfde(const double E, const double p) + { + return -exp(-E/dispz - 0.5*p*p/dispx)/dispz * norm; + } + + double dfdv(const double E, const double p) + { + return -exp(-E/dispz - 0.5*p*p/dispx)*p/dispx * norm; + } + +}; + + + +class Sech2mu : public OneDModel +{ +private: + double mu, h; + double dispz, dispx; + double dnorm, knorm, fnorm; + + static double HMAX; + +public: + + Sech2mu(void) + { + mu = 1.0; + dispz = 1.0; + dispx = 1.0; + h = 1.0; + // + // Units: G = mu = 1 + // + dnorm = 0.25*mu/h; + knorm = 2.0*M_PI*mu*h/dispz; + fnorm = mu/(4.0*h*knorm*sqrt(2.0*M_PI*dispz)); + dist_defined = true; + } + + Sech2mu(const double DISPZ, const double H, const double DISPX=1.0) + { + mu = 1.0; + dispz = DISPZ; + dispx = DISPX; + h = H; + // + // Units: G = mu = 1 + // + dnorm = 0.25*mu/h; + knorm = 2.0*M_PI*mu*h/dispz; + fnorm = mu/(4.0*h*knorm*sqrt(2.0*M_PI*dispz)); + dist_defined = true; + } + + void setMu(double Sigma0) + { + mu = Sigma0; + // + // Units: G = 1, mu = Sigma0 + // + dnorm = 0.25*mu/h; + knorm = 2.0*M_PI*mu*h/dispz; + fnorm = mu/(4.0*h*knorm*sqrt(2.0*M_PI*dispz)); + dist_defined = true; + } + + double get_mass(const double z) { + return mu*exp(z/h)/(1.0 + exp(z/h)); + } + + double get_density(const double z) { + double zz = fabs(z); + double fac = exp(zz/h); + return 4.0*dnorm/(fac + 1.0/fac + 2.0); + } + + double get_pot(const double z) { + double zz = fabs(z); + return 2.0*dispz*(-M_LN2 + 0.5*zz/h + log(1.0 + exp(-zz/h)) ) - dispz*log(knorm); + } + + double get_dpot(const double z) { + double zz = fabs(z); + double ret = (1.0 - exp(-zz/h))/(1.0 + exp(-zz/h)); + return 0.5*dispz*ret/h * z/(zz+1.0e-18); + } + + double get_dpot2(const double z) { + double zz = fabs(z); + double ret = 2.0*exp(-0.5*zz/h)/(1.0 + exp(-zz/h)); + return 0.5*dispz*ret*ret/h/h; + } + + void get_pot_dpot(const double z, double& p, double& dp) { + double zz = fabs(z); + p = 2.0*dispz*(-M_LN2 + 0.5*zz/h + log(1.0 + exp(-zz/h)) ) - dispz*log(knorm); + double ret = (1.0 - exp(-zz/h))/(1.0 + exp(-zz/h)); + dp = 0.5*dispz*ret/h * z/(zz+1.0e-18); + } + + double get_mass(const double x, const double y, const double z) { + return get_mass(z); + } + + double get_density(const double x, const double y, const double z) { + return get_density(z); + } + + double get_pot(const double x, const double y, const double z) { + return get_pot(z); + } + + double get_min_radius(void) { return 0.0; } + double get_max_radius(void) { return HMAX*h; } + double get_scale_height(void) { return h; } + + static void set_hmax(double hmax) { HMAX = hmax; } + + double distf(const double E, const double p) { + return exp(-E/dispz - 0.5*p*p/dispx) * fnorm; + } + + double dfde(const double E, const double p) { + return -exp(-E/dispz - 0.5*p*p/dispx)/dispz * fnorm; + } + + double dfdv(const double E, const double p) { + return -exp(-E/dispz - 0.5*p*p/dispx)*p/dispx * fnorm; + } + +}; + + +class Sech2Halo : public OneDModelTable +{ +private: + double h, rho0; + double dispz, dispx; + double dratio, hratio; + double hh, rho0h; + double norm, hmax; + + bool model_computed; + + static double HMAX; + + void reset(); + +public: + + static int NTABLE; + static double OFFSET; + static bool MU; + + Sech2Halo(void) { + dispz = 1.0; + dispx = 1.0; + dratio = 0.0; + hratio = 1.0; + + dist_defined = false; + model_computed = false; + } + + Sech2Halo(const double DISPZ, const double DRATIO, const double HRATIO, + const double DISPX=1.0); + + double get_pot(const double z) { + if (!model_computed) reset(); + return OneDModelTable::get_pot(z) + 4.0*M_PI*hh*hh*rho0h*log(cosh(z/hh)); + } + + double get_dpot(const double z) { + if (!model_computed) reset(); + return OneDModelTable::get_dpot(z) + 4.0*M_PI*hh*rho0h*tanh(z/hh); + } + + double get_dpot2(const double z) { + if (!model_computed) reset(); + double sech = 1.0/cosh(z/hh); + return OneDModelTable::get_dpot2(z) + 4.0*M_PI*rho0h*sech*sech; + } + + std::tuple get_pot_dpot(const double z) { + if (!model_computed) reset(); + auto [p, dp] = OneDModelTable::get_pot_dpot(z); + p += 4.0*M_PI*hh*hh*rho0h*log(cosh(z/hh)); + dp += 4.0*M_PI*hh*rho0h*tanh(z/hh); + return {p, dp}; + } + + + double get_min_radius(void) { return 0.0; } + double get_max_radius(void) { return hmax*h; } + double get_scale_height(void) { return h; } + double get_scale_height_halo(void) { return hh; } + double get_rho0(void) { return rho0; } + double get_rho0_halo(void) { return rho0h; } + + static void set_hmax(double hmax) { HMAX = hmax; } + + double distf(const double E, const double p=0.0) { + if (!model_computed) reset(); + return exp(-E/dispz - 0.5*p*p/dispx) * norm; + } + + double dfde(const double E, const double p=0.0) { + if (!model_computed) reset(); + return -exp(-E/dispz - 0.5*p*p/dispx)/dispz * norm; + } + + double dfdv(const double E, const double p=0.0) { + if (!model_computed) reset(); + return -exp(-E/dispz - 0.5*p*p/dispx)*p/dispx * norm; + } + +}; + + +#endif + diff --git a/utils/ICs/massmodel1d.cc b/utils/ICs/massmodel1d.cc new file mode 100755 index 000000000..858c55faa --- /dev/null +++ b/utils/ICs/massmodel1d.cc @@ -0,0 +1,367 @@ +/***************************************************************************** + * Description: + * ----------- + * + * These routines computes massmodels for 1-d slab + * + * + * Call sequence: + * ------------- + * + * Parameters: + * ---------- + * + * x as above + * + * Returns: + * ------- + * + * Value + * + * Notes: + * ----- + * + * + * By: + * -- + * + * MDW 11/13/88 + * + ***************************************************************************/ + + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +double Sech2::HMAX=1.0e6; +double Sech2mu::HMAX=1.0e6; +double Sech2Halo::HMAX=3.0e1; + +OneDModelTable::OneDModelTable(std::string filename, int PARM) +{ + int i, imodel; + char line[144]; + double radius; + + std::istringstream iline(line); + + std::ifstream in(filename); + if (!in) { + std::cerr << "Error opening: " << filename << " . . . quitting" + << std::endl; + exit(-1); + } + + // Read header + + in.getline((char *)line, 144); + while (string(line).find_first_of("!#") != string::npos) + in.getline((char *)line, 144); + + + // Assign space for model + iline >> imodel; + + std::vector z(imodel), d(imodel), m(imodel), p(imodel); + + for (int i=0; i> z[i]; + iline >> d[i]; + iline >> m[i]; + iline >> p[i]; + } + + if (PARM) { + in.getline(line, 144); + + istringstream ins(line); + string word; + while (1) { + ins >> word; + if (ins.good()) params.push_back(atof(word.c_str())); + } + } + + // Compute splines + dens = Spline1d(z, d, 0.0, 0.0); + mass = Spline1d(z, m, 0.0, 0.0); + pot = Spline1d(z, p, 0.0, 2.0*M_PI*m[imodel-1]); + + even = 0; + if (fabs((z[1]-z[0]) - (z[2] - z[1])) < 1.0e-6) even = 1; + +} + +double OneDModelTable::get_mass(const double z) +{ + double zz = fabs(z); + if (zz>mass.xhi()) return mass.eval(mass.xhi()); + + return mass.eval(zz); +} + +double OneDModelTable::get_density(const double z) +{ + double zz = fabs(z); + if (z>dens.xhi()) return 0.0; + + return dens.eval(zz); +} + +double OneDModelTable::get_pot(const double z) +{ + double zz = fabs(z); + + if (zz>pot.xhi()) { + double z0 = pot.xlo(), z1 = pot.xhi(); + double dz = (z1 - z0)*0.01; + double p1 = pot.eval(z1), pm = pot.eval(z1-dz); + return p1 + (p1 - pm)/dz * (zz - z1); + } + + return pot.eval(zz); +} + +double OneDModelTable::get_dpot(const double z) +{ + double ans, dum; + double zz = fabs(z); + + if (zz>pot.xhi()) { + double z0 = pot.xlo(), z1 = pot.xhi(); + double dz = (z1 - z0)*0.01; + double p1 = pot.eval(z1), pm = pot.eval(z1-dz); + ans = (p1 - pm)/dz; + } else { + ans = pot.deriv(zz); + } + + return ans*z/(zz+1.0e-18); +} + +std::tuple OneDModelTable::get_pot_dpot(const double z) +{ + double zz = fabs(z), ur, dur; + + if (zz>pot.xhi()) { + double z0 = pot.xlo(), z1 = pot.xhi(); + double dz = (z1 - z0)*0.01; + double p1 = pot.eval(z1), pm = pot.eval(z1-dz); + ur = p1 + (p1 - pm)/dz * (zz - z1); + dur = (p1 - pm)/dz; + } else { + ur = pot.eval(zz); + dur = pot.deriv(zz); + } + + dur *= z/(zz+1.0e-18); + + return {ur, dur}; +} + +double OneDModelTable::get_dpot2(const double z) +{ + double ans; + double zz = fabs(z); + + if (zz>pot.xhi()) + ans = 0.0; + else + ans = dens.eval(zz); + + return 4.0*M_PI*ans; +} + +void LowIso::setup_model(void) +{ + w0 = params[0]; + Bfac = params[1]; + betak = 1.0/params[2]; + gammak = params[3] / (8.0*sqrt(2.0)*M_PI); + + normx = 1.0/sqrt(2.0*M_PI*dispx); +} + +double LowIso::distf(const double E, const double V) +{ + if (E>0.0) + return 0.0; + else + return gammak*(exp(-betak*E) - 1.0) * normx * exp(-0.5*V*V/dispx); +} + +double LowIso::dfde(const double E, const double V) +{ + if (E>0.0) + return 0.0; + else + return -betak*gammak*exp(-betak*E) * normx * exp(-0.5*V*V/dispx);; +} + +double LowIso::dfdv(const double E, const double V) +{ + if (E>0.0) + return 0.0; + else + return -normx*exp(-0.5*V*V)*V/dispx * gammak*(exp(-betak*E) - 1.0); +} + + +double LowIso::get_pot(const double z) +{ + double zmax = get_max_radius(); + + if (fabs(z) > zmax) + return OneDModelTable::get_pot(zmax) + 2.0*M_PI*get_mass(zmax)*(z-zmax) + + Bfac*(z*z - zmax*zmax); + else + return OneDModelTable::get_pot(z); +} + +double LowIso::get_dpot(const double z) +{ + double zmax = get_max_radius(); + + if (fabs(z) > zmax) + return 2.0*M_PI*get_mass(zmax)*z/(fabs(z)+1.0e-16) + 2.0*Bfac*z; + else + return OneDModelTable::get_dpot(z); +} + +double LowIso::get_dpot2(const double z) +{ + return OneDModelTable::get_dpot2(z) + 2.0*Bfac; +} + +std::tuple LowIso::get_pot_dpot(const double z) +{ + double zmax = get_max_radius(); + + if (fabs(z) > zmax) { + double ur = OneDModelTable::get_pot(zmax) + 2.0*M_PI*get_mass(zmax)*(z-zmax) + + Bfac*(z*z - zmax*zmax); + double dur = 2.0*M_PI*get_mass(zmax)*z/(fabs(z)+1.0e-16) + 2.0*Bfac*z; + return {ur, dur}; + } + else + return OneDModelTable::get_pot_dpot(z); +} + +static double halo_fac, halo_height; + +bool Sech2Halo::MU = true; +int Sech2Halo::NTABLE = 400; +double Sech2Halo::OFFSET = 1.0e-4; + +Sech2Halo::Sech2Halo(const double DISPZ, const double DRATIO, + const double HRATIO, const double DISPX) +{ + dispz = DISPZ; + dispx = DISPX; + dratio = DRATIO; + hratio = HRATIO; + + h = 1.0; + rho0 = 1.0; + hh = hratio * h; + rho0h = dratio * rho0; + + dist_defined = false; + + reset(); +} + + +void Sech2Halo::reset() +{ + even = 1; + hmax = HMAX*h; + halo_height = hh/h; + halo_fac = halo_height*halo_height * rho0h/rho0; + + std::vector x(NTABLE), d(NTABLE), m(NTABLE), p(NTABLE); + + double w1, dz = hmax/(NTABLE-1); + double step = dz/100.0; + + x[0] = 0.0; + d[0] = rho0; + m[0] = p[0] = 0.0; + + double z, lastz=OFFSET, tol=1.0e-6; + + w1 = 2.0*halo_fac*log(cosh(lastz/halo_height)); + Eigen::VectorXd y(2); + y(0) = exp(-w1)*lastz*lastz*(1.0 - exp(-w1)*lastz*lastz/6.0); + y(1) = 2.0*exp(-w1)*lastz*(1.0 - exp(-w1)*lastz*lastz/2.0); + + Eigen::VectorXd dy(2); + + auto halo_derivs = [&](double x, ODE::RK4::Vref y) + { + dy(0) = y(1); + double w1 = 2.0*halo_fac*log(cosh(x/halo_height)); + dy(1) = 2.0*exp(-w1-y(0)); + return ODE::RK4::Vref(dy); + }; + + ODE::RK4 rk4(halo_derivs); + + for (int i=1; i= z) break; + } + + p[i] = y[0] * dispz; + w1 = 2.0*halo_fac*log(cosh(z/halo_height)); + d[i] = rho0*exp(-w1-y[0]); + m[i] = y[1]; + + lastz = z; + } + + // Scaling + if (MU) { + double mfac = m[NTABLE-1]; + for (auto & v : d) v *= 2.0*M_PI/(dispz*mfac*mfac)/rho0; + for (auto & m : d) m /= mfac; + rho0 = 2.0*M_PI/(dispz*mfac*mfac); + } + else { + for (auto & v : d) v /= rho0; + for (auto & v : m) v *= sqrt(0.5*dispz/M_PI); + + rho0 = 1.0; + } + + h = sqrt(dispz/(2.0*M_PI*rho0)); + rho0h = rho0 * dratio; + hh = h * hratio; + + for (auto & v : x) v *= h; + + dens = Spline1d(x, d, 0.0, 0.0); + mass = Spline1d(x, m, 0.0, 0.0); + pot = Spline1d(x, p, 0.0, p[NTABLE-1]/x[NTABLE-1]); + + norm = d[0]/( sqrt(2.0*M_PI*dispz) ); + model_computed = true; + dist_defined = true; +} From a9011a0c9da7f228d1b04604a7cfd0f46949458c Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 2 Apr 2024 12:10:57 -0400 Subject: [PATCH 062/167] Updated usage flags [no ci] --- utils/ICs/genslab.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils/ICs/genslab.cc b/utils/ICs/genslab.cc index 3cf773070..94926107c 100755 --- a/utils/ICs/genslab.cc +++ b/utils/ICs/genslab.cc @@ -2,7 +2,7 @@ * Description: * ----------- * - * Generate sech^2 slab in a unit sqaure + * Generate slab initial conditions in a unit sqaure * * * Call sequence: @@ -50,7 +50,6 @@ main(int argc, char **argv) std::string outfile, config, modfile, modelType; bool Mu; - // Parse command line // std::string message = "Generate unit-box slab initial conditions\n"; From d839e2645cd1629cd188604f1d5c6774ea7b369c Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 2 Apr 2024 13:21:43 -0400 Subject: [PATCH 063/167] Give the single process/standlong SL solver same parameters as parallel implementation [no ci] --- exputil/SLGridMP2.cc | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 44b681702..9415122b0 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -2629,20 +2629,17 @@ void SLGridSlab::get_force(Eigen::VectorXd& vec, double x, int kx, int ky, int w void SLGridSlab::compute_table(struct TableSlab* table, int KX, int KY) { - - double cons[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - double tol[6] = {1.0e-4,1.0e-5, 1.0e-4,1.0e-5, 1.0e-4,1.0e-5}; - int VERBOSE=0; - integer NUM, N; - logical type[8] = {1, 0, 0, 0, 1, 0, 0, 0}; + double cons[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ZBEG, zmax}; + double tol[6] = {1.0e-6, 1.0e-7, 1.0e-6,1.0e-7, 1.0e-6,1.0e-7}; + logical type[8] = {1, 0, 0, 0, 1, 0, 0, 0}; logical endfin[2] = {1, 1}; - + integer NUM, N; + int VERBOSE=0; + #ifdef DEBUG_SLEDGE if (myid==0) VERBOSE = SLEDGE_VERBOSE; #endif - cons[6] = ZBEG; - cons[7] = zmax; NUM = numz; // Divide total functions into symmetric // and antisymmetric (keeping equal number @@ -2962,14 +2959,12 @@ void SLGridSlab::init_table(void) void SLGridSlab::compute_table_worker(void) { - // double cons[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - // double tol[6] = {1.0e-4,1.0e-5, 1.0e-4,1.0e-5, 1.0e-4,1.0e-5}; + double tol[6] = {1.0e-6,1.0e-7, 1.0e-6,1.0e-7, 1.0e-6,1.0e-7}; + logical type[8] = {1, 0, 0, 0, 1, 0, 0, 0}; + logical endfin[2] = {1, 1}; double cons[8]; - double tol[6] = {1.0e-6,1.0e-7, 1.0e-6,1.0e-7, 1.0e-6,1.0e-7}; int VERBOSE=0; integer NUM; - logical type[8] = {1, 0, 0, 0, 1, 0, 0, 0}; - logical endfin[2] = {1, 1}; struct TableSlab table; int KX, KY, N; From 3f6e62e1800c5281d02ae1cd53c03c80c95fc094 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 3 Apr 2024 10:04:36 -0400 Subject: [PATCH 064/167] Check in for the Slab::getBasis() member. Forgot this in the original commit. [no ci] --- expui/BiorthBasis.H | 8 +++++- expui/BiorthBasis.cc | 62 ++++++++++++++++++++++++++++++++++++++++++ pyEXP/BasisWrappers.cc | 39 ++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/expui/BiorthBasis.H b/expui/BiorthBasis.H index cc2727c4a..ce88f02f7 100644 --- a/expui/BiorthBasis.H +++ b/expui/BiorthBasis.H @@ -585,7 +585,7 @@ namespace BasisClasses public: using BasisMap = std::map; - using BasisArray = std::vector>; + using BasisArray = std::vector>>; private: @@ -689,6 +689,12 @@ namespace BasisClasses //! Return current maximum harmonic order in expansion Eigen::Vector3i getNmax() { return {nmaxx, nmaxy, nmaxz}; } + //! Return potential-density pair of a vector of a vector of 1d + //! basis-function grids for the SL slab, linearly spaced + //! between [zmin, zmax] + BasisArray getBasis + (double zmin=-1.0, double zmax=0.5, int numgrid=2000); + //! Compute the orthogonality of the basis by returning inner //! produce matrices std::vector orthoCheck(); diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index 64a9f9366..b13e39e56 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -2281,6 +2281,68 @@ namespace BasisClasses return {0, den, den, 0, pot, pot, potr, pott, potp}; } + + Slab::BasisArray Slab::getBasis + (double zmin, double zmax, int numgrid) + { + // Assign storage for returned basis array. The Maximum + // wavenumber for the SLGridSlab is the maximum of the X and Y + // wavenumbers. + int nnmax = std::max(nmaxx, nmaxy); + + BasisArray ret (nnmax+1); // X wavenumbers + for (auto & v1 : ret) { + v1.resize(nnmax+1); // Y wavenumbers + + for (auto & v2 : v1) { + v2.resize(nmaxz); // Z basis + + for (auto & u : v2) { + u["potential"].resize(numgrid); // Potential + u["density" ].resize(numgrid); // Density + u["zforce" ].resize(numgrid); // Vertical force + } + } + } + + // Vertical grid spacing + double dz = (zmax - zmin)/(numgrid-1); + + // Basis evaluation storage + Eigen::VectorXd vpot(nmaxz), vfrc(nmaxz), vden(nmaxz); + + // Construct the tensor + for (int ix=0; ix<=nnmax; ix++) { + + for (int iy=0; iy<=nmaxx; iy++) { + + for (int i=0; i=iy) { + ortho->get_pot (vpot, z, ix, iy); + ortho->get_force(vfrc, z, ix, iy); + ortho->get_dens (vden, z, ix, iy); + } else { + ortho->get_pot (vpot, z, iy, ix); + ortho->get_force(vfrc, z, iy, ix); + ortho->get_dens (vden, z, iy, ix); + } + + for (int n=0; n Slab::orthoCheck() { return ortho->orthoCheck(); diff --git a/pyEXP/BasisWrappers.cc b/pyEXP/BasisWrappers.cc index ae339f9a7..2eb64e749 100644 --- a/pyEXP/BasisWrappers.cc +++ b/pyEXP/BasisWrappers.cc @@ -1465,6 +1465,45 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); Cube the new instance )", py::arg("YAMLstring")) + .def("getBasis", &BasisClasses::Slab::getBasis, + R"( + Get basis functions + + Evaluate the potential-density basis functions on a linearly spaced grid for + inspection. The structure is a three-dimensional grid of dimension (nmaxx+1) by + (nmaxy+1) by (nmaxz) each pointing to a dictionary of 1-d arrays ('potential', + 'density', 'rforce') of dimension numz. + + Parameters + ---------- + zmin : float, default=-1.0 + minimum height + zmax : float, default=1.0 + maximum height + numz : int, default=400 + number of equally spaced output points + + Returns + ------- + list(list(dict)) + dictionaries of basis functions as lists indexed by nx, ny, nz + + Example + ------- + To plot the nx=ny=0 basis functions, you might use the following code: + + >>> mat = slab_basis.getBasis(-0.5, 0.5, 200) + >>> z = np.linspace(-0.5, 0.5, 200) + >>> for n in range(len(mat[0][0])): + >>> plt.plot(z, mat[0][0][n]['potential'], label=str(n)) + >>> plt.legend() + >>> plt.xlabel('z') + >>> plt.ylabel('potential') + >>> plt.show() + )", + py::arg("zmin")=-1.0, + py::arg("zmax")=1.0, + py::arg("numz")=400) .def("orthoCheck", [](BasisClasses::Cube& A) { return A.orthoCheck(); From cec90b1c58619b759961cc1b14d54836e1d63597 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 3 Apr 2024 13:48:33 -0400 Subject: [PATCH 065/167] Add sign convention to SlabSL basis [no ci] --- exputil/SLGridMP2.cc | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 9415122b0..d01e895e0 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -1224,7 +1224,7 @@ void SLGridSph::compute_table(struct TableSph* table, int l) // Choose sign conventions for the ef table // - int nfid = std::min(nevsign, N) - 1; + int nfid = std::min(nevsign, NUM) - 1; Eigen::VectorXi sgn = Eigen::VectorXi::Ones(N); for (int j=0; j(nevsign, N) - 1; + int nfid = std::min(nevsign, NUM) - 1; Eigen::VectorXi sgn = Eigen::VectorXi::Ones(N); for (int j=0; jev.resize(nmax); for (int i=0; iev[i*2] = ev[i]; - table->ef.resize(nmax, numz); - for (int i=0; ief(j*2, i) = ef[j*NUM+i]; + // Choose sign conventions for the ef table + // + { + int nfid = std::min(nevsign, NUM/2) - 1; + Eigen::VectorXi sgn = Eigen::VectorXi::Ones(N); + for (int j=0; jef.resize(nmax, numz); + for (int i=0; ief(j*2, i) = ef[j*NUM+i] * sgn(j); + } } @@ -2914,9 +2924,19 @@ void SLGridSlab::compute_table(struct TableSlab* table, int KX, int KY) for (int i=0; iev[i*2+1] = ev[i]; - for (int i=0; ief(j*2+1, i) = ef[j*NUM+i]; + // Choose sign conventions for the ef table + // + { + int nfid = std::min(nevsign, NUM/2) - 1; + Eigen::VectorXi sgn = Eigen::VectorXi::Ones(N); + for (int j=0; jef(j*2+1, i) = ef[j*NUM+i] * sgn(j); + } } // Correct for symmetrizing From 3662a4c148c6c4eaec7fc805b1d4797933f678a7 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 3 Apr 2024 15:07:22 -0400 Subject: [PATCH 066/167] Choose a better position in vertical arrays for sign convention in SlGridSlab. [no ci] --- exputil/SLGridMP2.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index d01e895e0..436851bfb 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -2806,10 +2806,10 @@ void SLGridSlab::compute_table(struct TableSlab* table, int KX, int KY) // Choose sign conventions for the ef table // { - int nfid = std::min(nevsign, NUM/2) - 1; + int nfid = std::min(nevsign, NUM) - 1; Eigen::VectorXi sgn = Eigen::VectorXi::Ones(N); for (int j=0; jef.resize(nmax, numz); @@ -2927,10 +2927,10 @@ void SLGridSlab::compute_table(struct TableSlab* table, int KX, int KY) // Choose sign conventions for the ef table // { - int nfid = std::min(nevsign, NUM/2) - 1; + int nfid = std::min(nevsign, NUM) - 1; Eigen::VectorXi sgn = Eigen::VectorXi::Ones(N); for (int j=0; j Date: Wed, 3 Apr 2024 16:25:28 -0400 Subject: [PATCH 067/167] Forgot the sign convention in the MPI version [no ci] --- exputil/SLGridMP2.cc | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 436851bfb..f1b4acbd9 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -3177,13 +3177,21 @@ void SLGridSlab::compute_table_worker(void) table.ev.resize(nmax); for (int i=0; i(nevsign, NUM) - 1; + Eigen::VectorXi sgn = Eigen::VectorXi::Ones(N); + for (int j=0; j(nevsign, NUM) - 1; + Eigen::VectorXi sgn = Eigen::VectorXi::Ones(N); + for (int j=0; j Date: Wed, 3 Apr 2024 16:32:41 -0400 Subject: [PATCH 068/167] Forgot the sign convention in the MPI version (fix typo in even fcts) [no ci] --- exputil/SLGridMP2.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index f1b4acbd9..489bdb7d7 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -2819,7 +2819,6 @@ void SLGridSlab::compute_table(struct TableSlab* table, int KX, int KY) } } - // Odd BC, Inner zero value cons[0] = 1.0; cons[2] = 0.0; @@ -3188,7 +3187,7 @@ void SLGridSlab::compute_table_worker(void) for (int i=0; i Date: Wed, 3 Apr 2024 17:55:32 -0400 Subject: [PATCH 069/167] Fixed missing allocation for even/odd sign version in MPI. Checked and shoud be good to go. [no ci] --- exputil/SLGridMP2.cc | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 489bdb7d7..c74f84115 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -2798,9 +2798,14 @@ void SLGridSlab::compute_table(struct TableSlab* table, int KX, int KY) } } - // Load table + // Allocate memory for table + // table->ev.resize(nmax); + table->ef.resize(nmax, numz); + + // Load table + // for (int i=0; iev[i*2] = ev[i]; // Choose sign conventions for the ef table @@ -2812,7 +2817,6 @@ void SLGridSlab::compute_table(struct TableSlab* table, int KX, int KY) if (ef[j*NUM+nfid]<0.0) sgn(j) = -1; } - table->ef.resize(nmax, numz); for (int i=0; ief(j*2, i) = ef[j*NUM+i] * sgn(j); @@ -3171,9 +3175,14 @@ void SLGridSlab::compute_table_worker(void) } } - // Load table + // Allocate memory for table (even and odd) + // table.ev.resize(nmax); + table.ef.resize(nmax, numz); + + // Load table (even) + // for (int i=0; i Date: Tue, 16 Apr 2024 09:37:50 -0400 Subject: [PATCH 070/167] Remove Cooling test code --- utils/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index c84384ff8..0da9da223 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -1,5 +1,4 @@ add_subdirectory(Analysis) -add_subdirectory(Cooling) add_subdirectory(ICs) add_subdirectory(PhaseSpace) add_subdirectory(SL) From dd4a3a05629790155f625ae93bbbf34d01722e7b Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 09:41:27 -0400 Subject: [PATCH 071/167] Remove ENABLE_DSMC configuration flag --- CMakeLists.txt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bb4763ad..eadaa76e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,6 @@ option(ENABLE_SLURM "Enable SLURM checkpointing support" FALSE) option(ENABLE_XDR "Enable RPC/XDR support for Tipsy standard" FALSE) option(ENABLE_VTK "Configure VTK if available" FALSE) option(ENABLE_CUDA_SINGLE "Use real*4 instead of real*8 for CUDA" FALSE) -option(ENABLE_DSMC "Enable DSMC module" FALSE) option(ENABLE_USER "Enable basic user modules" ON) option(ENABLE_SLCHECK "Enable *careful* Sturm-Liouville solutions" TRUE) option(ENABLE_TESTS "Enable build tests for EXP, pyEXP and helpers" ON) @@ -149,9 +148,6 @@ endif() if(PNG_FOUND AND ENABLE_PNG) set(HAVE_LIBPNGPP TRUE) endif() -if(ENABLE_DSMC) - set(DSMC_ENABLED 1) -endif() if(ENABLE_CUDA_SINGLE) add_compile_definitions(O_SINGLE=1) endif() @@ -237,10 +233,6 @@ endif() add_subdirectory(extern/user-modules) -if (ENABLE_DSMC) - add_subdirectory(extern/DSMC/src) -endif() - # Build the tests; set ENABLE_TEST=OFF to disable if(ENABLE_TESTS) include(CTest) From b752c9bb1eb94c23d92cceabaf3d1ecf822c6a2a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 09:42:07 -0400 Subject: [PATCH 072/167] Remove DSMC-based toggles --- src/CMakeLists.txt | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a9ebe6cf8..e758dd6c8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,8 +3,8 @@ set(CUDA_SRC) if (ENABLE_CUDA) list(APPEND CUDA_SRC cudaPolarBasis.cu cudaSphericalBasis.cu cudaCylinder.cu cudaEmpCylSL.cu cudaComponent.cu NVTX.cc - cudaHOT.cu cudaIncpos.cu cudaIncvel.cu cudaMultistep.cu - cudaOrient.cu cudaBiorthCyl.cu cudaCube.cu) + cudaIncpos.cu cudaIncvel.cu cudaMultistep.cu cudaOrient.cu + cudaBiorthCyl.cu cudaCube.cu) endif() set(exp_SOURCES Basis.cc Bessel.cc CBrock.cc Component.cc @@ -19,28 +19,20 @@ set(exp_SOURCES Basis.cc Bessel.cc CBrock.cc Component.cc OutMulti.cc OutRelaxation.cc OrbTrace.cc OutDiag.cc OutLog.cc OutVel.cc OutCoef.cc multistep.cc parse.cc Slab.cc SlabSL.cc step.cc tidalField.cc ultra.cc ultrasphere.cc MPL.cc OutFrac.cc OutCalbr.cc - ParticleFerry.cc pCell.cc chkSlurm.c chkTimer.cc pHOT.cc - GravKernel.cc ${CUDA_SRC} CenterFile.cc PolarBasis.cc FlatDisk.cc - signals.cc) + ParticleFerry.cc chkSlurm.c chkTimer.cc GravKernel.cc ${CUDA_SRC} + CenterFile.cc PolarBasis.cc FlatDisk.cc signals.cc) set(common_INCLUDE_DIRS $ $ $ - $ $ ${CMAKE_BINARY_DIR} ${DEP_INC} ${CMAKE_CURRENT_SOURCE_DIR} ${EIGEN3_INCLUDE_DIR}) -set(DSMC_LIBS) -if (ENABLE_DSMC) - # add_subdirectory(${PROJECT_SOURCE_DIR}/extern/DSMC/src DSMC) - list(APPEND DSMC_LIBS expdsmc) -endif() - -set(common_LINKLIB ${DSMC_LIBS} exputil expui OpenMP::OpenMP_CXX - MPI::MPI_CXX yaml-cpp ${VTK_LIBRARIES}) +set(common_LINKLIB exputil expui OpenMP::OpenMP_CXX MPI::MPI_CXX + yaml-cpp ${VTK_LIBRARIES}) if(PNG_FOUND) list(APPEND common_LINKLIB PNG::PNG) From 1a834d54c5156797c18fafea49c95d5ca73bd73b Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 09:42:59 -0400 Subject: [PATCH 073/167] Remove pHOT support and add access to particle map --- src/Component.H | 45 +++++++++++++++++++++++++++++---------------- src/Component.cc | 29 +++++++++-------------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/Component.H b/src/Component.H index ae0b1b687..fd4339ca4 100644 --- a/src/Component.H +++ b/src/Component.H @@ -24,8 +24,6 @@ #endif -class pHOT; - //! Structure used to sort old & new indices for load balancing struct loadb_datum { @@ -191,7 +189,6 @@ class Component friend class ScatterMFP; friend class OrbTrace; friend class ComponentContainer; - friend class pHOT; private: @@ -306,9 +303,6 @@ protected: //! Points to associated particles using sequence number PartMap particles; - //! Oct tree - pHOT *tree; - //! Level occupation output int nlevel; @@ -638,7 +632,7 @@ public: return particles; } - //! Access to particle + //! Access to particle as a pointer Particle *Part(unsigned long i) { PartMap::iterator tp = particles.find(i); if (tp == particles.end()) { @@ -647,15 +641,43 @@ public: return tp->second.get(); } + //! Access to particle via the shared pointer + PartPtr partPtr(unsigned long i) { + PartMap::iterator tp = particles.find(i); + if (tp == particles.end()) { + throw BadIndexException(i, particles.size(), __FILE__, __LINE__); + } + return tp->second; + } + + //! Check if particle index is in the component + bool partExists(unsigned long i) { + PartMap::iterator tp = particles.find(i); + if (tp == particles.end()) { + return false; + } + return true; + } + //! Generate new particle: this adds a new particle and returns a //! pointer to the same particle. It is the caller's responsbility //! to populate the phase space and particle attributes with sane //! values. Particle *GetNewPart(); + //! Add a particle to the component + void AddPart(PartPtr p); + //! Remove a particle from the component void DestroyPart(PartPtr p); + //! Erase a particle with no level list check + void ErasePart(PartPtr p) + { + particles.erase(p->indx); + nbodies = particles.size(); + } + //! Particle vector size unsigned Number() { return particles.size(); @@ -895,21 +917,12 @@ public: //! Print out the level lists to stdout for diagnostic purposes void print_level_lists(double T); - //! Temporary access to tree (until I figure out what is really needed) - pHOT* Tree() { return tree; } - //! Running clock on the current potential/force evaluation Timer time_so_far; //! Time in potential/force computation so far double get_time_sofar() { return time_so_far.getTime(); } - //! Create a hashed octree for this component - void HOTcreate(std::set spec_list); - - //! Delete the hashed octree for this component - void HOTdelete(); - //! Check for indexing inline bool Indexing() { return indexing; } diff --git a/src/Component.cc b/src/Component.cc index 609c1b07e..d27c86cf5 100644 --- a/src/Component.cc +++ b/src/Component.cc @@ -23,7 +23,6 @@ #include #include #include -#include #include #include "expand.H" @@ -264,8 +263,6 @@ Component::Component(YAML::Node& CONF) coa_lev = vector(3*(multistep+1), 0); com_mas = vector(multistep+1, 0); - tree = 0; - pbuf.resize(PFbufsz); // Enter unset defaults in YAML conf @@ -368,19 +365,6 @@ void Component::set_default_values() } -void Component::HOTcreate(std::set spec_list) -{ - delete tree; - tree = new pHOT(this, spec_list); -} - - -void Component::HOTdelete() -{ - delete tree; -} - - class thrd_pass_reset { public: @@ -788,8 +772,6 @@ Component::Component(YAML::Node& CONF, istream *in, bool SPL) : conf(CONF) reset_level_lists(); - tree = 0; - pbuf.resize(PFbufsz); } @@ -1276,8 +1258,6 @@ Component::~Component(void) delete [] com0; delete [] cov0; delete [] acc0; - - delete tree; } void Component::read_bodies_and_distribute_ascii(void) @@ -3969,3 +3949,12 @@ void Component::DestroyPart(PartPtr p) nbodies--; modified++; } + +void Component::AddPart(PartPtr p) +{ + particles[p->indx] = p; + + // Refresh size of local particle list + nbodies = particles.size(); +} + From 2a15d42b671e1bdda7fc2cdf37b67584d05358af Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 09:43:45 -0400 Subject: [PATCH 074/167] Remove TreeDSMC source method for force parsing list --- src/ExternalCollection.cc | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/ExternalCollection.cc b/src/ExternalCollection.cc index abc0be485..4ae89919f 100644 --- a/src/ExternalCollection.cc +++ b/src/ExternalCollection.cc @@ -21,10 +21,6 @@ #include #include -#if DSMC_ENABLED>0 -#include -#endif - ExternalCollection::ExternalCollection(void) { // Do nothing @@ -88,11 +84,6 @@ void ExternalCollection::initialize() force_list.insert(force_list.end(), new PeriodicBC(node)); -#if DSMC_ENABLED>0 - else if ( !name.compare("TreeDSMC") ) - - force_list.insert(force_list.end(), new TreeDSMC(node)); -#endif else if ( !name.compare("HaloBulge") ) force_list.insert(force_list.end(), new HaloBulge(node)); From e5990225bbbd190d28ca11ff45a83b05f4ef5cc9 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 09:44:12 -0400 Subject: [PATCH 075/167] Remove pHOT dependence --- src/ParticleFerry.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ParticleFerry.cc b/src/ParticleFerry.cc index c060bc5e7..16418dba7 100644 --- a/src/ParticleFerry.cc +++ b/src/ParticleFerry.cc @@ -5,7 +5,7 @@ #include "global.H" #include "ParticleFerry.H" -#include "pHOT.H" +// #include "pHOT.H" // #define DEBUG @@ -452,3 +452,4 @@ void ParticleFerry::bufferKeyCheck() return; } + From a0172cb7fc9ade66b88a8b19b60bbda511e5bc7a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 09:44:37 -0400 Subject: [PATCH 076/167] Move DSMC IC code to the modules utils --- utils/ICs/CMakeLists.txt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/utils/ICs/CMakeLists.txt b/utils/ICs/CMakeLists.txt index f10de0787..e5aa9e716 100644 --- a/utils/ICs/CMakeLists.txt +++ b/utils/ICs/CMakeLists.txt @@ -27,16 +27,6 @@ set(common_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/.. ${HDF5_INCLUDE_DIRS}) -if(ENABLE_DSMC) - list(APPEND bin_PROGRAMS makeIon phIon phConv) - list(APPEND common_INCLUDE - $) - add_executable(makeIon makeIonIC.cc - $) - add_executable(phIon ph_ion.cc) - add_executable(phConv ph_conv.cc) -endif() - add_executable(shrinkics shrinkics.cc) add_executable(gensph gensph.cc SphericalSL.cc EllipForce.cc From 5fa0b4cb6733e96d5650de853658fd4f80d193bc Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 09:45:17 -0400 Subject: [PATCH 077/167] Move all remaining DSMC specific code to the module --- src/cudaHOT.cu | 467 --- src/pCell.H | 237 -- src/pCell.cc | 1021 ------ src/pHOT.H | 470 --- src/pHOT.cc | 5644 ----------------------------- src/pHOT_types.H | 113 - src/user/UserDSMC.H | 147 - src/user/UserDSMC.cc | 663 ---- utils/Analysis/PlotColl_2.py | 93 - utils/Analysis/allEnergy.py | 68 - utils/Analysis/allTemps.py | 43 - utils/Analysis/checkICDens.py | 58 - utils/Analysis/checkICWeights.py | 121 - utils/Analysis/collide_histo.py | 95 - utils/Analysis/econs.py | 195 - utils/Analysis/electronD | 115 - utils/Analysis/getSpecies.py | 81 - utils/Analysis/ion_coll_ediag.py | 240 -- utils/Analysis/ion_coll_energy.py | 184 - utils/Analysis/ion_coll_number.py | 320 -- utils/Analysis/ion_coll_ratio.py | 400 -- utils/Analysis/ion_coll_smooth.py | 418 --- utils/Analysis/ion_coll_time.py | 392 -- utils/Analysis/ion_collide.py | 226 -- utils/Analysis/ion_dist.py | 232 -- utils/Analysis/ion_energy.py | 183 - utils/Analysis/ion_frac.py | 235 -- utils/Analysis/ion_frac_ch.py | 291 -- utils/Analysis/ion_list_number.py | 159 - utils/Analysis/ion_rate.py | 198 - utils/Analysis/ion_rate_ch.py | 216 -- utils/Analysis/ion_spec.py | 160 - utils/Analysis/multiSpecies | 107 - utils/Analysis/multispecies.py | 72 - utils/Analysis/plotColl.py | 260 -- utils/Analysis/plotEcons.py | 117 - utils/Analysis/plotHistoE.py | 114 - utils/Analysis/plotOUTLOG.py | 153 - utils/Analysis/plotSpecies.py | 243 -- utils/Analysis/plot_species.py | 252 -- utils/Analysis/psp_dist.py | 386 -- utils/Analysis/psp_distL.py | 387 -- utils/Analysis/readem.py | 61 - utils/Analysis/readsp.py | 50 - utils/Analysis/recompute_temps.py | 196 - utils/Analysis/speciesT.py | 72 - utils/Analysis/splitEnergy.py | 49 - utils/Analysis/splitEnergy2.py | 50 - utils/Analysis/splitEnergyN.py | 63 - utils/Analysis/splitTemps.py | 48 - utils/Analysis/splitTemps2.py | 60 - utils/Analysis/uptime.py | 36 - utils/Cooling/CMakeLists.txt | 25 - utils/Cooling/hctest.cc | 192 - utils/ICs/makeIon.config | 29 - utils/ICs/makeIonIC.cc | 2068 ----------- utils/ICs/ph_conv.cc | 214 -- utils/ICs/ph_ion.cc | 147 - 58 files changed, 18936 deletions(-) delete mode 100644 src/cudaHOT.cu delete mode 100644 src/pCell.H delete mode 100644 src/pCell.cc delete mode 100644 src/pHOT.H delete mode 100644 src/pHOT.cc delete mode 100644 src/pHOT_types.H delete mode 100644 src/user/UserDSMC.H delete mode 100644 src/user/UserDSMC.cc delete mode 100755 utils/Analysis/PlotColl_2.py delete mode 100755 utils/Analysis/allEnergy.py delete mode 100755 utils/Analysis/allTemps.py delete mode 100755 utils/Analysis/checkICDens.py delete mode 100755 utils/Analysis/checkICWeights.py delete mode 100755 utils/Analysis/collide_histo.py delete mode 100755 utils/Analysis/econs.py delete mode 100755 utils/Analysis/electronD delete mode 100755 utils/Analysis/getSpecies.py delete mode 100755 utils/Analysis/ion_coll_ediag.py delete mode 100755 utils/Analysis/ion_coll_energy.py delete mode 100755 utils/Analysis/ion_coll_number.py delete mode 100755 utils/Analysis/ion_coll_ratio.py delete mode 100755 utils/Analysis/ion_coll_smooth.py delete mode 100755 utils/Analysis/ion_coll_time.py delete mode 100755 utils/Analysis/ion_collide.py delete mode 100755 utils/Analysis/ion_dist.py delete mode 100755 utils/Analysis/ion_energy.py delete mode 100755 utils/Analysis/ion_frac.py delete mode 100755 utils/Analysis/ion_frac_ch.py delete mode 100755 utils/Analysis/ion_list_number.py delete mode 100755 utils/Analysis/ion_rate.py delete mode 100755 utils/Analysis/ion_rate_ch.py delete mode 100755 utils/Analysis/ion_spec.py delete mode 100755 utils/Analysis/multiSpecies delete mode 100755 utils/Analysis/multispecies.py delete mode 100755 utils/Analysis/plotColl.py delete mode 100755 utils/Analysis/plotEcons.py delete mode 100755 utils/Analysis/plotHistoE.py delete mode 100755 utils/Analysis/plotOUTLOG.py delete mode 100755 utils/Analysis/plotSpecies.py delete mode 100755 utils/Analysis/plot_species.py delete mode 100755 utils/Analysis/psp_dist.py delete mode 100755 utils/Analysis/psp_distL.py delete mode 100755 utils/Analysis/readem.py delete mode 100755 utils/Analysis/readsp.py delete mode 100755 utils/Analysis/recompute_temps.py delete mode 100755 utils/Analysis/speciesT.py delete mode 100755 utils/Analysis/splitEnergy.py delete mode 100755 utils/Analysis/splitEnergy2.py delete mode 100755 utils/Analysis/splitEnergyN.py delete mode 100755 utils/Analysis/splitTemps.py delete mode 100755 utils/Analysis/splitTemps2.py delete mode 100755 utils/Analysis/uptime.py delete mode 100644 utils/Cooling/CMakeLists.txt delete mode 100644 utils/Cooling/hctest.cc delete mode 100644 utils/ICs/makeIon.config delete mode 100644 utils/ICs/makeIonIC.cc delete mode 100644 utils/ICs/ph_conv.cc delete mode 100644 utils/ICs/ph_ion.cc diff --git a/src/cudaHOT.cu b/src/cudaHOT.cu deleted file mode 100644 index 2503e1442..000000000 --- a/src/cudaHOT.cu +++ /dev/null @@ -1,467 +0,0 @@ -// -*- C++ -*- - -#include -#include - -#include -#include -#include -#include - -#include - -#include - -// Debug keys across all nodes (uses MPI calls) -static bool DEBUG_KEYS = false; - - -__device__ -inline uint64_t split3( unsigned int a ) -{ - // we only use the first 21 bits - uint64_t x = a & 0x1fffff; - x = (x | x << 32) & 0x001f00000000ffff; // shift left 32 bits, OR with self, and 00011111000000000000000000000000000000001111111111111111 - x = (x | x << 16) & 0x001f0000ff0000ff; // shift left 32 bits, OR with self, and 00011111000000000000000011111111000000000000000011111111 - x = (x | x << 8 ) & 0x100f00f00f00f00f; // shift left 32 bits, OR with self, and 0001000000001111000000001111000000001111000000001111000000000000 - x = (x | x << 4 ) & 0x10c30c30c30c30c3; // shift left 32 bits, OR with self, and 0001000011000011000011000011000011000011000011000011000100000000 - x = (x | x << 2 ) & 0x1249249249249249; - return x; -} - -__device__ -inline uint64_t mortonEncode_mask( unsigned int* u ) -{ - uint64_t answer = 0; - answer |= split3(u[0]) | split3(u[1]) << 1 | split3(u[2]) << 2; - return answer; -} - -__device__ -inline uint64_t genKey( double x, double y, double z, int nbits ) -{ - const unsigned int maxI = 0x1fffff; - unsigned u[3]; - u[2] = x * maxI; - u[1] = y * maxI; - u[0] = z * maxI; - - uint64_t nkey = mortonEncode_mask(&u[0]); - - uint64_t place(1u); - place <<= 3*nbits; - unsigned shift = 63 - 3*nbits; - nkey >>= shift; - nkey += place; - - return nkey; -} - - -__global__ void computeKeysKernel(dArray x, - dArray y, - dArray z, - dArray k, - int nbits, int stride) -{ - const int tid = blockDim.x * blockIdx.x + threadIdx.x; - const int N = k._s; - - for (int s=0; s keywght) -{ - // Cuda propertices - cudaDeviceProp deviceProp; - cudaGetDeviceProperties(&deviceProp, cc->cudaDevice); - - // For diagnostics - Timer *timer_debug; - if (DEBUG_KEYS && myid==0) { - timer_debug = new Timer(); - timer_debug->start(); - } - - timer_keygenr.start(); - - size_t pN = cc->Particles().size(); - - keywght.resize(pN); // Return key-weight list - - std::vector x_h, y_h, z_h; - std::vector mask(pN, true); - - int I=0; - for (auto & v : cc->Particles()) { - - double X = (v.second->pos[0] + offset[0])/sides[0]; - double Y = (v.second->pos[1] + offset[1])/sides[1]; - double Z = (v.second->pos[2] + offset[2])/sides[2]; - - // Compute keys for in-bounds particles - if (X>0.0 and Y>0.0 and Z>0.0 and X<1.0 and Y<1.0 and Z<1.0) { - x_h.push_back(X); - y_h.push_back(Y); - z_h.push_back(Z); - } else { - mask[I] = false; - } - I++; - } - - size_t N = x_h.size(); - thrust::device_vector x_d = x_h; - thrust::device_vector y_d = y_h; - thrust::device_vector z_d = z_h; - thrust::device_vector k_d(N); - - int stride = N/BLOCK_SIZE/deviceProp.maxGridSize[0] + 1; - int gridSize = (N+BLOCK_SIZE*stride-1)/(BLOCK_SIZE*stride); - - computeKeysKernel<<>> - (toKernel(x_d), toKernel(y_d), toKernel(z_d), toKernel(k_d), - nbits, stride); - - thrust::host_vector k_h(k_d); - - I = 0; - oob.clear(); - for (auto & v : cc->Particles()) { - if (mask[I]) { - v.second->key = k_h[I]; - } else { - v.second->key = 0ull; - oob.insert(v.first); - } - keywght[I] = key_wght(k_h[I], 1.0); - I++; - } - - timer_keygenr.stop(); - timer_keysort.start(); - - spreadOOB(); - - // Sort the keys on the device - thrust::sort(k_d.begin(), k_d.end()); - k_h = k_d; // Now, get the sorted keys - - std::vector keys1(k_h.size()), keys; - std::copy(k_h.begin(), k_h.end(), keys1.begin()); - - parallelMergeOne(keys1, keys); - - vector keylist(keys.size()); - - for (unsigned i=0; i frate(numprocs); - - // Use an even rate - frate[0] = 1.0; - for (unsigned i=1; i - // for load balancing - // - - struct wghtDBL - { - bool operator()(const std::pair& a, - const std::pair& b) - { - return (a.second < b.second); - } - }; - - - if (myid==0) { - - // The overhead for computing these is small so - // no matter if they are not used below - - vector wbeg(numprocs), wfin(numprocs); // Weights for debugging - vector pbeg(numprocs), pfin(numprocs); // Counts for debugging - - // Compute the key boundaries in the partition - // - for (unsigned i=0; i::iterator - ret = lower_bound(keylist.begin(), keylist.end(), k, wghtDBL()); - kfin[i] = ret->first; - wfin[i] = ret->second; - pfin[i] = ret - keylist.begin(); - } - else { - kfin[i] = key_min; - wfin[i] = 0.0; - pfin[i] = 0; - } - } - - kfin[numprocs-1] = key_max; - wfin[numprocs-1] = 1.0; - pfin[numprocs-1] = keylist.size(); - - kbeg[0] = key_min; - wbeg[0] = 0.0; - pbeg[0] = 0; - for (unsigned i=1; i1) { - mdif /= numprocs; - mdif2 = sqrt((mdif2 - mdif*mdif*numprocs)/(numprocs-1)); - out << "---- mean wght = " << setw(10) << keylist.size() << endl - << "---- std. dev. = " << setw(10) << keylist.size() << endl; - } - } - } - - MPI_Bcast(&kbeg[0], numprocs, MPI_EXP_KEYTYPE, 0, MPI_COMM_WORLD); - MPI_Bcast(&kfin[0], numprocs, MPI_EXP_KEYTYPE, 0, MPI_COMM_WORLD); - - - if (DEBUG_KEYS) { // If true, print key totals - unsigned oobn = oobNumber(); - unsigned tkey1 = keys.size(), tkey0 = 0; - MPI_Reduce(&tkey1, &tkey0, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD); - if (myid==0) { - ofstream out(debugf.c_str(), ios::app); - out << endl - << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << "---- partitionKeys" << endl - << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << "---- list size = " << setw(10) << keylist.size() << endl - << "---- total keys = " << setw(10) << tkey0 << endl - << "---- total oob = " << setw(10) << oobn << endl - << "---- TOTAL = " << setw(10) << tkey0 + oobn << endl - << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << "---- Time (s) = " << setw(10) << timer_debug->stop() - << endl - << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << endl; - delete timer_debug; - } - } - - timer_keysort.stop(); - - if (DEBUG_KEYS) { // Deep debug output - static unsigned count = 0; - std::ostringstream sout; - sout << debugf << "." << myid << "." << count++; - ofstream out(sout.str()); - - for (unsigned i=0; i0 and keylist[i].first < keylist[i-1].first) - out << "####" << std::endl; - } - } // END: DEBUG_KEYS - -} - -// -// This routine combines two sorted vectors into one -// larger sorted vector -// -void pHOT::sortCombineOne(vector& one, vector& two, - vector& comb) -{ - int i=0, j=0; - int n = one.size()-1; - int m = two.size()-1; - - comb = vector(one.size()+two.size()); - - for (int k=0; k n) - comb[k] = two[j++]; - else if(j > m) - comb[k] = one[i++]; - else { - if(one[i] < two[j]) - comb[k] = one[i++]; - else - comb[k] = two[j++]; - } - } -} - -void pHOT::parallelMergeOne(std::vector& initl, - std::vector& final) -{ - MPI_Status status; - std::vector work; - unsigned n; - - // Find the largest power of two smaller than - // the number of processors - // - int M2 = 1; - while (M2*2 < numprocs) M2 = M2*2; - - // Combine the particles of the high nodes - // with those of the lower nodes so that - // all particles are within M2 nodes - // - // NB: if M2 == numprocs, no particles - // will be sent or received - // - if (myid >= M2) { - n = initl.size(); - MPI_Send(&n, 1, MPI_UNSIGNED, myid-M2, 11, MPI_COMM_WORLD); - if (n) { - MPI_Send(&initl[0], n, MPI_EXP_KEYTYPE, myid-M2, 12, - MPI_COMM_WORLD); - } - } - - std::vector data = initl; - - // - // Retrieve the excess particles - // - if (myid + M2 < numprocs) { - MPI_Recv(&n, 1, MPI_UNSIGNED, myid+M2, 11, MPI_COMM_WORLD, &status); - if (n) { - std::vector recv(n); - - MPI_Recv(&recv[0], n, MPI_EXP_KEYTYPE, status.MPI_SOURCE, 12, - MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - // data=data+new_data - sortCombineOne(initl, recv, data); - } - } - - if (myid < M2) { - - // - // Now do the iterative binary merge - // - while (M2 > 1) { - - M2 = M2/2; - - // When M2 = 1, we are on the the last iteration. - // The final node left will be the root with the entire sorted array. - - // - // The upper half of the nodes send to the lower half and is done - // - if (myid >= M2) { - n = data.size(); - MPI_Send(&n, 1, MPI_UNSIGNED, myid-M2, 11, MPI_COMM_WORLD); - if (n) { - MPI_Send(&data[0], n, MPI_EXP_KEYTYPE, myid-M2, 12, - MPI_COMM_WORLD); - } - break; - - } else { - MPI_Recv(&n, 1, MPI_UNSIGNED, MPI_ANY_SOURCE, 11, MPI_COMM_WORLD, - &status); - if (n) { - - vector recv(n); - - - MPI_Recv(&recv[0], n, MPI_EXP_KEYTYPE, status.MPI_SOURCE, 12, - MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - // - // The lower half sorts and loop again - // - sortCombineOne(data, recv, work); - data = work; - } - } - } - } - - - // - // We are done, return the result - // - - if (myid==0) final = data; - - unsigned sz = final.size(); - MPI_Bcast(&sz, 1, MPI_UNSIGNED, 0, MPI_COMM_WORLD); - if (myid) final.resize(sz); - MPI_Bcast(&final[0], sz, MPI_EXP_KEYTYPE, 0, MPI_COMM_WORLD); - - /* - std::cout << "[" << myid - << "] pHOT::parallelMerge: data size=" << final.size() << std::endl; - */ - - return; -} - diff --git a/src/pCell.H b/src/pCell.H deleted file mode 100644 index 1f49787e1..000000000 --- a/src/pCell.H +++ /dev/null @@ -1,237 +0,0 @@ -#ifndef _pCell_H -#define _pCell_H - -#include -#include -#include -#include -#include - -#include -#include - -using namespace std; - -class pHOT; -class Component; - - -//---------------------------------------------------------------------- -// Cell class definition follows -//---------------------------------------------------------------------- -// -class sCell -{ -public: - - int owner; - unsigned level; - std::map count; - unsigned ctotal; - - /** - @param 0 mass - @param 1 vel2x - @param 2 vel2y - @param 3 vel2z - @param 4 velx - @param 5 vely - @param 6 velz - @param 7 posx - @param 8 posy - @param 9 posz - */ - map > state; - vector stotal; - - //@{ - //! Return kinetic energy per unit mass per cell (total & temperature) - void KE(speciesKey indx, double &tot, double &dsp); - void KE(double &tot, double &dsp); - //@} - - //@{ - //! Return total mass in cell - double Mass(); - double Mass(speciesKey); - //@} - - //@{ - //! Return total number in cell - unsigned Count(); - unsigned Count(speciesKey); - //@} - - //! Return total volume of cell - virtual double Volume() = 0; - - //! Return the length scale factor from the quad tree - virtual double Scale() = 0; - - //! Mean position - //@{ - void MeanPos(speciesKey indx, double &x, double &y, double& z); - void MeanPos(speciesKey indx, vector& p); - void MeanPos(double &x, double &y, double& z); - void MeanPos(vector& p); - //@} - - //! Mean velocity - //@{ - void MeanVel(speciesKey indx, double &x, double &y, double& z); - void MeanVel(speciesKey indx, vector& v); - void MeanVel(double &x, double &y, double& z); - void MeanVel(vector& v); - //@} -}; - -class pCell : public sCell -{ -private: - - pHOT* tree; - Component *C; - std::set bodies; - - //! For debugging . . . - void match(pCell* target, int& mcount, key_type& key); - - //! Collect bodies - void collect(std::set& bset); - -public: - - //! Track number of instances - static int live; - - //! Target collision bucket size - static unsigned bucket; - - //! Target macroscopic bucket size - static unsigned Bucket; - - //! Maximum # of cell expansions to find macroscopic sample cell - static unsigned deltaL; - - //! Sampling method - static bool NTC; - - //! Constructors - pCell(pHOT* tr); - pCell(pCell* mom, unsigned id); - - //! Destructor - ~pCell(); - - //! Attributes - std::map dattrib; - std::map iattrib; - - key_type mykey; - key_type mask; - unsigned maxplev; - - pCell* parent; - pCell* sample; - map children; - bool isLeaf; - - key_indx keys; - std::vector bods; - - //! Track last time cell was evaluated - double time; - - //! Add a body, return the current frontier node - pCell* Add(const key_pair&, change_list* change=0); - - //! Remove a body - bool Remove(const key_pair&, change_list* change=0); - - //! Remove all bodies - void RemoveAll(); - - /** - Find the cell node in the current frontier that should contain - this body and return null if not found - */ - pCell* findNode(const key_type&); - - //! Check that this key belongs to this branch - bool isMine(const key_type& key) { - if (key==0u) return false; - key_type sig = (key_type)(key - mask) >> 3*(nbits-level); - if (sig!=0u) return false; - return true; - } - - //! Remove key from list - void RemoveKey(const key_pair& oldpair); - - //! Update keys list - void UpdateKeys(const key_pair& oldpair, const key_pair& newpair); - - //! Recursively zero the tree's state vector - void zeroState(); - - //! Walk down, accumulating state - void accumState(); - void accumState(sKeyUmap& count, sKeyvDmap& _state); - - //@{ - //! Compute state values for the maximum level - void Find(key_type key, unsigned& curcnt, unsigned& lev, - vector& state); - - void Find(key_type key, sKeyUmap& curcnt, unsigned& lev, - sKeyvDmap& st); - //@} - - - //! Compute the node key at the child level - unsigned childId(key_type key); - - //! Remake the maxplev variable from the particles - unsigned remake_plev(); - - //! Find the cells whose body number is "just" larger than Bucket. - //! Optional "message" string is for tagging debugging lines - pCell* findSampleCell(const std::string& message=""); - - //@{ - //! Return bodies from this cell - Particle* Body(vector::iterator k); - Particle* Body(unsigned long i); - //@} - - //! Return velocity statistics for particles in the cell - void Vel(double &mass, vector& v, vector& v2); - - //! Return total volume of cell - double Volume(); - - //! Return the length scale factor from the quad tree - double Scale(); - - //! Return list of bodies in the leaves with this cell as a parent - std::set& Bodies() - { - bodies.clear(); - collect(bodies); - return bodies; - } - - //! For debugging: test that this cell is in the sample cell - bool sampleTest() - { - int mcount = 0; - key_type key = 0xffffffffffffffff; - sample->match(this, mcount, key); - return mcount == 1; - } - -}; - -std::ostream& operator<< (std::ostream& stream, const sKeyPair& v); - -#endif diff --git a/src/pCell.cc b/src/pCell.cc deleted file mode 100644 index 63cb8e495..000000000 --- a/src/pCell.cc +++ /dev/null @@ -1,1021 +0,0 @@ -#include -#include -#include -#include -#include - -#include "ParticleFerry.H" -#include "pCell.H" -#include "pHOT.H" -#include "global.H" - -int pCell::live = 0; // Track number of instances - -// Target microscopic (collision) bucket size -unsigned pCell::bucket = 7; - -// Target macroscopic bucket size -unsigned pCell::Bucket = 64; - -// Maximum number of cell expansions to get sample cell -unsigned pCell::deltaL = 2; - -static unsigned ctargt = 0; - -string printKey(key_type p) -{ - const key_type one(1u); - ostringstream sout, sret; - - unsigned short cnt = 0; - unsigned Nbits = sizeof(p)*8; - for (unsigned k=0; k>1; - } - - string s = sout.str(); // Reverse the string - for (unsigned k=0; kcc), isLeaf(true) -{ - live++; - - owner = myid; - // I am the root node - parent = 0; - sample = 0; - mykey = 1u; - level = 0; - maxplev = 0; - time = -std::numeric_limits::max(); // -Infinity - - // My body mask - mask = mykey << 3*(nbits - level); - - // Initialize state - for (auto i : tr->spec_list) { - count[i] = 0; - state[i] = vector(10, 0.0); - } - - ctotal = 0; - stotal = vector(10, 0.0); - - - // Begin with a clean frontier - tree->frontier.erase(tree->frontier.begin(), tree->frontier.end()); - - // Root is born on the frontier and is the only one to start - tree->frontier[mykey] = this; - -} - - -pCell::pCell(pCell* mom, unsigned id) : - tree(mom->tree), C(mom->C), parent(mom), isLeaf(true) -{ - live++; - - owner = myid; - // My map key - mykey = (parent->mykey << 3) + id; - // My level - level = parent->level + 1; - // Maximum particle level - maxplev = 0; - // My body mask - mask = mykey << 3*(nbits - level); - - // Inherit evaluation time - time = parent->time; - - // Initialize state - for (auto i : tree->spec_list) { - count[i] = 0; - state[i] = vector(10, 0.0); - } - - stotal = vector(10, 0.0); - - // Uninitialized sample cell - sample = 0; - ctotal = 0; - - // Copy attribute lists - dattrib = mom->dattrib; - iattrib = mom->iattrib; - - tree->frontier[mykey] = this; // All nodes born on the frontier - -} - -pCell::~pCell() -{ - live--; - - // Recursively kill all the cells - for (auto i : children) delete i.second; -} - -unsigned pCell::childId(key_type key) -{ - key_type id = key - mask; - id >>= 3*(nbits - 1 - level); - unsigned cid = static_cast(id); - if (cid>7) { - cout << "Process " << myid << ": crazy cid value: " << cid - << " level=" << level << " id=" << hex << id << dec << endl; - } - return cid; -} - - -pCell* pCell::Add(const key_pair& keypair, change_list* change) -{ - key_type key = keypair.first; - key_type dif = key - mask; - unsigned key2; - - // Check that this key belongs to this branch - key_type sig = dif >> 3*(nbits-level); - - // Cell key, particle index pair - key_pair cellindx(mykey, keypair.second); - - // Wrong branch! - if (!!sig) { - // Sanity check . . . if this is the root, - // we might be in the wrong tree (a bug) - if (parent == 0) { - cout << "Process " << myid << ": ERROR level=" << level << endl << hex - << " key=" << key << endl - << " mask=" << mask << endl - << " diff=" << dif << endl - << " sig=" << sig << endl - << " xsig=" << ( (key - mask) >> 3*(nbits-level) ) << endl << dec - << " shft=" << 3*(nbits-level) << endl; - - // Get the particle info - key_indx::iterator p = - lower_bound(tree->keybods.begin(), tree->keybods.end(), keypair, - ltPAIR()); - - while (p->first==keypair.first && p->second==keypair.second ) { - cout << "pos="; - for (int k=0; k<3; k++) - cout << setw(18) << C->Particles()[p->second]->pos[k]; - cout << endl; - p++; - } - cout << std::string(60, '-') << endl; - } - // Move up the tree . . . - return parent->Add(keypair, change); - } - - // If this cell is a leaf, try to add the new body - // - if (isLeaf && keys.find(keypair)==keys.end()) { - - // I am still a leaf . . . - if (bods.size() < bucket || level+1==nbits) { - keys.insert(keypair); - tree->bodycell.insert(key_item(key, cellindx)); - bods.push_back(keypair.second); - // Flag to recompute sample cell. - if (change) change->push_back(cell_indx(this, pHOT::RECOMP)); - maxplev = max(maxplev, C->Particles()[keypair.second]->level); - - return this; - } - - // NB: it is possible to add a cell to the change list for RECOMP - // but have this cell subsequently become a branch cell before the - // RECOMP is executed. This needs to be checked at the top level - // loop. - - // Bucket is full, I need to make leaves and become a branch - // - for (auto n : keys) { - // Give all of my body keys to my - // children - key2 = childId(n.first); - if (children.find(key2) == children.end()) { - children[key2] = new pCell(this, key2); - if (change) change->push_back(cell_indx(children[key2], pHOT::CREATE)); - } - - key2Range ik = tree->bodycell.equal_range(n.first); - key2Itr jk = ik.first, rmv; - while ((rmv=jk++)!=ik.second) { - if (rmv->second.second == n.second) - tree->bodycell.erase(rmv); - } - - children[key2]->Add(n, change); - } - // Erase my list - keys.clear(); - bods.clear(); - // Erase my cell key from the frontier - tree->frontier.erase(mykey); - if (change) change->push_back(cell_indx(this, pHOT::REMOVE)); - // I'm a branch now . . . - isLeaf = false; - } - - // Now add the *new* key - key2 = childId(key); - if (children.find(key2) == children.end()) { - children[key2] = new pCell(this, key2); - if (change) change->push_back(cell_indx(children[key2], pHOT::CREATE)); - } - - - return children[key2]->Add(keypair, change); -} - - -void pCell::RemoveKey(const key_pair& pr) -{ - // Erase pair from my keys list - key_indx::iterator it=keys.find(pr); - if (it != keys.end()) keys.erase(it); - -#ifdef DEBUG - else { - //----------------------------------------------------------------- - cout << "Process " << myid << ": cell=" - << hex << mykey << dec - << " key not in keys list!" << endl; - //----------------------------------------------------------------- - } -#endif - // Erase the body from this tree's - // cell list - key2Range ik = tree->bodycell.equal_range(pr.first); - key2Itr ij = ik.first, rmv; - - while (ij!=ik.second) { - if ((rmv=ij++)->second.second == pr.second) { - tree->bodycell.erase(rmv); - } - } - // Erase the key from the tree's - // key-body index - key_indx::iterator ip = tree->keybods.find(pr); - if (ip != tree->keybods.end()) tree->keybods.erase(ip); - -#ifdef DEBUG - else { - //----------------------------------------------------------------- - cout << "Process " << myid << ": cell=" - << hex << mykey << dec - << " missing keypair entry in keybods,"; - cout << "key=" - << hex << pr.first << dec - << " index=" << pr.second - << endl; - //----------------------------------------------------------------- - } -#endif - -#ifdef ADJUST_INFO - //----------------------------------------------------------------- - cout << "Process " << myid << ": pCell::REMOVED KEY=" - << hex << pr.first << dec - << " index=" << pr.second << endl; - //----------------------------------------------------------------- -#endif -} - -void pCell::UpdateKeys(const key_pair& oldpair, const key_pair& newpair) -{ - RemoveKey(oldpair); - keys.insert(newpair); - tree->keybods.insert(newpair); - tree->bodycell.insert(key_item(newpair.first, - key_pair(mykey, newpair.second))); -#ifdef ADJUST_INFO - //----------------------------------------------------------------- - cout << "Process " << myid << ": " - << "pCell::INSERTED KEY=" << newpair.first.toHex() - << " index=" << newpair.second << endl; - //----------------------------------------------------------------- -#endif -} - -bool pCell::Remove(const key_pair& keypair, change_list* change) -{ - bool ret = false; - // Sanity check: is this really my - // key? - if (isMine(keypair.first)) { - // Remove keypair from cell list -#ifdef DEBUG - //----------------------------------------------------------------- - if (keys.find(keypair) == keys.end()) { - cout << "Process " << myid << ": " - << "pCell::Remove: ERROR finding keypair in cell's list" - << hex << " cur=" << mykey - << " key=" << keypair.first << dec - << " index=" << keypair.second - << endl; - return ret; - } - //----------------------------------------------------------------- -#endif - keys.erase(keypair); - // Remove from body/cell key list - key2Itr ik = tree->bodycell.find(keypair.first); -#ifdef DEBUG - //----------------------------------------------------------------- - if (ik == tree->bodycell.end()) { - cout << "Process " << myid << ": " - << "pCell::Remove: ERROR finding key in bodycell" - << " key=" << hex << keypair.first << dec - << " index=" << keypair.second - << endl; - return ret; - } - //----------------------------------------------------------------- -#endif - - // Remove the key-body entry - key_indx::iterator p = tree->keybods.find(keypair); -#ifdef DEBUG - //----------------------------------------------------------------- - if (p==tree->keybods.end()) { - cout << "Process " << myid << ": " - << "pCell::Remove: ERROR missing keypair entry in keybods," - << " key=" << hex << keypair.first << dec - << " index=" << keypair.second - << endl; - return ret; - } - //----------------------------------------------------------------- -#endif - if (p!=tree->keybods.end()) tree->keybods.erase(p); - - // Remove the index from the cell body list - // - vector::iterator ib = find(bods.begin(), bods.end(), - keypair.second); - if (ib!=bods.end()) bods.erase(ib); -#ifdef DEBUG - else { - //----------------------------------------------------------------- - cout << "Process " << myid << ": " - << "pCell::Remove: ERROR missing index in bods," - << " key=" << hex << keypair.first << dec - << " index=" << keypair.second - << endl; - return ret; - //----------------------------------------------------------------- - } -#endif - - // Remove this cell if it is now empty (and not the root node) - // - if (bods.empty() && parent!=NULL) { - - // Find the parent delete the cell from the parent list - // - bool found = false; - for (auto ic=parent->children.begin(); ic!=parent->children.end(); ic++) { - if (ic->second == this) { - parent->children.erase(ic); - found = true; - break; - } - } - if (!found) { - cout << "Process " << myid - << ": pCell::Remove: ERROR child not found on parent's list!" - << endl; - } - - // Remove me from the frontier - // - tree->frontier.erase(mykey); - - // Remove the old pair from the current cell - // (only transactions added are sample cells) - // queue for removal from level lists - // - change->push_back(cell_indx(this, pHOT::REMOVE)); - - // queue for deletion - // - change->push_back(cell_indx(this, pHOT::DELETE)); - - ret = true; - } - else change->push_back(cell_indx(this, pHOT::RECOMP)); - - } else { - cout << "Process " << myid - << ": pCell::Remove: ERROR body not in my cell" - << ", cell key=" << hex << mykey - << ", body key=" << keypair.first - << ", sig=" - << ((key_type)(keypair.first - mask) >> 3*(nbits-level)) << dec - << " body index=" << keypair.second << endl; - } - - return ret; -} - - -void pCell::RemoveAll() -{ - key_indx::iterator k; - key_key ::iterator ik; - key_indx::iterator p; - - while (keys.size()) { - k = keys.begin(); - ik = tree->bodycell.find(k->first); - if (ik != tree->bodycell.end()) { - tree->bodycell.erase(ik); - } - p = tree->keybods.find(*k); - if (p!=tree->keybods.end()) { - tree->keybods.erase(p); - } - keys.erase(k); - } - - bods.clear(); - if (mykey!=1u) tree->frontier.erase(mykey); - - if (parent) { - for (auto ic=parent->children.begin(); ic!=parent->children.end(); ic++) { - if (ic->second == this) { - parent->children.erase(ic); - return; - } - } - - cout << "Process " << myid - << ": pCell::RemoveAll: " - << "ERROR child not found on parent's list!" << endl; - - } else { - - if (mykey!=1u) { - cout << "Process " << myid << ": ERROR no parent and not root!" - << " owner=" << owner << hex - << " mykey=" << mykey - << " mask=" << mask << dec - << " level=" << level - << " count=" << ctotal - << " maxplev=" << maxplev << endl; - - } - } - - maxplev = 0; - ctotal = 0; -} - -pCell* pCell::findNode(const key_type& key) -{ - // Check that this key belongs to this branch - key_type sig = (key_type)(key - mask) >> 3*(nbits-level); - - if (!!sig) { - - if (parent == 0) { - cout << "pHOT::findNode: impossible condition, process " - << myid << ": level=" << level << hex - << " key=" << key << endl - << " sig=" << sig << endl << dec; - } - - return parent->findNode(key); - } - - // You found me! - if (isLeaf) return this; - // Which child - unsigned key2 = childId(key); - // Not in my tree? - if (children.find(key2)==children.end()) return 0; - - // Look for node amongst children - return children[key2]->findNode(key); -} - -void pCell::zeroState() -{ - for (auto i : tree->spec_list) { - count[i] = 0; - std::vector & s = state[i]; - for (auto & v : s) v = 0.0; - } - - for (auto i : children) i.second->zeroState(); - - ctotal = 0; - for (auto & v : stotal) v = 0.0; -} - - -void pCell::accumState() -{ - // March through the body list - for (auto j: bods) { - PartPtr p = C->Particles()[j]; - speciesKey spc = p->skey; - if (C->keyPos>=0 and spc==Particle::defaultKey) { - spc = p->skey = KeyConvert(p->iattrib[C->keyPos]).getKey(); - } - // Only do the mapping to vector once - // in the loop - std::vector & s = state[spc]; - double ms = p->mass; - s[0] += ms; - for (int k=0; k<3; k++) { - double pos = p->pos[k], vel = p->vel[k]; - s[1+k] += ms * vel*vel; - s[4+k] += ms * vel; - s[7+k] += ms * pos; - } - count[spc]++; - } - - for (auto i : tree->spec_list) { - ctotal += count[i]; // Only do the mapping to vector once - std::vector & s = state[i]; - for (int k=0; k<10; k++) stotal[k] += s[k]; - } - - // Walk up the tree . . . - if (parent) parent->accumState(count, state); -} - -void pCell::accumState(sKeyUmap& _count, sKeyvDmap& _state) -{ -#pragma omp critical - { - for (auto i : tree->spec_list) { - ctotal += _count[i]; - count[i] += _count[i]; - // Only do the mappings to vector once - std::vector & s = state[i]; - std::vector & _s = _state[i]; - for (int k=0; k<10; k++) { - stotal[k] += _s[k]; - s[k] += _s[k]; - } - } - } - - if (parent) parent->accumState(_count, _state); -} - - -void pCell::Find(key_type key, unsigned& curcnt, unsigned& lev, - vector& st) -{ - if (key==0u) { - curcnt = 0; - lev = 0; - for (auto & v : st) v = 0; - return; - } - - - // Check to see if this key belongs to one of the children - // - key_type cid = key - mask; - cid = cid >> 3*(nbits - 1 - level); - - for(auto i : children) { - if (cid == i.first) { - i.second->Find(key, curcnt, lev, st); - return; - } - } - - // Return the values from this cell - // - curcnt = ctotal; - lev = level; - st = stotal; - - return; -} - - -void pCell::Find(key_type key, sKeyUmap& curcnt, unsigned& lev, sKeyvDmap& st) -{ - if (key==0u) { - lev = 0; - - for (auto i : tree->spec_list) { - curcnt[i] = 0; - auto & s = st[i]; - for (auto & v : s) v = 0; - } - - return; - } - - - // Check to see if this key belongs to one of the children - // - key_type cid = key - mask; - cid = cid >> 3*(nbits - 1 - level); - - for (auto i : children) { - if (cid == i.first) { - i.second->Find(key, curcnt, lev, st); - return; - } - } - - // Return the values from this cell - // - curcnt = count; - lev = level; - st = state; - - return; -} - -double sCell::Mass() -{ - double mass = 0.0; - for (auto i : state) mass += i.second[0]; - return mass; -} - -double sCell::Mass(speciesKey indx) -{ - sKeyvDmap::iterator it = state.find(indx); - if (it != state.end()) return (it->second)[0]; - else return 0.0; -} - -unsigned sCell::Count() -{ - double number = 0.0; - for (auto i : state) number += i.second[10]; - return number; -} - -unsigned sCell::Count(speciesKey indx) -{ - sKeyUmap::iterator it = count.find(indx); - if (it != count.end()) return it->second; - else return 0; -} - -void sCell::MeanPos(speciesKey indx, double &x, double &y, double& z) -{ - sKeyvDmap::iterator it = state.find(indx); - - if (it==state.end()) { - x = y = z = 0.0; - return; - } - - if ((it->second)[0]<=0.0) { - x = y = z = 0.0; - return; - } - - x = (it->second)[7]/(it->second)[0]; - y = (it->second)[8]/(it->second)[0]; - z = (it->second)[9]/(it->second)[0]; -} - -void sCell::MeanPos(speciesKey indx, vector &p) -{ - p = vector(3, 0); - sKeyvDmap::iterator it = state.find(indx); - if (it == state.end()) return; - if ((it->second)[0]<=0.0) return; - for (int k=0; k<3; k++) p[k] = (it->second)[7+k]/(it->second)[0]; -} - -void sCell::MeanVel(speciesKey indx, double &u, double &v, double& w) -{ - sKeyvDmap::iterator it = state.find(indx); - - if (it == state.end()) { - u = v = w = 0.0; - return; - } - - if ((it->second)[0]<=0.0) { - u = v = w = 0.0; - return; - } - u = (it->second)[4]/(it->second)[0]; - v = (it->second)[5]/(it->second)[0]; - w = (it->second)[6]/(it->second)[0]; -} - -void sCell::MeanVel(speciesKey indx, vector &p) -{ - p = vector(3, 0); - - sKeyvDmap::iterator it = state.find(indx); - if (it == state.end()) return; - if ((it->second)[0]<=0.0) return; - - for (int k=0; k<3; k++) p[k] = (it->second)[4+k]/(it->second)[0]; -} -void sCell::MeanPos(double &x, double &y, double& z) -{ - if (stotal[0]<=0.0) { - x = y = z = 0.0; - return; - } - x = stotal[7]/stotal[0]; - y = stotal[8]/stotal[0]; - z = stotal[9]/stotal[0]; -} - -void sCell::MeanPos(vector &p) -{ - p = vector(3, 0); - if (stotal[0]<=0.0) return; - for (int k=0; k<3; k++) p[k] = stotal[7+k]/stotal[0]; -} - -void sCell::MeanVel(double &u, double &v, double& w) -{ - if (stotal[0]<=0.0) { - u = v = w = 0.0; - return; - } - u = stotal[4]/stotal[0]; - v = stotal[5]/stotal[0]; - w = stotal[6]/stotal[0]; -} - -void sCell::MeanVel(vector &p) -{ - p = vector(3, 0); - if (stotal[0]<=0.0) return; - for (int k=0; k<3; k++) p[k] = stotal[4+k]/stotal[0]; -} - -void sCell::KE(speciesKey indx, double &total, double &dispr) -{ - total = 0.0; - dispr = 0.0; - - sKeyvDmap::iterator it = state.find(indx); - if (it == state.end()) return; - - if ((it->second)[0]>0.0) { - for (int k=0; k<3; k++) { - total += 0.5*(it->second)[1+k]; - dispr += 0.5*((it->second)[1+k] - (it->second)[4+k]*(it->second)[4+k]/(it->second)[0]); - } - - if (count[indx]<2) dispr=0.0; - -#ifdef DEBUG - //----------------------------------------------------------------- - static int cnt = 0; - if (dispr<0.0) { - ostringstream sout; - sout << "pCell_tst." << myid << "." << cnt++; - ofstream out(sout.str().c_str()); - out << "# number=" << count[indx] - << ", index=(" << indx.first << "," << indx.second << ")" << endl; - for (unsigned i=0; i<10; i++) - out << setw(3) << i << setw(15) << (it->second)[i] << endl; - } - //----------------------------------------------------------------- -#endif - - dispr = max(0.0, dispr); - - // Return energy per unit mass - // - total /= (it->second)[0]; - dispr /= (it->second)[0]; - } - -} - -void sCell::KE(double &total, double &dispr) -{ - total = 0.0; - dispr = 0.0; - - if (stotal[0]>0.0) { - for (int k=0; k<3; k++) { - total += 0.5*stotal[1+k]; - dispr += 0.5*(stotal[1+k] - stotal[4+k]*stotal[4+k]/stotal[0]); - } - - if (ctotal<2) dispr=0.0; - -#ifdef DEBUG - //----------------------------------------------------------------- - static int cnt = 0; - if (dispr<0.0) { - ostringstream sout; - sout << "pCell_tst." << myid << "." << cnt++; - ofstream out(sout.str().c_str()); - out << "# number=" << ctotal << endl; - for (unsigned i=0; i<10; i++) - out << setw(3) << i << setw(15) << stotal[i] << endl; - } - //----------------------------------------------------------------- -#endif - - dispr = max(0.0, dispr); - - // Return energy per unit mass - // - total /= stotal[0]; - dispr /= stotal[0]; - } - -} - -void pCell::Vel(double &mass, vector& v1, vector& v2) -{ - mass = 0.0; - v1 = vector(3, 0.0); - v2 = vector(3, 0.0); - - if (isLeaf) { - for (auto i : bods) { - for (int k=0; k<3; k++) { - v1[k] += C->Particles()[i]->mass * - C->Particles()[i]->vel[k]; - - v2[k] += C->Particles()[i]->mass * - C->Particles()[i]->vel[k] * C->Particles()[i]->vel[k]; - } - mass += C->Particles()[i]->mass; - } - } - -} - -double pCell::Volume() -{ - return tree->volume/static_cast(key_type(1u) << 3*level); -} - -double pCell::Scale() -{ - return 1.0/static_cast(key_type(1u) << level); -} - -void pCell::match(pCell* target, int& mcount, key_type& key) -{ - if (target == this) { - mcount++; - key = this->mykey; - } - else if (children.size()) { - for (auto i : children) i.second->match(target, mcount, key); - } else { - if (target == this) mcount++; - else key = this->mykey; - } -} - -void pCell::collect(std::set& bset) -{ - if (children.size()) { - for (auto i : children) i.second->collect(bset); - } else { - bset.insert(bods.begin(), bods.end()); - } -} - -pCell* pCell::findSampleCell(const std::string & msg) -{ - pCell *cur = this; // Begin with this cell - // +----- set to false to turn off debugging - // V - if (true) { - if (this->children.size()) { - std::cout << "Looking for sample cell with children"; - if (msg.size()) std::cout << " [" << msg << "]"; - std::cout << ": key=" << this->mykey - << ", lev=" << this->level - << ", children=" << this->children.size() - << std::endl; - } - } - unsigned dbl = 0; // Count the number of levels upwards - while (cur->ctotal < Bucket) { - // Maximum expansion reached or we are - // at the root - if (cur->parent==0 || dbl==deltaL) break; - - cur = cur->parent; // Keep walking up the tree . . . - dbl++; - } - - sample = cur; // The answer. - - // +----- set to false to turn off debugging - // V - if (true) { - static int tcount = 0; - static int bcount = 0; - int mcount = 0; - key_type tkey = 0xffffffffffffffff; - sample->match(this, mcount, tkey); - tcount++; - if (mcount == 0) { - std::vector minpos(3, 1.0e20); - std::vector maxpos(3,-1.0e20); - for (auto b : this->bods) { - for (int k=0; k<3; k++) { - double v = C->Particles()[b]->pos[k]; - minpos[k] = std::min(minpos[k], v); - maxpos[k] = std::max(maxpos[k], v); - } - } - - bcount++; - std::cout << "Cell not in the sample cell"; - if (msg.size()) std::cout << " [" << msg << "]"; - std::cout << ": key=" << sample->mykey - << ", lev=" << sample->level - << ", target key=" << this->mykey - << ", target lev=" << this->level - << ", found key=" << tkey - << ", " << bcount << "/" << tcount - << ", nbods=" << this->bods.size() - << ", N=" << ctotal << ", (x, y, z)=" - << "(" << minpos[0] - << "," << minpos[1] - << "," << minpos[2] << ")" - << "(" << maxpos[0] - << "," << maxpos[1] - << "," << maxpos[2] << ")" - << std::endl; - - std::cout << "CRAZY CHECK"; - if (msg.size()) std::cout << " [" << msg << "]"; - std::cout << ": begin" << std::endl; - tree->checkSampleCells("REMOVE THIS CRAZY CHECK"); - std::cout << "CRAZY CHECK"; - if (msg.size()) std::cout << " [" << msg << "]"; - std::cout << ": end" << std::endl; - - } else if (mcount != 1) { - bcount++; - std::cout << "Muliple sample cell matches"; - if (msg.size()) std::cout << " [" << msg << "]"; - std::cout << " = " << mcount << std::endl; - } - } - - return sample; -} - -Particle* pCell::Body(vector::iterator k) -{ - if (k==bods.end()) return 0; - return C->Particles()[*k].get(); -} - -Particle* pCell::Body(unsigned long i) -{ - return C->Particles()[i].get(); -} - -unsigned pCell::remake_plev() -{ - maxplev = 0; - for (auto i : bods) { - maxplev = max(maxplev, C->Particles()[i]->level); - } - maxplev = min(maxplev, multistep); - return maxplev; -} - -std::ostream& operator<< (std::ostream& stream, const sKeyPair& v) -{ - std::ostringstream istr; - istr << "[(" - << std::setw(3) << v.first.first << ", " - << std::setw(3) << v.first.second << "), (" - << std::setw(3) << v.second.first << ", " - << std::setw(3) << v.second.second << ")]"; - return stream << istr.str(); -} diff --git a/src/pHOT.H b/src/pHOT.H deleted file mode 100644 index 1008c042f..000000000 --- a/src/pHOT.H +++ /dev/null @@ -1,470 +0,0 @@ -#ifndef _pHOT_H -#define _pHOT_H - -#include - -#include -#include -#include -#include -#include -#include -#include - -class CellDiag -{ -public: - unsigned ntre, bcel, btot, mind, maxd; - double navg, nvar, bavg, bvar; - - CellDiag(unsigned nt=0) : ntre(nt), bcel(0), btot(0), - mind(std::numeric_limits::max()), maxd(0), - navg(0.0), nvar(0.0), bavg(0.0), bvar(0.0) - {} -}; - - -class pHOT -{ - - friend class pCell; - friend class pHOT_iterator; - -protected: - - Component *cc; - - unsigned number; - - // Tree element lists - // ------------------ - key_indx keybods; // Link body key to body index - key_cell frontier; // Link cell key to cell ptr - key_key bodycell; // Link body key to cell key - - // Out of bounds list - set oob; - - pCell *root; - - double volume; - - //! Send contents of this cell to another process - void sendCell(key_type key, int to, unsigned num); - - //! Receive the contents of a cell from another process - void recvCell(int from, unsigned num); - - //! Get the cell key for the beginning of the key-sorted particle list - key_type getHeadKey(); - - //! Get the cell key for the end of the key-sorted particle list - key_type getTailKey(); - - //@{ - /** Allow for adding multiple types of partitioning variants. So - far, I have implemented only the initial Hilbert curve - partition. - */ - enum PartitionType {Hilbert} partType; - - //! Standard Hibert curve partitioning algorithm - void partitionKeysHilbert(vector& keys, - vector& kbeg, vector& kfin); - - //! Call the requested algorithm - void partitionKeys(vector& keys, - vector& kbeg, vector& kfin) { - if (partType==Hilbert) partitionKeysHilbert (keys, kbeg, kfin); - else partitionKeysHilbert (keys, kbeg, kfin); - } - //@} - - //@{ - //! For combining key-weight lists for partitioning - void parallelMerge(vector& in, vector& out); - void sortCombine(vector& one, vector& two, - vector& comb); - void rrMerge(vector& in, vector& out); - //@} - - //! Which processor owns the cell - unsigned find_proc(vector& keys, key_type key); - - //@{ - //! Partition the out-of-bounds particles among the procesors - void spreadOOB(); - void checkOOB(vector&); - //@} - - //@{ - //! State accumulation - vector numfront; - vector displace; - //@} - - //! For repartition - static bool use_weight; - - //! Frontier iterator - key_cell::iterator fit; - bool reset; - unsigned total_cells; - - ParticleFerryPtr pf; - - //! For debugging - string debugf; - - key_type key_min, key_max; - - unsigned min_cell, max_cell, adjcnt, d_val, d_pval; - unsigned cntr_total, cntr_new_key, cntr_mine, cntr_not_mine, cntr_ship; - vector chist; - change_list change; - vector kbeg, kfin, loclist; - unsigned sumstep, sumzero; - - vector offset; - - unsigned n_xchange, m_xchange, numkeys, numk; - Timer timer_keymake, timer_xchange, timer_convert, timer_overlap; - Timer timer_prepare, timer_cupdate, timer_scatter, timer_repartn; - Timer timer_tadjust, timer_cellcul, timer_keycomp, timer_keybods; - Timer timer_waiton1, timer_waiton2, timer_keynewc, timer_keyoldc; - Timer timer_waiton0, timer_keysort, timer_keygenr, timer_diagdbg; - - vector keymk3, exchg3, cnvrt3, tovlp3, prepr3, updat3; - vector scatr3, reprt3, tadjt3, celcl3, keycm3, keybd3; - vector wait03, wait13, wait23, keync3, keyoc3, barri3; - vector diagd3, keyst3, keygn3; - vector numk3; - - map clevlst; // Map cell ptrs to cell level - vector< set > clevels; // List of cells at each level - - //@{ - //! For CellLevelList - vector Pcnt, Plev; - unsigned Nlev; - //@} - - template - void getQuant(vector& in, vector& out); - - void bomb(const string& membername, const string& msg); - - //static bool use_weight; - - static unsigned klen; - - static void qtile_initialize(); // Quantile set up - -#if HAVE_LIBCUDA==1 - - void keyProcessCuda(std::vector keys); - - void sortCombineOne(std::vector& one, - std::vector& two, - std::vector& comb); - - void parallelMergeOne(std::vector& initl, - std::vector& final); - -#endif - -public: - - //! Effort value hysteresis (default: 0.25) - static double hystrs; - - //! 3-vector of sides of rectangular prism (default: [2, 2, 2]) - static std::vector sides; - - //! Origin of the prism (default: [1, 1, 1]) - //! E.g. the default selction defines a cube with coordinates [-1, 1]^3 - static std::vector offst; - - //! Quantiles for reporting distribution of time per process - static std::vector qtile; - - //! Number of quantiles - static unsigned ntile; - - //! Use key subsampling to improve run time in the key partioning step - //! (default: true) - - static bool sub_sample; - - //! Print out sample cell diagnostics (default: false) - static bool samp_debug; - - //! For communication - static MPI_Datatype CellDiagType; - - //@{ - //! Species info - //int species; - //set spec_list; - sKeySet spec_list; - //@} - - //! Constructor - //pHOT(Component* C, int species, set spec_list); - pHOT(Component* C, sKeySet spec_list); - - //! Destructor - ~pHOT(); - - //! Get the particle key. Out of bounds (OOB) particles have key "0u" - key_type getKey(double *); - static string printKey(key_type); - - //! Turn on/off weighted partitioning - void setWeights(bool onoff) { use_weight = onoff; } - - /** - Particle handling - */ - //@{ - void makeTree(); - void Repartition(unsigned); - void adjustTree(unsigned); - Particle * Body(unsigned k) const - { - PartMap::iterator ie = cc->particles.end(); - PartMap::iterator it = cc->particles.find(k); - if (it==ie) throw "pHOT::Body: particle not found"; - - return it->second.get(); - } - - - //! Tree geometry - void setSides(double x, double y, double z) - { - sides = {x, y, z}; - volume = x*y*z; - } - - void setOffset(double x, double y, double z) - { - offst = {x, y, z}; - offset = offst; - } - - //! Number of cells in tree - unsigned Number() { return frontier.size(); } - unsigned TotalNumber() { return total_cells; } - unsigned CellCount(double pctl); - - enum ChangeFlag {CREATE, REMOVE, DELETE, RECOMP}; - - set& CLevels(int M) { - if (clevels.size()==0) makeCellLevelList(); - return clevels[M]; - } - - void makeCellLevelList(); - void adjustCellLevelList(unsigned); - void gatherCellLevelList(); - void printCellLevelList(ostream& out, const string& msg); - void computeCellStates(); - - /** - Density routines - */ - //@{ - //! Do the extra work to determine the kinetic state per cell - void makeState(); - - //! Evaluate the kinetic state for the given input position - void State(double *pos, double& dens, double& temp, - double& vx, double& vy, double& vz); - double minVol(); - double maxVol(); - double medianVol(); - double Volume() { return volume; } - //@} - - - /** - Debugging and diagostics - */ - //@{ - //! Print the distribution of cell levels on the frontier - void logFrontierStats(); - - //! Check that sample cells exist for all frontier cells - void checkSampleCells(const std::string&); - - //! Check that clevlst and frontier are consistent - void checkCellLevelList(const std::string&); - - //! Check that clevlst and clevels are consistent - void checkLevelLists(const std::string&); - - //! Check that bodies refer to the same cells as the body lists - bool checkCellClevelSanity(const std::string& msg, unsigned mlevel); - - //! Check that cells in the multilevel lists are also on the master list - bool checkCellClevel(const std::string& msg, unsigned mlevel); - - //! Check that every particle has an entry in the key-body list - bool checkKeybods(const std::string&); - - //! Check that every particle has an entry in the key-body list (at - //! this level and below) - bool checkPartKeybods(const std::string& msg, unsigned mlev); - - //! Check that every particle has an entry in the key-body list, - //! body-cell list, and that the cell is on the frontier - bool checkKeybodsFrontier(const std::string& msg); - - //! Check that every particle has an entry in the key-cell list - bool checkBodycell(const std::string&); - - //! Check that every body in a cell is on the frontier - bool checkCellFrontier(const std::string&); - - //! Check that every body in the tree points to a body in the particle list - bool checkParticles(ostream &out, const std::string& msg, bool pc=true); - - //! Check that bodies are not duplicated on the particle list - bool checkDupes1(const std::string& msg); - - //! Check that bodies are not duplicated in multiple cells - bool checkDupes2(); - - //! Check that every cell in the level list is on the frontier - bool checkFrontier(ostream &out, const std::string& msg); - - //! Get the total kinetic energy and dispersion - double totalKE(double& KEtot, double& KEdsp); - - //! Get the total gass mass - void totalMass(unsigned& Counts, double& Mass); - - //! Sanity count check for body indices - void checkIndices(); - - //! Print the frontier and summary statistics - void dumpFrontier(std::ostream& out); - - //! - void densEmit(unsigned lev, pCell *p); - void densCheck(); - void statFrontier(); - void countFrontier(vector& ncells, vector& bodies); - unsigned checkNumber(); - - //! Return the cached number of out-of-bounds particles - unsigned oobNumber(); - void testFrontier(string& filename); - void Slice(int nx, int ny, int nz, string cut, string prefix); - void Slab(vector& n, vector& pmin, vector& pmax, - string cut, - vector& x, vector& dens, vector& temp, - vector& velx, vector& vely, vector& velz); - - //! Scan for and count out-of-bounds particles - void checkBounds(double, const char *); - void adjustCounts(ostream& out); - - //! Check effort weighting in current partition - void checkEffort(unsigned mlevel); - double checkAdjust() { - double result = 0.0; - if (myid) return result; - if (sumstep) { - result = static_cast(sumzero)/sumstep; - sumstep = sumzero = 0; - } - return result; - } - void Xchange(unsigned int& nX, unsigned int& mX) { - nX = n_xchange; - mX = m_xchange; - n_xchange = m_xchange = 0; - } - - bool onFrontier(key_type k) { - if (frontier.find(k) == frontier.end()) return false; - else return true; - } - - //@} - - - //! Return timing info for essential routines - //@{ - //! Collect timing from all processes - void CollectTiming(); - - //! Process and return timing stats from all processes - void Timing(vector &keymake, vector &exchange, - vector &convert, vector &overlap, - vector &prepare, vector &update, - vector &scatter, vector &repartn, - vector &tadjust, vector &celcull, - vector &keycomp, vector &keybods, - vector &keysort, vector &keygenr, - vector &waiton0, vector &waiton1, - vector &waiton2, vector &keynewc, - vector &keyoldc, vector &treebar, - vector &diagdbg, vector &numk ); - //@} - -}; - -/** - Reentrant iterator -*/ -class pHOT_iterator -{ -private: - //! Calling tree - pHOT *tr; - - //! Frontier iterator - key_cell::iterator fit; - - //! Volume of tree - double volume; - - bool first; - -public: - - pHOT_iterator(pHOT& p) : - tr(&p), fit(p.frontier.begin()), volume(p.volume), first(true) {} - - unsigned nextCell() - { - if (first) first = false; - else fit++; - if (fit==tr->frontier.end()) return 0; - return fit->second->bods.size(); - } - - pCell* Cell() { - if (fit==tr->frontier.end()) { -#ifdef DEBUG - cout << "No cell!" << endl; -#endif - return 0; - } - return fit->second; - } - - double Volume() { return volume/( (key_type)1u << (3*fit->second->level) ); } - - void KE(double &tot, double &dsp) { fit->second->KE(tot, dsp); } - - double Mass() { return fit->second->Mass(); } - - Particle *Body(vector::iterator k) { return fit->second->Body(k); } - -}; - -#endif diff --git a/src/pHOT.cc b/src/pHOT.cc deleted file mode 100644 index 64fb4c06e..000000000 --- a/src/pHOT.cc +++ /dev/null @@ -1,5644 +0,0 @@ - -#include - -#ifdef HAVE_OMP_H -#include -#endif - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -// Hardwired DEBUGGING statements for pHOT -// [set all to false for production] - -// Chatty debugging statements with minimal CPU overhead -static bool DEBUG_NOISY = false; - -// Debugging that checks internal lists -static bool DEBUG_CHECK = false; - -// Extra-verbose output with internal checking -static bool DEBUG_EXTRA = false; - -// Extra debugging for the adjustTree algorithm -static bool DEBUG_ADJUST = false; - -// Clean up bad entries in particle list -static bool DEBUG_CLEAN = false; - -// Check for malformed particles in cells -static bool DEBUG_SANITY = false; - -// Debug keys across all nodes (uses MPI calls) -static bool DEBUG_KEYS = false; - -// Warn when root is on frontier -static bool DEBUG_ROOT = false; - -// Execute VERY SLOW body count report -static bool DEBUG_BCOUNT = false; - -// Execute body count summary report -static bool BC_SUMMARY = true; - -// Execute body count per process report -static bool BC_PROCESS = true; - -// Debug key list -static bool DEBUG_KEYLIST = false; - -// Debug OOB diagnostic -static bool DEBUG_OOB = true; - -// Test the parallel merge algorithm against round robin -static bool DEBUG_MERGE = false; - -// Use round-robin distribution and scalar sort rather than parallel sort -static bool USE_RROBIN = false; - -#ifdef USE_GPTL -#include -#endif - -using namespace std; - -#include "global.H" -#include "pHOT.H" - -// Default side lengths for prism to be partitioned -std::vector pHOT::sides(3, 2.0); - -// Location of origin -std::vector pHOT::offst(3, 1.0); - -// Default quantiles for diagnostic -unsigned pHOT::ntile; -std::vector pHOT::qtile; - -// Use computed effort per particle in domain decomposition (default: true) -bool pHOT::use_weight = false; - -double pHOT::hystrs = 0.5; - -void pHOT::qtile_initialize() -{ - if (qtile.size()) return; - - qtile.push_back(10); - qtile.push_back(50); - qtile.push_back(90); - - ntile = qtile.size(); -} - -static bool wghtKEY(const pair& a, - const pair& b) -{ - return (a.first < b.first); -} - -static bool wghtDBL(const pair& a, - const pair& b) -{ - return (a.second < b.second); -} - -// -// Check sample cell sanity (for debugging) -// [set to false for production] -// -bool pHOT::samp_debug = true; - -// -// Turn on/off subsampling the key list for partitioning -// -bool pHOT::sub_sample = false; - -// For formatting -unsigned pHOT::klen = 3*nbits/4+6; - -// For diagnostic MPI communication -MPI_Datatype pHOT::CellDiagType; - -template -struct pair_compare -{ - bool operator()(const pair& a, const pair& b) - { return (a.first<=b.first); } -}; - -// Error messages -// -void pHOT::bomb(const string& membername, const string& msg) -{ - std::ostringstream sout; - sout << "pHOT::" << membername << "(): " << msg << endl; - throw GenericError(sout.str(), __FILE__, __LINE__, 1037, true); -} - -/* - Constructor: initialize domain -*/ -pHOT::pHOT(Component *C, sKeySet spec_list) -{ - qtile_initialize(); // Quantile set up - -#ifdef HAVE_OMP_H - omp_set_num_threads(nthrds); // OpenMP set up -#endif - - cc = C; // Register the calling component - - // Register multiple species - this->spec_list = spec_list; - if (spec_list.size() == 0) spec_list.insert(Particle::defaultKey); - - partType = Hilbert; // Partition type - - // Sanity check - if (nbits*3 >= sizeof(key_type)*8) { - unsigned maxb = sizeof(key_type)*8/3; - if (maxb*3 >= sizeof(key_type)*8) maxb--; - ostringstream mesg; - mesg << "nbits=" << nbits << " but must be less than " << maxb; - bomb("pHOT::pHOT", mesg.str()); - } - klen = 3*nbits/4+6; - - volume = sides[0]*sides[1]*sides[2]; // Total volume of oct-tree region - root = 0; - - offset = vector(3); - for (unsigned k=0; k<3; k++) offset[k] = offst[k]; - - kbeg = vector(numprocs); - kfin = vector(numprocs); - - key_min = key_type(1u) << (nbits*3); - key_max = key_type(1u) << (nbits*3+1); - - m_xchange = n_xchange = 0; - - sumstep = sumzero = 0; - - cntr_total = cntr_new_key = cntr_mine = cntr_not_mine = cntr_ship = 0; - - numkeys = 0; - - // Initialize timing structures - // - keymk3 = exchg3 = cnvrt3 = tovlp3 = prepr3 = updat3 = - scatr3 = reprt3 = tadjt3 = celcl3 = keycm3 = keybd3 = - keyst3 = keygn3 = wait03 = wait13 = wait23 = keync3 = - keyoc3 = barri3 = diagd3 = vector(numprocs); - - numk3 = vector(numprocs); - numfront = vector(numprocs); - displace = vector(numprocs); - - // Make the tree diagnostic MPI structure - // - const int nf = 9; - MPI_Aint disp[nf]; - int blocklen [nf] = {1, 1, 1, 1, 1, 1, 1, 1, 1}; - MPI_Datatype type[nf] = {MPI_UNSIGNED, MPI_UNSIGNED, MPI_UNSIGNED, - MPI_UNSIGNED, MPI_UNSIGNED, - MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE}; - CellDiag buf; - MPI_Get_address(&buf.ntre, &disp[0]); - MPI_Get_address(&buf.bcel, &disp[1]); - MPI_Get_address(&buf.btot, &disp[2]); - MPI_Get_address(&buf.mind, &disp[3]); - MPI_Get_address(&buf.maxd, &disp[4]); - MPI_Get_address(&buf.navg, &disp[5]); - MPI_Get_address(&buf.nvar, &disp[6]); - MPI_Get_address(&buf.bavg, &disp[7]); - MPI_Get_address(&buf.bvar, &disp[8]); - - for (int i=nf-1; i>=0; i--) disp[i] -= disp[0]; - - MPI_Type_create_struct(nf, blocklen, disp, type, &CellDiagType); - MPI_Type_commit(&CellDiagType); - - // Initialize particle ferry - pf = ParticleFerryPtr(new ParticleFerry(cc->niattrib, cc->ndattrib)); - - // Filename for debugging info - debugf = outdir + runtag + ".pHOT_debug"; -} - -pHOT::~pHOT() -{ - delete root; -} - -#undef NEWKEY - -uint64_t split3( unsigned int a ) -{ - // we only use the first 21 bits - uint64_t x = a & 0x1fffff; - x = (x | x << 32) & 0x001f00000000ffff; // shift left 32 bits, OR with self, and 00011111000000000000000000000000000000001111111111111111 - x = (x | x << 16) & 0x001f0000ff0000ff; // shift left 32 bits, OR with self, and 00011111000000000000000011111111000000000000000011111111 - x = (x | x << 8 ) & 0x100f00f00f00f00f; // shift left 32 bits, OR with self, and 0001000000001111000000001111000000001111000000001111000000000000 - x = (x | x << 4 ) & 0x10c30c30c30c30c3; // shift left 32 bits, OR with self, and 0001000011000011000011000011000011000011000011000011000100000000 - x = (x | x << 2 ) & 0x1249249249249249; - return x; -} - -uint64_t mortonEncode_mask( unsigned int* u ) -{ - uint64_t answer = 0; - answer |= split3(u[0]) | split3(u[1]) << 1 | split3(u[2]) << 2; - return answer; -} - -uint64_t newKey(double *d) -{ - const unsigned int maxI = 0x1fffff; - unsigned int u[3]; - - for (int k=0; k<3; k++) u[k] = d[k] * maxI; - - return mortonEncode_mask(&u[0]); -} - - -key_type pHOT::getKey(double *p) -{ -#ifdef USE_GPTL - GPTLstart("pHOT::getKey"); -#endif - - // Bad values - // - for (unsigned k=0; k<3; k++) { - if (std::isnan(p[k]) || std::isinf(p[k])) { - d_val++; - timer_keybods.stop(); - return key_type(0u); - } - } - - // Out of bounds? - // - for (unsigned k=0; k<3; k++) { - if (fabs((p[k]+offset[k])/sides[k])> 1.0) { - if (DEBUG_NOISY) { - cout << "Coordinate out of pbounds in pHOT::key: "; - for (int l=0; l<3; l++) cout << setw(18) << p[l] ; - cout << endl; - } -#ifdef USE_GPTL - GPTLstop("pHOT::getKey"); -#endif - return key_type(0u); - } - } - -#ifdef NEWKEY - - double dd[3]; - const uint64_t lead = (1ull << nbits*3); - for (unsigned k=0; k<3; k++) dd[2-k] = (p[k]+offset[k])/sides[k]; - key_type _key = newKey(dd) | lead; - -#else - - const double factor = static_cast(key_type(1u)< bins(3, 0u); - - // Reverse the order - for (unsigned k=0; k<3; k++) - bins[2-k] = key_type( floor( ((p[k]+offset[k])/sides[k])*factor ) ); - - key_type place = 1u; - key_type _key = 0u; - - for (unsigned i=0; i> 1; - } - } - - _key += place; // Leading placeholder for cell masking - -#endif - -#ifdef USE_GPTL - GPTLstop("pHOT::getKey"); -#endif - - return _key; -} - -string pHOT::printKey(key_type p) -{ - ostringstream sout, sret; - - unsigned short cnt = 0; - for (unsigned k=0; k>1; - } - - string s = sout.str(); // Reverse the string - for (unsigned k=0; kzeroState(); - - // Make temporary to get around OpenMP limitations. This can be - // changed eventually when the GnuC suite supports STL iterators. - // - size_t iTmp = 0, cTmp = frontier.size(); - std::vector tmp(cTmp); - for (auto it : frontier) tmp[iTmp++] = it.second; - - // March through the frontier and accumulate the counts - // -#pragma omp parallel for default(none) private(iTmp) shared(tmp, cTmp) - for (iTmp = 0; iTmp < cTmp; iTmp++) { - tmp[iTmp]->accumState(); - } - - - // March through the frontier to find the sample cells - // -#pragma omp parallel for default(none) private(iTmp) shared(tmp, cTmp) - for (iTmp = 0; iTmp < cTmp; iTmp++) { - tmp[iTmp]->findSampleCell("Frontier scan"); - } - - // Sanity check - // - if (samp_debug) { - timer_diagdbg.start(); - - ostringstream sout; - sout << "pHOT::computeCellStates, myid=" << std::setw(5) << myid - << ", time=" << std::setw(14) << tnow << " [this is a BAD ERROR]"; - checkSampleCells(sout.str()); - - static unsigned msgcnt=0, maxcnt=10; - if (msgcnt < maxcnt) { - // Look for root key on the frontier - key_cell::iterator it=frontier.find(root->mykey); - if (DEBUG_ROOT && it != frontier.end()) { - cout << "computeCellStates, root on frontier" - << ", T=" << tnow - << ", owner=" << it->second->owner - << ", level=" << it->second->level - << ", count=" << it->second->ctotal - << ", mass=" << it->second->stotal[0] - << ", root key=" << hex << it->second->mykey - << ", sample key=" << hex << it->second->mykey - << ", sample cell=" << hex << it->second->sample - << dec << endl; - if (++msgcnt==maxcnt) - cout << "computeCellStates, suppressing non-fatal " - << "\"root on frontier\" messages after " << maxcnt - << " on Proc# " << myid << endl; - } - } - - timer_diagdbg.stop(); - } -} - -void pHOT::logFrontierStats() -{ - timer_diagdbg.start(); - - vector fstat1(nbits, 0), fstat(nbits); - for (auto it : frontier) fstat1[it.second->level]++; - - MPI_Reduce(&fstat1[0], &fstat[0], nbits, MPI_UNSIGNED, MPI_SUM, 0, - MPI_COMM_WORLD); - - if (myid==0) { - string filename = outdir + "frontier_debug." + runtag; - ofstream out(filename.c_str(), ios::app); - out << "# Time=" << tnow << endl; - out << setw(6) << left << "Proc"; - for (unsigned j=0; jfirst) == bodycell.end()) { - cout << "pHOT::getHeadKey, process " << myid << ": bad key=" - << hex << keybods.begin()->first << dec - << " #cells=" << bodycell.size() << endl; - } - timer_diagdbg.stop(); - } - - headKey = bodycell.find(keybods.begin()->first)->second.first; - // Number of bodies in my head cell - if (DEBUG_CHECK) { - timer_diagdbg.start(); - // Debug: check for key in frontier - if (frontier.find(headKey) == frontier.end()) { - cout << "pHOT::getHeadKey, process " << myid << ": headKey=" - << headKey << dec << " is NOT on frontier with frontier size=" - << frontier.size() << " [1]" << endl; - std::cout << std::string(45, '-') << std::endl - << "--- Bodycell list ---" << std::endl - << std::string(45, '-') << std::endl; - for (auto k : bodycell) { - std::cout << std::setw(15) << k.first - << std::setw(15) << k.second.first - << std::setw(15) << k.second.second - << std::endl; - } - } - timer_diagdbg.stop(); - } - } - - return headKey; -} - -key_type pHOT::getTailKey() -{ - key_type tailKey = 0ul; - // Compute the tailkey - if (keybods.size()) { - if (DEBUG_CHECK) { - timer_diagdbg.start(); - // check validity of key - if (bodycell.find(keybods.rbegin()->first) == bodycell.end()) { - cout << "pHOT::getTailKey, process " << myid << ": bad tail key=" - << hex << keybods.rbegin()->first << dec - << " #cells=" << bodycell.size() << endl; - } - timer_diagdbg.stop(); - } - - tailKey = bodycell.find(keybods.rbegin()->first)->second.first; - - if (DEBUG_CHECK) { - if (tailKey == 1ul) { - if (frontier.find(tailKey) == frontier.end()) { - cout << "pHOT::getTailKey, process " << myid << ": tailKey=" - << tailKey << dec << " not on frontier! [3]" << endl; - } else { - // Verbose info - if (false) { - cout << "pHOT::getTailKey, process " << myid << ": tailKey=" - << tailKey << dec << " IS on frontier with frontier size=" - << frontier.size() << " [3]" << endl; - cout << std::string(30, '-') << std::endl << std::setfill('-') - << "--- Frontier list ---" << std::endl << std::setfill(' ') - << std::string(30, '-') << std::endl << std::hex; - for (key_cell::iterator - kit=frontier.begin(); kit!=frontier.end(); kit++) - { - std::cout << std::setw(15) << kit->first - << std::endl; - } - std::cout << std::string(30, '-') << std::endl << std::dec; - } - } - } - } - - if (DEBUG_CHECK) { - timer_diagdbg.start(); - // Debug: check for tail key in frontier - if (frontier.find(tailKey) == frontier.end()) { - cout << "pHOT::getTailKey, process " << myid << ": tailKey=" - << tailKey << dec << " is NOT on frontier with frontier size=" - << frontier.size() << " [1]" << endl; - std::cout << std::string(45, '-') << std::endl - << "--- Bodycell list ---" << std::endl - << std::string(45, '-') << std::endl; - for (auto k : bodycell) { - std::cout << std::setw(15) << k.first - << std::setw(15) << k.second.first - << std::setw(15) << k.second.second - << std::endl; - } - } - timer_diagdbg.stop(); - } - } - - return tailKey; -} - - -void pHOT::makeTree() -{ - (*barrier)("pHOT::entering makeTree", __FILE__, __LINE__); - -#ifdef USE_GPTL - GPTLstart("pHOT::makeTree"); -#endif - // - // Clean up - // - frontier.clear(); - bodycell.clear(); - adjcnt = 0; - - delete root; - - if (DEBUG_EXTRA) { - timer_diagdbg.start(); - - string sname = runtag + ".pHOT_storage"; - for (int n=0; n= key_max) ) { - double* pos = cc->Particles()[it.second]->pos; - key_type tkey = getKey(pos); - cout << "Process " << myid << ": in makeTree, key=" - << hex << it.first << ", tkey=" << tkey - << "[" << key_min << ", " << key_max << "]" - << endl << dec - << " pos = [" << pos[0] << ", " << pos[1] << ", " - << pos[2] << "]" << endl; - } - - p = p->Add(it); // Do the work - } - - // Sanity checks and debugging - if (DEBUG_BCOUNT) { - // Report on particle sizes for each node - if (BC_SUMMARY) { - unsigned long bdcel1=cc->Particles().size(), bdcelmin, bdcelmax, bdcelsum, bdcelsm2; - - (*barrier)("pHOT::makeTree initial body report", __FILE__, __LINE__); - - MPI_Reduce(&bdcel1, &bdcelmin, 1, MPI_UNSIGNED_LONG, MPI_MIN, 0, MPI_COMM_WORLD); - MPI_Reduce(&bdcel1, &bdcelmax, 1, MPI_UNSIGNED_LONG, MPI_MAX, 0, MPI_COMM_WORLD); - MPI_Reduce(&bdcel1, &bdcelsum, 1, MPI_UNSIGNED_LONG, MPI_SUM, 0, MPI_COMM_WORLD); - bdcel1 = bdcel1*bdcel1; - MPI_Reduce(&bdcel1, &bdcelsm2, 1, MPI_UNSIGNED_LONG, MPI_SUM, 0, MPI_COMM_WORLD); - - if (myid==0) - cout << endl << "In makeTree, particles" - << " min=" << bdcelmin - << " max=" << bdcelmax - << " mean=" << bdcelsum/numprocs - << " stdv=" << sqrt( (bdcelsm2 - bdcelsum*bdcelsum/numprocs)/(numprocs-1) ) - << " time=" << tnow - << endl; - } - // Report on body counts for each node - if (BC_PROCESS) { - const int w1 = 5, w2 = 10, w3 = 14; - for (int i=0; i pos(3, 0); - unsigned cnt = 0; - for (PartMapItr - it=cc->Particles().begin(); it!=cc->Particles().end(); it++) - { - for (int k=0; k<3; k++) pos[k] += it->second->pos[k]; - cnt++; - } - cout << right << setw(w1) << i - << setw(w2) << cnt - << setw(w2) << frontier.size(); - if (cnt) - cout << setw(w3) << pos[0]/cnt - << setw(w3) << pos[1]/cnt - << setw(w3) << pos[2]/cnt; - cout << endl; - } - (*barrier)("pHOT::makeTree body count report", __FILE__, __LINE__); - } - if (myid==0) { - cout << right << setfill('-') - << setw(w1) << '+' - << setw(w2) << '+' << setw(w2) << '+' - << setw(w3) << '+' << setw(w3) << '+' - << setw(w3) << '+' << setfill(' ') << endl; - } - } - - if (false && bodycell.size()==0) { - cout << "pHOT::makeTree, process " << myid - << ": unusual condition #bodycell=0" - << " with #keybods=" << keybods.size() - << " and #bodies=" << cc->Particles().size() - << endl; - } - } - - // - // Adjust boundaries bodies to prevent cell duplication on the boundary - // - -#ifdef USE_GPTL - GPTLstart("pHOT::makeTree::adjustBoundaries"); -#endif - - // Exchange boundary keys - key_type headKey=0u, tailKey=0u, prevKey=0u, nextKey=0u; - unsigned head_num=0, tail_num=0, next_num=0, prev_num=0; - - // Do the boundaries sequentially to prevent - // inconstencies - - for (int n=1; n0ul) tail_num = frontier[tailKey]->bods.size(); - - MPI_Send(&tailKey, 1, MPI_EXP_KEYTYPE, n, 1000, MPI_COMM_WORLD); - MPI_Send(&tail_num, 1, MPI_UNSIGNED, n, 1001, MPI_COMM_WORLD); - - MPI_Recv(&nextKey, 1, MPI_EXP_KEYTYPE, n, 1002, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - MPI_Recv(&next_num, 1, MPI_UNSIGNED, n, 1003, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - - if (tailKey != 0u && tailKey == nextKey) { - if (tail_num <= next_num) { - if (tail_num) { - if (frontier.find(tailKey) == frontier.end()) { - std::cout << "pHOT::makeTree, process " << myid << ": tailKey=" - << hex << tailKey - << dec << " is NOT on frontier with frontier size=" - << frontier.size() << " [2]" << endl; - std::cout << std::string(30, '-') << std::endl << std::setfill('-') - << "--- Frontier list ---" << std::endl << std::setfill(' ') - << std::string(30, '-') << std::endl << std::hex; - for (auto k : frontier) { - std::cout << std::setw(15) << k.first - << std::endl; - } - std::cout << std::string(45, '-') << std::endl - << "--- Bodycell list ---" << std::endl - << std::string(45, '-') << std::endl; - for (auto k : bodycell) { - std::cout << std::setw(15) << k.first - << std::setw(15) << k.second.first - << std::setw(15) << k.second.second - << std::endl; - } - std::cout << std::string(45, '-') << std::endl << std::dec; - } - sendCell(tailKey, n, tail_num); - } else - cout << "pHOT::makeTree, process " << myid - << ": not sending cell with zero particles" << endl; - } else { - if (next_num) - recvCell(n, next_num); - else - cout << "Process " << myid << ": not receiving cell with zero particles" << endl; - } - } - - } - // Send the previous node my head value - // to compare with its tail - if (myid==n) { - - headKey = getHeadKey(); - if (headKey>0ul) head_num = frontier[headKey]->bods.size(); - - MPI_Send(&headKey, 1, MPI_EXP_KEYTYPE, n-1, 1002, MPI_COMM_WORLD); - MPI_Send(&head_num, 1, MPI_UNSIGNED, n-1, 1003, MPI_COMM_WORLD); - - MPI_Recv(&prevKey, 1, MPI_EXP_KEYTYPE, n-1, 1000, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - MPI_Recv(&prev_num, 1, MPI_UNSIGNED, n-1, 1001, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - - if (headKey != 0u && headKey == prevKey) { - if (head_num < prev_num) { - if (head_num) { - if (frontier.find(headKey) == frontier.end()) - cout << "pHOT::makeTree, process " << myid << ": headKey=" - << headKey << dec << " not on frontier! [2]" << endl; - sendCell(headKey, n-1, head_num); - } else - cout << "pHOT::makeTree, process " << myid - << ": not sending cell with zero particles" << endl; - } else { - if (prev_num) - recvCell(n-1, prev_num); - else - cout << "pHOT::makeTree, process " << myid - << ": not receiving cell with zero particles" << endl; - } - } - - } - - } - - (*barrier)("pHOT::makeTree(): boundaries adjusted", __FILE__, __LINE__); - -#ifdef USE_GPTL - GPTLstop ("pHOT::makeTree::adjustBoundaries"); - GPTLstart("pHOT::makeTree::getFrontier"); -#endif - - // Compute the physical states in each cell for the entire tree and - // find the sample cells - // - computeCellStates(); - - // Get the true partition - key_type kbeg1 = 0xffffffffffffffff, kfin1 = 0ul; - - for (auto i : keybods) { - kbeg1 = min(kbeg1, i.first); - kfin1 = max(kfin1, i.first); - } - - MPI_Allgather(&kbeg1, 1, MPI_EXP_KEYTYPE, &kbeg[0], 1, MPI_EXP_KEYTYPE, - MPI_COMM_WORLD); - - MPI_Allgather(&kfin1, 1, MPI_EXP_KEYTYPE, &kfin[0], 1, MPI_EXP_KEYTYPE, - MPI_COMM_WORLD); - - unsigned isiz = loclist.size(); - loclist = kbeg; - loclist.push_back(kfin[numprocs-1]); // End point for binary search - - // Find min and max cell occupation - // - unsigned min1=std::numeric_limits::max(), max1=0, nt; - for (auto i : frontier) { - nt = i.second->bods.size(); - min1 = min(min1, nt); - max1 = max(max1, nt); - } - - MPI_Allreduce(&min1, &min_cell, 1, MPI_UNSIGNED, MPI_MIN, MPI_COMM_WORLD); - MPI_Allreduce(&max1, &max_cell, 1, MPI_UNSIGNED, MPI_MAX, MPI_COMM_WORLD); - - vector chist1(max_cell-min_cell+1, 0); - chist = vector(max_cell-min_cell+1, 0); - for (auto i : frontier) - chist1[i.second->bods.size()-min_cell]++; - - MPI_Allreduce(&chist1[0], &chist[0], max_cell-min_cell+1, - MPI_UNSIGNED, MPI_SUM, MPI_COMM_WORLD); - - for (unsigned i=1; i(k_min, i.first); - k_max = std::max(k_max, i.first); - } - - // Field sizes and headers - const int nn = 6, nf = 20; - const int tt = nn + 3*nf; - if (myid==0) { - std::cout << std::string(tt, '-') << std::endl - << std::setfill('-') << std::left - << std::setw(tt) << "--- Frontier keys " << std::endl - << std::string(tt, '-') << std::endl - << std::right << std::setfill(' ') - << std::setw(nn) << "Node" - << std::setw(nf) << "Min key" - << std::setw(nf) << "Max key" - << std::setw(nf) << "Number" - << std::endl << std::setfill('-') - << std::setw(nn) << '+' - << std::setw(nf) << '+' - << std::setw(nf) << '+' - << std::setw(nf) << '+' - << std::endl << std::setfill(' '); - } - - for (int id=0; id cntlevN(MaxLev+1, 0); - vector kidlevN(MaxLev+1, 0); - vector maslevN(MaxLev+1, 0); - vector vollevN(MaxLev+1, 0); - - MPI_Recv(&cntlevN[0], MaxLev+1, MPI_UNSIGNED, n, 144, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - MPI_Recv(&kidlevN[0], MaxLev+1, MPI_UNSIGNED, n, 145, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - MPI_Recv(&maslevN[0], MaxLev+1, MPI_DOUBLE, n, 146, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - MPI_Recv(&vollevN[0], MaxLev+1, MPI_DOUBLE, n, 147, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - - cout << endl << "Node #" << n << endl; - for (unsigned k=0; k<=MaxLev; k++) { - if (cntlevN[k]) - cout << setw(8) << k - << setw(8) << cntlevN[k] - << setw(8) << (int)floor((double)kidlevN[k]/cntlevN[k]+0.5) - << setw(18) << maslevN[k] - << setw(18) << vollevN[k] - << setw(18) << vollevN[k]/cntlevN[k] - << endl; - } - cout << endl; - - } else if (n==myid) { - MPI_Send(&cntlev[0], MaxLev+1, MPI_UNSIGNED, 0, 144, MPI_COMM_WORLD); - MPI_Send(&kidlev[0], MaxLev+1, MPI_UNSIGNED, 0, 145, MPI_COMM_WORLD); - MPI_Send(&maslev[0], MaxLev+1, MPI_DOUBLE, 0, 146, MPI_COMM_WORLD); - MPI_Send(&vollev[0], MaxLev+1, MPI_DOUBLE, 0, 147, MPI_COMM_WORLD); - } - (*barrier)("pHOT: density check report", __FILE__, __LINE__); - } - - vector cntlev0(MaxLev+1, 0); - vector kidlev0(MaxLev+1, 0); - vector maslev0(MaxLev+1, 0); - vector vollev0(MaxLev+1, 0); - - MPI_Reduce(&cntlev[0], &cntlev0[0], MaxLev+1, MPI_UNSIGNED, MPI_SUM, 0, - MPI_COMM_WORLD); - MPI_Reduce(&kidlev[0], &kidlev0[0], MaxLev+1, MPI_UNSIGNED, MPI_SUM, 0, - MPI_COMM_WORLD); - MPI_Reduce(&maslev[0], &maslev0[0], MaxLev+1, MPI_DOUBLE, MPI_SUM, 0, - MPI_COMM_WORLD); - MPI_Reduce(&vollev[0], &vollev0[0], MaxLev+1, MPI_DOUBLE, MPI_SUM, 0, - MPI_COMM_WORLD); - - if (myid==0) { - cout << endl << "Total" << endl; - cout << setw(8) << "Level" - << setw(8) << "Count" - << setw(8) << "Child" - << setw(18) << "Mass" - << setw(18) << "Volume" - << setw(18) << "Vol/cell" - << endl - << setw(8) << "------" - << setw(8) << "------" - << setw(8) << "------" - << setw(18) << "------" - << setw(18) << "------" - << setw(18) << "------" - << endl; - - for (unsigned n=0; n<=MaxLev; n++) { - if (cntlev[n]) - cout << setw(8) << n - << setw(8) << cntlev0[n] - << setw(8) << (int)floor((double)kidlev0[n]/cntlev0[n]+0.5) - << setw(18) << maslev0[n] - << setw(18) << vollev0[n] - << setw(18) << vollev0[n]/cntlev0[n] - << endl; - } - cout << endl << setw(74) << setfill('=') << '=' - << setfill(' ') << endl << endl; - } - - timer_diagdbg.stop(); -} - -void pHOT::dumpFrontier(std::ostream& out) -{ - unsigned sum = 0, cnt=0; - double mean=0.0, disp=0.0; - double totmass=0.0, totvol=0.0, tmp; - - timer_diagdbg.start(); - - if (myid==0) { // Write output header - out << "#" << std::endl - << "# Frontier info" << std::endl - << "# id " - << std::setw(12) << "key" - << std::setw( 8) << "level" - << std::setw(18) << "num" - << std::setw(18) << "mass" - << std::setw(18) << "density" - << std::setw(10) << "pos(x)" - << std::setw(10) << "var(x)" - << std::setw(10) << "min(x)" - << std::setw(10) << "max(x)" - << std::setw(10) << "pos(y)" - << std::setw(10) << "var(y)" - << std::setw(10) << "min(y)" - << std::setw(10) << "max(y)" - << std::setw(10) << "pos(z)" - << std::setw(10) << "var(z)" - << std::setw(10) << "min(z)" - << std::setw(10) << "max(z)" - << std::endl - << "# [1] " - << std::setw(12) << "[2]" - << std::setw( 8) << "[3]" - << std::setw(18) << "[4]" - << std::setw(18) << "[5]" - << std::setw(18) << "[6]" - << std::setw(10) << "[7]" - << std::setw(10) << "[8]" - << std::setw(10) << "[9]" - << std::setw(10) << "[10]" - << std::setw(10) << "[11]" - << std::setw(10) << "[12]" - << std::setw(10) << "[13]" - << std::setw(10) << "[14]" - << std::setw(10) << "[15]" - << std::setw(10) << "[16]" - << std::setw(10) << "[17]" - << std::setw(10) << "[18]" - << std::endl; - } - - // I suppose I could do this with MPI_IO, but that seems like - // overkill for gathering debug info . . . - // - for (int n=0; n output; - - if (n==myid) { - - for (auto i : frontier) { - // The output line for this cell - std::ostringstream line; - - std::vector pmin(3, std::numeric_limits::max()); - std::vector pmax(3, -std::numeric_limits::max()); - std::vector mpos(3, 0.0), vpos(3, 0.0); - unsigned num = i.second->bods.size(); - double mass = 0.0; - - for (auto j : i.second->bods) { - mass += cc->particles[j]->mass; - for (unsigned k=0; k<3; k++) { - tmp = cc->particles[j]->pos[k]; - mpos[k] += tmp; - vpos[k] += tmp*tmp; - pmin[k] = std::min(pmin[k], tmp); - pmax[k] = std::max(pmax[k], tmp); - } - } - - totmass += mass; - totvol += volume/static_cast(key_type(1u)<<(3*i.second->level)); - - line << setw( 6) << myid - << setw(12) << hex << i.first << dec - << setw( 8) << i.second->level - << setw(18) << num - << setw(18) << mass - << setw(18) << mass/(volume/static_cast(key_type(1u)<<(3*i.second->level))); - - for (unsigned k=0; k<3; k++) { - mpos[k] /= num; - if (num>1) - vpos[k] = sqrt( (vpos[k] - mpos[k]*mpos[k]*num)/(num-1) ); - else - vpos[k] = 0.0; - - line << setprecision(4) << setw(10) << mpos[k] - << setprecision(4) << setw(10) << vpos[k] - << setprecision(4) << setw(10) << pmin[k] - << setprecision(4) << setw(10) << pmax[k]; - } - mean += num; - disp += num*num; - sum += num; - cnt++; - - output.push_back(line.str()); - } - - if (n != 0) { - unsigned osize = output.size(); - MPI_Send(&osize, 1, MPI_UNSIGNED, 0, 233, MPI_COMM_WORLD); - for (auto s : output) - MPI_Send(s.c_str(), s.size(), MPI_CHAR, 0, 234, MPI_COMM_WORLD); - } - - } // END: myid==n - - if (myid==0) { - - if (n != 0) { - MPI_Status status; - unsigned osize; - int len; - - MPI_Recv(&osize, 1, MPI_UNSIGNED, n, 233, MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - for (unsigned k=0; k buf(new char[len]); - MPI_Recv(buf.get(), len, MPI_CHAR, n, 234, MPI_COMM_WORLD, &status); - // Add to output list - output.push_back(std::string(buf.get(), len)); - } - } - - for (auto s : output) out << s << std::endl; - - } - (*barrier)("pHOT: dump frontier", __FILE__, __LINE__); - } - - unsigned sum0=0, cnt0=0; - double mean0=0.0, disp0=0.0, totmass0=0.0, totvol0=0.0; - - MPI_Reduce(&sum, &sum0, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD); - MPI_Reduce(&cnt, &cnt0, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD); - MPI_Reduce(&mean, &mean0, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); - MPI_Reduce(&disp, &disp0, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); - MPI_Reduce(&totmass, &totmass0, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); - MPI_Reduce(&totvol, &totvol0, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); - - if (myid==0) { - out << "#" << endl - << "#" << setw(12) << "Total" << setw(18) << sum0 << endl - << "#" << setw(12) << "Mass" << setw(18) << totmass0 << endl - << "#" << setw(12) << "Volume" << setw(18) << totvol0 << endl - << "#" << setw(12) << "Mean" << setw(18) << mean0/cnt0 << endl - << "#" << setw(12) << "Sigma" << setw(18) - << sqrt((disp0 - mean0*mean0/cnt0)/cnt0) << endl << "#" << endl; - } - - timer_diagdbg.stop(); -} - -void pHOT::statFrontier() -{ - timer_diagdbg.start(); - - unsigned sum1=0, cnt1=0, sum=0, cnt=0, num; - double mean1=0.0, disp1=0.0, mean=0.0, disp=0.0; - vector freq1(pCell::bucket+1, 0), freq(pCell::bucket+1, 0); - - for (auto i : frontier) { - num = i.second->bods.size(); - mean1 += num; - disp1 += num*num; - sum1 += num; - freq1[num]++; - cnt1++; - } - - MPI_Reduce(&sum1, &sum, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD ); - MPI_Reduce(&cnt1, &cnt, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD ); - MPI_Reduce(&mean1, &mean, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD ); - MPI_Reduce(&disp1, &disp, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD ); - MPI_Reduce(&freq1[0], &freq[0], pCell::bucket+1, - MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD ); - - if (myid==0) { - cout << endl; - cout << setw(12) << "Size" << setw(18) << frontier.size() << endl; - cout << setw(12) << "Total" << setw(18) << sum << endl; - cout << setw(12) << "Mean" << setw(18) << mean/cnt << endl; - cout << setw(12) << "Sigma" << setw(18) - << sqrt((disp - mean*mean/cnt)/cnt) << endl; - - cout << endl; - for (unsigned n=0; n prec(fields, 18); - for (unsigned n=0; n<5; n++) prec[n] = 10; - prec[0] = 14; - prec[2] = 14; - - vector fmt(fields, ios::dec); - fmt[1] = ios::hex; - - vector typ(fields, ios::fixed); - typ[0] = typ[5] = typ[6] = ios::scientific; - - if (myid==0) { - - char labels[][18] = { - "Time |", - "Proc |", - "Key |", - "Level |", - "Number |", - "Mass |", - "Volume |", - "Density |", - "Temp |", - "Mean X |", - "Mean Y |", - "Mean Z |", - "Mean U |", - "Mean V |", - "Mean W |"}; - - ifstream in(filename.c_str()); - in.close(); - - if (in.fail()) { - - ofstream out(filename.c_str()); - - out << right; - - out << "#" << endl << "#"; - for (unsigned n=0; n::iterator ib = p->bods.begin(); - while (ib != p->bods.end()) { - mass += cc->particles[*ib]->mass; - for (int k=0; k<3; k++) { - pos[k] += - cc->particles[*ib]->mass * - cc->particles[*ib]->pos[k]; - vel[k] += - cc->particles[*ib]->mass * - cc->particles[*ib]->vel[k]; - temp += - cc->particles[*ib]->mass * - cc->particles[*ib]->vel[k] * - cc->particles[*ib]->vel[k]; - } - ib++; - } - - double v2=0.0; - for (int k=0; k<3; k++) { - pos[k] /= mass; - vel[k] /= mass; - v2 += vel[k]*vel[k]; - } - - temp = 0.333333333333*(temp/mass - v2); - - unsigned n=0; - - double vol = volume/static_cast( key_type(1u) << (3*p->level)); - - out << " "; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << tnow; - n++; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << myid; - n++; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << hex << c.first << dec; - n++; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << p->level; - n++; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << p->bods.size(); - n++; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << mass; - n++; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << vol; - n++; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << mass/vol; - n++; - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << temp; - n++; - for (int k=0; k<3; k++) { - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << pos[k]; - n++; - } - for (int k=0; k<3; k++) { - out << setw(prec[n]) << setiosflags(fmt[n]|typ[n]) << vel[k]; - n++; - } - out << endl; - } - } - - (*barrier)("HOT: test frontier", __FILE__, __LINE__); - } - - timer_diagdbg.stop(); - -} - - -void pHOT::countFrontier(vector& ncells, vector& bodies) -{ - pCell *p; - map > data; - map >::iterator d; - unsigned maxLev1=0, maxLev=0; - - timer_diagdbg.start(); - - for (auto i : frontier) { - p = i.second; - maxLev1 = max(maxLev1, p->level); - if ((d=data.find(p->level)) == data.end()) { - data[p->level] = pair(1, p->bods.size()); - } else { - d->second.first++; - d->second.second += p->bods.size(); - } - } - - MPI_Allreduce(&maxLev1, &maxLev, 1, MPI_UNSIGNED, MPI_MAX, - MPI_COMM_WORLD); - - vector tcellcnts(maxLev+1, 0), tbodscnts(maxLev+1, 0); - if (myid==0) { - ncells = vector(maxLev+1); - bodies = vector(maxLev+1); - } - - for (auto d : data) { - tcellcnts[d.first] = d.second.first; - tbodscnts[d.first] = d.second.second; - } - - MPI_Reduce(&tcellcnts[0], &ncells[0], maxLev+1, MPI_UNSIGNED, MPI_SUM, - 0, MPI_COMM_WORLD); - - MPI_Reduce(&tbodscnts[0], &bodies[0], maxLev+1, MPI_UNSIGNED, MPI_SUM, - 0, MPI_COMM_WORLD); - - timer_diagdbg.stop(); -} - - -void pHOT::sendCell(key_type key, int to, unsigned num) -{ -#ifdef USE_GPTL - GPTLstart("pHOT::sendCell"); -#endif - - key_cell::iterator kit = frontier.find(key); - pCell *p = 0; - - if (kit == frontier.end()) { - std::cout << "pHOT::sendCell: myid=" << myid - << ", key=" << key << " is NOT on frontier " - << " with frontier size=" << frontier.size() << std::endl; - std::cout << std::string(30, '-') << std::endl << std::setfill('-') - << "--- Frontier list ---" << std::endl << std::setfill(' ') - << std::string(30, '-') << std::endl; - for (auto k : frontier) - std::cout << std::setw(10) << k.first - << std::endl; - std::cout << std::string(30, '-') << std::endl; - num = 0; - } else { - p = frontier.find(key)->second; - } - - if (DEBUG_NOISY) { - std::cout << "Process " << std::left << std::setw(4) << myid - << std::setw(12) << ": sending " << std::setw(4) << num - << " to " << std::setw(4) << to << endl; - } - - vector erased; - vector buffer1(3*num); - vector buffer2(num); - vector buffer3(num); - - if (DEBUG_SANITY) { - unsigned crazy = 0; - vector::iterator ib = p->bods.begin(); - for (unsigned j=0; jparticles[*(ib++)]->indx == 0) crazy++; - } - if (crazy) std::cout << "[sendCell node " << myid << " has " << crazy - << " crazy bodies out of " << num << "]" << std::endl; - } - - pf->ShipParticles(to, myid, num); - - key_pair tpair; - vector::iterator ib = p->bods.begin(); - for (unsigned j=0; jSendParticle(cc->particles[*ib]); - - // Find the record and delete it - tpair.first = cc->particles[*ib]->key; - tpair.second = cc->particles[*ib]->indx; - key_indx::iterator it = keybods.find(tpair); - - if (it != keybods.end()) { - // Remove the key from the cell list - { - key2Range ij = bodycell.equal_range(it->first); - - if (ij.first != ij.second) { - key_key::iterator ijk=ij.first, rmv; - while (ijk!=ij.second) { - if ((rmv=ijk++)->second.second == tpair.second) bodycell.erase(rmv); - } - } else { - std::cout << "pHOT::sendCell, myid=" << setw(5) << myid - << ", body not in bodycell list" << std::endl; - } - } - - cc->particles.erase(*ib); - if (DEBUG_CHECK) erased.push_back(*ib); - - { - key_indx::iterator ij = keybods.find(*it); - - if (ij != keybods.end()) { - keybods.erase(ij); - } else { - std::cout << "pHOT::sendCell, myid=" << setw(5) << myid - << ", body not in keybods list" << std::endl; - } - } - - - } else { - cerr << "pHOT::sendCell, myid=" << myid << ": error, " - << "removing body from keybods list that does not exist! " - << endl; - } - ib++; - } - - // If this cell is not the root - // - if (p->parent) { - - // Delete this cell from the parent - p->parent->children.erase( (p->mykey & 0x7u) ); - - if (DEBUG_CHECK) { - timer_diagdbg.start(); - if (frontier.find(p->mykey)==frontier.end()) { - cout << "Process " << myid << ": in pHOT:sendCell: " - << " key not on frontier as expected" << endl; - } - timer_diagdbg.stop(); - } - - // Delete this cell from the frontier - frontier.erase(p->mykey); - - // Delete the cell altogether - delete p; - - } else { // Special treatment for root - p->keys.clear(); - p->bods.clear(); - } - - if (DEBUG_CHECK) { - timer_diagdbg.start(); - for (auto i : erased) { - if (cc->particles.find(i) != cc->particles.end()) - cout << "pHOT::sendCell proc=" << myid - << " found erased index=" << i << endl; - } - timer_diagdbg.stop(); - } - - // Refresh size of local particle list - cc->nbodies = cc->particles.size(); - -#ifdef USE_GPTL - GPTLstop("pHOT::sendCell"); -#endif -} - - -void pHOT::recvCell(int from, unsigned num) -{ -#ifdef USE_GPTL - GPTLstart("pHOT::recvCell"); -#endif - - if (DEBUG_NOISY) - std::cout << "Process " << std::left << std::setw(4) << myid - << std::setw(12) << ": receiving " << std::setw(4) << num - << " from " << std::setw(4) << from << endl; - - pCell *p = root; - - pf->ShipParticles(myid, from, num); - - for (unsigned j=0; jRecvParticle(); - if (part->indx==0 || part->mass<=0.0 || std::isnan(part->mass)) { - cout << "[recvCell, myid=" << myid - << ", will ignore crazy body with indx=" << part->indx - << ", j=" << j << ", num=" << num << ", mass=" << part->mass - << ", key=" << hex << part->key << dec << "]" - << " from Node " << from << std::endl; - } else { - cc->particles[part->indx] = part; - if (part->key == 0u) continue; - if (part->key < key_min || part->key >= key_max) { - cout << "Process " << myid << ": in recvCell, key=" - << hex << part->key << dec << "]" - ; - } - key_pair tpair(part->key, part->indx); - keybods.insert(tpair); - p = p->Add(tpair); - } - } - - // Refresh size of local particle list - cc->nbodies = cc->particles.size(); - -#ifdef USE_GPTL - GPTLstop("pHOT::recvCell"); -#endif -} - -void pHOT::makeState() -{ - // Currently unused -} - - -void pHOT::State(double *x, double& dens, double& temp, - double& velx, double& vely, double& velz) -{ - key_type key = getKey(x); - - dens = temp = velx = vely = velz = 0.0; - - // Walk tree to get count - // - unsigned count, level; - vector state(7); - root->Find(key, count, level, state); - - vector stt1(numprocs*7, 0); - vector stt0(numprocs*7, 0); - vector cnt1(numprocs, 0), lev1(numprocs, 0); - vector cnt0(numprocs, 0), lev0(numprocs, 0); - - cnt1[myid] = count; - lev1[myid] = level; - for (int k=0; k<7; k++) stt1[myid*7+k] = state[k]; - - - MPI_Reduce(&cnt1[0], &cnt0[0], numprocs, MPI_UNSIGNED, MPI_SUM, - 0, MPI_COMM_WORLD); - - MPI_Reduce(&lev1[0], &lev0[0], numprocs, MPI_UNSIGNED, MPI_SUM, - 0, MPI_COMM_WORLD); - - MPI_Reduce(&lev1[0], &lev0[0], numprocs, MPI_UNSIGNED, MPI_SUM, - 0, MPI_COMM_WORLD); - - MPI_Reduce(&stt1[0], &stt0[0], numprocs*5, MPI_DOUBLE, MPI_SUM, - 0, MPI_COMM_WORLD); - - - // Compute the state variables for the "deepest" cell(s) - // - if (myid==0) { - vector< pair > dlist; - for (int n=0; n(lev0[n], n)); - - sort(dlist.begin(), dlist.end(), greater< pair >()); - - // This is the deepest level - unsigned clv = dlist[0].first; - unsigned cnt = 1; - for (int k=0; k<7; k++) state[k] = stt0[7*dlist[0].second + k]; - - // Add others at the same level - for (int n=1; n0.0) { - double disp = 0.0; - for (int k=0; k<3; k++) - disp += (state[1+k] - state[4+k]*state[4+k]/state[0])/state[0]; - - dens = state[0] * static_cast(key_type(1u) << (3*clv))/(volume*cnt); - temp = 0.333333333333*disp; - velx = state[4]/state[0]; - vely = state[5]/state[0]; - velz = state[6]/state[0]; - } - else { - dens = temp = velx = vely = velz = 0.0; - } - - } - -} - - -void pHOT::Slice(int nx, int ny, int nz, string cut, string prefix) -{ - vector pt(3); - double dens, temp, vx, vy, vz; - - double dx = sides[0]/nx; - double dy = sides[1]/ny; - double dz = sides[2]/nz; - - ofstream out; - - if (myid==0) out.open(string(prefix + "." + cut).c_str()); - - makeState(); - - if (cut.compare("XY")==0) { - - // X-Y slice - pt[2] = 0.5*sides[2] - offset[2]; - for (int i=0; i& n, vector& pmin, vector& pmax, string cut, - vector& x, vector& dens, vector& temp, - vector& velx, vector& vely, vector& velz) -{ - vector pt(3); - double Dens, Temp, Velx, Vely, Velz; - - double dx = (pmax[0] - pmin[0])/n[0]; - double dy = (pmax[1] - pmin[1])/n[1]; - double dz = (pmax[2] - pmin[2])/n[2]; - - ofstream out; - - makeState(); - - if (cut.compare("X")==0) { - - x = vector(n[0], 0.0); - dens = vector(n[0], 0.0); - temp = vector(n[0], 0.0); - velx = vector(n[0], 0.0); - vely = vector(n[0], 0.0); - velz = vector(n[0], 0.0); - - // X cut - for (int i=0; i(n[1], 0.0); - dens = vector(n[1], 0.0); - temp = vector(n[1], 0.0); - velx = vector(n[1], 0.0); - vely = vector(n[1], 0.0); - velz = vector(n[1], 0.0); - - // Y cut - for (int j=0; j(n[2], 0.0); - dens = vector(n[2], 0.0); - temp = vector(n[2], 0.0); - velx = vector(n[2], 0.0); - vely = vector(n[2], 0.0); - velz = vector(n[2], 0.0); - - // Z cut - for (int k=0; k(MaxLev, i.second->level); - - double vol1, vol; - vol1 = volume/static_cast(key_type(1u) << (3*MaxLev)); - MPI_Allreduce(&vol1, &vol, 1, MPI_DOUBLE, MPI_MIN, MPI_COMM_WORLD); - - return vol; -} - -double pHOT::maxVol() -{ - unsigned MinLev = std::numeric_limits::max(); - for (auto i : frontier) - MinLev = min(MinLev, i.second->level); - - double vol1, vol; - vol1 = volume/static_cast(key_type(1u) << (3*MinLev)); - MPI_Allreduce(&vol1, &vol, 1, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD); - - return vol; -} - -double pHOT::medianVol() -{ - unsigned mlev, num; - vector lev; - - for (auto i : frontier) - lev.push_back(i.second->level); - - if (myid==0) { - - for (int n=1; n lev1(num); - MPI_Recv(&lev1[0], num, MPI_UNSIGNED, n, 62, MPI_COMM_WORLD, - MPI_STATUS_IGNORE); - for (unsigned j=0; j(key_type(1u) << (3*mlev)); -} - -void pHOT::Repartition(unsigned mlevel) -{ -#ifdef USE_GPTL - GPTLstart("pHOT::Repartition"); - GPTLstart("pHOT::Repartition::entrance_waiting"); - (*barrier)("pHOT: repartition entrance wait", __FILE__, __LINE__); - GPTLstop ("pHOT::Repartition::entrance_waiting"); -#endif - - PartMapItr it; - - volume = sides[0]*sides[1]*sides[2]; // Total volume of oct-tree region - - - // No need to repartition - // if there are no bodies - if (cc->nbodies_tot==0) { - if (myid==0) - cout << "pHOT::Repartition with ZERO bodies, continuing" << endl; -#ifdef USE_GPTL - GPTLstop("pHOT::Repartition"); -#endif - return; - } - - // For debugging - vector erased; - - timer_repartn.start(); - - // - // Recompute keys and compute new partition - // -#ifdef USE_GPTL - GPTLstart("pHOT::Repartition::compute_keys"); -#endif - - std::vector keys; - - static unsigned long d1a=0, d1b=0; - - bool have_cuda = false; -#if HAVE_LIBCUDA==1 - have_cuda = true; -#endif - - if (use_cuda and have_cuda) { - -#if HAVE_LIBCUDA==1 - keyProcessCuda(keys); -#endif - - } // END: use_cuda - else { - - timer_keygenr.start(); - - oob.clear(); - for (it=cc->Particles().begin(); it!=cc->Particles().end(); it++) { - - it->second->key = getKey(&(it->second->pos[0])); - if (it->second->key == 0u) { - oob.insert(it->first); - } else { - if (use_weight) { - // Floor effort flag to prevent divide-by-zero - it->second->effort = - std::max(Particle::effort_default, it->second->effort); - - // Push onto vector - keys.push_back(key_wght(it->second->key, it->second->effort)); - - // Reset effort value with some hysteresis - it->second->effort = hystrs*(1.0 - hystrs)*it->second->effort; - - } else { - keys.push_back(key_wght(it->second->key, 1.0)); - } - } - } - - - if (checkDupes1("pHOT::Repartition: after new keys")) { - cout << "Process " << myid << " at T=" << tnow - << ", L=" << mlevel - << ": duplicate check failed after new keys, cnt=" - << d1a << endl; - } - d1a++; - - if (DEBUG_NOISY) - std::cout << "Process " << std::left << std::setw(4) << myid - << ": part #=" << std::setw(10) << cc->Particles().size() - << " key size=" << std::setw(10) << keys.size() - << " oob size=" << std::setw(10) << oob.size() << endl; - -#ifdef USE_GPTL - GPTLstop ("pHOT::Repartition::compute_keys"); - GPTLstart("pHOT::Repartition::compute_keys_waiting"); - (*barrier)("pHOT: repartition key wait", __FILE__, __LINE__); - GPTLstop ("pHOT::Repartition::compute_keys_waiting"); - GPTLstart("pHOT::Repartition::spreadOOB"); -#endif - - timer_keygenr.stop(); - timer_keysort.start(); - - spreadOOB(); - -#ifdef USE_GPTL - GPTLstop ("pHOT::Repartition::spreadOOB"); - GPTLstart("pHOT::Repartition::partitionKeys"); -#endif - - partitionKeys(keys, kbeg, kfin); - -#ifdef USE_GPTL - GPTLstop ("pHOT::Repartition::partitionKeys"); - GPTLstart("pHOT::bodyList"); -#endif - - timer_keysort.stop(); - - if (DEBUG_KEYS) { // Deep debug output - static unsigned count = 0; - std::ostringstream sout; - sout << debugf << "." << myid << "." << count++; - ofstream out(sout.str()); - - for (unsigned i=0; i0 and keys[i].first < keys[i-1].first) - out << "####" << std::endl; - } - } // END: DEBUG_KEYS - - } // END: not have_cuda - - timer_prepare.start(); - - // - // Nodes compute send list - // - loclist = kbeg; - loclist.push_back(kfin[numprocs-1]); // End point for binary search - - unsigned Tcnt=0, Fcnt, sum; - vector sendcounts(numprocs, 0), recvcounts(numprocs, 0); - vector sdispls(numprocs), rdispls(numprocs); - - vector< vector > bodylist(numprocs); - unsigned t; - for (it=cc->Particles().begin(); it!=cc->Particles().end(); it++) { - // Skip an OOB particle - if (it->second->key == 0u) continue; - // Look for key in this node's list - t = find_proc(loclist, it->second->key); - if (t == numprocs) { - cerr << "Process " << myid << ": loclist found last entry, " - << " key=" << hex << it->second->key << dec - ; - - cerr << ", end pt=" - << hex << loclist.back() << dec - << ", index=" << t << endl; - } - if (t == myid) continue; - bodylist[t].push_back(it->first); - sendcounts[t]++; - } - - for (unsigned k=0; kgetBufsize(); - - // Allocate send and receive buffers (bytes) - std::vector psend(Tcnt*bufsiz), precv(Fcnt*bufsiz); - - timer_convert.start(); - for (int toID=0; toIDparticlePack(cc->Particles()[bodylist[toID][i]], &psend[(ps+i)*bufsiz]); - cc->Particles().erase(bodylist[toID][i]); - } - } - timer_convert.stop(); - timer_xchange.start(); - - // Multiply counts and displacements by particle buffer size - for (auto & v : sendcounts) v *= bufsiz; - for (auto & v : recvcounts) v *= bufsiz; - for (auto & v : sdispls ) v *= bufsiz; - for (auto & v : rdispls ) v *= bufsiz; - - MPI_Alltoallv(&psend[0], &sendcounts[0], &sdispls[0], MPI_CHAR, - &precv[0], &recvcounts[0], &rdispls[0], MPI_CHAR, - MPI_COMM_WORLD); - - timer_xchange.stop(); - timer_convert.start(); - - if (Fcnt) { - for (unsigned i=0; i(); - pf->particleUnpack(part, &precv[i*bufsiz]); - if (part->mass<=0.0 || std::isnan(part->mass)) { - cout << "[Repartition, myid=" << myid - << ": crazy body with indx=" << part->indx - << ", mass=" << part->mass << ", key=" - << hex << part->key << dec - << ", i=" << i << " out of " << Fcnt << "]" << endl; - } - cc->Particles()[part->indx] = part; - } - - // Refresh size of local particle list - cc->nbodies = cc->particles.size(); - } - timer_convert.stop(); - timer_prepare.stop(); - - // - // Remake key body index - // - keybods.clear(); - unsigned oob1_cnt=0, oob_cnt=0; - for (PartMapItr n=cc->Particles().begin(); n!=cc->Particles().end(); n++) { - if (n->second->key==0u) { - oob1_cnt++; - continue; - } - if (n->second->indx==0) { - cout << "pHOT::Repartition bad particle indx=0!" << endl; - oob1_cnt++; - } else { - keybods.insert(key_pair(n->second->key, n->second->indx)); - } - } - - // checkBounds(2.0, "AFTER repartition"); - - MPI_Reduce(&oob1_cnt, &oob_cnt, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD); - unsigned oob_tot = oobNumber(); - if (myid==0 && oob_cnt != oob_tot) - cout << endl << "pHOT::Repartition: " << oob_cnt << " out of bounds," - << " expected " << oob_tot << endl; - - if (DEBUG_CLEAN) { - // - // Sanity checks for bad particle indices and bad particle counts - // - std::list badP; - for (PartMapItr ip=cc->particles.begin(); ip!=cc->particles.end(); ip++) { - if (ip->second->indx==0) { - cout << "pHOT::Repartition BAD particle in proc=" << myid - << ", mass=" << ip->second->mass << ", key=" - << hex << ip->second->key << dec - << endl; - badP.push_back(ip); - } - } - - if (badP.size()) { - cout << "pHOT::Repartition: removing " << badP.size() << " bad entries " - << "from particle list" << std::endl; - for (auto i : badP) cc->particles.erase(i); - } - - // Refresh size of local particle list - cc->nbodies = cc->particles.size(); - - // Count total number of particles as sanity check - if (DEBUG_SANITY) { - int nbodies1 = cc->nbodies, nbodies0=0; - MPI_Reduce(&nbodies1, &nbodies0, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD); - if (myid==0) { - if (nbodies0 != cc->nbodies_tot) - std::cout << "pHOT::Repartition: leaving with total # mismatch" - << ", total number=" << nbodies0 - << ", expected number=" << cc->nbodies_tot - << endl; - } - } - } - - if (DEBUG_KEYS) { // Summary/diaganostic output - // - unsigned int nsiz = cc->Particles().size(); - unsigned int ksiz = keys.size(); - vector nsize(numprocs), ksize(numprocs); - - timer_diagdbg.start(); - - MPI_Gather(&nsiz, 1, MPI_UNSIGNED, &nsize[0], 1, MPI_UNSIGNED, - 0, MPI_COMM_WORLD); - - MPI_Gather(&ksiz, 1, MPI_UNSIGNED, &ksize[0], 1, MPI_UNSIGNED, - 0, MPI_COMM_WORLD); - - if (myid==0) { - ofstream out(debugf.c_str(), ios::app); - unsigned nhead = 35+2*klen; - - out << setfill('-') << setw(nhead) << '-' << endl - << "---- Post-scatter summary, T = " << tnow << endl - << setw(nhead) << '-' << endl << setfill(' ') << left - << setw(5) << "#" << right - << setw(klen) << "kbeg" - << setw(klen) << "kfin" - << setw(15) << "nkeys" - << setw(15) << "bodies" << endl << "#" << endl; - for (int i=0; imykey==1u && i.second->ctotal==0u) { - cout << "Process " << myid - << ": empty root node in checkCellLevelList" << endl; - continue; - } - if (clevlst.find(i.second) == clevlst.end()) - missing_frontier_cell++; - } - - for (auto i : clevlst) { - - if (frontier.find(i.first->mykey) == frontier.end()) - missing_clevlst_cell++; - } - - if (missing_frontier_cell) - cout << msg << ", " << missing_frontier_cell - << " frontier cells not in level list" << endl; - - if (missing_clevlst_cell) - cout << msg << ", " << missing_clevlst_cell - << " level list cells not on frontier" << endl; - - timer_diagdbg.stop(); -} - -void pHOT::checkSampleCells(const std::string& msg) -{ - timer_diagdbg.start(); - - // Check for missing sample cells - - unsigned cnt=0; - map missing; - for (auto i : frontier) { - if (i.second->sample == 0x0) { - cnt++; - if (missing.find(i.second->level) == missing.end()) - missing[i.second->level] = 1; - else - missing[i.second->level]++; - } - } - - if (cnt) { - cout << msg << ", " << cnt << " missing sample cells" << endl << left - << setw(6) << "Level" << setw(6) << "Count" << endl - << setw(6) << "-----" << setw(6) << "-----" << endl; - for (auto k : missing) - cout << left << setw(6) << k.first << setw(6) << k.second << endl; - } - - // Check for ophaned cells: cells not in sample cell child list - - unsigned bad1=0, bad2=0; - set orphan1, orphan2; - for (auto i : frontier) { - if (i.second->sample) { - if (!i.second->sampleTest()) { - bad1++; - orphan1.insert(i.second); - } - } - if (i.second->parent) { - bool found = false; - for (auto v : i.second->parent->children) { - if (v.second == i.second) found = true; - } - if (!found) { - bad2++; - orphan2.insert(i.second); - } - } - } - - if (bad1) { - cout << msg << ", " << bad1 << " orphaned frontier cells [sample]" << endl; - cout << setw(10) << "Cell" << setw(10) << "Sample" - << setw(10) << "Check" << endl - << setw(10) << "------" << setw(10) << "------" - << setw(10) << "------" << endl; - for (auto k : orphan1) { - cout << left << setw(10) << k - << setw(10) << k->sample - << setw(10) << k->findSampleCell() - << endl; - } - } - - if (bad2) { - cout << msg << ", " << bad2 << " orphaned frontier cells [parent]" << endl; - for (auto k : orphan2) - cout << left << setw(10) << k << endl; - } - - timer_diagdbg.stop(); -} - - -void pHOT::makeCellLevelList() -{ - ostringstream sout; - ofstream out; - - if (DEBUG_EXTRA) { - sout << "pHOT_cells." << runtag << "." << myid; - out.open(sout.str().c_str(), ios::out | ios::app); - } - - // Make new lists - clevlst.clear(); - clevels = vector< set >(multistep+1); - - if (DEBUG_EXTRA) { - out << "Process " << myid << " in makeCellLevelList()" - << ", frontier size=" << frontier.size() << endl; - } - - unsigned ng=0, nt=0; - for (auto i : frontier) { - // Check for empty root node - if (i.second->mykey==1u && i.second->ctotal==0u) { - if (DEBUG_EXTRA) - out << "Process " << myid << " in makeCellLevelList()" - << ", empty root node" << endl; - - continue; - } - - nt++; // Otherwise, count this one - i.second->remake_plev(); - clevlst[i.second] = i.second->maxplev; - clevels[i.second->maxplev].insert(i.second); - if (i.second->bods.size() == 0) { - cerr << "Process " << myid - << ": makeCellLevelList has a broken frontier!\n"; - } else { - ng++; - } - } - - if (nt!=ng && DEBUG_EXTRA) { - out << "Process " << myid << ": made level list with " << ng - << " good cells out of " << nt << " expected" << endl; - - std::ostringstream sout; - sout << "pHOT::makeLevelList, myid=" << std::setw(5) << myid; - printCellLevelList (out, sout.str()); - checkParticles (out, sout.str()); - } -} - -void pHOT::gatherCellLevelList() -{ - timer_diagdbg.start(); - - // - // Working vectors per node - // - vector pcnt(multistep+1, 0); - vector plev(multistep+1, 0); - unsigned nlev = cc->particles.size(); - - // - // Enter data - // - for (auto p : clevlst) pcnt[p.second]++; - - for (unsigned M=0; M<=multistep; M++) plev[M] = CLevels(M).size(); - - // - // Recv vectors; make sure space has been allocated. std::vector is - // smart about this. - // - Pcnt.resize(multistep+1); - Plev.resize(multistep+1); - - MPI_Reduce(&pcnt[0], &Pcnt[0], multistep+1, MPI_UNSIGNED, MPI_SUM, 0, - MPI_COMM_WORLD); - - MPI_Reduce(&plev[0], &Plev[0], multistep+1, MPI_UNSIGNED, MPI_SUM, 0, - MPI_COMM_WORLD); - - MPI_Reduce(&nlev, &Nlev, 1, MPI_UNSIGNED, MPI_SUM, 0, - MPI_COMM_WORLD); - - timer_diagdbg.stop(); -} - -void pHOT::printCellLevelList(ostream& out, const std::string& msg) -{ - // Sanity check - - if (Pcnt.size() != Plev.size() || Pcnt.size() != multistep+1) return; - - // OK - - timer_diagdbg.start(); - - out << msg << endl; - out << left << setw(60) << setfill('-') << "-" << endl << setfill(' ') - << "*** T=" << tnow << " N=" << Nlev << endl - << setw(10) << "M" << setw(10) << "number" - << setw(10) << "counts" << endl; - for (unsigned M=0; M<=multistep; M++) - out << setw(10) << M << setw(10) << Plev[M] - << setw(10) << Pcnt[M] << endl; - out << left << setw(60) << setfill('-') << "-" << endl << setfill(' '); - - timer_diagdbg.stop(); -} - - -void pHOT::adjustCellLevelList(unsigned mlevel) -{ - if (multistep==0) return; // No need to bother if multistepping is off - // Otherwise . . . - - ostringstream sout; - ofstream out; - - if (DEBUG_EXTRA) { - sout << "pHOT_cells." << runtag << "." << myid; - out.open(sout.str().c_str(), ios::out | ios::app); - } - -#ifdef USE_GPTL - GPTLstart("pHOT::adjustCellLevelList"); -#endif - - unsigned ng=0, nt=0, ns=0, m, cnt; - for (unsigned M=mlevel; M<=multistep; M++) { - nt += CLevels(M).size(); - cnt = 0; - if (CLevels(M).size()>0) { - set::iterator it = CLevels(M).begin(), nit; - while (it != CLevels(M).end()) { - // Skip the root cell if it's empty - // (lazy kludge) - if ( (*it)->mykey==1u && (*it)->ctotal==0 ) { - nt--; // Reduce the node count by one - it++; // Go the next set in the set . . . - continue; - } - - cnt++; // Count the (presumably good) cells - - // For diagnostic info only - if ((*it)->bods.size()) ng++; - else { // This shouldn't happen - cout << "Process " << myid << ": pHOT::adjustCellLevelList: " - << cnt << "/" << CLevels(M).size() - << " zero!" << endl; - } - // Newly computed level: - // we may move cell down but not up . . . - m = max(mlevel, (*it)->remake_plev()); - nit = it++; - if (M!=m) { - clevels[m].insert(*nit); - clevlst[*nit] = m; - clevels[M].erase(nit); - ns++; - } - - if (CLevels(M).empty()) break; - - } - } - } - - // Diagnostic output . . . - if (nt!=ng) - cout << "Process " << myid << ": adjusted level list with " << ng - << " good cells out of " << nt << " expected, " << ns - << " cells moved" << endl; - - if (DEBUG_EXTRA) { - std::ostringstream sout; - sout << "pHOT::adjustLevelList, myid=" << std::setw(5) << myid; - - printCellLevelList (out, sout.str()); - checkParticles (out, sout.str()); - } - -#ifdef USE_GPTL - GPTLstop("pHOT::adjustCellLevelList"); -#endif -} - -void pHOT::adjustTree(unsigned mlevel) -{ -#ifdef USE_GPTL - GPTLstart("pHOT::adjustTree"); - GPTLstart("pHOT::keyBods"); -#endif - // Barrier to make sure that the timer - // gives a sensible measurement of key time - timer_waiton0.start(); - (*barrier)("pHOT: repartition key timer [0]", __FILE__, __LINE__); - timer_waiton0.stop(); - - timer_tadjust.start(); - - if (clevels.size()==0) makeCellLevelList(); - - if (DEBUG_ADJUST) { - std::ostringstream sout; - sout << "pHOT::adjustTree, Node " << myid - << ": ERROR bodycell BEFORE adjustTree(), T=" << tnow - << " mlevel=" << mlevel << endl; - - checkBodycell (sout.str()); - checkPartKeybods (sout.str(), mlevel); - checkKeybods (sout.str()); - checkKeybodsFrontier (sout.str()); - checkCellFrontier (sout.str()); - checkCellClevel (sout.str(), mlevel); - checkCellClevelSanity (sout.str(), mlevel); - checkSampleCells (sout.str()); - checkCellLevelList (sout.str()); - checkLevelLists (sout.str()); - } - - adjcnt++; // For debug labeling only . . . - - timer_keymake.start(); - - pCell* c; - key_type newkey, oldkey; - list oldp; - - // - // Exchange list - // - timer_keybods.start(); - vector< vector > exchange(numprocs); - - // - // Make body list from frontier cells for this level - // OOB particles must wait until a full tree build - // - for (unsigned M=mlevel; M<=multistep; M++) { - set::iterator it = CLevels(M).begin(); - set::iterator itend = CLevels(M).end(); - for (;it!=itend; it++) { - oldp.insert(oldp.end(), (*it)->bods.begin(), (*it)->bods.end()); - } - } - timer_keybods.stop(); - -#ifdef USE_GPTL - GPTLstop ("pHOT::keyBods"); - GPTLstart("pHOT::keyCells"); -#endif - - // Barrier to make sure that the timer - // gives a sensible measurement of key time - timer_waiton1.start(); - (*barrier)("pHOT: repartition key timer [1]", __FILE__, __LINE__); - timer_waiton1.stop(); - - // - // Update body by body using the oldp list - // - unsigned newproc; - for (auto ip : oldp) { - - timer_keybods.start(); - Particle *p = cc->Part(ip); - if (p==0) { // Sanity check - cout << "Process " << myid - << ": pHOT::adjustTree: ERROR, requested particle index " - << "does not exist!" << endl; - } - numkeys++; - timer_keybods.stop(); - - // - // Get and recompute keys - // - oldkey = p->key; - newkey = getKey(&(p->pos[0])); - - // - // Get this particle's cell - // - timer_keybods.start(); - key_key::iterator ij = bodycell.find(oldkey); - - // Bad key sanity check (should NEVER happen) - if (ij == bodycell.end()) { // - cout << "Process " << myid - << ": pHOT::adjustTree: ERROR could not find cell for particle" - << " key=" << hex << oldkey << dec << ", index=" << p->indx - << " pnumber=" << cc->Number() << " bodycell=" << bodycell.size() - << endl; - timer_keybods.stop(); - continue; - } - - key_cell::iterator cit = frontier.find(ij->second.first); - - // Bad cell (should NEVER happen) - if (cit == frontier.end() ) { // - cout << "Process " << myid - << ": pHOT::adjustTree: ERROR could not find expected cell" - << " on frontier, count=" << adjcnt - << " oldbody=" << hex << oldkey << dec - << " newbody=" << hex << newkey << dec - << " cell=" << hex << bodycell.find(oldkey)->second.first << dec - << " index=" << p->indx - << endl; - timer_keybods.stop(); - continue; - } - - // - // This is the cell for this body - // - c = cit->second; - - timer_keybods.stop(); - - cntr_total++; - - // - // Are the new and old keys the same? I.e. same cell? - // - timer_keycomp.start(); - - if (newkey != oldkey) { - - cntr_new_key++; - // Key pairs - // - key_pair newpair(newkey, p->indx); - key_pair oldpair(oldkey, p->indx); - - // Put the particle in a new cell? - // - if ( !(c->isMine(newkey)) ) { - - timer_keynewc.start(); - cntr_not_mine++; - - c->Remove(oldpair, &change); - - // Same processor and in bounds? - if (newkey != 0u) { - newproc = find_proc(loclist, newkey); - if (newproc != myid) { - cntr_ship++; - // Ship this particle elsewhere - exchange[newproc].push_back(ip); - - } else { - // Add the new pair to the tree - keybods.insert(newpair); - c->Add(newpair, &change); - } - - } else { // Out of bounds - oob.insert(p->indx); - } - - p->key = newkey; // Assign the new key to the particle - - timer_keynewc.stop(); - - } else { // Same cell: update body cell index - // for the new key - timer_keyoldc.start(); - cntr_mine++; - // Update key list - c->UpdateKeys(oldpair, newpair); - // Assign the new key to the particle - p->key = newkey; - - timer_keyoldc.stop(); - } - } - timer_keycomp.stop(); - - } - timer_keymake.stop(); - // Barrier to make sure that the timer - // gives a sensible measurement of key time - timer_waiton2.start(); - (*barrier)("pHOT: repartition key timer [2]", __FILE__, __LINE__); - timer_waiton2.stop(); - - timer_cupdate.start(); - -#ifdef USE_GPTL - GPTLstop ("pHOT::keyCells"); - GPTLstart("pHOT::adjExchange"); -#endif - - - if (DEBUG_CHECK) { - std::ostringstream sout; - sout << "pHOT::adjustTree, Node " << myid - << ", BEFORE particle exchange, T=" << tnow; - - checkCellFrontier (sout.str()); - checkKeybodsFrontier (sout.str()); - checkCellClevel (sout.str(), mlevel); - checkLevelLists (sout.str()); - } - - // - // Exchange particles - // - - Particle part; - unsigned Tcnt=0, Fcnt, sum; - std::vector sdispls(numprocs), rdispls(numprocs); - std::vector sendcounts(numprocs, 0), recvcounts(numprocs, 0); - - timer_prepare.start(); - for (unsigned k=0; kgetBufsize(); - - // Allocate send and receive buffers - std::vector psend(Tcnt*bufsiz), precv(Fcnt*bufsiz); - - timer_convert.start(); - for (int toID=0; toIDparticlePack(cc->Particles()[exchange[toID][i]], &psend[(ps+i)*bufsiz]); - cc->Particles().erase(exchange[toID][i]); - } - } - timer_convert.stop(); - - timer_xchange.start(); - - // Mulitiply counts and displacements by particle buffer size - for (auto & v : sendcounts) v *= bufsiz; - for (auto & v : recvcounts) v *= bufsiz; - for (auto & v : sdispls ) v *= bufsiz; - for (auto & v : rdispls ) v *= bufsiz; - - MPI_Alltoallv(&psend[0], &sendcounts[0], &sdispls[0], MPI_CHAR, - &precv[0], &recvcounts[0], &rdispls[0], MPI_CHAR, - MPI_COMM_WORLD); - - timer_xchange.stop(); - - timer_convert.start(); - - for (unsigned i=0; i(); - pf->particleUnpack(part, &precv[i*bufsiz]); - if (part->mass<=0.0 || std::isnan(part->mass)) { - cout << "[adjustTree, myid=" << myid - << ": crazy body indx=" << part->indx - << ", mass=" << part->mass << ", key=" - << hex << part->key<< dec - << ", i=" << i << " out of " << Fcnt << "]" << endl; - } - - cc->Particles()[part->indx] = part; - - if (part->key != 0u) { - key_pair newpair(part->key, part->indx); - keybods.insert(newpair); - root->Add(newpair, &change); - } - } - - // Refresh size of local particle list - cc->nbodies = cc->particles.size(); - - timer_convert.stop(); - } - - // - // Debug counter - // - if (myid==0) { - n_xchange += sum; - m_xchange++; - sumstep++; - if (sum==0) sumzero++; - } - - -#ifdef USE_GPTL - GPTLstop ("pHOT::adjExchange"); - GPTLstart("pHOT::overlap"); -#endif - - - if (DEBUG_CHECK) { - std::ostringstream sout; - sout << "pHOT::adjustTree, Node " << myid - << ", BEFORE cell overlap, T=" << tnow; - - checkCellFrontier (sout.str()); - checkKeybodsFrontier (sout.str()); - checkCellClevel (sout.str(), mlevel); - checkLevelLists (sout.str()); - } - - - // - // Cell overlap? - // - - key_type headKey=0u, tailKey=0u; - unsigned head_num=0, tail_num=0; - - timer_overlap.start(); - - size_t bufsiz = pf->getBufsize(); - - for (int n=0; nfirst)->second.first; - tail_num = frontier[tailKey]->bods.size(); - } else { - tailKey = 0u; - tail_num = 0; - } - - // Send my tail cell info to next node - // - MPI_Send(&tailKey, 1, MPI_EXP_KEYTYPE, n+1, 131, MPI_COMM_WORLD); - MPI_Send(&tail_num, 1, MPI_UNSIGNED, n+1, 132, MPI_COMM_WORLD); - - // Get next node's head cell info - // - MPI_Recv(&headKey, 1, MPI_EXP_KEYTYPE, n+1, 133, - MPI_COMM_WORLD, MPI_STATUS_IGNORE); - MPI_Recv(&head_num, 1, MPI_UNSIGNED, n+1, 134, - MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - if (tailKey==headKey && tailKey!=0u) { - - c = frontier[tailKey]; // Cell in question: my last cell - - if (tail_num>head_num) { - // - // Receive particles - // - std::vector Precv(head_num*bufsiz); - - MPI_Recv(&Precv[0], head_num*bufsiz, MPI_CHAR, - n+1, 136, MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - for (int i=0; i(); - pf->particleUnpack(part, &Precv[i*bufsiz]); - cc->Particles()[part->indx] = part; - key_pair newpair(part->key, part->indx); - keybods.insert(newpair); - c->Add(newpair, &change); - } - - } else { - // - // Send particles - // - unsigned k=0; - std::vector Psend(tail_num*bufsiz); - vector::iterator ib; - for (auto b : c->bods) { - pf->particlePack(cc->Particles()[b], &Psend[k++*bufsiz]); - cc->Particles().erase(b); - } - - c->RemoveAll(); - - if (c->mykey!=1u) { // Don't remove the root node - - // queue for removal from level lists - change.push_back(cell_indx(c, REMOVE)); - - // queue for deletion - change.push_back(cell_indx(c, DELETE)); - } - - MPI_Send(&Psend[0], tail_num*bufsiz, MPI_CHAR, - n+1, 135, MPI_COMM_WORLD); - } - } - } - - if (n+1==myid) { - - if (keybods.size()) { - key_indx::iterator it = keybods.begin(); // Sanity check: - if (bodycell.find(it->first) == bodycell.end()) { - cerr << "In adjustTree: No cell for body=" - << hex << it->first << dec - << " bodycell size=" << bodycell.size() << endl; - headKey = 0u; - head_num = 0; - } else { - headKey = bodycell.find(it->first)->second.first; - head_num = frontier[headKey]->bods.size(); - } - } else { - headKey = 0u; - head_num = 0; - } - - // Get previous nodes tail cell info - MPI_Recv(&tailKey, 1, MPI_EXP_KEYTYPE, n, 131, - MPI_COMM_WORLD, MPI_STATUS_IGNORE); - MPI_Recv(&tail_num, 1, MPI_UNSIGNED, n, 132, - MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - // Send head node into to previous node - MPI_Send(&headKey, 1, MPI_EXP_KEYTYPE, n, 133, MPI_COMM_WORLD); - MPI_Send(&head_num, 1, MPI_UNSIGNED, n, 134, MPI_COMM_WORLD); - - if (tailKey==headKey && tailKey!=0u) { - - c = frontier[headKey]; // Cell in question - - if (tail_num>head_num) { - // - // Send particles - // - unsigned k=0; - std::vector Psend(head_num*bufsiz); - for (auto b : c->bods) { - pf->particlePack(cc->Particles()[b], &Psend[k++*bufsiz]); - cc->Particles().erase(b); - } - - c->RemoveAll(); - - if (c->mykey!=1u) { // Dont remove the root node! - - // queue for removal from level lists - change.push_back(cell_indx(c, REMOVE)); - - // queue for deletion - change.push_back(cell_indx(c, DELETE)); - } - - MPI_Send(&Psend[0], head_num*bufsiz, MPI_CHAR, - n, 136, MPI_COMM_WORLD); - - } else { - // - // Receive particles - // - std::vector Precv(tail_num*bufsiz); - MPI_Recv(&Precv[0], tail_num*bufsiz, MPI_CHAR, - n, 135, MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - for (int i=0; i(); - pf->particleUnpack(part, &Precv[i*bufsiz]); - cc->Particles()[part->indx] = part; - key_pair newpair(part->key, part->indx); - keybods.insert(newpair); - c->Add(newpair, &change); - } - } - } - } - - } - - - timer_overlap.stop(); - -#ifdef USE_GPTL - GPTLstop ("pHOT::overlap"); - GPTLstart("pHOT::cUpdate"); -#endif - - if (DEBUG_CHECK) { - std::ostringstream sout; - sout << "pHOT::adjustTree, Node " << myid - << ", BEFORE cell list reconstruction, T=" << tnow; - - checkCellFrontier (sout.str()); - checkKeybodsFrontier (sout.str()); - checkCellClevel (sout.str(), mlevel); - checkLevelLists (sout.str()); - } - - // - // Compute the physical states in each cell for the entire tree and - // find the sample cells, if anything changed - // - timer_cellcul.start(); - - computeCellStates(); - - // - // Work around for OpenMP (maybe this is better anyhow) - // - std::vector createL, removeL, deleteL, recompL; - for (auto i : change) { - if (i.second == CREATE) createL.push_back(i.first); - if (i.second == REMOVE) removeL.push_back(i.first); - if (i.second == DELETE) deleteL.push_back(i.first); - if (i.second == RECOMP) recompL.push_back(i.first); - } - change.clear(); // Reset the change list for next time - - if (DEBUG_CHECK) { - - // Deep check (verbose) - // - if (false) { - unsigned preLevlst = 0, preFrontier = 0; - for (int i=0; imykey) == frontier.end()) - preFrontier++; - } - } - - if (preLevlst) - std::cout << "pHOT::adjustTree, Node " << myid - << ", before reconstruction: " - << preLevlst << "/" << clevlst.size() - << " cells missing from level list and " << preFrontier - << "/" << frontier.size() << " cells missing from frontier" - << std::endl; - } - - std::ostringstream sout; - sout << "pHOT::adjustTree, Node " << myid - << ", before reconstruction, " - << "ERROR mlevel=" << mlevel << ", T=" << tnow; - - checkBodycell(sout.str()); - - // Check the remove list for duplicates . . . - sort(removeL.begin(), removeL.end()); - unsigned imult = 0; - for (size_t i=1; ibods.size()) { -#pragma omp critical - { - unsigned m = max(c->maxplev, mlevel); - clevlst[c] = m; - clevels[m].insert(c); - } - - // Locate the new sample cell - c->findSampleCell("adjustTree"); - - } - } - - // - // Remove the former leaves from the lists - // -#pragma omp parallel for default(shared) - for (int i=0; imykey) == frontier.end()) - std::cout << " and gone from frontier"; - else - std::cout << " but is in frontier"; - std::cout << std::endl; - // continue; - } - } - unsigned m = clevlst[c]; -#pragma omp critical - { - clevlst.erase(c); - if (DEBUG_CHECK) { - if (clevels[m].find(c) == clevels[m].end()) { -#ifdef HAVE_OMP_H - std::cout << "pHOT::adjustTree(REMOVE) [" << omp_get_thread_num() - << "]: cell=" << std::hex << c -#else - std::cout << "pHOT::adjustTree(REMOVE) [" << 1 - << "]: cell=" << std::hex << c -#endif - << std::dec << " not in level " << m << std::endl; - } - } - clevels[m].erase(c); - } - } - - // - // Delete empty leaves - // -#pragma omp parallel for default(shared) - for (int i=0; ichildren.size() == 0) - recompL[i]->findSampleCell("adjustTree"); - } - } - - - if (DEBUG_ADJUST) { - std::ostringstream sout; - sout << "pHOT::adjustTree at END, Node " << myid - << ", ERROR mlevel=" << mlevel; - - checkBodycell (sout.str()); - checkKeybodsFrontier (sout.str()); - checkParticles (cout, sout.str()); - checkFrontier (cout, sout.str()); - checkCellClevel (sout.str(), mlevel); - checkCellClevelSanity (sout.str(), mlevel); - - checkDupes2(); - checkIndices(); - checkSampleCells (sout.str()); - checkCellLevelList (sout.str()); - checkKeybods (sout.str()); - checkPartKeybods (sout.str(), mlevel); - } - - // Check the load balance based on the total effort - checkEffort(mlevel); - - timer_cellcul.stop(); - - timer_cupdate.stop(); - - timer_tadjust.stop(); - - if (DEBUG_KEYS) { // Summary/diaganostic output - // - unsigned int nsiz = cc->Particles().size(); - vector nsize(numprocs); - - MPI_Gather(&nsiz, 1, MPI_UNSIGNED, &nsize[0], 1, MPI_UNSIGNED, - 0, MPI_COMM_WORLD); - - if (myid==0) { - ofstream out(debugf.c_str(), ios::app); - unsigned nhead = 20 + 2*klen; - - out << left << setfill('-') << setw(nhead) << '-' << endl - << "---- Post-adjustTree summary [" << mlevel << "], T = " - << tnow << endl << setw(nhead) << '-' << endl << setfill(' '); - out << left << setw(5) << "#" - << setw(klen) << right << "kbeg" << setw(klen) << "kfin" - << setw(15) << "bodies" << endl << "#" << endl; - for (int i=0; i > outdat; - - for (unsigned M=0; M<=multistep; M++) { - if (CLevels(M).size()) { - cnt = 0; - for (auto i : CLevels(M)) { - cnt++; - // FOR CELL OUTPUT - outdat[i] = pair(M, i->bods.size()); - - if (i->bods.size()) { - if (pc) { - for (auto b : i->bods) { - if (!cc->Part(b)) { - out << "pHOT::checkParticles:: M=" << M << ", bad body at " - << cnt << "/" << CLevels(M).size() - << " cell=" << hex << i << dec << endl; - badb++; - } - } - } - } else { - out << "pHOT::checkParticles:: M=" << M << ", zero bods at " - << cnt << "/" << CLevels(M).size() - << " cell=" << hex << i << dec << endl; - out << "pHOT::checkParticles:: More info for " - << hex << i << dec << ": marked as "; - if (i->isLeaf) out << "LEAF, "; - else out << "BRANCH, "; - if (i->parent) { - if (i->parent->isLeaf) out << "parent IS a leaf. "; - else out << "parent is NOT a leaf. "; - } else { - out << "parent does NOT exist. "; - } - out << "Node has " << i->children.size() << " children and "; - if (frontier.find(i->mykey) == frontier.end()) - out << "is gone from frontier." << std::endl; - else - out << "is in frontier" << std::endl; - - badc++; - } - } - } - } - - // OUTPUT CELL LIST - ostringstream origfile1, backfile1; - - origfile1 << "chkcell." << myid; - backfile1 << "chkcell." << myid << ".bak"; - if (rename(origfile1.str().c_str(), backfile1.str().c_str())) { - perror("pHOT"); - ostringstream message; - message << "error creating backup file <" << backfile1.str() << ">"; - } - - ostringstream origfile2, backfile2; - - origfile2 << "chklist." << myid; - backfile2 << "chklist." << myid << ".bak"; - if (rename(origfile2.str().c_str(), backfile2.str().c_str())) { - perror("pHOT"); - ostringstream message; - message << "error creating backup file <" << backfile2.str() << ">"; - } - - ofstream out1(origfile1.str().c_str()); - ofstream out2(origfile2.str().c_str()); - - if (!out1) { - ostringstream message; - message << "error opening output file <" << origfile1.str() << ">"; - bomb(membername, message.str()); - } - - if (!out2) { - ostringstream message; - message << "error opening output file <" << origfile2.str() << ">"; - bomb(membername, message.str()); - } - - for (auto l : outdat) { - out1 << setw(15) << hex << l.first << dec - << setw(8) << l.second.first - << setw(8) << l.second.second - << endl; - } - - for (auto l : clevlst) { - out2 << setw(15) << hex << l.first << dec - << setw(8) << l.second - << endl; - } - // END OUTPUT CELL LIST - - timer_diagdbg.stop(); - - if (msg.size() && (badb || badc) ) { - out << msg << ": pHOT::checkParticles, bad cell=" << badc; - if (pc) out << " bad bod=" << badb; - out << endl; - return false; - } - else return true; - -} - - -bool pHOT::checkFrontier(ostream& out, const std::string& msg) -{ - unsigned bad=0; - bool good=true; - - timer_diagdbg.start(); - - for (unsigned M=0; M<=multistep; M++) { - if (CLevels(M).size()) { - for (auto i : CLevels(M)) { - if (frontier.find(i->mykey) == frontier.end()) { - out << "pHOT::checkFrontier error on M=" << M - << ", cell=" << hex << i << dec << endl; - bad++; - good = false; - } - } - } - } - - if (bad && msg.size()) { - out << msg << ": pHOT::checkFrontier, bad cell=" - << bad << endl; - } - - timer_diagdbg.stop(); - - return good; -} - -// -// This big nasty mess should only be used for debugging! -// -bool pHOT::checkDupes1(const std::string& msg) -{ - bool ret = false; - if (!DEBUG_KEYS) return ret; - - timer_diagdbg.start(); - - // Check for duplicate keys, duplicate sequence numbers - multimap plist; - set > keydup; - -#ifdef USE_GPTL - GPTLstart("pHOT::checkDupes1::duplicate_check"); -#endif - - // Make multimap of all index/key pairs - for (PartMapItr it=cc->Particles().begin(); it!=cc->Particles().end(); it++) - plist.insert(pair(it->first, it->second->key)); - - // Hold unique duplicated indices - multiset dups; - map > kdup; - typedef pair > ptype ; - multimap::iterator k=plist.begin(), kl=plist.begin(); - - if (k != plist.end()) { - - for (k++; k!=plist.end(); k++) { - - while (k->first == kl->first && k!=plist.end()) { - // Make a list for this index, if it's a new dup - if (kdup.find(kl->first)==kdup.end()) { - kdup.insert(ptype(kl->first, list())); - kdup[kl->first].push_back(kl->second); - } - - // Add the key to the list for this index - kdup[k->first].push_back(k->second); - - // Add this index to the dup index multiset - dups.insert(k->first); - - // Add the key to the unique key map - keydup.insert(k->second); - } - - kl = k; - } - - } - - if (dups.size()) { - ret = true; - ostringstream sout; - sout << outdir << runtag << ".pHOT_crazy." << myid; - ofstream out(sout.str().c_str(), ios::app); - out << endl - << "#" << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << "#---- " << msg << endl - << "#---- Time=" << tnow - << ", N=" << cc->Number() - << ", Duplicates=" << dups.size() - << ", Unique keys=" << keydup.size() - << endl - << "#" << setfill('-') << setw(60) << '-' << setfill(' ') << endl; - for (auto b : kdup) { - Particle *p = cc->Part(b.first); - out << setw(10) << b.first << setw(18) << p->mass; - for (int k=0; k<3; k++) out << setw(18) << p->pos[k]; - out << setw(10) << p->indx - << " " << hex << p->key << dec << endl - ; - for (auto k : b.second) { - out << left << setw(10) << "---" << hex << k << dec << endl; - } - } - out << "#" << setfill('-') << setw(60) << '-' << setfill(' ') << endl; - } -#ifdef USE_GPTL - GPTLstop("pHOT::checkDupes1::duplicate_check"); -#endif - - timer_diagdbg.stop(); - - return ret; -} - - -bool pHOT::checkDupes2() -{ - timer_diagdbg.start(); - - vector indices; - for (auto i : frontier) - indices.insert(indices.end(), i.second->bods.begin(), i.second->bods.end()); - - sort(indices.begin(), indices.end()); - - unsigned dup = 0; - for (unsigned n=1; nsecond->pos[0] << ", " << n->second->pos[1] - << ", " << n->second->pos[2] << ")" << endl; - } - ok = false; - cnt++; - } - } - } - - if (msg.size() && cnt) { - cout << msg << ": checkKeybods: " - << cnt << " unmatched particles" << endl; - } - - timer_diagdbg.stop(); - - return ok; -} - - -bool pHOT::checkKeybodsFrontier(const std::string& msg) -{ - timer_diagdbg.start(); - - bool ok = true; - unsigned pcnt=0, bcnt=0; - std::set check; - - for (PartMapItr n = cc->Particles().begin(); n!=cc->Particles().end(); n++) { - // Skip oob particles - // - if (n->second->key) { - // a keybods (std::set) key - key_pair tpair(n->second->key, n->second->indx); - key_indx::iterator it = keybods.find(tpair); - - if (it==keybods.end()) { - if (DEBUG_NOISY) { - cout << "pHOT::checkKeybodsFrontier, " << msg - << ": count=" << std::setw(5) << pcnt - << " unmatched particle" << std::endl - << "(x, y, z)=(" - << std::setw(16) << n->second->pos[0] << ", " - << std::setw(16) << n->second->pos[1] << ", " - << std::setw(16) << n->second->pos[2] << ")" << std::endl - << "(u, v, w)=(" - << std::setw(16) << n->second->vel[0] << ", " - << std::setw(16) << n->second->vel[1] << ", " - << std::setw(16) << n->second->vel[2] << ")" << std::endl; - } - ok = false; - pcnt++; - - } else { - // - // Look for the particle key in the bodycell multimap - // - key2Range ij = bodycell.equal_range(n->second->key); - key_type ckey = 0l; - - if (ij.first != ij.second) { - key2Itr ijk = ij.first; - while (ijk != ij.second) { - if (ijk->second.second == tpair.second) { - ckey = ijk->second.first; - break; - } - ijk++; - } - } - - if (ckey == 0l) { - if (DEBUG_NOISY) { - // Check for index on in bodycell with - // a sequential scan - key_type fCell = 0l; - for (key2Itr kt=bodycell.begin(); kt!=bodycell.end(); kt++) { - if (kt->second.second == n->second->indx) { - fCell = kt->second.first; - break; - } - } - - cout << "pHOT::checkKeybodsFrontier, " << msg - << ": body count=" << std::setw(5) << bcnt - << ", key=" << std::hex << n->second->key << std::dec - << ", indx=" << n->second->indx - << ", found=" << std::hex << fCell << std::dec - << ", particle with no bodycell" << std::endl - << "(x, y, z)=(" - << std::setw(16) << n->second->pos[0] << ", " - << std::setw(16) << n->second->pos[1] << ", " - << std::setw(16) << n->second->pos[2] << ")" << std::endl - << "(u, v, w)=(" - << std::setw(16) << n->second->vel[0] << ", " - << std::setw(16) << n->second->vel[1] << ", " - << std::setw(16) << n->second->vel[2] << ")" << std::endl; - } - ok = false; - bcnt++; - - } else { - key_cell::iterator kt = frontier.find(ckey); - - if (kt == frontier.end()) { - if (DEBUG_NOISY) { - cout << "pHOT::checkKeybodsFrontier, " << msg - << ": frontier count=" << std::setw(5) << check.size() - << " unmatched particle" << std::endl - << "(x, y, z)=(" - << std::setw(16) << n->second->pos[0] << ", " - << std::setw(16) << n->second->pos[1] << ", " - << std::setw(16) << n->second->pos[2] << ")" << std::endl - << "(u, v, w)=(" - << std::setw(16) << n->second->vel[0] << ", " - << std::setw(16) << n->second->vel[1] << ", " - << std::setw(16) << n->second->vel[2] << ")" << std::endl; - } - ok = false; // Count the missing cells - if (check.find(ckey) == check.end()) check.insert(ckey); - } - } - } - } - } - - - unsigned fcnt = check.size(); - - if (msg.size() && (pcnt || bcnt || fcnt)) { - cout << msg << ": checkKeybods: " - << pcnt << " unmatched particles, " - << bcnt << " body/cell entries, " - << fcnt << " cells not on frontier" << std::endl; - } - - timer_diagdbg.stop(); - - return ok; -} - - -bool pHOT::checkPartKeybods(const std::string& msg, unsigned mlevel) -{ - unsigned bcelbod = 0; - unsigned bfrontr = 0; - bool ok = true; - - timer_diagdbg.start(); - - // - // Make body list from frontier cells for this level - // - for (unsigned M=mlevel; M<=multistep; M++) { - for (auto i : CLevels(M)) { - for (auto b : i->bods) { - key_type key = cc->Particles()[b]->key; - unsigned indx = cc->Particles()[b]->indx; - - // Look for cell for this body - // - if (bodycell.find(key) == bodycell.end()) { - bcelbod++; - ok = false; - } - // Look for cell in frontier . . . - // - if (frontier.find(bodycell.find(key)->second.first) == frontier.end()) { - bfrontr++; - ok = false; - } - } - } - } - // - // Report - // - if (!ok && msg.size()) { - cout << msg - << ": pHOT::checkPartKeybods: ERROR bad bodycell=" << bcelbod - << " bad frontier=" << bfrontr << endl; - - } - - timer_diagdbg.stop(); - - return ok; -} - - -bool pHOT::checkBodycell(const std::string& msg) -{ - timer_diagdbg.start(); - - bool ok = true; - unsigned cnt=0; - for (PartMapItr n=cc->Particles().begin(); n!=cc->Particles().end(); n++) { - // Ignore OOB particle - if (n->second->key==0u) continue; - - // Look for the bodycell - key_key::iterator it = bodycell.find(n->second->key); - if (it==bodycell.end()) { - ok = false; - cnt++; - if (DEBUG_NOISY) { - cout << "Process " << myid << ": checkBodycell: " - << cnt << " unmatched particle: key=" << hex - << n->second->key << dec << " index=" - << n->second->indx << ", (x, y, z)=(" - << n->second->pos[0] << ", " << n->second->pos[1] - << ", " << n->second->pos[2] << ")" << endl; - } - } - } - - if (msg.size() && cnt) { - cout << msg << ": checkBodycell: " - << cnt << " unmatched particles" << endl; - } - - timer_diagdbg.stop(); - - return ok; -} - - -bool pHOT::checkCellClevel(const std::string& msg, unsigned mlevel) -{ - bool ok = true; - timer_diagdbg.start(); - - unsigned bad = 0, total = 0; - for (unsigned M=mlevel; M<=multistep; M++) { - for (auto i : CLevels(M)) { - if (clevlst.find(i) == clevlst.end()) bad++; - } - } - - if (msg.size() && bad) { - cout << msg << ": " - << bad << "/" << total << " unmatched cells" << endl; - } - - timer_diagdbg.stop(); - - if (bad) return false; - else return true; -} - -bool pHOT::checkCellClevelSanity(const std::string& msg, unsigned mlevel) -{ - bool ok = true; - timer_diagdbg.start(); - - unsigned error = 0; - std::set expect, found; - - for (unsigned M=mlevel; M<=multistep; M++) { - for (auto i : CLevels(M)) { - expect.insert(i); - for (auto b : i->bods) { - // Look for the cell - key_key::iterator kk = bodycell.find(Body(b)->key); - // Is the cell on the frontier? - if (kk != bodycell.end()) { - key_cell::iterator kc = frontier.find(kk->second.first); - if (kc==frontier.end()) { - error++; - } else { - found.insert(kc->second); - } - } - } - } - } - - // Check lists - bool size_mismatch = (expect.size() == found.size() ? false : true); - unsigned missing = 0; - for (auto i : found) { - if (expect.find(i) == expect.end()) missing++; - } - - if (size_mismatch || missing || error) { - ok = false; - - if (msg.size()) { - std::cout << msg << ", checkCellClevelSanity: "; - if (size_mismatch) - std::cout << "expected " << expect.size() << " and found " - << found.size(); - if (error) - std::cout << ": " << error << " cells not on in expected list"; - if (missing) - std::cout << ": " << missing << " cells not on frontier"; - } - } - - return ok; -} - - - -bool pHOT::checkCellFrontier(const std::string& msg) -{ - bool ok = true; - timer_diagdbg.start(); - - std::set cells; - for (auto n : bodycell) { - if (n.first != 0u) cells.insert(n.second.first); - } - - size_t found=0, empty=0, branch=0, missed=0, total=cells.size(), cnt=0; - - for (auto n : cells) { - cnt++; - key_cell::iterator it = frontier.find(n); - if (it != frontier.end()) { - found++; - pCell* c = it->second; - if (c->isLeaf) { - if (c->bods.size() == 0) { - empty++; - if (DEBUG_NOISY) { - std::cout << "Process " << myid << ": checkCellFrontier: " - << cnt << "/" << total << "---" - << "cell key=" << std::hex << c->mykey << std::dec - << " is a leaf with no bodies" << std::endl; - } - } - } else { - branch++; - if (DEBUG_NOISY) { - std::cout << "Process " << myid << ": checkCellFrontier: " - << cnt << "/" << total << "---" - << "cell key=" << std::hex << c->mykey << std::dec - << " is a not a leaf bu is on the frontier" - << std::endl; - } - } - } else { - missed++; - if (DEBUG_NOISY) { - std::cout << "Process " << myid << ": checkCellFrontier: " - << cnt << "/" << total << "---" - << " unmatched cell key=" - << std::hex << n << std::dec << std::endl; - } - } - } - - if (empty || branch || missed || total != frontier.size()) { - ok = false; - if (msg.size()) { - std::cout << msg << ", checkCellFrontier: " - << cnt << "/" << total << " cells counted, " << found - << " on frontier, " << empty << " empty leaves, " - << branch << " branches, and " << missed << " frontier misses" - << std::endl; - } - } - - timer_diagdbg.stop(); - - return ok; -} - - -void pHOT::checkBounds(double rmax, const char *msg) -{ - timer_diagdbg.start(); - - int bad = 0; - for (PartMapItr n = cc->Particles().begin(); n!=cc->Particles().end(); n++) { - for (int k=0; k<3; k++) if (fabs(n->second->pos[k])>rmax) bad++; - } - if (bad) { - cout << "Process " << myid << ": has " << bad << " out of bounds"; - if (msg) cout << ", " << msg << endl; - else cout << endl; - } - - timer_diagdbg.stop(); -} - -unsigned pHOT::oobNumber() -{ - unsigned number=0, number1=oob.size(); - MPI_Reduce(&number1, &number, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD); - return number; -} - -void pHOT::checkOOB(vector& sendlist) -{ - timer_diagdbg.start(); - - bool aok = true; - unsigned bcnt=0; - if (myid==0) { - vector recvlist(numprocs*numprocs); - for (unsigned n=1; n list0(numprocs, 0), delta(numprocs); - - long list1 = oob.size(); - MPI_Allgather(&list1, 1, MPI_LONG, &list0[0], 1, MPI_LONG, MPI_COMM_WORLD); - -#ifdef USE_GPTL - GPTLstop ("pHOT::spreadOOB::in_reduce"); - GPTLstart("pHOT::spreadOOB::spread_comp"); -#endif - - double tot = 0.5; // Round off - for (unsigned n=0; n(floor(tot/numprocs)); - - long maxdif=0; - map nsend, nrecv; - for (unsigned n=0; n Receive some - Negative delta ===> Send some - Zero delta ===> Just right - */ - maxdif = max(maxdif, abs(delta[n])); - if (delta[n]>0) nrecv[n] = delta[n]; - if (delta[n]<0) nsend[n] = -delta[n]; - } - - // Debug output - if (DEBUG_OOB) { - ostringstream sout; - sout << "In spreadOOB, proc=" << std::setw(4) << myid - << " maxdif=" << std::setw(12) << maxdif - << " #=" << std::setw(10) << cc->nbodies_tot - << " ns=" << std::setw( 8) << nsend.size() - << " rs=" << std::setw( 8) << nrecv.size(); - } - -#ifdef USE_GPTL - GPTLstop("pHOT::spreadOOB::spread_comp"); -#endif - - // Don't bother if changes are small - if (maxdif < cc->nbodies_tot/tol) return; - - // Nothing to send or receive - if (nsend.size()==0 || nrecv.size()==0) return; - -#ifdef USE_GPTL - GPTLstart("pHOT::spreadOOB::make_list"); -#endif - - map::iterator isnd = nsend.begin(); - map::iterator ircv = nrecv.begin(); - vector sendlist(numprocs*numprocs, 0); - - while (1) { - sendlist[numprocs*isnd->first + ircv->first]++; - if (--(isnd->second) == 0) isnd++; - if (--(ircv->second) == 0) ircv++; - if (isnd == nsend.end() || ircv == nrecv.end()) break; - } - - if (DEBUG_CHECK) checkOOB(sendlist); - - unsigned Tcnt=0, Fcnt=0; - for (int i=0; i::iterator ioob; - size_t bufsiz = pf->getBufsize(); - char *psend=0, *precv=0; - std::vector rql; - MPI_Request r; - int ierr; - - if (Tcnt) psend = new char [Tcnt*bufsiz]; - if (Fcnt) precv = new char [Fcnt*bufsiz]; - - // - // Exchange particles between processes - // - for (int frID=0; frIDparticlePack(cc->Particles()[*ioob], &psend[(ps+i)*bufsiz]); - cc->Particles().erase(*ioob); - if (oob.find(*ioob) == oob.end()) - cerr << "Process " << myid << ": serious error, oob=" - << *ioob << endl; - else oob.erase(ioob); - } - rql.push_back(r); - if ( (ierr=MPI_Isend(&psend[ps*bufsiz], To*bufsiz, MPI_CHAR, - toID, 49, MPI_COMM_WORLD, &rql.back())) - != MPI_SUCCESS) { - cout << "Process " << myid << ": error in spreadOOP sending " - << To << " particles to #" << toID - << " ierr=" << ierr << endl; - } - ps += To; - } - } - // - // Current process receives particles (blocking) - if (myid==toID) { // - unsigned From = sendlist[numprocs*frID+toID]; - if (From) { - rql.push_back(r); - if ( (ierr=MPI_Irecv(&precv[pr*bufsiz], From*bufsiz, MPI_CHAR, - frID, 49, MPI_COMM_WORLD, &rql.back())) - != MPI_SUCCESS) - { - cout << "Process " << myid << ": error in spreadOOP receiving " - << From << " particles from #" << frID - << " ierr=" << ierr << endl; - } - pr += From; - } - } - - } // Receipt loop - } - - // - // Wait for completion of sends and receives - // - - if ( (ierr=MPI_Waitall(rql.size(), &rql[0], MPI_STATUSES_IGNORE)) != MPI_SUCCESS ) - { - cout << "Process " << myid << ": error in spreadOOB Waitall" - << ", ierr=" << ierr << endl; - } - -#ifdef USE_GPTL - GPTLstop ("pHOT::spreadOOB::exchange_particles"); - GPTLstart("pHOT::spreadOOB::add_to_particles"); -#endif - - // - // Add particles - // - - if (Fcnt) { - Particle part; - for (unsigned i=0; i(); - pf->particleUnpack(part, &precv[i*bufsiz]); - if (part->mass<=0.0 || std::isnan(part->mass)) { - cout << "[spreadOOB, myid=" << myid - << ", crazy body with indx=" << part->indx - << ", mass=" << part->mass << ", key=" - << hex << part->key << dec - << ", i=" << i << " out of " << Fcnt << "]" << endl; - } - cc->Particles()[part->indx] = part; - oob.insert(part->indx); - } - - // Refresh size of local particle list - cc->nbodies = cc->particles.size(); - } - -#ifdef USE_GPTL - GPTLstop("pHOT::spreadOOB::add_to_particles"); -#endif - -} - - -void pHOT::partitionKeysHilbert(vector& keys, - vector& kbeg, vector& kfin) -{ -#ifdef USE_GPTL - GPTLstart("pHOT::partitionKeysHilbert"); -#endif - - // For diagnostics - Timer *timer_debug; - if (DEBUG_KEYS && myid==0) { - timer_debug = new Timer(); - timer_debug->start(); - } - // Sort the keys - sort(keys.begin(), keys.end(), wghtKEY); - - vector keylist, keylist1; - - double srate = 1; // Only used for subsampling - - // Set to to enable subsampling - if (sub_sample) { - // Default rate (a bit more than - // one per DSMC cell) - const double subsample = 0.1; - - srate = subsample; // Actual value, may be reset for - // consistency - - // Desired number of samples - // - // Want at least 32, if possible - // since this is a good number for - // a DSMC cell - unsigned nsamp = - max(32, static_cast(floor(srate*keys.size()))); - - // Too many for particle count - if (nsamp > keys.size()) nsamp = keys.size(); - - // Consistent sampling rate - if (nsamp) srate = static_cast(nsamp)/keys.size(); - - // Subsample the key list, with weight accumulation - // - if (keys.size()) { - double twght = 0.0; - unsigned j = 1; - for (unsigned i=0; i(floor(srate*(i+1)-std::numeric_limits::min())) == j) { - keylist1.push_back(key_wght(keys[i].first, twght)); - twght = 0.0; - j++; - } - } - } - } else { - keylist1 = keys; - } - - // DEBUG - // (set to DEBUG_KEYS "true" at top of file to enable key range diagnostic) - // - if (DEBUG_KEYS) { - - timer_diagdbg.start(); - - if (myid==0) { - ofstream out(debugf.c_str(), ios::app); - std::string hdr(60, '-'); - out << endl - << hdr << endl << setfill('-') - << setw(60) << "--- Sampling stats " << setfill(' ') << endl - << hdr << endl << left - << setw(5) << "Id" - << setw(10) << "#keys" - << setw(15) << "rate" - << setw(10) << "samples" - << endl - << setw(5) << "--" - << setw(10) << "-----" - << setw(15) << "----" - << setw(10) << "-------" - << endl; - } - for (int n=0; n0) { - out << right - << hex - << setw(klen) << keys[0].first - << setw(klen) << keys[keys.size()/4].first - << setw(klen) << keys[keys.size()/2].first - << setw(klen) << keys[keys.size()*3/4].first - << setw(klen) << keys[keys.size()-1].first << dec - << endl; - } - } - (*barrier)("pHOT: partitionKeys debug 2", __FILE__, __LINE__); - } - - if (myid==0) { - ofstream out(debugf.c_str(), ios::app); - out << left << setfill('-') << setw(nhead) << "-" << endl - << setfill('-') << endl; - } - - // - // set to "true" to enable key list diagnostic - // - if (DEBUG_KEYLIST) { - const unsigned cols = 3; // # of columns in output - const unsigned cwid = 35; // column width - - if (myid==0) { - ofstream out(debugf.c_str(), ios::app); - std::string hdr(60, '-'); - out << hdr << endl - << std::setw(60) << setfill('-') - << "----- Partition keys " << endl << setfill(' ') - << hdr << endl; - - for (unsigned q=0; q1) { - for (unsigned j=1; jkeylist1[j]) ok = false; - } - ostringstream sout; - sout << left - << setw(4) << myid - << setw(8) << keylist1.size() - << setw(8) << keys.size(); - if (ok) sout << left << setw(8) << "GOOD"; - else sout << left << setw(8) << "BAD"; - - out << left << setw(cwid) << sout.str() << flush; - if (n % cols == cols-1 || n == numprocs-1) out << endl; - } - (*barrier)("pHOT: partitionKeys debug 3", __FILE__, __LINE__); - } - } - - timer_diagdbg.stop(); - } - // - // END DEBUG - // - - // Tree aggregation (merge sort) of the entire key list - // - // is (and must be) sorted to start for parallelMerge - // - // Round-robin exchange (rrMerge) followed by std::sort can be - // faster than the parallel merge (parallelMerge) for small particle - // numbers, but was intended for testing only - // - (*barrier)("pHOT: partitionKeys before pMerge", __FILE__, __LINE__); - if (USE_RROBIN) - rrMerge(keylist1, keylist); - else - parallelMerge(keylist1, keylist); - - (*barrier)("pHOT: partitionKeys after pMerge", __FILE__, __LINE__); - - MPI_Barrier(MPI_COMM_WORLD); - - if (myid==0) { - - // Accumulate weight list - for (unsigned i=1; i frate(numprocs); - - // Use an even rate - frate[0] = 1.0; - for (unsigned i=1; i - // for load balancing - // - - - // The overhead for computing these is small so - // no matter if they are not used below - - vector wbeg(numprocs), wfin(numprocs); // Weights for debugging - vector pbeg(numprocs), pfin(numprocs); // Counts for debugging - - // Compute the key boundaries in the partition - // - for (unsigned i=0; i::iterator - ret = lower_bound(keylist.begin(), keylist.end(), k, wghtDBL); - kfin[i] = ret->first; - wfin[i] = ret->second; - pfin[i] = ret - keylist.begin(); - } - else { - kfin[i] = key_min; - wfin[i] = 0.0; - pfin[i] = 0; - } - } - - kfin[numprocs-1] = key_max; - wfin[numprocs-1] = 1.0; - pfin[numprocs-1] = keylist.size(); - - kbeg[0] = key_min; - wbeg[0] = 0.0; - pbeg[0] = 0; - for (unsigned i=1; i1) { - mdif /= numprocs; - mdif2 = sqrt((mdif2 - mdif*mdif*numprocs)/(numprocs-1)); - out << "---- mean wght = " << setw(10) << keylist.size() << endl - << "---- std. dev. = " << setw(10) << keylist.size() << endl; - } - } - } - - MPI_Bcast(&kbeg[0], numprocs, MPI_EXP_KEYTYPE, 0, MPI_COMM_WORLD); - MPI_Bcast(&kfin[0], numprocs, MPI_EXP_KEYTYPE, 0, MPI_COMM_WORLD); - - - if (DEBUG_KEYS) { // If true, print key totals - unsigned oobn = oobNumber(); - unsigned tkey1 = keys.size(), tkey0 = 0; - MPI_Reduce(&tkey1, &tkey0, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD); - if (myid==0) { - ofstream out(debugf.c_str(), ios::app); - out << endl - << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << "---- partitionKeys" << endl - << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << "---- list size = " << setw(10) << keylist.size() << endl - << "---- total keys = " << setw(10) << tkey0 << endl - << "---- total oob = " << setw(10) << oobn << endl - << "---- TOTAL = " << setw(10) << tkey0 + oobn << endl - << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << "---- Time (s) = " << setw(10) << timer_debug->stop() - << endl - << setfill('-') << setw(60) << '-' << setfill(' ') << endl - << endl; - delete timer_debug; - } - } - -#ifdef USE_GPTL - GPTLstop("pHOT::partitionKeysHilbert"); -#endif -} - - -// -// Binary search -// -unsigned pHOT::find_proc(vector& keys, key_type key) -{ - unsigned num = keys.size(); - - if (num==0) { - cerr << "pHOT::find_proc: crazy input, no keys!" << endl; - return 0; - } - - if (key<=keys[0]) return 0; - if (key>=keys[num-1]) return num-2; - - unsigned beg=0, end=num-1, cur; - - while (end-beg>1) { - cur = (beg + end)/2; - if (key < keys[cur]) - end = cur; - else - beg = cur; - } - - return beg; -} - - -// -// This routine combines two sorted vectors into one -// larger sorted vector -// -void pHOT::sortCombine(vector& one, vector& two, - vector& comb) -{ - int i=0, j=0; - int n = one.size()-1; - int m = two.size()-1; - - comb = vector(one.size()+two.size()); - - for (int k=0; k n) - comb[k] = two[j++]; - else if(j > m) - comb[k] = one[i++]; - else { - if(one[i].first < two[j].first) - comb[k] = one[i++]; - else - comb[k] = two[j++]; - } - } -} - -// -// This routine combines the initial input vector on -// each node (sorted to start) by a binary merge algorithm -// -void pHOT::parallelMerge(vector& initl, vector& final) -{ - MPI_Status status; - vector work; - unsigned n; - -#ifdef USE_GPTL - GPTLstart("pHOT::parallelMerge"); -#endif - - // Find the largest power of two smaller than - // the number of processors - // - int M2 = 1; - while (M2*2 < numprocs) M2 = M2*2; - - // Combine the particles of the high nodes - // with those of the lower nodes so that - // all particles are within M2 nodes - // - // NB: if M2 == numprocs, no particles - // will be sent or received - // - if (myid >= M2) { - n = initl.size(); - if (false && barrier_debug) { - std::cout << "pHOT::parallelMerge: myid=" << setw(5) << myid - << setw(10) << " sending " << setw(6) << n - << setw(10) << " to id=" << setw(5) << myid-M2 - << std::endl; - } - MPI_Send(&n, 1, MPI_UNSIGNED, myid-M2, 11, MPI_COMM_WORLD); - if (n) { - vector one(n); - vector two(n); - - for (unsigned k=0; k data = initl; - - // - // Retrieve the excess particles - // - if (myid + M2 < numprocs) { - MPI_Recv(&n, 1, MPI_UNSIGNED, myid+M2, 11, MPI_COMM_WORLD, &status); - if (false && barrier_debug) { - std::cout << "pHOT::parallelMerge: myid=" << setw(5) << myid - << setw(10) << " received " << setw(6) << n - << setw(10) << " from id=" << setw(5) << status.MPI_SOURCE - << ", expected id=" << myid+M2 - << std::endl; - } - if (n) { - vector recv1(n); - vector recv2(n); - - MPI_Recv(&recv1[0], n, MPI_EXP_KEYTYPE, status.MPI_SOURCE, 12, - MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - MPI_Recv(&recv2[0], n, MPI_DOUBLE, status.MPI_SOURCE, 13, - MPI_COMM_WORLD, MPI_STATUS_IGNORE); - - vector recv(n); - for (unsigned k=0; k 1) { - - M2 = M2/2; - - // When M2 = 1, we are on the the last iteration. - // The final node left will be the root with the entire sorted array. - - // - // The upper half of the nodes send to the lower half and is done - // - if (myid >= M2) { - n = data.size(); - if (false && barrier_debug) { - std::cout << "pHOT::parallelMerge: myid=" << setw(5) << myid; - std::ostringstream sout; - sout << " [" << n << "] "; - std::cout << left << setw(10) << " sending" << setw(12) - << sout.str() << setw(6) << " to" - << " node=" << setw(5) << myid-M2 << std::endl << right; - } - MPI_Send(&n, 1, MPI_UNSIGNED, myid-M2, 11, MPI_COMM_WORLD); - if (n) { - vector one(n); - vector two(n); - - for (unsigned k=0; k& initl, vector& final) -{ - Timer timer; // For debugging - - if (myid==0 && DEBUG_MERGE) { - timer.start(); // Time the trivial version - } - - - final = initl; // Put this node's keys on the list - - unsigned n = initl.size(), total = 0; - unsigned p = n; - // Extend final vector to total size - MPI_Allreduce(&n, &total, 1, MPI_UNSIGNED, MPI_SUM, MPI_COMM_WORLD); - final.resize(total); - - for (int id=0; id one(n); - vector two(n); - // Load the temporary vectors - if (id==myid) { - for (unsigned k=0; k final0; - - parallelMerge(initl, final0); - - if (myid==0) { - std::cout << "Parallel sort in " << timer.stop() - << " seconds" << std::endl; - - // Check sizes - if (final0.size() != final.size()) { - std::cout << "pHOT::rrMerge: sizes differ, " << final0.size() - << " [parallel] vs " << final.size() << " [round-robin]" - << std::endl - << "pHOT::rrMerge: input size is " << total << std::endl; - } else { - unsigned cnt = 0; - for (int i=0; ibods.size(); - - MPI_Reduce(&nbods1, &nbods, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD); - - timer_diagdbg.stop(); - - return nbods; -} - -void pHOT::CollectTiming() -{ - float fval; - - fval = timer_keymake.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &keymk3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_xchange.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &exchg3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_convert.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &cnvrt3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_overlap.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &tovlp3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_prepare.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &prepr3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_cupdate.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &updat3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_scatter.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &scatr3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_repartn.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &reprt3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_tadjust.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &tadjt3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_cellcul.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &celcl3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_keycomp.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &keycm3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_keybods.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &keybd3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_keysort.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &keyst3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_keygenr.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &keygn3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_waiton0.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &wait03[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_waiton1.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &wait13[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_waiton2.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &wait23[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_keynewc.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &keync3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_keyoldc.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &keyoc3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = barrier->getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &barri3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - fval = timer_diagdbg.getTime(); - MPI_Gather(&fval, 1, MPI_FLOAT, &diagd3[0], 1, MPI_FLOAT, 0, MPI_COMM_WORLD); - - MPI_Gather(&numkeys, 1, MPI_UNSIGNED, &numk3[0], 1, MPI_UNSIGNED, 0, MPI_COMM_WORLD); - - - timer_keymake.reset(); - timer_xchange.reset(); - timer_convert.reset(); - timer_overlap.reset(); - timer_prepare.reset(); - timer_cupdate.reset(); - timer_scatter.reset(); - timer_repartn.reset(); - timer_tadjust.reset(); - timer_cellcul.reset(); - timer_keycomp.reset(); - timer_keybods.reset(); - timer_keysort.reset(); - timer_keygenr.reset(); - timer_waiton1.reset(); - timer_waiton2.reset(); - timer_keynewc.reset(); - timer_keyoldc.reset(); - timer_waiton0.reset(); - timer_diagdbg.reset(); - - numk = numkeys; - numkeys = 0; -} - - -template -void pHOT::getQuant(vector& in, vector& out) -{ - sort(in.begin(), in.end()); - out = vector(ntile+2); - out[0] = in.front(); - for (int k=0; k(floor(in.size()*0.01*qtile[k]))]; - out[ntile+1] = in.back(); -} - -void pHOT::Timing(vector &keymake, vector &exchange, - vector &convert, vector &overlap, - vector &prepare, vector &update, - vector &scatter, vector &repartn, - vector &tadjust, vector &cellcul, - vector &keycomp, vector &keybods, - vector &keysort, vector &keygenr, - vector &waiton0, vector &waiton1, - vector &waiton2, vector &keynewc, - vector &keyoldc, vector &treebar, - vector &diagdbg, vector &numk) -{ - getQuant(keymk3, keymake); - getQuant(exchg3, exchange); - getQuant(cnvrt3, convert); - getQuant(tovlp3, overlap); - getQuant(prepr3, prepare); - getQuant(updat3, update); - getQuant(scatr3, scatter); - getQuant(reprt3, repartn); - getQuant(tadjt3, tadjust); - getQuant(celcl3, cellcul); - getQuant(keycm3, keycomp); - getQuant(keybd3, keybods); - getQuant(keyst3, keysort); - getQuant(keygn3, keygenr); - getQuant(wait03, waiton0); - getQuant(wait13, waiton1); - getQuant(wait23, waiton2); - getQuant(keync3, keynewc); - getQuant(keyoc3, keyoldc); - getQuant(barri3, treebar); - getQuant(diagd3, diagdbg); - getQuant(numk3, numk ); -} - - -double pHOT::totalKE(double& KEtot, double& KEdsp) -{ - vector state(10); - - // Test - // - vector state1(10, 0.0); - - for (auto i : frontier) { - for (unsigned k=0; k<10; k++) - state1[k] += i.second->stotal[k]; - } - - MPI_Reduce(&state1[0], &state[0], 10, MPI_DOUBLE, MPI_SUM, - 0, MPI_COMM_WORLD); - // - // End test - - KEtot = KEdsp = 0.0; - - double mass = state[0]; - double *vel2 = &state[1]; - double *vel1 = &state[4]; - - if (mass>0.0) { - for (int k=0; k<3; k++) { - KEtot += 0.5*vel2[k]; - KEdsp += 0.5*(vel2[k] - vel1[k]*vel1[k]/mass); - } - - KEtot /= mass; - KEdsp /= mass; - } - - return mass; -} - -void pHOT::totalMass(unsigned& Counts, double& Mass) -{ - double mass1 = root->stotal[0]; - unsigned count1 = root->ctotal; - - MPI_Reduce(&mass1, &Mass, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); - MPI_Reduce(&count1, &Counts, 1, MPI_UNSIGNED, MPI_SUM, 0, MPI_COMM_WORLD); -} - - -void pHOT::adjustCounts(ostream& out) -{ - out << setfill('-') << setw(60) << '-' << setfill(' ') << endl; - out << " " << left << setw(20) << "Total" << " : " << cntr_total - << endl; - out << " " << left << setw(20) << "New key" << " : " << cntr_new_key - << endl; - out << " " << left << setw(20) << "Same cell" << " : " << cntr_mine - << endl; - out << " " << left << setw(20) << "New cell" << " : " << cntr_not_mine - << endl; - out << " " << left << setw(20) << "Shipped" << " : " << cntr_ship - << endl; - out << setfill('-') << setw(60) << '-' << setfill(' ') << endl; - - cntr_total = cntr_new_key = cntr_mine = cntr_not_mine = cntr_ship = 0; -} - -void pHOT::checkEffort(unsigned mlevel) -{ - if (!DEBUG_KEYS) return; - - timer_diagdbg.start(); - - double eff=0.0; - for (auto i : frontier) { - vector::iterator ib; - for (auto b : i.second->bods) - eff += cc->Particles()[b]->effort; - } - - vector eq(numprocs); - MPI_Gather(&eff, 1, MPI_DOUBLE, &eq[0], 1, MPI_DOUBLE, 0, MPI_COMM_WORLD); - - if (myid==0) { - ofstream out(string(outdir + runtag + ".pHOT_effort").c_str(), ios::app); - if (out) { - double mean=0.0, mean2=0.0, emin=1e20, emax=0.0; - for (int n=0; n(emin, eq[n]); - emax = max(emax, eq[n]); - } - mean /= numprocs; - mean2 = mean2 - mean*mean*numprocs; - if (numprocs>1) mean2 = sqrt(mean2/(numprocs-1)); - out << setw(40) << setfill('-') << '-' << setfill(' ') << endl - << "---- Time = " << tnow << endl; - if (mlevel == -1) - out << "---- Level = " << "full tree" << endl; - else - out << "---- Level = " << mlevel << endl; - out << "---- Mean = " << mean << endl - << "---- Stdev = " << mean2 << endl - << "---- E_min = " << emin << endl - << "---- E_max = " << emax << endl - << "---- Ratio = " << emax/emin << endl - << setw(8) << left << "Proc #" << setw(12) << "Effort" << endl; - for (int n=0; n - -#include -#include - -using namespace std; - -class pCell; -struct Partstruct; - -typedef unsigned long indx_type; -typedef unsigned long key_type; -typedef pair key_pair; -typedef pair key_item; -typedef pair cell_indx; - -struct eqULL { - bool operator()(const key_type __i1, - const key_type __i2) const - { return (__i1==__i2); } -}; - -struct ltPAIR { - bool operator()(const pair __p1, - const pair __p2) const - { - if (__p1.first==__p2.first) return (__p1.second<__p2.second); - else return (__p1.first<__p2.first); - } -}; - -struct hashULL { - size_t operator()(unsigned long __i) const - { return static_cast(__i); } -}; - - // For cell transaction list -typedef vector change_list; - - // A list of key/index pairs for - // particles These pairs must be - // unique because the particle indices - // are unique -typedef set key_indx; - // Particle key points to cell key - // multimap permits multiple identical pairs -typedef multimap key_key; - // For searching the key_key multimap -typedef key_key::iterator key2Itr; -typedef std::pair key2Range; - - - // For load balancing and partitioning -typedef pair key_wght; - - -/* - Now, deal with the transition to ANSI undordered_maps from pre-ANSI - hash_maps using the config variables -*/ - -#ifdef HAVE_CXX0X -//============================================================================ -#include - // Cell key points to cell pointer -typedef std::unordered_map key_cell; - // Particle index points to particle key -typedef std::unordered_map > indx_key; -//============================================================================ - -#else - -#ifdef HAVE_TR1 - -//============================================================================ -#include - // Cell key points to cell pointer -typedef std::tr1::unordered_map key_cell; - // Particle index points to particle key -typedef std::tr1::unordered_map > indx_key; -//============================================================================ - -#else - -#ifdef HAVE_HASH - -//============================================================================ -#include - // Cell key points to cell pointer -typedef hash_map key_cell; - // Particle index points to particle key -typedef hash_map > indx_key; -//============================================================================ - -#else - -//============================================================================ - // Cell key points to cell pointer -typedef std::map key_cell; - // Particle index points to particle key -typedef std::map indx_key; -//============================================================================ - -#endif // HAVE_HASH - -#endif // HAVE_TR1 - -#endif // HAVE_CXX0X - -#endif diff --git a/src/user/UserDSMC.H b/src/user/UserDSMC.H deleted file mode 100644 index bce4b3e9e..000000000 --- a/src/user/UserDSMC.H +++ /dev/null @@ -1,147 +0,0 @@ -#ifndef _UserDSMC_H -#define _UserDSMC_H - -#include - -#include -#include -#include - -#include -#include - -/** - Helper class describing a comparison for sorting particles in radius -*/ -class RadiusSort -{ -public: - double radius; - int curindx; - int curnode; - - bool operator() (const RadiusSort& x, const RadiusSort& y) - { - return x.radius < y.radius; - } - - bool operator() (const double& x, const RadiusSort& y) - { - return x < y.radius; - } - - bool operator() (const RadiusSort& x, const double& y) - { - return x.radius < y; - } -}; - - -/** - Helper class describing a comparison for sorting the most bound particles -*/ -class BoundSort -{ -public: - - typedef pair Bound; - - bool operator() (const Bound& x, const Bound& y) - { - return x.first < y.first; - } - - bool operator() (const double& x, const Bound& y) - { - return x < y.first; - } - - bool operator() (const Bound& x, const double& y) - { - return x.first < y; - } -}; - - -/** DSMC particle routine - - @param Rcloud is the effective radius of interaction (e.g. diameter of the sphere) - @param NR is the number of radial bins - @param NZ is the number of vertidal bins - @param NP is the number of azimuthal bins - @param Rmax is the maximum radius of the binned cylinders - @param Zmax is the maximum extent of the cylinder above and below the midplane - @param efftol is the minimum efficient allowed before triggering a repartition - @param vfrac is the fraction of the mean velocity used to estimate the peak relative particle velocity - @param seed is the random number seed - @param debug may be set to true (non-zero value) to turn on debugging output -*/ -class UserDSMC : public ExternalForce -{ -private: - - string comp_name; - Component *c0; - - void determine_acceleration_and_potential(void); - void * determine_acceleration_and_potential_thread(void * arg); - void initialize(); - - double rcloud, rmin, rmax, efftol, zmax, vfrac; - int NR, NZ, NP, seed; - - bool debug; - - // Internal variables - // ------------------ - - int dN; // Number of radial bins per process - double dPhi; // Aximuthal bin spacing - - // For determining the bin boundaries - // - vector radiiT, rbound; - vector radii; - vector< pair > rboundI, zboundI; - vector< pair >::iterator iI; - - // Process assignment for each radial bin - vector owners; - // Particle numbers per process - vector counts, countsT, nbegpts; - // Efficiency estimate for repartitioning - vector eff, effT; - bool regrid_flag; - - // Redistribution lists - vector redist, redistT, redistT2, nbin; - - // Bin assignments per processor - vector< vector > binlist; - - // Variables for collision computation - vector< vector > vcom, vrel, delv; - vector vmax; - - // Pseudo-random-number generation - std::uniform_real_distribution<> unif; - - void makeGrid(); - void makeSort(); - void userinfo(); - -#ifdef TIMER - Timer *timer; -#endif - -public: - - //! Constructor - UserDSMC(string &line); - - //! Destructor - ~UserDSMC(); - -}; - -#endif diff --git a/src/user/UserDSMC.cc b/src/user/UserDSMC.cc deleted file mode 100644 index eaa052794..000000000 --- a/src/user/UserDSMC.cc +++ /dev/null @@ -1,663 +0,0 @@ -#include -#include - -#include "expand.H" - -#define TIMER - -#include - - -UserDSMC::UserDSMC(string &line) : ExternalForce(line) -{ - id = "DSMCParticleAlgorithm"; - - rcloud = 0.001; // Cloud radius - rmin = 0.0; // Inner radius of cylindrical grid - rmax = 1.0; // Outer radius of cylindrical grid - zmax = 1.0; // Half-height of cylindrical grid - vfrac = 0.2; // Fraction of mean velocity for rejection - efftol = 0.5; // Minimum permitted inefficiency - NR = 64; // Number of radial bins - NZ = 10; // Number of vertical bins - NP = 16; // Number of azimuthal bins - seed = 11; // Seed for random number generator - comp_name = ""; // Component to apply the DSMC algorithm - debug = false; // No debugging by default - - initialize(); - - // Look for the fiducial component for - // stickiness - bool found = false; - for (auto c : comp->components) { - if ( !comp_name.compare(c->name) ) { - c0 = c; - found = true; - break; - } - } - - if (!found) { - std::ostringstream sout; - sout << "Can't find desired component <" << comp_name << ">"; - throw GenericError(sout.str(), __FILE__, __LINE__, 35, false); - } - - userinfo(); - - // Assign radial bins processors - dN = (int)floor(NR/numprocs); - for (int n=0; n"; - - if (debug) - cout << ", debugging is *ON*"; - - cout << endl; - - print_divider(); -} - -void UserDSMC::initialize() -{ - string val; - - if (get_value("compname", val)) comp_name = val; - if (get_value("Rcloud", val)) rcloud = atof(val.c_str()); - if (get_value("Rmin", val)) rmin = atof(val.c_str()); - if (get_value("Rmax", val)) rmax = atof(val.c_str()); - if (get_value("Zmax", val)) zmax = atof(val.c_str()); - if (get_value("Vfrac", val)) vfrac = atof(val.c_str()); - if (get_value("efftol", val)) efftol = atof(val.c_str()); - if (get_value("NR", val)) NR = atoi(val.c_str()); - if (get_value("NZ", val)) NZ = atoi(val.c_str()); - if (get_value("NP", val)) NP = atoi(val.c_str()); - if (get_value("seed", val)) seed = atoi(val.c_str()); - if (get_value("debug", val)) debug = atol(val); -} - - -void UserDSMC::makeSort() -{ - // Exchange particles - - // Serialize redistribution list into an integer array - // of numprocs stanzas, each with the format - // n -- current node - // M -- number to redistribute (may be zero) - // index_1 - // tonode_1 - // index_2 - // tonode_2 - // . - // . - // index_M - // tonode_M - // - // so each stanza has 2(M+1) integers - - for (int n=0; nNumber(); k++) { - - p = c0->Part(k); - - R = sqrt(p->pos[0]*p->pos[0] + p->pos[1]*p->pos[1]); - - iI = upper_bound(rboundI.begin(), rboundI.end(), R, BoundSort()); - if (iI == rboundI.end()) continue; - - if (owners[iI->second] != myid) { - redistT.push_back(k); - redistT.push_back(owners[iI->second]); - redistT[1]++; - } - } - - countsT[myid] = redistT.size(); - MPI_Allreduce(&countsT[0], &counts[0], numprocs, MPI_UNSIGNED, MPI_SUM, - MPI_COMM_WORLD); - int ntot = 0; - for (int n=0; nredistributeByList(redist); - - // Determine efficiency - // - for (int n=0; nNumber(); - MPI_Allreduce(&effT[0], &eff[0], numprocs, MPI_UNSIGNED, MPI_SUM, - MPI_COMM_WORLD); - - unsigned int maxT=0, minT=std::numeric_limits::max(); - for (int n=0; n(minT, eff[n]); - maxT = max(maxT, eff[n]); - } - if ((double)minT/maxT < efftol) regrid_flag = true; - - - // Clean the bin list - // - for (auto i : binlist) i.clear(); - - for (int i=0; iNumber(); k++) { - x = c0->Pos(k, 0); - y = c0->Pos(k, 1); - z = c0->Pos(k, 2); - - u = c0->Vel(k, 0); - v = c0->Vel(k, 1); - w = c0->Vel(k, 2); - - R = sqrt(x*x + y*y); - - if (R< rmin || R>rmax) continue; - if (z<-zmax || z>zmax) continue; - - phi = atan2(y, x); - - V = sqrt( (u*u+v*v*w*w)/3.0 ); - - iI = upper_bound(rboundI.begin(), rboundI.end(), R, BoundSort()); - if (iI == rboundI.end()) continue; - ir = iI->second; - - if (owners[ir] != myid) continue; - - iI = upper_bound(zboundI.begin(), zboundI.end(), R, BoundSort()); - if (iI == zboundI.end()) continue; - iz = iI->second; - - ip = (int)floor(phi/(2.0*M_PI)*NP); - - indx = ( (ir-myid*dN)*NZ + iz )*NP + ip; - - binlist[indx].push_back(k); - - vmax[ir - myid*dN] += V; - nbin[ir - myid*dN] += 1; - } - - // Compute mean velocity per bin - // - for (int i=0; i0) vmax[i] = vmax[i]*vfrac/nbin[i]; - } - -} - -void UserDSMC::makeGrid() -{ - if (!regrid_flag) return; - - MPI_Status status; - - // Load and send particle numbers to master - // - vector countT(numprocs, 0); - countT[myid] = c0->Number(); - MPI_Reduce(&countT[0], &counts[0], numprocs, MPI_INT, MPI_SUM, 0, - MPI_COMM_WORLD); - - - // Resize the particle buffers - // - if (myid==0) { - if (radii.size() != c0->nbodies_tot) radii.resize(c0->nbodies_tot); - int nb = 0; - for (int n=1; n(nb, counts[n]); - if ((int)radiiT.size() == nb) radiiT.resize(nb); - } else { - if (radiiT.size() != c0->Number()) radiiT.resize(c0->Number()); - } - - - - // Put my particles into the buffer and the master list - // - nbegpts[0] = 0; - for (int n=1; nNumber(); k++) { - p = c0->Part(k); - radii[k].curnode = 0; - radii[k].curindx = k; - radii[k].radius = sqrt(p->pos[0]*p->pos[0] + p->pos[1]*p->pos[1]); - } - } else { - for (unsigned int k=0; kNumber(); k++) { - p = c0->Part(k); - radiiT[k] = sqrt(p->pos[0]*p->pos[0] + p->pos[1]*p->pos[1]); - } - } - - - // Root gets the radial list - // - if (myid==0) { - for (int n=1; n::iterator ir = - upper_bound(radii.begin(), radii.end(), rmax, RadiusSort()); - radii.erase(ir, radii.end()); - } - - // Check for minimum radius of cylindrical grid - // - if (rmin > radii.front().radius) { - vector::iterator ir = - lower_bound(radii.begin(), radii.end(), rmin, RadiusSort()); - radii.erase(radii.begin(), ir); - } - - // Serialize redistribution list into an integer array - // of numprocs stanzas, each with the format - // n -- current node - // M -- number to redistribute (may be zero) - // index_1 - // tonode_1 - // index_2 - // tonode_2 - // . - // . - // index_M - // tonode_M - // - // so each stanza has 2(M+1) integers - - redist.clear(); - // Boundaries - // - int ibeg, iend, cntr; - for (int n=0; nNumber(); - MPI_Send(&radiiT[0], nbodies, MPI_FLOAT, 0, 21+myid, MPI_COMM_WORLD); - } - - - // Send boundaries to all processors - // - MPI_Bcast(&rbound[0], NR, MPI_FLOAT, 0, MPI_COMM_WORLD); - rboundI.clear(); - for (int n=0; n(rbound[n], n) ); - - // Send redistribution array count - // to all processors - unsigned int nredist; - if (myid==0) nredist= redist.size(); - MPI_Bcast(&nredist, numprocs, MPI_UNSIGNED, 0, MPI_COMM_WORLD); - if (myid) redist.resize(nredist); - MPI_Bcast(&redist, nredist, MPI_INT, 0, MPI_COMM_WORLD); - - - // Tell the component to redistribute - // - c0->redistributeByList(redist); - - // Now, finally, make the grid for each - // node - dPhi = 2.0*M_PI/NP; - vector vertical; - for (unsigned int k=0; kNumber(); k++) vertical.push_back(c0->Pos(k, 2)); - sort(vertical.begin(), vertical.end()); - - // Check for min & max height - // - double zbot; - if (zmax < vertical.back()) { - vector::iterator iz = - upper_bound(vertical.begin(), vertical.end(), zmax); - vertical.erase(iz, vertical.end()); - } - if (-zmax > vertical.front()) { - vector::iterator iz = - lower_bound(vertical.begin(), vertical.end(), -zmax); - vertical.erase(vertical.begin(), iz); - zbot = -zmax; - } else { - zbot = vertical.front(); - } - - zboundI.clear(); - zboundI.push_back( pair(zbot, 0) ); - for (int n=1; n(zbot, n) ); - } - - regrid_flag = false; -} - -void UserDSMC::determine_acceleration_and_potential(void) -{ -#if HAVE_LIBCUDA==1 // Cuda compatibility - getParticlesCuda(c0); -#endif - -#ifdef TIMER - if (myid==0 && debug) { - timer->reset(); - timer->start(); - } -#endif -//---------------------------------------------------------------------- - makeGrid(); -//---------------------------------------------------------------------- -#ifdef TIMER - if (debug) { - MPI_Barrier(MPI_COMM_WORLD); - if (myid==0) { - std::cout << "Grid constructed in " << timer->stop() << " seconds" << std::endl; - - timer->reset(); - timer->start(); - } - } -#endif -//---------------------------------------------------------------------- - makeSort(); -//---------------------------------------------------------------------- -#ifdef TIMER - if (debug) { - MPI_Barrier(MPI_COMM_WORLD); - if (myid==0) { - std::cout << "Sort completed in " << timer->stop() << " seconds" << std::endl; - timer->reset(); - timer->start(); - } - } -#endif -//---------------------------------------------------------------------- - exp_thread_fork(false); -//---------------------------------------------------------------------- -#ifdef TIMER - if (debug) { - MPI_Barrier(MPI_COMM_WORLD); - if (myid==0) { - std::cout << "Collisions completed in " << timer->stop() << " seconds" << std::endl; - } - } -#endif -} - - -void * UserDSMC::determine_acceleration_and_potential_thread(void * arg) -{ - unsigned int number = binlist.size(); - int id = *((int*)arg); - int nbeg = number*id/nthrds; - int nend = number*(id+1)/nthrds; - - double Vc, rho, Rmin, Rmax, Zmin, Zmax; - int ir, iz, iw, ncount, Nc; - - PartMapItr it = cC->Particles().begin(); - unsigned long i; - - for (int q=0 ; qfirst; - // Number in this cell - // - Nc = binlist[i].size(); - if (Nc<2) continue; // Can't have any collisions, next cell - - // Determine radial boundaries - // - iw = i/(NZ*NP); - ir = iw + myid*dN; - Rmin = rboundI[ir].first; - if (ir+1Part(binlist[i][n1]); - Particle *p2 = c0->Part(binlist[i][n2]); - - // Calculate pair's relative speed - // - double rvel=0.0; - for (int k=0; k<3; k++) { - vrel[id][k] = p1->vel[k] - p2->vel[k]; - rvel += vrel[id][k]*vrel[id][k]; - } - rvel = sqrt(rvel); - - // If relative speed larger than vmax - // then reset vmax to larger value - if (rvel > vmaxT) vmaxT = rvel; - - // Accept or reject candidate pair - // according to relative speed - if (rvel/vmax[iw] > unif(random_gen) ) { - - // Compute post-collision velocities - - // Center of mass velocity - for(int k=0; k<3; k++ ) - vcom[id][k] = 0.5*(p1->vel[k] + p2->vel[k]); - - // Random orientation - - // Cosine and sine of collision angle theta - // - double cos_th = 1.0 - 2.0*unif(random_gen); - double sin_th = sqrt(1.0 - cos_th*cos_th); - - // Collision angle phi - // - double phi = 2.0*M_PI*unif(random_gen); - - // Compute hard-sphere displacement following - // Alexander, Garcia & Alder (1995, PRL, 74, 5212) - // - delv[id][0] = rvel*cos_th - vrel[id][0]; - delv[id][1] = rvel*sin_th*cos(phi) - vrel[id][1]; - delv[id][2] = rvel*sin_th*sin(phi) - vrel[id][2]; - - double dvel = 0.0; - for (int k=0; k<3; k++) dvel += delv[id][k]*delv[id][k]; - dvel = sqrt(dvel); - if (dvel>0.0) { - for (int k=0; k<3; k++) { - p1->pos[k] += delv[id][k]*rcloud/dvel; - p2->pos[k] -= delv[id][k]*rcloud/dvel; - } - } - // Compute post-collision relative velocity - // - vrel[id][0] = rvel*cos_th; - vrel[id][1] = rvel*sin_th*cos(phi); - vrel[id][2] = rvel*sin_th*sin(phi); - for (int k=0; k<3; k++ ) { - p1->vel[k] = vcom[id][k] + 0.5*vrel[id][k]; // Update post-collision - p2->vel[k] = vcom[id][k] - 0.5*vrel[id][k]; // velocities - } - - } - - vmax[iw] = vmaxT; // Update max relative speed - - } - - } - - return (NULL); -} - - -extern "C" { - ExternalForce *makerDSMC(string& line) - { - return new UserDSMC(line); - } -} - -class proxysticky { -public: - proxysticky() - { - factory["usersticky"] = makerDSMC; - } -}; - -static proxysticky p; diff --git a/utils/Analysis/PlotColl_2.py b/utils/Analysis/PlotColl_2.py deleted file mode 100755 index e44ce294e..000000000 --- a/utils/Analysis/PlotColl_2.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/python -i - -# For Python 3 compatibility -from __future__ import absolute_import, division, print_function, unicode_literals - -import re, sys -import numpy as np -import numpy as np - -import matplotlib -matplotlib.use('GTKAgg') -import matplotlib.pyplot as plt - -from scipy.optimize import curve_fit -from scipy.interpolate import interp1d -from scipy.signal import savgol_filter - -data = {} -labs = [] -rtag = sys.argv[-1] -Tscale = 1e3 - -def readem(Tag=''): - global data, labs, rtag - - if Tag != '': rtag = Tag - - file = open('{}.ION_coll'.format(rtag), 'r') - data = {} - labs = [] - for line in file: - if line[0] == '#': - if 'Time' in line: - line = re.sub('[#| ]+', ' ', line) - toks = line.split() - for tok in toks: - labs.append(tok) - data[tok] = [] - else: - line = re.sub('[| ]+', ' ', line) - toks = line.split() - for v in zip(labs, toks): data[v[0]].append(float(v[1])) - -def plotem(l, Log=False, Smooth=False, Only=False, Tag='', Intvl=10): - global rtag - - if Tag != '': rtag = Tag - readem() - - t = np.array(data['Time'])*Tscale - for v in l: - y = np.array(data[v]) - if not Smooth or not Only: - if Log: - plt.semilogy(t, y, '-', label=v) - else: - plt.plot(t, y, '-', label=v) - - if Smooth: - if Log: y = np.log(y) - # print("tsize=", t.shape[0], " ysize=", y.shape[0]) - itp = interp1d(t, y, kind='linear') - wsize = 11 - dsize = int(y.shape[0]/Intvl) - if dsize > wsize: - wsize = dsize - if 2*int(wsize/2) == wsize: wsize += 1 - porder = 3 - # print("wsize=", wsize) - yy = savgol_filter(itp(t), wsize, porder) - if Smooth and Only: - if Log: - plt.semilogy(t, np.exp(yy), '-', label=v) - else: - plt.plot(t, yy, '-', label=v) - else: - if Log: - plt.semilogy(t, np.exp(yy), '-', linewidth=2) - else: - plt.plot(t, yy, '-', linewidth=2) - - plt.xlabel('Time (years)') - plt.ylabel(v) - plt.grid() - plt.legend().draggable() - plt.show() - -def plotLoss(Log=True, Smooth=True, Only=False, Tag='', Intvl=10): - plotem(['Elost'], Log=Log, Smooth=Smooth, Only=Only, Tag=Tag, Intvl=Intvl) - -def plotKE(Log=False, Smooth=True, Only=False, Tag='', Intvl=10): - plotem(['EkeI', 'EkeE'], Log=Log, Smooth=Smooth, Only=Only, Tag=Tag, Intvl=Intvl) - diff --git a/utils/Analysis/allEnergy.py b/utils/Analysis/allEnergy.py deleted file mode 100755 index 32dc7fb71..000000000 --- a/utils/Analysis/allEnergy.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, argparse -import numpy as np -import matplotlib.pyplot as plt -import getSpecies as gs - -parser = argparse.ArgumentParser(description='Read DSMC species file and plot energies') -parser.add_argument('-t', '--tscale', default=1000.0, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, help='Maximum time in years') -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -fields = [ ['Ions_E', 'Eion(1)', 'Eion(2)'], ['Elec_E', 'Eelc(1)', 'Eelc(2)'], 'Totl_E', 'Cons_G', 'Cons_E'] - -d = {} -for v in labs: - d[v] = gs.readDB(v) - -totl = len(fields) -cols = 2 -rows = totl/cols -if cols * rows < totl: rows += 1 -f, ax = plt.subplots(rows, cols, sharex='col') - -for x in ax: - for y in x: - box = y.get_position() - newBox = [box.x0, box.y0, 0.8*box.width, box.height] - y.set_position(newBox) - -ax[rows-1, cols-1].axis('off') - -def plotme(d, v, f, ax, l): - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - ax.plot(d[v]['Time'][0:indx]*args.tscale, d[v][f][0:indx], '-', label=l) - if f == 'Totl_E': - tt = d[v]['Ions_E'][0:indx] + d[v]['Elec_E'][0:indx] - ax.plot(d[v]['Time'][0:indx], tt, '-', label=v+':comb') - -cnt = 0 -for f in fields: - row = cnt/cols - col = cnt - row*cols - for v in labs: - if type(f) is list: - for ff in f: - plotme(d, v, ff, ax[row, col], v+':'+ff) - else: - plotme(d, v, f, ax[row, col], v) - - if type(f) is list: - ax[row, col].set_title(f[0]) - else: - ax[row, col].set_title(f) - ax[row, col].legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) - if col==0: - ax[row, col].set_ylabel('Energy') - if row+1==rows: ax[row, col].set_xlabel('Time') - cnt += 1 - -plt.show() diff --git a/utils/Analysis/allTemps.py b/utils/Analysis/allTemps.py deleted file mode 100755 index 67b246342..000000000 --- a/utils/Analysis/allTemps.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, os, argparse -import numpy as np -import matplotlib.pyplot as plt -import getSpecies as gs - -parser = argparse.ArgumentParser(description='Read DSMC species file and plot temperatures') -parser.add_argument('-t', '--tscale', default=1000.0, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, help='Maximum time in years') -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -fields = ['Telc(1)', 'Telc(2)'] -fields = ['Telc(1)', 'Telc(2)', 'Tion(1)', 'Tion(2)'] - -d = {} -for v in labs: - d[v] = gs.readDB(v) - -f, ax = plt.subplots(1, 1) - -box = ax.get_position() -newBox = [box.x0, box.y0, 0.9*box.width, box.height] -ax.set_position(newBox) - -cnt = 0 -for v in labs: - for f in fields: - if f in d[v]: - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - ax.plot(d[v]['Time'][0:indx]*args.tscale, d[v][f][0:indx], '-', label=v+':'+f) - -ax.set_xlabel('Time') -ax.set_ylabel('Temperature') -ax.legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) -plt.show() diff --git a/utils/Analysis/checkICDens.py b/utils/Analysis/checkICDens.py deleted file mode 100755 index 4dbb582fa..000000000 --- a/utils/Analysis/checkICDens.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/python - -# For Python 3 compatibility -# -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys, os, argparse -import astroval as C - -# Argument parsing -# -parser = argparse.ArgumentParser(description='Evaluate global statistics for body file created by makeIonIC') -parser.add_argument('-b', '--body', default='gas.bod', help='Name of body file') -parser.add_argument('-m', '--msun', default=1.0, type=float, help='Number of M_sun per unit mass') -parser.add_argument('-l', '--lpc', default=1.0, type=float, help='Number of parsecs per unit length') - -args = parser.parse_args() - -file = open(args.body) -file.readline() -mval = 1.0e32 -mtot = 0.0 -minD = [mval, mval, mval, mval, mval, mval] -maxD = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] -avgP = [0.0, 0.0, 0.0] -avgV = [0.0, 0.0, 0.0] -for line in file: - v = line.split() - m = float(v[0]) - mtot += m - for i in range(0,6): - minD[i] = min(minD[i], float(v[i+1])) - maxD[i] = max(maxD[i], float(v[i+1])) - for i in range(0,3): - avgP[i] += m*float(v[i+1]) - avgV[i] += m*float(v[i+4]) - -mfac = args.msun*C.Msun/C.m_p/(args.lpc*C.pc)**3 -dens = mtot/((maxD[0]-minD[0])*(maxD[1]-minD[1])*(maxD[2]-minD[2])) * mfac - -for i in range(0,3): - avgP[i] /= mtot - avgV[i] /= mtot - -print() -print("Value : {:>13s} {:>13s} {:>13s}".format('x|u', 'y|v', 'z|w')) -print("-------------- : {:13s} {:13s} {:13s}".format('-'*13, '-'*13, '-'*13)) -print("Minimum pos : {:13.6e} {:13.6e} {:13.6e}".format(minD[0], minD[1], minD[2])) -print("Maximum pos : {:13.6e} {:13.6e} {:13.6e}".format(maxD[0], maxD[1], maxD[2])) -print("Minimum vel : {:13.6e} {:13.6e} {:13.6e}".format(minD[3], minD[4], minD[5])) -print("Maximum vel : {:13.6e} {:13.6e} {:13.6e}".format(maxD[3], maxD[4], maxD[5])) -print("Average pos : {:13.6e} {:13.6e} {:13.6e}".format(avgP[0], avgP[1], avgP[2])) -print("Average vel : {:13.6e} {:13.6e} {:13.6e}".format(avgV[0], avgV[1], avgV[2])) -print("-------------- : {:13s} {:13s} {:13s}".format('-'*13, '-'*13, '-'*13)) -print("Density (n/cc) : {:13.6e}".format(dens)) -print("Total mass : {:13.6e}".format(mtot)) -print() - diff --git a/utils/Analysis/checkICWeights.py b/utils/Analysis/checkICWeights.py deleted file mode 100755 index a29d92c3b..000000000 --- a/utils/Analysis/checkICWeights.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/python - -# For Python 3 compatibility -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys, os, argparse -import math -import numpy as np -import astroval as C - -parser = argparse.ArgumentParser(description='Evaluate global statistics for body and spec file created by makeIonIC\nwith specific hybrid-method values') -parser.add_argument('-b', '--body', default='gas.bod', help='Name of body file') -parser.add_argument('-s', '--spec', default='species.spec', help='Name of species map file') -parser.add_argument('-m', '--msun', default=1.0, type=float, help='Number of M_sun per unit mass') -parser.add_argument('-l', '--lpc', default=1.0, type=float, help='Number of parsecs per unit length') - -args = parser.parse_args() - -file = open(args.spec) -line = file.readline() -if line[0:6] != 'hybrid': - print("This only makes sense for a hybrid IC file") - exit(1) - -line = file.readline() -hpos = int(line.split()[1]) - -file = open(args.body) -line = file.readline() -toks = line.split() -numb = int(toks[0]) -inatr = int(toks[1]) -dnatr = int(toks[2]) - -bpos = 7 + inatr + hpos -maxD = [0.0, 0.0, 0.0, 0.0] -masS = {} -tots = 0 -engI = {} -engE = {} -spcS = {} - -atomic_masses = [0.000548579909, 1.00794, 4.002602] - -for line in file: - v = line.split() - mass = float(v[0]) - maxD[0] += mass - ityp = int(v[7]) - - v2E = 0.0 - masE = mass * atomic_masses[0]/atomic_masses[ityp] - if ityp not in engI: engI[ityp] = 0.0 - if ityp not in engE: engE[ityp] = 0.0 - - for i in range(3): - vI = float(v[4+i]) - vE = float(v[-4+i]) - engI[ityp] += 0.5 * mass * vI*vI - engE[ityp] += 0.5 * masE * vE*vE - - if ityp not in masS: masS[ityp] = (0, 0.0) - masS[ityp] = (masS[ityp][0] + 1, masS[ityp][1] + mass) - for i in range(1,4): maxD[i] = max(maxD[i], float(v[i])) - sum = 0.0 - for i in range(0,3): sum += float(v[bpos+i]) - if math.fabs(sum - 1.0) > 1.0e-6: tots += 1 - if ityp not in spcS: spcS[ityp] = np.zeros(ityp+1) - for i in range(0,ityp+1): spcS[ityp][i] += float(v[bpos+i]) - -mfac = args.msun*C.Msun/C.m_p/(args.lpc*C.pc)**3 -dens = maxD[0]/(maxD[1]*maxD[2]*maxD[3]) * mfac - -print('-'*60) -print("Total mass:", maxD[0]) -print('-'*60) -print("Subspecies tally") -print('-'*60) -print("{:>4s}: {:>8s} {:>13s} {:>13s}".format("Sp", "Count", "Mass", "Mass frac")) -print("{:>4s}: {:>8s} {:>13s} {:>13s}".format("--", "-----", "----", "---------")) - -for k in masS: - print("{:-4d}: {:8d} {:13.6e} {:13.6e}".format(k, masS[k][0], masS[k][1], masS[k][1]/maxD[0])) - -print('-'*60) -print('Subspecies states') -print('-'*60) -print("{:>4s}:".format("Sp"), end='') -zMax = max(spcS.keys()) -for z in range(1,zMax+2): print(' {:>13s}'.format('C={}'.format(z)), end='') -print() -print('{:>4s}:'.format("--"), end='') -zMax = max(spcS.keys()) -for z in range(1,zMax+2): print(' {:>13s}'.format('-----'), end='') -print() -for k in spcS: - print('{:-4d}:'.format(k), end='') - sum = 0.0 - for v in spcS[k]: sum += v - for v in spcS[k]: print(' {:13.6e}'.format(v/sum), end='') - print() -print('-'*60) -print('Subspecies energies') -print('-'*60) -print('{:>4s}: {:>13s} {:>13s}'.format("Sp", "Ion", "Elec")) -print('{:>4s}: {:>13s} {:>13s}'.format("--", "--------", "--------")) -sumI = 0.0 -sumE = 0.0 -for k in engI: - print('{:>4d}: {:>13.6e} {:>13.6e}'.format(k, engI[k], engE[k])) - sumI += engI[k] - sumE += engE[k] -print('{:>4s}: {:>13.6e} {:>13.6e}'.format("Tot", sumI, sumE)) -print('-'*60) -tmpl0 = "{:<18s} = [{:9.6f} {:9.6f} {:9.6f}]" -tmpl1 = "{:<18s} = {}" -tmpl2 = "{:<18s} = {}/{}" -print(tmpl0.format("Maximum pos", maxD[1], maxD[2], maxD[3])) -print(tmpl1.format("Density (n/cc)", dens)) -print(tmpl2.format("Out of bounds", tots, numb)) -print('-'*60) diff --git a/utils/Analysis/collide_histo.py b/utils/Analysis/collide_histo.py deleted file mode 100755 index 4dab9ce37..000000000 --- a/utils/Analysis/collide_histo.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/python - -import numpy as np -import matplotlib.pyplot as plt -import sys -import psp_io - -# -# Last argument should be filename and must exist -# - -help_string = "Usage: {} [-s n | --species n] [-c n | -coll n] [-k n | -key n] [-d | -diff] [-o n | --offset n] runtag".format(sys.argv[0]) - -argc = len(sys.argv) - -if argc<=1: - print help_string - exit(1) - -# -# Check for point type and log time -# -spc = 2 -key = 0 -col = 1 -diff = False -offs = 1 - -for i in range(1,argc): - if sys.argv[i] == '-k' or sys.argv[i] == '--key': - key = int(sys.argv[i+1]) - if sys.argv[i] == '-s' or sys.argv[i] == '--species': - spc = int(sys.argv[i+1]) - if sys.argv[i] == '-c' or sys.argv[i] == '--coll': - col = int(sys.argv[i+1]) - if sys.argv[i] == '-d' or sys.argv[i] == '--diff': - diff = True - if sys.argv[i] == '-o' or sys.argv[i] == '--offset': - offs = int(sys.argv[i+1]) - if sys.argv[i] == '-h' or sys.argv[i] == '--help': - print help_string - exit(1) -# -# Parse data file -# -if diff: - hi = int(sys.argv[-1]) - lo = max(hi - offs, 0) - psp0 = psp_io.Input('OUT.{}.{:05d}'.format(sys.argv[-2], lo), comp='gas') - psp1 = psp_io.Input('OUT.{}.{:05d}'.format(sys.argv[-2], hi), comp='gas') - s = getattr(psp0, 'i{}'.format(key)) - c0 = getattr(psp0, 'i{}'.format(col)) - c1 = getattr(psp1, 'i{}'.format(col)) -else: - nd = int(sys.argv[-1]) - psp1 = psp_io.Input('OUT.{}.{:05d}'.format(sys.argv[-2], nd), comp='gas') - s = getattr(psp1, 'i{}'.format(key)) - c1 = getattr(psp1, 'i{}'.format(col)) - -data = [] -for i in range(len(s)): - if s[i] == spc: - if diff: - data.append(float(c1[i] - c0[i])) - else: - data.append(float(c1[i])) - -a = np.array(data); - -minC = min(a) -maxC = max(a) - -num = int(np.sqrt(len(data))) -dC = (maxC - minC)/(num - 1) - -hist = np.zeros(num) - -for v in a: - indx = int( (v - minC)/dC ) - hist[indx] += 1 - -for v in range(num): - print "{:13.4g} {:13.4g} : {}".format(minC + dC*v, minC + dC*(v+1), hist[v]) - -n, bins, patches = plt.hist(a, num, normed=1, alpha=0.75) - -plt.xlabel("Collisions") -plt.ylabel("Frequency") -plt.grid(True) -plt.show() - - - - - diff --git a/utils/Analysis/econs.py b/utils/Analysis/econs.py deleted file mode 100755 index 62c93b04d..000000000 --- a/utils/Analysis/econs.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, os, argparse -import numpy as np -import matplotlib.pyplot as plt -import string - -parser = argparse.ArgumentParser(description='Read ION_Coll file and plot energy disgnostics') -parser.add_argument('-t', '--tscale', default=1000.0, type=float, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, type=float, help='Maximum time in years') -parser.add_argument('-l', '--log', default=False, action='store_true', help='Logarithmic vertical scale') -parser.add_argument('-a', '--aux', default=False, action='store_true', help='Sum energy fields') -parser.add_argument('-k', '--keonly', default=False, action='store_true', help='Plot kinetic energy only') -parser.add_argument('-K', '--ke', default=False, action='store_true', help='Total kinetic energy') -parser.add_argument('-d', '--delta', default=False, action='store_true', help='Plot fraction of deferred energy to total') -parser.add_argument('-c', '--compare', default=False, action='store_true', help='Total energy minus kinetic energy') -parser.add_argument('-b', '--both', default=False, action='store_true', help='Plot KE and Total E separately') -parser.add_argument('-r', '--ratio', default=False, action='store_true', help='Plot ratio of E(cons)/E(total)') -parser.add_argument('-w', '--lwidth', default=2.0, type=float, help='linewidth for curves') -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -lw = args.lwidth - -# Translation table to convert vertical bars and comments to spaces -# -trans = string.maketrans("#|", " ") - -d = {} -lead = 2 -labels = [] -lindex = {} -field = [ 'Time', - 'Temp', - 'Disp', - 'Etotl', - 'Efrac', - 'EdspE', - 'misE', - 'clrE', - 'delE', - 'delI', - 'PotI', - 'EkeE', - 'EkeI', - 'KlosC', - 'Klost', - 'ElosC', - 'Elost'] - -f, ax = plt.subplots(1, 1) - -box = ax.get_position() -newBox = [box.x0, box.y0, 0.9*box.width, box.height] -ax.set_position(newBox) - -toplot = ['Etotl', 'ElosC', 'KlosC', 'Elost', 'Klost', 'EkeE', 'EkeI', 'delI', 'delE'] - -kesum = ['EkeE', 'EkeI'] - -delE = ['delE', 'delI', 'clrE'] - -# aux = {'ElosC':1, 'KlosC':1, 'EkeE':1, 'EkeI':1, 'delI':-1, 'delE':-1} -aux = {'ElosC':1, 'EkeE':1, 'EkeI':1, 'delI':-1, 'delE':-1} - -for v in labs: - # Read and parse the file - # - fin = v + '.ION_coll' - if not os.path.exists(fin): - print 'File <{}> does not exist'.format(fin) - exit(-1) - file = open(fin) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - nlabs = len(labels) - tindx = labels.index('Elost') - tail = nlabs - tindx - if 'Disp' in labels: lead = 3 - ncol = (tindx-lead)/5 - d[v] = {} - for i in range(len(labels)): - e = labels[i] - if e in field: - d[v][e] = [] - lindex[e] = i - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(lead,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - for e in field: - if e in labels: - d[v][e].append(float(toks[lindex[e]])) - - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - x = np.array(d[v]['Time'][0:indx])*args.tscale - - if args.ratio: - y1 = np.array(d[v]['delI'][0:indx]) - y2 = np.array(d[v]['delE'][0:indx]) - y3 = np.array(d[v]['Etotl'][0:indx]) - if args.log: - ax.semilogy(x, np.abs(y1/y3), '-', linewidth=lw, label=v+':I(cons)/Etotl') - ax.semilogy(x, np.abs(y2/y3), '-', linewidth=lw, label=v+':E(cons)/Etotl') - else: - ax.plot(x, y1/y3, '-', linewidth=lw, label=v+':I(cons)/Etotl') - ax.plot(x, y2/y3, '-', linewidth=lw, label=v+':E(cons)/Etotl') - - elif args.both: - y = np.copy(x) * 0.0 - yt = np.array(d[v]['Etotl'][0:indx]) - for f in kesum: y += np.array(d[v][f][0:indx]) - if args.log: - ax.semilogy(x, y, '-', linewidth=lw, label=v+':KE') - ax.semilogy(x, yt, '-', linewidth=lw, label=v+':Total E') - else: - ax.plot(x, y, '-', linewidth=lw, label=v+':KE') - ax.plot(x, yt, '-', linewidth=lw, label=v+':Total E') - - elif args.compare: - y = np.copy(x) * 0.0 - yt = np.array(d[v]['Etotl'][0:indx]) - for f in kesum: y += np.array(d[v][f][0:indx]) - if args.log: - ax.semilogy(x, (yt - y)/yt, '-', linewidth=lw, label=v+':Delta E/E') - else: - ax.plot(x, (yt - y)/yt, '-', linewidth=lw, label=v+':Delta E/E') - - elif args.delta: - y = np.copy(x) * 0.0 - denom = np.array(d[v]['Etotl'][0:indx]) - for f in delE: - y = np.array(d[v][f][0:indx])/denom - if args.log: - ax.semilogy(x, y, '-', linewidth=lw, label=v+':'+f) - else: - ax.plot(x, y, '-', linewidth=lw, label=v+':'+f) - - elif args.keonly: - for f in kesum: - y = np.array(d[v][f][0:indx]) - if args.log: - ax.semilogy(x, y, '-', linewidth=lw, label=v+':'+f) - else: - ax.plot(x, y, '-', linewidth=lw, label=v+':'+f) - - else: - - for f in toplot: - if f in d[v]: - y = np.abs(np.array(d[v][f][0:indx])) - if args.log: - ax.semilogy(x, y, '-', linewidth=lw, label=v+':'+f) - else: - ax.plot(x, y, '-', linewidth=lw, label=v+':'+f) - - if args.ke: - y = np.copy(x) * 0.0 - for f in kesum: y += np.array(d[v][f][0:indx]) - if args.log: - ax.semilogy(x, y, 'o', linewidth=lw, label=v+':KEsum') - else: - ax.plot(x, y, 'o', linewidth=lw, label=v+':KEsum') - - if args.aux: - y = np.copy(x) * 0.0 - for f in aux.keys(): y += np.array(d[v][f][0:indx])*aux[f] - if args.log: - ax.semilogy(x, y, '-o', linewidth=lw, label=v+':Esum') - else: - ax.plot(x, y, '-o', linewidth=lw, label=v+':Esum') - -leg = ax.legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) -# set the linewidth of each legend object -for legobj in leg.legendHandles: - legobj.set_linewidth(2.0) -ax.set_xlabel('Time') -ax.set_ylabel('Energy') -plt.show() diff --git a/utils/Analysis/electronD b/utils/Analysis/electronD deleted file mode 100755 index 55a7ec165..000000000 --- a/utils/Analysis/electronD +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- - -import os, sys, getopt, re, math, copy -import numpy as np -import matplotlib.pylab as plt - -def plot_data(name, temp, logy, flag): - """Plot the species files for the desired run tags - - Parameters: - - name (string): file name for input - - """ - - factor = temp*8.617342559624189e-05 - ms = 6 - ion = 0x10 - elec = 0x01 - - data = {} - labs = [] - if os.path.isfile(name): - file = open(name) - else: - print "No such file: ", name - exit() - - for line in file: - if line.find('Time')>=0: - labs = re.findall(r'\w+|\(\w+,\w+\)_\w', line) - for l in labs: data[l] = [] - elif line.find('---')<0 and line.find('Overflow')<0: - vals = [float(v) for v in line.split()] - for i in range(len(vals)): data[labs[i]].append(vals[i]) - - for v in labs: - if v not in ['Time', 'Energy']: - if max(data[v]) > 0.0: - if v.find('_e')>0 and not (flag & elec): continue - if v.find('_i')>0 and not (flag & ion ): continue - mark = "s" - if v.find('_e')>0: mark = "o" - if logy: - plt.semilogy(data['Energy'], data[v], '-', label=v, - marker=mark, linewidth=3, markersize=ms, - markerfacecolor="None") - else: - plt.plot(data['Energy'], data[v], '-', label=v, - marker=mark, linewidth=3, markersize=ms, - markerfacecolor="None") - - if factor>0.0: - thr = copy.deepcopy(data['Energy']) - for i in range(len(thr)): - thr[i] = math.sqrt(thr[i]) * math.exp(-data['Energy'][i]/factor) - nrm = 0.0; - if flag & elec: - nrmE = max(data['Total_e']) - if not math.isnan(nrmE): nrm = nrmE - if flag & ion: - nrmI = max(data['Total_i']) - if not math.isnan(nrmI): nrm = max(nrm, nrmI) - nrm = nrm/max(thr) - for i in range(len(thr)): thr[i] *= nrm - if logy: - plt.semilogy(data['Energy'], thr, '-', label='Maxwell', linewidth=2) - - else: - plt.plot(data['Energy'], thr, '-', label='Maxwell', linewidth=2) - - - plt.legend() - plt.xlabel('Energy') - plt.ylabel('Distribution') - plt.title('File: ' + name) - plt.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - temp = -1.0 - logy = False - ion = 0x10 - elec = 0x01 - flag = ion | elec - - hstr = '[-h | --help | -E | -I | -L | --LogY | -T val | --Temp=val] filename]' - - try: - opts, args = getopt.getopt(argv,"hLT:EI", ["help", "LogY", "Temp="]) - except getopt.GetoptError: - print sys.argv[0], hstr - sys.exit(2) - - for opt, arg in opts: - if opt in ("-h", "--help"): - print sys.argv[0], hstr - sys.exit() - elif opt in ("-T", "--Temp"): - temp = float(arg) - elif opt in ("-L", "--Logy"): - logy = True - elif opt == "-E": - flag = elec - elif opt == "-I": - flag = ion - - - plot_data(args[0], temp, logy, flag) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/getSpecies.py b/utils/Analysis/getSpecies.py deleted file mode 100755 index dfea747d1..000000000 --- a/utils/Analysis/getSpecies.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/python - -"""Module with predefined plotting and targets for UserTreeDSMC::CollideIon output - -The main functions begin with make* function. After make* (or a -readDB() which is called by a make*), you may use showlabs() to see -the name tags for all available fields. - - readDB(tag) : read *.species files and build database - - listAll() : show the fields available for plotting - -""" - -# For Python 3 compatibility -from __future__ import absolute_import, division, print_function, unicode_literals - -import matplotlib.pyplot as plt -import numpy as np -import os, sys, re, getopt - -def readDB(tag): - global species, db, labs - - # leading variables and per species variables - head = 2 - stanza = 16 - - # Clear DB - db = {} - - # Labels - labs = [] - - # Check file - filename = tag + '.species' - if not os.path.exists(filename): - raise NameError('Path <{}> does not exist'.format(filename)) - - # Open file - file = open(tag + '.species') - - # Get all the labels - line = file.readline() - labs = re.findall('([a-zA-Z0-9()\,\_\[\]]+)', line) - - # Skip the separator line - line = file.readline() - - # Number of fields for checking integrity of data line - # - nlab = len(labs) - - # Initialize db from labels and species info - db = {} - for v in labs: db[v] = [] - - # Process the data from the file - # - for line in file: - toks = re.findall('([\+\-]*(?:inf|INF|nan|NAN|[\+\-0-9.eE]+))', line) - nvec = len(toks) - # Check number of fields with expected - # - if nvec == nlab: - for i in range(nvec): - try: - db[labs[i]].append(float(toks[i])) - except: - print("Trouble reading float value??") - print("Toks[{}]={}".format(len(toks), toks)) - print("Line=", line) - print("labels[{}]={}".format(len(labs), labs)) - print("Attempting to read column {} out of {} expect {}".format(i, len(toks), len(labs))) - - # Convert lists to numpy arrays - # - for k in db: - db[k] = np.array(db[k]) - - return db diff --git a/utils/Analysis/ion_coll_ediag.py b/utils/Analysis/ion_coll_ediag.py deleted file mode 100755 index dd2528d98..000000000 --- a/utils/Analysis/ion_coll_ediag.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -""" -Program to display the particle collision counts for each type -using diagnostic output from the CollideIon class in the UserTreeDSMC -module. - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_coll_energy.py -m 10 run2 - -Plots the collision count types for each ion type. - -""" - -import sys, argparse -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - - -def plot_data(filename, eloss, msz, logY, dot, tscale): - """Parse and plot the *.ION_coll output files generated by - CollideIon - - Parameters: - - filename (string): is the input datafile name - - msz (numeric): marker size - - logY(bool): use logarithmic y axis - - dot (bool): if True, markers set to dots - - """ - - # Marker type - # - if dot: mk = '.' - else: mk = '*' - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - disp = [] - etot = [] - erat = [] - edsp = [] - ekeE = [] - ekeI = [] - delI = [] - delE = [] - misE = [] - clrE = [] - potI = [] - elsC = [] - elos = [] - ncol = 17 - lead = 2 - tail = 12 - data = {} - - # Species - # - spec5 = ['H', 'H+', 'He', 'He+', 'He++'] - spec1 = ['All'] - spec = [] - nstanza = 0 - - # Read and parse the file - # - file = open(filename + ".ION_coll") - for line in file: - if line.find('Species')>=0: - if line.find('(65535, 65535)')>=0: - spec = spec1 - else: - spec = spec5 - - nstanza = len(spec) - for v in spec: data[v] = {} - - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - nlabs = len(labels) - tindx = labels.index('Elost') - tail = nlabs - tindx - if 'Disp' in labels: lead = 3 - ncol = (tindx-lead)/nstanza - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - # print "lead={} toks={}".format(lead, len(toks)) - for i in range(lead, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-lead) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(lead,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - if 'Disp' in labels: disp.append(float(toks[2])) - etot.append(float(toks[-1])) - erat.append(float(toks[-2])) - edsp.append(float(toks[-3])) - misE.append(float(toks[-4])) - clrE.append(float(toks[-5])) - delE.append(float(toks[-6])) - delI.append(float(toks[-7])) - potI.append(float(toks[-8])) - ekeE.append(float(toks[-9])) - ekeI.append(float(toks[-10])) - elsC.append(float(toks[-11])) - elos.append(float(toks[-12])) - for i in range(lead,len(toks)-tail): - idx = (i-tail) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - time_1 = [] - temp_1 = [] - temp_2 = [] - file = open(filename + ".species") - for line in file: - if line.find('#')<0: - toks = line.split() - time_1.append(float(toks[0])) - temp_1.append(float(toks[1])) - temp_2.append(float(toks[3])) - - for i in range(len(time)): time[i] *= tscale - for i in range(len(time_1)): time_1[i] *= tscale - - # Fields to plot - # - pl.subplot(2,2,1) - pl.xlabel('Time') - pl.ylabel('Temp') - pl.plot(time_1, temp_1, '-', label="ion") - pl.plot(time_1, temp_2, '-', label="elec") - pl.legend(prop={'size':10}).draggable() - # - pl.subplot(2,2,2) - pl.xlabel('Time') - pl.ylabel('Ratio') - pl.plot(time, erat, '-') - # - ax = pl.subplot(2,2,3) - if logY: - # ax.semilogy(time, etot, '-', label="total") - ax.semilogy(time, ekeI, '-', label="ion") - ax.semilogy(time, ekeE, '-', label="electron") - ax.semilogy(time, potI, '-', label="ion pot") - ax.semilogy(time, edsp, '-', label="e-disp") - else: - # ax.plot(time, etot, '-', label="total") - ax.plot(time, ekeI, '-', label="ion") - ax.plot(time, ekeE, '-', label="electron") - ax.plot(time, potI, '-', label="ion pot") - ax.plot(time, edsp, '-', label="e-disp") - # Shrink current axis by 20% - # box = ax.get_position() - # ax.set_position([box.x0, box.y0, box.width * 0.8, box.height]) - # Put a legend to the right of the current axis - # ax.legend(loc='center left', bbox_to_anchor=(1, 0.5)) - ax.legend(prop={'size':10}).draggable() - # Labels - ax.set_xlabel('Time') - ax.set_ylabel('Energy') - # - pl.subplot(2,2,4) - pl.xlabel('Time') - pl.ylabel('Energy') - if eloss: - esKE = np.add(ekeI, ekeE) - etot = np.add(etot, potI) - if logY: - pl.semilogy(time, etot, '-', label="E total") - pl.semilogy(time, elos, '-', label="E lost (D)") - pl.semilogy(time, elsC, '-', label="E lost (C)") - pl.semilogy(time, potI, '-', label="Pot") - pl.semilogy(time, esKE, '-', label="KE") - else: - pl.plot(time, etot, '-', label="E total") - pl.plot(time, elos, '-', label="E lost (D)") - pl.plot(time, elsC, '-', label="E lost (C)") - pl.plot(time, potI, '-', label="Pot") - pl.plot(time, esKE, '-', label="KE") - pl.legend(prop={'size':10}).draggable() - else: - pl.plot(time, etot, '-') - # - pl.show() - - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - parser = argparse.ArgumentParser(description='Read DSMC ION_coll file and plot weight, count, and energy quantities') - parser.add_argument('-p', '--point', default=False, action='store_true', help='Plot points') - parser.add_argument('-l', '--log', default=False, action='store_true', help='Logarithmic vertical scale') - parser.add_argument('-t', '--time', default=False, action='store_true', help='Use time rather than temperature for x-axis') - parser.add_argument('-e', '--eloss', default=False, action='store_true', help='Plot energy lost') - parser.add_argument('--tscale', default=1.0, type=float, help='Conversion between system time and years') - parser.add_argument('-m', '--size', default=4, type=int, help='Marker size') - parser.add_argument('tag', nargs='*', help='File to process') - - args = parser.parse_args() - - plot_data(args.tag[0], args.eloss, args.size, args.log, args.point, args.tscale) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_coll_energy.py b/utils/Analysis/ion_coll_energy.py deleted file mode 100755 index 4bb1ca9ec..000000000 --- a/utils/Analysis/ion_coll_energy.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -"""Program to display the particle collision counts for each type -using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_coll_energy -m 10 run2 - -Plots the collision count types for each ion type. - -""" - -import sys, argparse -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - - -def plot_data(filename, msz, logY, dot, tscale, useTime, fullScreen): - """Parse and plot the *.ION_coll output files generated by - CollideIon - - Parameters: - - filename (string): is the input datafile name - - msz (numeric): marker size - - logY(bool): use logarithmic y axis - - dot (bool): if True, markers set to dots - - """ - - # Marker type - # - if dot: mk = '.' - else: mk = '*' - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - lead = 2 - tail = 2 - data = {} - - # Species - # - spec5 = ['H', 'H+', 'He', 'He+', 'He++'] - spec1 = ['All'] - spec = [] - nstanza = 0 - - # Read and parse the file - # - file = open(filename + ".ION_coll") - for line in file: - if line.find('Species')>=0: - if line.find('(65535, 65535)')>=0: - spec = spec1 - else: - spec = spec5 - - nstanza = len(spec) - for v in spec: data[v] = {} - - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - nlabs = len(labels) - tindx = labels.index('Elost') - tail = nlabs - tindx - if 'Disp' in labels: lead = 3 - ncol = (tindx-lead)/nstanza - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(lead, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-lead) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(lead,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - etot.append(float(toks[-1])) - for i in range(lead,len(toks)-tail): - idx = (i-lead) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - # Fields to plot - # - ekeys = ['E(ce)', 'E(ci)', 'E(ff)', 'E(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - xaxis = [] - if useTime: - for i in range(len(time)): time[i] *= tscale - xaxis = time - else: - xaxis = temp - - for v in spec: - pl.subplot(1, 1, 1) - cnt = 0 - Plot = False - for k in range(4): - if logY: - x = [] - y = [] # Look for non-zero entries - for i in range(len(data[v][ekeys[k]])): - if data[v][ekeys[k]][i] > 0.0: - x.append(xaxis[i]) - y.append(data[v][ekeys[k]][i]) - if len(x)>0: - Plot = True - pl.semilogy(x, y, '-', marker=mk, - label=elabs[k], markersize=msz) - else: - Plot = True - pl.plot(xaxis, data[v][ekeys[k]], '-', marker=mk, - label=elabs[k], markersize=msz) - if Plot: - if useTime: - pl.xlabel('Time') - else: - pl.xlabel('Temperature') - pl.ylabel('Energy') - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - if fullScreen: pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - parser = argparse.ArgumentParser(description='Read DSMC ION_coll file and plot weight, count, and energy quantities') - parser.add_argument('-p', '--point', default=False, action='store_true', help='Plot points') - parser.add_argument('-l', '--log', default=False, action='store_true', help='Logarithmic vertical scale') - parser.add_argument('-t', '--time', default=False, action='store_true', help='Use time rather than temperature for x-axis') - parser.add_argument('-f', '--full', default=False, action='store_true', help='Switch plotting window to full screen') - parser.add_argument('--tscale', default=1.0, type=float, help='Conversion between system time and years') - parser.add_argument('-m', '--size', default=4, type=int, help='Marker size') - parser.add_argument('tag', nargs='*', help='File to process') - - args = parser.parse_args() - - plot_data(args.tag[0], args.size, args.log, args.point, args.tscale, args.time, args.full) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_coll_number.py b/utils/Analysis/ion_coll_number.py deleted file mode 100755 index 5540b2ca8..000000000 --- a/utils/Analysis/ion_coll_number.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -"""Program to display the particle collision counts for each type -using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_coll_number.py -d 2 run2 - -Plots the collision count types for each ion type. - -""" - -import sys, argparse -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - - -def plot_data(filename, msz, line, dot, elastic, useTime, fullScreen, stride, tscale, trange): - """Parse and plot the *.ION_coll output files generated by - CollideIon - - Parameters: - - filename (string): is the input datafile name - - line (bool): if True, connect the dots - - dot (bool): if True, markers set to dots - - elastic (bool): if True, plot elastic interaction counts - - stride (int): plot every stride points - """ - - # Marker type - # - mk = '' - if line: mk = '-' - if dot: mk += '.' - else: mk += '*' - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - lead = 2 - data = {} - - # Species - # - spec5 = ['H', 'H+', 'He', 'He+', 'He++'] - spec1 = ['All'] - spec = [] - nstanza = 0 - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Species')>=0: - if line.find('(65535, 65535)')>=0: - spec = spec1 - else: - spec = spec5 - - nstanza = len(spec) - for v in spec: data[v] = {} - - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - nlabs = len(labels) - tindx = labels.index('Elost') - tail = nlabs - tindx - if 'Disp' in labels: lead = 3 - ncol = (tindx-lead)/nstanza - - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(2, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-lead) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(lead, len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - etot.append(float(toks[-1])) - for i in range(lead, len(toks)-tail): - idx = (i-lead) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - # Fields to plot - # - nkeys = 4 - ncols = 2 - if elastic: - ekeys = ['N(ne)', 'N(ie)', 'N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['neut-elec', 'ion-elec', 'collide', 'ionize', 'free-free', 'recomb'] - nkeys = 6 - ncols = 3 - else: - ekeys = ['N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for i in range(len(time)): time[i] *= tscale - - if useTime: xaxis = time - else: xaxis = temp - - icnt = 0 - for k in range(0, nkeys): - icnt += 1 - pl.subplot(ncols, 2, icnt) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Counts') - for v in spec: - x = xaxis - y = data[v][ekeys[k]] - if stride>1 and stride= trange[0] and time[i] <= trange[1]: - x.append(xaxis[i]) - y.append(data[v][ekeys[k]][i]) - pl.plot(x, y, mk, label=v, markersize=msz) - pl.title(elabs[k]) - if icnt==nkeys: - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - if fullScreen: pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - # Fields to plot - # - ekeys = ['W(ce)', 'W(ci)', 'W(ff)', 'W(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - icnt = 0 - for k in range(0, 4): - icnt += 1 - pl.subplot(2, 2, icnt) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Weights') - # pl.ylim((0, 30)) - for v in spec: - x = xaxis - y = data[v][ekeys[k]] - if stride>1 and stride= trange[0] and time[i] <= trange[1]: - x.append(xaxis[i]) - y.append(data[v][ekeys[k]][i]) - pl.plot(x, y, mk, label=v, markersize=msz) - pl.title(elabs[k]) - if icnt==4: - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - if fullScreen: pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - # Fields to plot - # - ekeys = ['W(ce)', 'W(ci)', 'W(ff)', 'W(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Weights') - cnt = 0 - for k in range(4): - x = [] - y = [] - for i in range(0,len(xaxis),stride): - if time[i] >= trange[0] and time[i] <= trange[1]: - if data[v][ekeys[k]][i]>0.0: - x.append(xaxis[i]) - y.append(data[v][ekeys[k]][i]) - if len(x)>0: - pl.semilogy(x, y, mk, label=elabs[k], markersize=msz) - cnt += 1 - if cnt==0: continue - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - if fullScreen: pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - - # Fields to plot - # - if elastic: - ekeys = ['N(nn)', 'N(ne)', 'N(ie)', 'N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['neutral', 'neut-elec', 'ion-elec', 'collide', 'ionize', 'free-free', 'recomb'] - else: - ekeys = ['N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Counts') - cnt = 0 - for k in range(len(ekeys)): - x = xaxis - y = data[v][ekeys[k]] - if stride>1 and stride= trange[0] and time[i] <= trange[1]: - x.append(xaxis[i]) - y.append(data[v][ekeys[k]][i]) - pl.plot(x, y, mk, label=elabs[k], markersize=msz) - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - if fullScreen: pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - # Fields to plot - # - ekeys = ['E(ce)', 'E(ci)', 'E(ff)', 'E(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Energy') - cnt = 0 - for k in range(4): - x = xaxis - y = data[v][ekeys[k]] - if stride>1 and stride= trange[0] and time[i] <= trange[1]: - x.append(xaxis[i]) - y.append(data[v][ekeys[k]][i]) - pl.plot(x, y, mk, label=elabs[k], markersize=msz) - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - if fullScreen: pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - parser = argparse.ArgumentParser(description='Read DSMC ION_coll file and plot weight, count, and energy quantities') - parser.add_argument('-l', '--line', default=False, action='store_true', help='Plot lines') - parser.add_argument('-p', '--point', default=False, action='store_true', help='Plot points') - parser.add_argument('-e', '--elastic', default=False, action='store_true', help='Plot elastic interactions') - parser.add_argument('-t', '--time', default=False, action='store_true', help='Use time rather than temperature for x-axis') - parser.add_argument('--tscale', default=1.0, type=float, help='Conversion between system time and years') - parser.add_argument('-f', '--full', default=False, action='store_true', help='Set full-screen plot') - parser.add_argument('-m', '--size', default=4, type=int, help='Marker size') - parser.add_argument('-s', '--stride', default=1, type=int, help='Stride in line count') - parser.add_argument('--Tmin', default=0.0, help='Minimum time in years') - parser.add_argument('--Tmax', default=1.0e20, help='Maximum time in years') - parser.add_argument('tag', nargs='*', help='File to process') - - args = parser.parse_args() - - suffix = ".ION_coll" - filename = args.tag[0] + suffix; - - plot_data(filename, args.size, args.line, args.point, args.elastic, args.time, args.full, args.stride, args.tscale, [args.Tmin, args.Tmax]) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_coll_ratio.py b/utils/Analysis/ion_coll_ratio.py deleted file mode 100755 index 8f17338a2..000000000 --- a/utils/Analysis/ion_coll_ratio.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -"""Program to display the particle collision counts for each type -using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_coll_number -d 2 run2 - -Plots the collision count types for each ion type. - -""" - -import sys, getopt -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip -import statsmodels.api as sm - - -def plot_data(filename, msz, line, dot, summary, izr, smooth, useTime, stride, trange): - """Parse and plot the *.ION_coll output files generated by - CollideIon - - Parameters: - - filename (string): is the input datafile name - - dot (bool): if True, markers set to dots - - summary(bool): if True, print summary data with not plots - - """ - - # Marker type - # - mk = '' - if line: mk += '-' - if dot: mk += '.' - else: mk += '-*' - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - head = 2 - tail = 2 - data = {} - - # Species - # - spec5 = ['H', 'H+', 'He', 'He+', 'He++'] - spec1 = ['All'] - spec = [] - nstanza = 0 - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Species')>=0: - if line.find('(65535, 65535)')>=0: - spec = spec1 - else: - spec = spec5 - - nstanza = len(spec) - for v in spec: data[v] = {} - - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - if line.find("W(") >= 0: - ncol = 13 - if line.find("N(nn)") >= 0: - ncol = 16 - if line.find("EratC") >= 0 or line.find("Efrac") >= 0: - tail = 12 - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(head, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-head) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(2,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[ 0])) - temp.append(float(toks[ 1])) - if tail == 7: - etot.append(float(toks[-1])) - else: - etot.append(float(toks[-2])) - for i in range(head,len(toks)-tail): - idx = (i-head) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - # Scattering counts - # - scat = {} - for v in spec: - scat[v] = [] - for i in range(len(temp)): - scat[v].append(data[v]['N(nn)'][i] + - data[v]['N(ne)'][i] + - data[v]['N(ie)'][i] ) - - # Fields to plot - # - ekeys = ['N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - # Choose time or temp as xaxis - # - xaxis = temp - if useTime: xaxis = time - - if not summary: - icnt = 0 - flr = 1.0e-10 - for k in range(0, 4): - icnt += 1 - pl.subplot(2, 2, icnt) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Ratio') - for v in spec: - ratio = [] - xx = [] - for i in range(0, len(xaxis), stride): - if time[i] >= trange[0] and time[i] <= trange[1]: - if izr: - if scat[v][i] > 0.0 and data[v][ekeys[k]][i] > 0.0: - xx.append(xaxis[i]) - ratio.append(data[v][ekeys[k]][i]/scat[v][i]) - else: - xx.append(xaxis[i]) - if scat[v][i] > 0.0: - ratio.append(data[v][ekeys[k]][i]/scat[v][i]) - else: - ratio.append(flr) - - if izr: - pl.semilogy(xx, ratio, mk, label=v, markersize=msz) - elif smooth: - lws = sm.nonparametric.lowess(ratio, xx) - pl.semilogy(lws[:,0], lws[:,1], '-', label=v) - else: - pl.semilogy(xx, ratio, mk, label=v, markersize=msz) - pl.title(elabs[k]) - if icnt==4: - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - # Inverse - # - for v in spec: - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Ratio') - for k in range(len(ekeys)): - xx = [] - ratio = [] - for i in range(0, len(xaxis), stride): - if time[i] < trange[0] or time[i] > trange[1]: continue - if izr: - if scat[v][i] > 0.0 and data[v][ekeys[k]][i] > 0.0: - xx.append(xaxis[i]) - ratio.append(data[v][ekeys[k]][i]/scat[v][i]) - else: - xx.append(xaxis[i]) - if scat[v][i] > 0.0 and data[v][ekeys[k]][i]: - ratio.append(data[v][ekeys[k]][i]/scat[v][i]) - else: - ratio.append(flr) - - if izr: - pl.semilogy(xx, ratio, mk, label=elabs[k], markersize=msz) - elif smooth: - lws = sm.nonparametric.lowess(ratio, xx) - pl.semilogy(lws[:,0], lws[:,1], '-', label=elabs[k]) - else: - pl.semilogy(xx, ratio, mk, label=elabs[k], markersize=msz) - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - # Inverse - # - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Ratio') - - xx = [] - ratio = [] - v = 'He+' - s = 'N(ci)' - for i in range(0, len(xaxis), stride): - if time[i] >= trange[0] and time[i] <= trange[0]: - if scat[v][i] > 0.0 and data[v][ekeys[k]][i]: - xx.append(xaxis[i]) - ratio.append(data[v][s][i]/scat[v][i]) - else: - xx.append(xaxis[i]) - ratio.append(flr) - if smooth: - lws = sm.nonparametric.lowess(ratio, xx) - pl.semilogy(lws[:,0], lws[:,1], '-', label='He+ ionize') - else: - pl.semilogy(xx, ratio, mk, label='He+ ionize', markersize=msz) - - xx = [] - ratio = [] - v = 'He++' - s = 'N(rr)' - for i in range(0, len(xaxis), stride): - if time[i] >= trange[0] and time[i] <= trange[1]: - if scat[v][i] > 0.0 and data[v][ekeys[k]][i]: - xx.append(xaxis[i]) - ratio.append(data[v][s][i]/scat[v][i]) - else: - xx.append(xaxis[i]) - ratio.append(flr) - if smooth: - lws = sm.nonparametric.lowess(ratio, xx) - pl.semilogy(lws[:,0], lws[:,1], '-', label='He++ recomb') - else: - pl.semilogy(xx, ratio, mk, label='He++ recomb', markersize=msz) - - pl.title('He+/He++ equilibrium') - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - if not summary: - flr = 1.0e-10 - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Counts') - for v in spec: - xx = [] - yy = [] - for i in range(0, len(xaxis), stride): - if time[i] >= trange[0] and time[i] <= trange[1]: - if izr: - if scat[v][i] > 0.0: - xx.append(xaxis[i]) - yy.append(scat[v][i]) - else: - xx.append(xaxis[i]) - if scat[v][i] > 0.0: - ratio.append(scat[v][i]) - else: - ratio.append(flr) - - if smooth: - lws = sm.nonparametric.lowess(yy, xx) - pl.semilogy(lws[:,0], lws[:,1], '-', label=v) - else: - pl.semilogy(xx, yy, mk, label=v, markersize=msz) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.title('Scattering counts') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - # - # Summary data - # - H_scat = sum(scat['H']) - Hp_scat = sum(scat['H+']) - Hep_scat = sum(scat['He+']) - Hepp_scat = sum(scat['He++']) - - H_ionz = sum(data['H' ]['N(ci)']) - Hp_rcmb = sum(data['H+' ]['N(rr)']) - Hep_ionz = sum(data['He+' ]['N(ci)']) - Hepp_rcmb = sum(data['He++']['N(rr)']) - - print("--------------------------------") - print("Ionization/recombination summary") - print("--------------------------------") - - if H_scat > 0: - print("N(H, ionz) = {:13g} {:13g} {:13g}".format(H_scat, H_ionz, H_ionz/H_scat)) - else: - print("N(H, ionz) = {:13g} {:13g} {:<13s}".format(H_scat, H_ionz, "inf")) - - if Hp_scat > 0: - print("N(H+, rcmb) = {:13g} {:13g} {:13g}".format(Hp_scat, Hp_rcmb, Hp_rcmb/Hp_scat)) - else: - print("N(H+, rcmb) = {:13g} {:13g} {:<13s}".format(Hp_scat, Hp_rcmb, "inf")) - - if Hep_scat > 0: - print("N(He+, ionz) = {:13g} {:13g} {:13g}".format(Hep_scat, Hep_ionz, Hep_ionz/Hep_scat)) - else: - print("N(He+, ionz) = {:13g} {:13g} {:<13s}".format(Hep_scat, Hep_ionz, "inf")) - - if Hepp_scat > 0: - print("N(He++, rcmb) = {:13g} {:13g} {:13g}".format(Hepp_scat, Hepp_rcmb, Hepp_rcmb/Hepp_scat)) - else: - print("N(He++, rcmb) = {:13g} {:13g} {:<13s}".format(Hepp_scat, Hepp_rcmb, "inf")) - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - lin = False - dot = False - msz = 4 - rpt = False - izr = False - lss = False - tim = False - str = 1 - trange = [0.0, 1.0e20] - - usage = '[-p | --point | -m | --msize= | -s | --summary | -L | --lowess | --line | --stride= | --tmin= | --tmax=] ' - - try: - opts, args = getopt.getopt(argv,"hm:pszL", ["help", "msize=", "point", "summary", "ignore", "lowess", "line", "time", "stride=", "tmin=", "tmax="]) - except getopt.GetoptError: - print 'Syntax Error' - print sys.argv[0], usage - sys.exit(2) - for opt, arg in opts: - if opt in ("-h", "--help"): - print sys.argv[0], usage - sys.exit() - elif opt in ("-p", "--point"): - dot = True - elif opt in ("-m", "--msize"): - msz = int(arg) - elif opt in ("-s", "--summary"): - rpt = True - elif opt in ("-z", "--ignore"): - izr = True - elif opt in ("-L", "--lowess"): - lss = True - elif opt in ("--line"): - lin = True - elif opt in ("--time"): - tim = True - elif opt in ("--stride"): - str = int(arg) - elif opt in ("--tmin"): - trange[0] = float(arg) - elif opt in ("--tmax"): - trange[1] = float(arg) - - suffix = ".ION_coll" - if len(args)>0: - filename = args[0] + suffix; - else: - filename = "run" + suffix; - - plot_data(filename, msz, lin, dot, rpt, izr, lss, tim, str, trange) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_coll_smooth.py b/utils/Analysis/ion_coll_smooth.py deleted file mode 100755 index 9ca33689d..000000000 --- a/utils/Analysis/ion_coll_smooth.py +++ /dev/null @@ -1,418 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -"""Program to display the particle collision counts for each type -using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_coll_number -d 2 run2 - -Plots the collision count types for each ion type. - -""" - -import sys, getopt -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - -def savitzky_golay(y, window_size, order, deriv=0, rate=1): - """Smooth (and optionally differentiate) data with a Savitzky-Golay filter. - The Savitzky-Golay filter removes high frequency noise from data. - It has the advantage of preserving the original shape and - features of the signal better than other types of filtering - approaches, such as moving averages techniques. - Parameters - ---------- - y : array_like, shape (N,) - the values of the time history of the signal. - window_size : int - the length of the window. Must be an odd integer number. - order : int - the order of the polynomial used in the filtering. - Must be less then `window_size` - 1. - deriv: int - the order of the derivative to compute (default = 0 means only smoothing) - Returns - ------- - ys : ndarray, shape (N) - the smoothed signal (or it's n-th derivative). - Notes - ----- - The Savitzky-Golay is a type of low-pass filter, particularly - suited for smoothing noisy data. The main idea behind this - approach is to make for each point a least-square fit with a - polynomial of high order over a odd-sized window centered at - the point. - Examples - -------- - t = np.linspace(-4, 4, 500) - y = np.exp( -t**2 ) + np.random.normal(0, 0.05, t.shape) - ysg = savitzky_golay(y, window_size=31, order=4) - import matplotlib.pyplot as plt - plt.plot(t, y, label='Noisy signal') - plt.plot(t, np.exp(-t**2), 'k', lw=1.5, label='Original signal') - plt.plot(t, ysg, 'r', label='Filtered signal') - plt.legend() - plt.show() - References - ---------- - .. [1] A. Savitzky, M. J. E. Golay, Smoothing and Differentiation of - Data by Simplified Least Squares Procedures. Analytical - Chemistry, 1964, 36 (8), pp 1627-1639. - .. [2] Numerical Recipes 3rd Edition: The Art of Scientific Computing - W.H. Press, S.A. Teukolsky, W.T. Vetterling, B.P. Flannery - Cambridge University Press ISBN-13: 9780521880688 - """ - import numpy as np - from math import factorial - - try: - window_size = np.abs(np.int(window_size)) - order = np.abs(np.int(order)) - except ValueError, msg: - raise ValueError("window_size and order have to be of type int") - if window_size % 2 != 1 or window_size < 1: - raise TypeError("window_size size must be a positive odd number") - if window_size < order + 2: - raise TypeError("window_size is too small for the polynomials order") - order_range = range(order+1) - half_window = (window_size -1) // 2 - # precompute coefficients - b = np.mat([[k**i for i in order_range] for k in range(-half_window, half_window+1)]) - m = np.linalg.pinv(b).A[deriv] * rate**deriv * factorial(deriv) - # pad the signal at the extremes with - # values taken from the signal itself - firstvals = y[0] - np.abs( y[1:half_window+1][::-1] - y[0] ) - lastvals = y[-1] + np.abs(y[-half_window-1:-1][::-1] - y[-1]) - y = np.concatenate((firstvals, y, lastvals)) - return np.convolve( m[::-1], y, mode='valid') - -def smooth_data(x, y, delta, window, order): - """Smooth data into bins and apply rbf""" - xmin = min(x) - xmax = max(x) - xx = np.arange(xmin-0.5*delta, xmax+0.5*delta, delta) + 0.5*delta - yy = np.arange(xmin-0.5*delta, xmax+0.5*delta, delta) * 0.0 - nn = np.arange(xmin-0.5*delta, xmax+0.5*delta, delta) * 0.0 - for i in range(0, len(x)): - indx = int((x[i] - xmin + 0.5*delta)/delta) - indx = max(indx, 0) - indx = min(indx, len(xx)-1) - yy[indx] += y[i] - nn[indx] += 1 - - for i in range(0, len(xx)): - if nn[i]>0: yy[i] /= nn[i] - - ys = savitzky_golay(yy, window, order) - - return xx, yy, ys - - -def plot_data(nametag, window, order, msz, dot, scr, useTime): - """Parse and plot the *.ION_coll output files generated by - CollideIon - - Parameters: - - nametag (string): is the run id prefix - - dot (bool): if True, markers set to dots - - """ - - # Construct file name - # - filename = nametag + ".ION_coll" - - # Marker type - # - if dot: mk = '.' - else: mk = 'o' - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - head = 2 - tail = 2 - data = {} - - # Species - # - spec = ['H', 'H+', 'He', 'He+', 'He++'] - for v in spec: data[v] = {} - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - if line.find("W(") >= 0: - ncol = 13 - if line.find("N(nn)") >= 0: - ncol = 16 - if line.find("EratC") >= 0 or line.find("Efrac") >= 0: - tail = 12 - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(head, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-head) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(head,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - etot.append(float(toks[-1])) - for i in range(head,len(toks)-tail): - idx = (i-head) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - # Fields to plot - # - ekeys = ['N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - xaxis = [] - if useTime: - xaxis = time - else: - xaxis = temp - - tmin = min(xaxis) - tmax = max(xaxis) - dt = (tmax - tmin)/100.0 - xt = np.arange(tmin, tmax, dt) - - icnt = 0 - for k in range(0, 4): - icnt += 1 - pl.subplot(2, 2, icnt) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Counts') - for v in spec: - xi, yi, ys = smooth_data(xaxis, np.array(data[v][ekeys[k]]), - 1000, window, order) - pl.plot(xi, yi, mk, markersize=msz, mfc='none', mec='k') - if len(xi) == len(ys): - pl.plot(xi, ys, '-', lw=2, label=v) - pl.title(elabs[k]) - if icnt==4: - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - if leg is not None: - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, "allcount")) - if scr: pl.show() - else: pl.close() - - # Fields to plot - # - ekeys = ['W(ce)', 'W(ci)', 'W(ff)', 'W(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - icnt = 0 - for k in range(0, 4): - icnt += 1 - pl.subplot(2, 2, icnt) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Weights') - # pl.ylim((0, 30)) - for v in spec: - xi, yi, ys = smooth_data(xaxis, data[v][ekeys[k]], - 1000, window, order) - if max(yi) > 0.0: - yi[yi==0.0] = min(yi[np.nonzero(yi)]) * 0.1 - ys[ys==0.0] = min(ys[np.nonzero(ys)]) * 0.1 - pl.semilogy(xi, yi, mk, markersize=msz, mfc='None', mec='k') - if len(xi) == len(ys): - pl.semilogy(xi, ys, '-', lw=2, label=v) - # pl.plot(xi, yi, mk, markersize=msz, mfc='None', mec='k') - # pl.plot(xi, ys, '-', lw=2, label=v) - pl.title(elabs[k]) - if icnt==4: - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - if leg is not None: - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, "allweight")) - if scr: pl.show() - else: pl.close() - - # Fields to plot - # - ekeys = ['N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Counts') - cnt = 0 - for k in range(4): - x = [] - y = [] - for j in range(0, len(xaxis)): - if data[v][ekeys[k]][j]>0.0: - x.append(xaxis[j]) - y.append(data[v][ekeys[k]][j]) - if len(x)>0: - xi, yi, ys = smooth_data(x, y, 1000, window, order) - if len(xi) == len(ys): - pl.plot(xi, yi, mk, markersize=msz, mfc='None', mec='k') - pl.plot(xi, ys, '-', lw=2, label=elabs[k]) - cnt += 1 - if cnt==0: continue - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, v + "_count")) - if scr: pl.show() - else: pl.close() - - # Fields to plot - # - ekeys = ['W(ce)', 'W(ci)', 'W(ff)', 'W(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Weights') - cnt = 0 - for k in range(4): - x = [] - y = [] - for j in range(0, len(xaxis)): - if data[v][ekeys[k]][j]>0.0: - x.append(xaxis[j]) - y.append(data[v][ekeys[k]][j]) - if len(x)>0: - xi, yi, ys = smooth_data(x, y, 1000, window, order) - if len(xi) == len(ys): - pl.semilogy(xi, yi, mk, markersize=msz, mfc='None', mec='k') - pl.semilogy(xi, ys, '-', lw=2, label=elabs[k]) - cnt += 1 - if cnt==0: continue - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, v + "_weight")) - if scr: pl.show() - else: pl.close() - - # Fields to plot - # - ekeys = ['E(ce)', 'E(ci)', 'E(ff)', 'E(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - if useTime: pl.xlabel('Time') - else: pl.xlabel('Temperature') - pl.ylabel('Energy') - cnt = 0 - for k in range(4): - xi, yi, ys = smooth_data(xaxis, data[v][ekeys[k]], - 1000, window, order) - pl.plot(xi, yi, mk, markersize=msz, mfc='None', mec='k') - if len(xi) == len(ys): - pl.plot(xi, ys, '-', lw=2, label=elabs[k]) - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - if leg is not None: - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, v + "_energy")) - if scr: pl.show() - else: pl.close() - - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - dot = False - scr = False - time = False - msz = 4 - window = 17 - order = 2 - - hlpstr = '[-w | --window | -o | --order | -p | --point | -m | --msize= | -s | --screen] ' - - try: - opts, args = getopt.getopt(argv,"hw:o:m:pst", ["window=", "order=", "msize=", "point", "screen", "time"]) - except getopt.GetoptError: - print 'Syntax Error' - print sys.argv[0], hlpstr - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print sys.argv[0], hlpstr - sys.exit() - elif opt in ("-w", "--window"): - window = int(arg) - elif opt in ("-o", "--order"): - order = int(arg) - elif opt in ("-p", "--point"): - dot = True - elif opt in ("-s", "--screen"): - scr = True - elif opt in ("-t", "--time"): - time = True - elif opt in ("-m", "--msize"): - msz = int(arg) - - nametag = "run" - if len(args)>0: nametag = args[0] - - plot_data(nametag, window, order, msz, dot, scr, time) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_coll_time.py b/utils/Analysis/ion_coll_time.py deleted file mode 100755 index c3138fe04..000000000 --- a/utils/Analysis/ion_coll_time.py +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -"""Program to display the particle collision counts for each type -using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_coll_time.py -d 2 run2 - -Plots the collision count and weights for each ion type as a function -of time. - -""" - -import sys, getopt -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - -def savitzky_golay(y, window_size, order, deriv=0, rate=1): - """Smooth (and optionally differentiate) data with a Savitzky-Golay filter. - The Savitzky-Golay filter removes high frequency noise from data. - It has the advantage of preserving the original shape and - features of the signal better than other types of filtering - approaches, such as moving averages techniques. - Parameters - ---------- - y : array_like, shape (N,) - the values of the time history of the signal. - window_size : int - the length of the window. Must be an odd integer number. - order : int - the order of the polynomial used in the filtering. - Must be less then `window_size` - 1. - deriv: int - the order of the derivative to compute (default = 0 means only smoothing) - Returns - ------- - ys : ndarray, shape (N) - the smoothed signal (or it's n-th derivative). - Notes - ----- - The Savitzky-Golay is a type of low-pass filter, particularly - suited for smoothing noisy data. The main idea behind this - approach is to make for each point a least-square fit with a - polynomial of high order over a odd-sized window centered at - the point. - Examples - -------- - t = np.linspace(-4, 4, 500) - y = np.exp( -t**2 ) + np.random.normal(0, 0.05, t.shape) - ysg = savitzky_golay(y, window_size=31, order=4) - import matplotlib.pyplot as plt - plt.plot(t, y, label='Noisy signal') - plt.plot(t, np.exp(-t**2), 'k', lw=1.5, label='Original signal') - plt.plot(t, ysg, 'r', label='Filtered signal') - plt.legend() - plt.show() - References - ---------- - .. [1] A. Savitzky, M. J. E. Golay, Smoothing and Differentiation of - Data by Simplified Least Squares Procedures. Analytical - Chemistry, 1964, 36 (8), pp 1627-1639. - .. [2] Numerical Recipes 3rd Edition: The Art of Scientific Computing - W.H. Press, S.A. Teukolsky, W.T. Vetterling, B.P. Flannery - Cambridge University Press ISBN-13: 9780521880688 - """ - import numpy as np - from math import factorial - - try: - window_size = np.abs(np.int(window_size)) - order = np.abs(np.int(order)) - except ValueError, msg: - raise ValueError("window_size and order have to be of type int") - if window_size % 2 != 1 or window_size < 1: - raise TypeError("window_size size must be a positive odd number") - if window_size < order + 2: - raise TypeError("window_size is too small for the polynomials order") - order_range = range(order+1) - half_window = (window_size -1) // 2 - # precompute coefficients - b = np.mat([[k**i for i in order_range] for k in range(-half_window, half_window+1)]) - m = np.linalg.pinv(b).A[deriv] * rate**deriv * factorial(deriv) - # pad the signal at the extremes with - # values taken from the signal itself - firstvals = y[0] - np.abs( y[1:half_window+1][::-1] - y[0] ) - lastvals = y[-1] + np.abs(y[-half_window-1:-1][::-1] - y[-1]) - y = np.concatenate((firstvals, y, lastvals)) - return np.convolve( m[::-1], y, mode='valid') - -def plot_data(nametag, window, order, msz, dot, scr, scale): - """Parse and plot the *.ION_coll output files generated by - CollideIon - - Parameters: - - nametag (string): is the run id prefix - - dot (bool): if True, markers set to dots - - """ - - # Construct file name - # - filename = nametag + ".ION_coll" - - # Marker type - # - if dot: mk = '.' - else: mk = 'o' - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - head = 2 - tail = 2 - data = {} - - # Species - # - spec = ['H', 'H+', 'He', 'He+', 'He++'] - for v in spec: data[v] = {} - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - if line.find("W(") >= 0: - ncol = 13 - if line.find("N(nn)") >= 0: - ncol = 16 - if line.find("EratC") >= 0 or line.find("Efrac") >= 0: - tail = 12 - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(head, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-head) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(head,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - etot.append(float(toks[-1])) - for i in range(head,len(toks)-tail): - idx = (i-head) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - # Fields to plot - # - ekeys = ['N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - stride = 1 - if len(time)>200: stride = len(time)/200 - TT = np.array(time) * scale - tt = TT[::stride] - - print "Data length =", len(time) - print "Data stride =", stride - - icnt = 0 - for k in range(0, 4): - icnt += 1 - pl.subplot(2, 2, icnt) - pl.xlabel('Time') - pl.ylabel('Counts') - for v in spec: - yi = np.array(data[v][ekeys[k]]) - ys = savitzky_golay(yi, window, order) - pl.plot(tt, yi[::stride], mk, markersize=msz, mfc='none', mec='k') - pl.plot(time, ys, '-', lw=2, label=v) - pl.title(elabs[k]) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, "allcount")) - if scr: pl.show() - else: pl.close() - - # Fields to plot - # - ekeys = ['W(ce)', 'W(ci)', 'W(ff)', 'W(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - icnt = 0 - for k in range(0, 4): - icnt += 1 - pl.subplot(2, 2, icnt) - pl.xlabel('Time') - pl.ylabel('Weights') - # pl.ylim((0, 30)) - for v in spec: - yi = np.array(data[v][ekeys[k]]) - ys = savitzky_golay(yi, window, order) - if max(yi) > 0.0: - tT = TT[np.nonzero(yi)] - tY = ys[np.nonzero(yi)] - sT = TT[np.nonzero(ys)] - sY = ys[np.nonzero(ys)] - ni = 1 - if len(tT)>200: ni = len(tT)/200 - pl.semilogy(tT[::ni], tY[::ni], mk, - markersize=msz, mfc='None', mec='k') - pl.semilogy(sT, sY, '-', lw=2, label=v) - pl.title(elabs[k]) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, "allweight")) - if scr: pl.show() - else: pl.close() - - # Fields to plot - # - ekeys = ['N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - pl.xlabel('Time') - pl.ylabel('Counts') - cnt = 0 - for k in range(4): - x = [] - y = [] - for j in range(0, len(temp)): - if data[v][ekeys[k]][j]>0.0: - x.append(time[j]) - y.append(data[v][ekeys[k]][j]) - if len(x)>0: - xi = np.array(x) - yi = np.array(y) - ys = savitzky_golay(yi, window, order) - ni = 1 - if len(y) > 200: ni = len(y)/200 - if len(x) == len(ys): - pl.plot(xi[::ni], yi[::ni], mk, markersize=msz, mfc='None', mec='k') - pl.plot(xi, ys, '-', lw=2, label=elabs[k]) - cnt += 1 - if cnt==0: continue - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, v + "_count")) - if scr: pl.show() - else: pl.close() - - # Fields to plot - # - ekeys = ['W(ce)', 'W(ci)', 'W(ff)', 'W(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - pl.xlabel('Time') - pl.ylabel('Weights') - cnt = 0 - for k in range(4): - x = [] - y = [] - for j in range(0, len(temp)): - if data[v][ekeys[k]][j]>0.0: - x.append(time[j]) - y.append(data[v][ekeys[k]][j]) - if len(x)>0: - xi = np.array(x) - yi = np.array(y) - ys = savitzky_golay(yi, window, order) - ni = 1 - if len(y) > 200: ni = len(y)/200 - if len(x) == len(ys): - pl.semilogy(xi[::ni], yi[::ni], mk, - markersize=msz, mfc='None', mec='k') - pl.semilogy(xi, ys, '-', lw=2, label=elabs[k]) - cnt += 1 - if cnt==0: continue - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, v + "_weight")) - if scr: pl.show() - else: pl.close() - - # Fields to plot - # - ekeys = ['E(ce)', 'E(ci)', 'E(ff)', 'E(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - pl.subplot(1, 1, 1) - pl.xlabel('Time') - pl.ylabel('Energy') - cnt = 0 - for k in range(4): - yi = np.array(data[v][ekeys[k]]) - ys = savitzky_golay(yi, window, order) - pl.plot(tt, yi[::stride], mk, markersize=msz, mfc='None', mec='k') - pl.plot(time, ys, '-', lw=2, label=elabs[k]) - pl.title(v) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - pl.get_current_fig_manager().full_screen_toggle() - pl.savefig("%s_%s.png" % (nametag, v + "_energy")) - if scr: pl.show() - else: pl.close() - - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - dot = False - scr = False - msz = 4 - window = 17 - order = 2 - - hlpstr = '[-w | --window | -o | --order | -p | --point | -m | --msize= | -s | --screen] ' - - try: - opts, args = getopt.getopt(argv,"hw:o:m:T:ps", ["window=", "order=", "msize=", "scale=", "point", "screen"]) - except getopt.GetoptError: - print 'Syntax Error' - print sys.argv[0], hlpstr - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print sys.argv[0], hlpstr - sys.exit() - elif opt in ("-w", "--window"): - window = int(arg) - elif opt in ("-o", "--order"): - order = int(arg) - elif opt in ("-p", "--point"): - dot = True - elif opt in ("-s", "--screen"): - scr = True - elif opt in ("-m", "--msize"): - msz = int(arg) - elif opt in ("-T", "--scale"): - scale = float(arg) - - nametag = "run" - if len(args)>0: nametag = args[0] - - plot_data(nametag, window, order, msz, dot, scr, scale) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_collide.py b/utils/Analysis/ion_collide.py deleted file mode 100755 index 9ed36aaee..000000000 --- a/utils/Analysis/ion_collide.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/python - - -# -*- coding: utf-8 -*- - -"""Program to display the particle collision rates and temperature -profiles using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_collide -d 2 run2 - -Plots the energy-loss rates for each collision type and the -temperature and cooling profile for the run with runtag and -therefore species file named using polynomical -fitting with degree 2. To use a spline fit, specify the value of the -smoothing parameter, i.e. - - $ python ion_collide -s 10000000 run2 - -""" - -import sys, getopt -import copy -import string, re -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - - -def plot_data(filename, degree, smooth, tscale=1.0e5, logE=False): - """Parse and plot the *.ION_coll output files generated by - CollideIon - - Parameters: - - filename (string): is the input datafile name - - degree (int): is the degree of the polynomial fitting function. - If degree = 0, a spline fit is used - - smooth (real): is the smoothing parameter for the spline fit. - - tscale (real): years per system time unit - - logE (bool): use logarithmic energy scaling if True - - """ - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - lead = 2 - tail = 2 - data = {} - - # Species - # - spec = ['H', 'H+', 'He', 'He+', 'He++'] - for v in spec: data[v] = {} - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - labels = line.translate(trans).split() - nlabs = len(labels) - tindx = labels.index('Elost') - tail = nlabs - tindx - if 'Disp' in labels: lead = 3 - ncol = (tindx-lead)/5 - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(lead, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-lead) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(lead,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - etot.append(float(toks[-1])) - for i in range(lead,len(toks)-tail): - idx = (i-lead) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - # Find species - specs = [] - p = re.compile('\(\d+,\d+\)'); - for i in range(2,len(labels)): - if p.match(labels[i]): specs.append(i) - - # Fields to plot - # - ekeys = ['E(ce)', 'E(ci)', 'E(ff)', 'E(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - tm = np.array(time) - tp = np.array(temp) - if degree>0: - pf = np.polyfit(tm, tp, deg=degree) - yf = np.polyval(pf, tm) - pd = np.polyder(pf, m=1) - zf = np.polyval(pd, tm) - else: - tck = ip.splrep(tm, tp, s=smooth) - yf = ip.splev(tm, tck) - zf = ip.splev(tm, tck, der=1) - - pl.subplot(2, 2, 1) - pl.xlabel('Time (year)') - pl.ylabel('Temperature') - y = time - for i in range(0, len(y)): y[i] *= tscale - pl.plot(y, temp, '*', label='simulation') - if degree>0: - pl.plot(y, yf, '-', label='poly fit') - else: - pl.plot(y, yf, '-', label='spline fit') - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 2) - pl.xlabel('Temperature') - pl.ylabel('Slope') - if degree>0: - pl.plot(yf, -zf, '-', label='poly fit') - else: - pl.plot(yf, -zf, '-', label='spline fit') - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 3) - pl.xlabel('Time (year)') - pl.ylabel('Energy') - pl.title(elabs[0]) - for v in spec: - if logE: - pl.semilogy(time, data[v][ekeys[0]], '*', label=v, markersize=10) - else: - pl.plot(time, data[v][ekeys[0]], '*', label=v, markersize=10) - - pl.subplot(2, 2, 4) - pl.xlabel('Temperature (K)') - pl.ylabel('Energy') - pl.title(elabs[0]) - for v in spec: - if logE: - pl.semilogy(temp, data[v][ekeys[0]], '*', label=v, markersize=10) - else: - pl.plot(temp, data[v][ekeys[0]], '*', label=v, markersize=10) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - degree = 2 - smooth = 1 - tscale = 1e5 - logE = False - - try: - opts, args = getopt.getopt(argv,"hd:s:t:l", ["deg=", "smooth=", "timescale=", "logE"]) - except getopt.GetoptError: - print sys.argv[0], '[-d | --deg= | -s | --smooth= | -t | --timescale= | -l | --logE] ' - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print sys.argv[0], '[-d | --deg= | -s | --smooth= | -t | --timescale= | -l | --logE] ' - sys.exit() - elif opt in ("-d", "--deg"): - degree = int(arg) - elif opt in ("-s", "--smooth"): - smooth = float(arg) - degree = 0 - elif opt in ("-t", "--timescale"): - tscale = float(arg) - elif opt in ("-l", "--logE"): - logE = True - - suffix = ".ION_coll"; - if len(args)>0: - filename = args[0] + suffix; - else: - filename = "run" + suffix; - - plot_data(filename, degree, smooth, tscale, logE) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_dist.py b/utils/Analysis/ion_dist.py deleted file mode 100755 index 8c3d449bd..000000000 --- a/utils/Analysis/ion_dist.py +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- - -"""Program to compute the energy distributions based on the DSMC_log -file data - -Examples: - - $ python ion_dist.py -f electron run2 - -Plots the energy distributions for the named field (in the case above, -electrons) for the run with tag "run2". Field value is "electron" by -default. Other fields are "ion" and "interact" for the electron-ion -interaction kinetic energy. - -""" - -import re, sys, copy, getopt -import numpy as np -import matplotlib.pyplot as plt -import numpy as np -from scipy.optimize import curve_fit - - -def func(x, a, b): - """Fit energy distribution for amplitude and temperature""" - if a<=0.0: return 1e30 - if b<=0.0: return 1e30 - return a * np.sqrt(x) * np.exp(-b * x) / b**1.5 - -def plot_data(runtag, field, defaultT, until, deltaT, Z): - """Parse and plot the *.DSMC_log output files generated by CollideIon - - Parameters: - - filename (string): is the input datafile name - - field (string): is either "electron", "ion" or "interact" - - defaultT (float): is the default temperature (probably not needed - in most cases) - - until (float): skips all data until current time of is - reached - - deltaT (float): offset in temperature from current temperature for - comparison with expected distributions for a pure - Maxwell-Boltzmann distribution - - Z (int): atomic element for ion distribution - - """ - - # - # Patterns - # - num = '([0-9.+\-e]+)' - beg = '^[\[ ]+' - end = '[\] ]+' - sep = '[, ]+' - bar = '[\]| =]+' - pattern = beg + num + sep + num + end + bar + num - - if field == "electron" and Z==0: - fpat = '.*Electron energy.*' - elif field == "ion" and Z==0: - fpat = '.*Ion energy.*' - elif field == "electron" and Z>0: - fpat = ".*Electron \(Z={}\) energy.*".format(Z) - elif field == "ion" and Z>0: - fpat = ".*Ion \(Z={}\) energy.*".format(Z) - elif field == "interact": - fpat = '.*Electron interaction energy.*' - elif field == "subspecies": - fpat = '.*Subspecies electron energy.*' - else: - print "Bad field. Allowed fields are: electron, ion, interact" - return - - # - # Regex compilations - # - prog = re.compile(pattern) - begn = re.compile(fpat) - ctim = re.compile('^[ ]+' + num + '[ ]+' + 'current time') - clev = re.compile('^[ ]+' + num + '[ ]+' + 'current level') - temp = re.compile('^[ ]+' + num + '[ ]+' + 'mass-weighted temperature') - - file = open(runtag + ".DSMC_log") - - look = False - - # - # Plotting data - # - xb = [] - xe = [] - xx = [] - yy = [] - - # - # Default time - # - time = 0.0 - - slopeFac = 11604.50560112828 - slope = slopeFac/defaultT - level = 0 - - for line in file: - # - # Look for current time - # - result = ctim.match(line) - if result is not None: - time = float(result.group(1)) - # - # Look for current temp - # - result = temp.match(line) - if result is not None: - ttemp = float(result.group(1)) - if ttemp>0.0: - defaultT = ttemp - slope = slopeFac/defaultT - # - # Look for current level - # - result = clev.match(line) - if result is not None: - level = int(result.group(1)) - # - # If in a desired stanza, look for data - # - if look: - result = prog.match(line) - if result is not None: - if len(result.groups()) == 3: - xb.append(float(result.group(1))) - xe.append(float(result.group(2))) - xx.append(0.5*(xb[-1]+xe[-1])) - yy.append(float(result.group(3)) + 0.1) - elif len(xb)>0: # End of data: make the plot - # Fit for temperature - fc = 11604.5/defaultT # Initial guess for exponent - p0 = [sum(yy)/len(yy)*fc**1.5,fc] # Amplitude - popt, pcov = curve_fit(func, xx, yy, p0, sigma=np.sqrt(yy)) - # Temp - bT = slopeFac / popt[1] - # Make curves - tt = [] - for v in xx: tt.append(func(v, popt[0], popt[1])) - lt = [] - for v in tt: lt.append(np.log(v)) - # - ly = [] - for v in yy: ly.append(np.log(v)) - - fig, axes = plt.subplots(nrows=2, ncols=1) - - ax = axes[0] - ax.plot(xx, ly, '-o') - ax.plot(xx, lt, '-') - - if Z>0: - ax.set_title("{} (Z={}): t={} T(fit)={} T={}".format(field, Z,time,bT,ttemp)) - else: - ax.set_title("{}: t={} T(fit)={} T={}".format(field,time,bT,ttemp)) - ax.set_ylabel("Log(counts)") - ax.tick_params(axis='x', labelbottom='off') - - ay = axes[1] - ay.plot(xx, yy, '-o') - ay.plot(xx, tt, '-') - - ay.set_xlabel("Energy (eV)") - ay.set_ylabel("Counts") - - fig.tight_layout() - plt.show() - look = False - else: # Look for beginning of stanza - result = begn.match(line) - if result is not None and time >= until and level==0: - xb = [] # Zero out data - xe = [] - xx = [] - yy = [] - look = True - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - field = "electron" - until = 0.0 - defT = 100000.0 - delta = 0.2 - Z = 0 - - options = '[-f | --field= | -t | --time= | -T | --temp= | -d | --delta= | -Z | --Z ] ' - - try: - opts, args = getopt.getopt(argv,"hf:T:t:d:Z:", ["help","field=","temp=","time=","delta=","Z="]) - except getopt.GetoptError: - print sys.argv[0], - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print sys.argv[0], options - sys.exit() - elif opt in ("-f", "--field"): - field = arg - elif opt in ("-T", "--temp"): - defT = float(arg) - elif opt in ("-t", "--time"): - until = float(arg) - elif opt in ("-d", "--delta"): - delta = float(arg) - elif opt in ("-Z", "--Z"): - Z = int(arg) - - if len(args)>0: - filename = args[0] - else: - filename = "run" - - plot_data(filename, field, defT, until, delta, Z) - -if __name__ == "__main__": - main(sys.argv[1:]) - diff --git a/utils/Analysis/ion_energy.py b/utils/Analysis/ion_energy.py deleted file mode 100755 index 3b512e9d7..000000000 --- a/utils/Analysis/ion_energy.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/python - -"""Script to display the particle collision rates and temperature -profiles using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -Examples: - - $ python ion_energy run2 - -Plots the energy loss for each species and type of cross section for -the run with runtag and therefore species file named -. - -""" - -import sys, getopt -import copy -import string -import matplotlib.pyplot as pl - - -def plot_data(filename, tscale): - """Parse and plot the *.species output files generated by CollideIon - - Parameters: - - filename (string): is the input datafile name - - tscale (real): convert system time unit to years - - """ - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - head = 2 - tail = 2 - data = {} - - # Species - # - spec = ['H', 'H+', 'He', 'He+', 'He++'] - for v in spec: data[v] = {} - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - if line.find("W(") >= 0: - ncol = 13 - if line.find("N(nn)") >= 0: - ncol = 16 - if line.find("EratC") >= 0 or line.find("Efrac") >= 0: - tail = 12 - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(head, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-head) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(head,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts - # are the same (i.e. guard against the occasional - # badly written output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - if tail==7: - etot.append(float(toks[-1])) - else: - etot.append(float(toks[-2])) - for i in range(head,len(toks)-tail): - idx = (i-head) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - # Fields to plot - # - ekeys = ['E(ce)', 'E(ci)', 'E(ff)', 'E(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - c = 0 - for v in spec: - c += 1 - pl.subplot(2, 3, c) - pl.xlabel('Temperature') - pl.ylabel('Energy fraction') - pl.title(v) - # pl.ylim((1.0e-6, 1)) - for k in range(0, 4): - y = copy.deepcopy(data[v][ekeys[k]]) - for j in range(0, len(y)): - if etot[j]>0: y[j] /= etot[j] - y[j] += 1.0e-10 - pl.semilogy(temp, y, '*', label=elabs[k], markersize=10) - if c==5: - legend = pl.legend(loc='best',borderpad=0,labelspacing=0) - legend.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 3, 6) - pl.xlabel('Time (year)') - pl.ylabel('Temperature') - y = time - for i in range(0, len(y)): y[i] *= tscale - pl.plot(y, temp, '*') - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - minE = 1.0e+40 - maxE = 0.0 - for i in range(0, len(ekeys)): - for v in spec: - minE = min(minE, min(data[v][ekeys[i]])) - maxE = max(maxE, max(data[v][ekeys[i]])) - - c = 0 - for i in range(0, len(ekeys)): - c += 1 - pl.subplot(2, 2, c) - pl.xlabel('Temperature') - pl.ylabel('Energy') - pl.title(elabs[i]) - pl.ylim((minE*0.9, maxE*1.1)) - for v in spec: - pl.plot(temp, data[v][ekeys[i]], '*', label=v, markersize=10) - if c == 4: - pl.legend(loc='best',borderpad=0,labelspacing=0) - legend.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - tscale = 1.0e5 - try: - opts, args = getopt.getopt(argv,"hd:s:t:", ["deg=", "smooth=", "timescale="]) - except getopt.GetoptError: - print sys.argv[0], '[-t | --timescale=] ' - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print sys.argv[0], '[-t | --timescale=] ' - sys.exit() - elif opt in ("-t", "--timescale"): - tscale = float(arg) - - suffix = ".ION_coll"; - if len(args)>0: - filename = args[0] + suffix; - else: - filename = "run" + suffix; - - plot_data(filename, tscale) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_frac.py b/utils/Analysis/ion_frac.py deleted file mode 100755 index 7b9d9f5dc..000000000 --- a/utils/Analysis/ion_frac.py +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- - -"""Program to compute the ion fractions for CollideIon tests - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_frac -d 2 run2 - -Plots the temperature and ion fractions for the run with runtag - and therefore species file named using -polynomical fitting with degree 2. To use a spline fit, specify the value of the smoothing parameter, i.e. - - $ python ion_frac -s 10000000 run2 - -""" -from __future__ import print_function - -import sys, getopt -import copy -import string, re -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - - -def plot_data(filename, degree, smooth, msize, tscale=1.0e5, logY=False, dot=False, fullScreen=False): - """Parse and plot the *.species output files generated by CollideIon - - Parameters: - - filename (string): is the input datafile name - - degree (int): is the degree of the polynomial fitting function. - If degree = 0, a spline fit is used - - smooth (real): is the smoothing parameter for the spline fit. - - """ - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans('#|', ' ') - - - # Initialize data and header containers - # - tabl = {} - zmap = [] - time = [] - temp = [] - data = {} - - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - # Make species dicts - for i in range(2,len(labels)): - toks = labels[i][1:-1].split(',') - if len(toks)==2: - j = int(toks[0]) - if j not in tabl: - tabl[j] = [] - zmap.append(j) - tabl[j].append(i); - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(2,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - for i in range(2,len(toks)): - if i in data: - data[i].append(float(toks[i])) - else: - data[i] = [float(toks[i])] - else: - print('Bad line: toks=', len(toks), ' labels=', len(labels)) - - mk = '*' - if dot: mk = '.' - - # Find species fields - p = re.compile('\(\d+,\d+\)') - species = [] - for j in range(2,len(labels)): - if p.match(labels[j]): species.append(j) - - # Fields to plot - # - tm = np.array(time) - tp = np.array(temp) - if degree>0: - pf = np.polyfit(tm, tp, deg=degree) - yf = np.polyval(pf, tm) - pd = np.polyder(pf, m=1) - zf = np.polyval(pd, tm) - else: - tck = ip.splrep(tm, tp, s=smooth) - yf = ip.splev(tm, tck) - zf = ip.splev(tm, tck, der=1) - - pl.subplot(2, 2, 1) - pl.xlabel('Time (year)') - pl.ylabel('Temperature (K)') - y = time - for i in range(0, len(y)): y[i] *= tscale - pl.plot(y, temp, mk, label='simulation', markersize=msize) - if degree>0: - pl.plot(y, yf, '-', label='poly fit', linewidth=3) - else: - pl.plot(y, yf, '-', label='spline fit', linewidth=3) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 2) - pl.xlabel('Temperature (K)') - pl.ylabel('Slope') - if degree>0: - pl.plot(yf, -zf, '-', label='poly fit') - else: - pl.plot(yf, -zf, '-', label='spline fit') - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 3) - pl.xlabel('Temperature (K)') - pl.ylabel('Fraction') - for j in data.keys(): - for k in range(0, len(data[j])): data[j][k] += 1.0e-8 - - for i in data.keys(): - if i not in species: continue - if logY: - pl.semilogy(temp, data[i], mk, label=labels[i], markersize=msize) - else: - pl.plot(temp, data[i], mk, label=labels[i], markersize=msize) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 4) - pl.xlabel('Temperature (K)') - pl.ylabel('Normalized fraction') - # Compute norm - norm = {} - for n in zmap: - for k in range(0, len(temp)): - sum = 0.0 - for j in tabl[n]: - data[j][k] -= 1.0e-8; - sum += data[j][k] - for j in tabl[n]: - if sum>0.0: data[j][k] /= sum - data[j][k] += 1.0e-8 - for i in data.keys(): - if i not in species: continue - if logY: - pl.semilogy(temp, data[i], mk, label=labels[i], markersize=msize) - else: - pl.plot(temp, data[i], mk, label=labels[i], markersize=msize) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - if fullScreen: pl.get_current_fig_manager().full_screen_toggle() - pl.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - degree = 6 - smooth = 1 - msize = 4 - tscale = 1.0e5 - logY = False - dot = False - fullS = False - - option_string = '[-d | --deg= | -s | --smooth= | -m | --msize= | -l | --log | -p | --point | -F | --full-screen] ' - - try: - opts, args = getopt.getopt(argv,'hd:s:m:t:lpF', ['deg=', 'smooth=', 'msize=', 'timescale=', 'log', 'point', 'full-screen']) - except getopt.GetoptError: - print(sys.argv[0], option_string) - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print(sys.argv[0], option_string) - sys.exit() - elif opt in ('-d', '--deg'): - degree = int(arg) - elif opt in ('-s', '--smooth'): - smooth = float(arg) - degree = 0 - elif opt in ('-m', '--msize'): - msize = float(arg) - elif opt in ('-t', '--timescale'): - tscale = float(arg) - elif opt in ('-l', '--log'): - logY = True - elif opt in ('-p', '--point'): - dot = True - elif opt in ('-F', '--full-screen'): - fullS = True - - suffix = '.species'; - if len(args)>0: - filename = args[0] + suffix; - else: - filename = 'run' + suffix; - - plot_data(filename, degree, smooth, msize, tscale, logY, dot, fullS) - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_frac_ch.py b/utils/Analysis/ion_frac_ch.py deleted file mode 100755 index 1a0cb4fda..000000000 --- a/utils/Analysis/ion_frac_ch.py +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- - -"""Program to compute the ion fractions for CollideIon tests - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_frac_ch -d 2 run2 - -Plots the temperature and ion fractions for the run with runtag - and therefore species file named using -polynomical fitting with degree 2. To use a spline fit, specify the value of the smoothing parameter, i.e. - - $ python ion_frac_ch -s 10000000 run2 - -""" -from __future__ import print_function - -import sys, getopt -import copy -import string, re -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip -import chianti.core as ch - - -def plot_data(filename, degree, smooth, msize, dens, tmax=-1.0, tscale=1.0e5, logY=False, dot=False, fullScreen=False): - """Parse and plot the *.species output files generated by CollideIon - - Parameters: - - filename (string): is the input datafile name - - degree (int): is the degree of the polynomial fitting function. - If degree = 0, a spline fit is used - - smooth (real): is the smoothing parameter for the spline fit. - - """ - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - - # Initialize data and header containers - # - tabl = {} - zmap = [] - time = [] - temp = [] - data = {} - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - # Make species dicts - for i in range(2,len(labels)): - toks = labels[i][1:-1].split(',') - if len(toks)==2: - j = int(toks[0]) - if j not in tabl: - tabl[j] = [] - zmap.append(j) - tabl[j].append(i); - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(2,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - if tmax<0.0 or tmax > float(toks[0]): - time.append(float(toks[0])) - temp.append(float(toks[1])) - for i in range(2,len(toks)): - if i in data: - data[i].append(float(toks[i])) - else: - data[i] = [float(toks[i])] - else: - print("Bad line: toks=", len(toks), " labels=", len(labels)) - - mk = '*' - if dot: mk = '.' - - # Find species fields - p = re.compile('\(\d+,\d+\)') - species = [] - for j in range(2,len(labels)): - if p.match(labels[j]): species.append(j) - - # Fields to plot - # - Temp = np.array(temp) - Time = np.array(time) - - # Molecular weight - # - fH = 0.76 - fHe = 0.24 - mu = 1.0/(fH/1.0 + fHe/4.0) - - tp = Temp - tm = Time * tscale # Time in years - k_B = 1.3806504e-16 # Boltzmann constant - Econv = 1.5 * k_B * dens/mu # Convert to energy in erg - - if degree>0: - pf = np.polyfit(tm, tp, deg=degree) - yf = np.polyval(pf, tm) - pd = np.polyder(pf, m=1) - zf = np.polyval(pd, tm) * Econv - else: - tck = ip.splrep(tm, tp, s=smooth) - yf = ip.splev(tm, tck) - zf = ip.splev(tm, tck, der=1) * Econv - - pl.subplot(2, 2, 1) - pl.xlabel('Time (year)') - pl.ylabel('Temperature (K)') - pl.plot(tm, tp, mk, label='simulation', markersize=msize) - if degree>0: - pl.plot(tm, yf, '-', label='poly fit', linewidth=3) - else: - pl.plot(tm, yf, '-', label='spline fit', linewidth=3) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - Tmin = np.log10(Temp.min()) - Tmax = np.log10(Temp.max()) - - delT = (Tmax - Tmin - 0.000001)/2000.0 - chT = 10.0**np.arange(Tmin, Tmax, delT) - - rl = ch.radLoss(chT, dens, minAbund=0.01) - abund = rl.AbundanceAll['abundance'] - T_Ch = rl.RadLoss['temperature'] - R_Ch = rl.RadLoss['rate'] - yrs = 365.25*24.0*3600.0 - R_Ch *= yrs * dens**2 - elems = {} - for n in zmap: - elems[n] = ch.ioneq(n) - elems[n].calculate(chT) - - pl.subplot(2, 2, 2) - pl.xlabel('Temperature (K)') - pl.ylabel('Slope') - - if degree>0: - if logY: - pl.semilogy(yf, -zf, '-', label='poly fit') - else: - pl.plot(yf, -zf, '-', label='poly fit') - else: - if logY: - pl.semilogy(yf, -zf, '-', label='spline fit') - else: - pl.plot(yf, -zf, '-', label='spline fit') - - if logY: - pl.semilogy(T_Ch, R_Ch, '-', label='LTE') - else: - pl.plot(T_Ch, R_Ch, '-', label='LTE') - - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 3) - pl.xlabel('Temperature (K)') - pl.ylabel('Fraction') - for j in data.keys(): - for k in range(0, len(data[j])): data[j][k] += 1.0e-8 - - for i in data.keys(): - if i not in species: continue - if logY: - pl.semilogy(temp, data[i], mk, label=labels[i], markersize=msize) - else: - pl.plot(temp, data[i], mk, label=labels[i], markersize=msize) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 4) - pl.xlabel('Temperature (K)') - pl.ylabel('Normalized fraction') - # Compute norm - norm = {} - for n in zmap: - for k in range(0, len(temp)): - sum = 0.0 - for j in tabl[n]: - data[j][k] -= 1.0e-8; - sum += data[j][k] - for j in tabl[n]: - if sum>0.0: data[j][k] /= sum - data[j][k] += 1.0e-8 - for i in data.keys(): - if i not in species: continue - if logY: - pl.semilogy(temp, data[i], mk, label=labels[i], markersize=msize) - else: - pl.plot(temp, data[i], mk, label=labels[i], markersize=msize) - - for n in zmap: - for i in range(0, len(elems[n].Ioneq)): - lab = 'CH(%d, %d)' % (n, i+1); - if logY: - pl.semilogy(elems[n].Temperature, elems[n].Ioneq[i], '-', label=lab) - else: - pl.plot(elems[n].Temperature, elems[n].Ioneq[i], '-', label=lab) - - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - if fullScreen: pl.get_current_fig_manager().full_screen_toggle() - pl.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - dens = 0.05 - degree = 6 - smooth = 1 - msize = 4 - tmax = -1.0 - tscale = 1.0e5 - logY = False - dot = False - fullS = False - - option_string = '[-D | --density= | -d | --deg= | -s | --smooth= | -m | --msize= | -l | --log | -p | --point | -t tscale | --timescale= | -T tmax | --tmax= | -F | --full-screen] ' - - try: - opts, args = getopt.getopt(argv,'hD:d:s:m:t:T:lpF', ['density=', 'deg=', 'smooth=', 'msize=', 'timescale=', 'tmax=', 'log', 'point', 'full-screen']) - except getopt.GetoptError: - print(sys.argv[0], option_string) - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print(sys.argv[0], option_string) - sys.exit() - elif opt in ('-D', '--density'): - dens = float(arg) - elif opt in ('-d', '--deg'): - degree = int(arg) - elif opt in ('-s', '--smooth'): - smooth = float(arg) - degree = 0 - elif opt in ('-m', '--msize'): - msize = float(arg) - elif opt in ('-T', '--tmax'): - tmax = float(arg) - elif opt in ('-t', '--timescale'): - tscale = float(arg) - elif opt in ('-l', '--log'): - logY = True - elif opt in ('-p', '--point'): - dot = True - elif opt in ('-F', '--full-screen'): - fullS = True - - suffix = '.species'; - if len(args)>0: - filename = args[0] + suffix; - else: - filename = 'run' + suffix; - - plot_data(filename, degree, smooth, msize, dens, tmax, tscale, logY, dot, fullS) - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_list_number.py b/utils/Analysis/ion_list_number.py deleted file mode 100755 index f04b39c4c..000000000 --- a/utils/Analysis/ion_list_number.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -"""Program to display the particle collision counts for each type -using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_split_number run2 - -Print count types for each ion type. - -""" - -import sys, getopt -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - - -def split_data(filename): - """Parse the *.ION_coll output files generated by CollideIon - - Parameters: - - filename (string): is the input datafile name - - """ - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - head = 2 - tail = 2 - data = {} - - # Species - # - spec = ['H', 'H+', 'He', 'He+', 'He++'] - for v in spec: data[v] = {} - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - if line.find("W(") >= 0: - ncol = 13 - if line.find("N(nn)") >= 0: - ncol = 16 - if line.find("EratC") >= 0 or line.find("Efrac") >= 0: - tail = 12 - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(head, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-head) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(2,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - etot.append(float(toks[-1])) - for i in range(head,len(toks)-tail): - idx = (i-head) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "toks=", len(toks), " labels=", len(labels) - - # Fields to print - # - ekeys = ['N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - if ncol==16: - ekeys = ['N(nn)', 'N(ne)', 'N(ie)', 'N(ce)', 'N(ci)', 'N(ff)', 'N(rr)'] - elabs = ['neut-neut', 'neut-elec', 'ion-elec', 'collide', 'ionize', 'free-free', 'recomb'] - - for v in spec: - print - print '{:*^94}'.format(" [" + v + "] ") - print - print "{0:>14s} {1:>14s}".format("Time", "Temp"), - for e in ekeys: - print "{:>8s}".format(e), - print - print "{0:>14s} {0:>14s}".format("-------"), - for e in ekeys: - print "{:>8s}".format("-------"), - print - sums = {} - for e in ekeys: sums[e] = 0 - for k in range(len(time)): - print "{0:>14.2e} {1:>14.2e}".format(time[k], temp[k]), - for e in ekeys: - print "{:>8.0f}".format(data[v][e][k]), - sums[e] += data[v][e][k] - print - print "{0:>14s} {0:>14s}".format("-------"), - for e in ekeys: - print "{:>8s}".format("-------"), - print - print "{0:>14s} {1:>14s}".format("", "Totals"), - for e in ekeys: - print "{:>8.0f}".format(sums[e]), - print - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - try: - opts, args = getopt.getopt(argv, "h", ["help"]) - except getopt.GetoptError: - print 'Syntax Error' - print sys.argv[0], '[-h | --help] ' - sys.exit(2) - for opt, arg in opts: - if opt == ("-h", "--help"): - print sys.argv[0], '[-h | --help] ' - - suffix = ".ION_coll" - if len(args)>0: - filename = args[0] + suffix; - else: - filename = "run" + suffix; - - split_data(filename) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_rate.py b/utils/Analysis/ion_rate.py deleted file mode 100755 index 773fb0df8..000000000 --- a/utils/Analysis/ion_rate.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- - -"""Program to compute the cooling rate for CollideIon tests - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_rate -d 2 run2 - -Plots the temperature and cooling profile for the run with runtag - and therefore species file named using -polynomical fitting with degree 2. To use a spline fit, specify the value of the smoothing parameter, i.e. - - $ python ion_rate -s 10000000 run2 - -""" - -import sys, getopt -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - - -def plot_data(filename, degree, smooth, tscale, tmax): - """Parse and plot the *.species output files generated by CollideIon - - Parameters: - - filename (string): is the input datafile name - - degree (int): is the degree of the polynomial fitting function. - If degree = 0, a spline fit is used - - smooth (real): is the smoothing parameter for the spline fit. - - """ - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - trce = 2 - data = {} - - # Species - # - spec = ['H', 'H+', 'He', 'He+', 'He++'] - for v in spec: data[v] = {} - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time') >= 0: # Get the labels - next = True - labels = line.translate(trans).split() - if line.find('W(') >= 0: - ncol = 13 - trce = 3 - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(2, len(toks)-1): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-trce) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(2,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - ok = True - if len(time)>0: - if float(toks[0]) == time[-1]: - ok = False - if float(toks[0]) > tmax: - ok = False - if ok: - time.append(float(toks[0])) - temp.append(float(toks[1])) - etot.append(float(toks[-1])) - for i in range(2,len(toks)-1): - idx = (i-trce) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "Bad line: toks=", len(toks), " labels=", len(labels) - - # Fields to plot - # - ekeys = ['E(ce)', 'E(ci)', 'E(ff)', 'E(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - tm = np.array(time) - tp = np.array(temp) - if degree>0: - pf = np.polyfit(tm, tp, deg=degree) - yf = np.polyval(pf, tm) - pd = np.polyder(pf, m=1) - zf = np.polyval(pd, tm) - else: - # tck = ip.splrep(tm, tp, s=smooth) - print tm - print tp - tck = ip.splrep(tm, tp) - yf = ip.splev(tm, tck) - zf = ip.splev(tm, tck, der=1) - - pl.subplot(2, 2, 1) - pl.xlabel('Time (year)') - pl.ylabel('Temperature') - y = time - for i in range(0, len(y)): y[i] *= tscale - pl.plot(y, temp, '*', label='simulation') - if degree>0: - pl.plot(y, yf, '-', label='poly fit', linewidth=3) - else: - pl.plot(y, yf, '-', label='spline fit', linewidth=3) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 3) - pl.xlabel('Time (year)') - pl.ylabel('Slope') - if degree>0: - pl.plot(y, zf, '-', label='poly fit') - else: - pl.plot(y, zf, '-', label='spline fit') - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(1, 2, 2) - pl.xlabel('Temperature') - pl.ylabel('Slope') - azf = -zf - pl.plot(yf, azf, '-', label='poly fit') - - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - degree = 6 - smooth = 1 - tscale = 1.0e5 - tmax = 1.0e10 - try: - opts, args = getopt.getopt(argv,"hd:s:t:T:", ["deg=", "smooth=", "timescale=", "maxT="]) - except getopt.GetoptError: - print sys.argv[0], '[-d | --deg= | -s | --smooth= | -t | --timescale= | -T | --maxT=] ' - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print sys.argv[0], '[-d | --deg= | -s | --smooth= | -t | --timescale= | -T | --maxT=] ' - sys.exit() - elif opt in ("-d", "--deg"): - degree = int(arg) - elif opt in ("-s", "--smooth"): - smooth = float(arg) - degree = 0 - elif opt in ("-t", "--timescale"): - tscale = float(arg) - elif opt in ("-T", "--maxT"): - tmax = float(arg) - - suffix = ".ION_coll"; - if len(args)>0: - filename = args[0] + suffix; - else: - filename = "run" + suffix; - - plot_data(filename, degree, smooth, tscale, tmax) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_rate_ch.py b/utils/Analysis/ion_rate_ch.py deleted file mode 100755 index 8a259d62b..000000000 --- a/utils/Analysis/ion_rate_ch.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- - -"""Program to compute the cooling rate for CollideIon tests - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_rate_ch -d 2 run2 - -Plots the temperature and cooling profile for the run with runtag - and therefore species file named using -polynomical fitting with degree 2. To use a spline fit, specify the value of the smoothing parameter, i.e. - - $ python ion_rate_ch -s 10000000 run2 - -""" - -import sys, getopt -import copy -import string -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip -import chianti.core as ch - - -def plot_data(filename, degree, smooth, tscale, tmax, dens): - """Parse and plot the *.species output files generated by CollideIon - - Parameters: - - filename (string): is the input datafile name - - degree (int): is the degree of the polynomial fitting function. - If degree = 0, a spline fit is used - - smooth (real): is the smoothing parameter for the spline fit. - - """ - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - - # Initialize data and header containers - # - tabl = {} - time = [] - temp = [] - etot = [] - ncol = 9 - head = 2 - tail = 2 - data = {} - - # Species - # - spec = ['H', 'H+', 'He', 'He+', 'He++'] - for v in spec: data[v] = {} - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - if line.find('W(') >= 0: - ncol = 13 - head = 3 - if line.find('N(ie') >= 0: - ncol = 16 - head = 3 - if line.find('EratC') >= 0 or line.find('Efrac') >= 0: - tail = 12 - if line.find('[1]')>=0: # Get the column indices - toks = line.translate(trans).split() - for i in range(head, len(toks)-tail): - j = int(toks[i][1:-1]) - 1 - tabl[labels[j]] = i - idx = (i-head) / ncol - data[spec[idx]][labels[j]] = [] - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(2, len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - if float(toks[0]) <= tmax: - time.append(float(toks[0])) - temp.append(float(toks[1])) - etot.append(float(toks[-1])) - for i in range(head, len(toks)-tail): - idx = (i-head) / ncol - data[spec[idx]][labels[i]].append(float(toks[i])) - else: - print "Bad line: toks=", len(toks), " labels=", len(labels) - - # Fields to plot - # - ekeys = ['E(ce)', 'E(ci)', 'E(ff)', 'E(rr)'] - elabs = ['collide', 'ionize', 'free-free', 'recomb'] - - tm = np.array(time) - tp = np.array(temp) - if degree>0: - pf = np.polyfit(tm, tp, deg=degree) - yf = np.polyval(pf, tm) - pd = np.polyder(pf, m=1) - zf = np.polyval(pd, tm) - else: - tck = ip.splrep(tm, tp, s=smooth) - yf = ip.splev(tm, tck) - zf = ip.splev(tm, tck, der=1) - - pl.subplot(2, 2, 1) - pl.xlabel('Time (year)') - pl.ylabel('Temperature') - y = time - for i in range(0, len(y)): y[i] *= tscale - pl.plot(y, temp, '*', label='simulation') - if degree>0: - pl.plot(y, yf, '-', label='poly fit', linewidth=3) - else: - pl.plot(y, yf, '-', label='spline fit', linewidth=3) - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - pl.subplot(2, 2, 3) - pl.xlabel('Time (year)') - pl.ylabel('Slope') - if degree>0: - pl.plot(y, zf, '-', label='poly fit') - else: - pl.plot(y, zf, '-', label='spline fit') - leg = pl.legend(loc='best',borderpad=0,labelspacing=0) - leg.get_title().set_fontsize('6') - pl.setp(pl.gca().get_legend().get_texts(), fontsize='12') - - Tmin = np.log10(yf.min()) - Tmax = np.log10(yf.max()) - delT = (Tmax - Tmin - 0.000001)/2000.0 - chT = 10.0**np.arange(Tmin, Tmax, delT) - rl = ch.radLoss(chT, 1.0, minAbund=0.01) - - T_Ch = rl.RadLoss['temperature'] - R_Ch = rl.RadLoss['rate'] - k_B = 1.3806504e-16 - yr5 = 365.25*24*3600*1e5 - R_Ch *= yr5/k_B * dens**2 - - pl.subplot(1, 2, 2) - pl.xlabel('Temperature') - pl.ylabel('Slope') - azf = -zf - if degree>0: - pl.plot(yf, azf, '-', label='poly fit') - else: - pl.plot(yf, azf, '-', label='spline fit') - pl.plot(T_Ch, R_Ch, '-', label='LTE') - pl.legend() - - pl.get_current_fig_manager().full_screen_toggle() - pl.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - degree = 6 - smooth = 1 - tscale = 1.0e5 - tmax = 1.0e10 - dens = 0.1 - try: - opts, args = getopt.getopt(argv,"hd:s:t:T:D:", ["deg=", "smooth=", "timescale=", "maxT=", "density="]) - except getopt.GetoptError: - print sys.argv[0], '[-d | --deg= | -s | --smooth= | -t | --timescale= | -T | --maxT= | -D | --density=] ' - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print sys.argv[0], '[-d | --deg= | -s | --smooth= | -t | --timescale= | -T | --maxT=> | -D | --density=] ' - sys.exit() - elif opt in ("-d", "--deg"): - degree = int(arg) - elif opt in ("-s", "--smooth"): - smooth = float(arg) - degree = 0 - elif opt in ("-t", "--timescale"): - tscale = float(arg) - elif opt in ("-T", "--maxT"): - tmax = float(arg) - elif opt in ("-D", "--density"): - dens = float(arg) - - suffix = ".ION_coll"; - if len(args)>0: - filename = args[0] + suffix; - else: - filename = "run" + suffix; - - plot_data(filename, degree, smooth, tscale, tmax, dens) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/ion_spec.py b/utils/Analysis/ion_spec.py deleted file mode 100755 index e14aca18b..000000000 --- a/utils/Analysis/ion_spec.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- - -"""Program to plot the ion fractions for CollideIon tests - -There are two simple routines here. The main routine that parses the -input command line and a plotting/parsing routine. - -Examples: - - $ python ion_spec run2 - -""" - -import sys, getopt -import copy -import string, re -import numpy as np -import matplotlib.pyplot as pl -import scipy.interpolate as ip - - -def plot_data(filename, use_time=True, msize=4, tscale=1.0e5): - """Parse and plot the *.species output files generated by CollideIon - - Parameters: - - filename (string): is the input datafile name - - """ - - # Translation table to convert vertical bars and comments to spaces - # - trans = string.maketrans("#|", " ") - - - # Initialize data and header containers - # - tabl = {} - zmap = [] - time = [] - temp = [] - data = {} - labels = [] - - - # Read and parse the file - # - file = open(filename) - for line in file: - if line.find('Time')>=0: # Get the labels - next = True - labels = line.translate(trans).split() - # Make species dicts - for i in range(2,len(labels)): - toks = labels[i][1:-1].split(',') - if len(toks)==2: - j = int(toks[0]) - if j not in tabl: - tabl[j] = [] - zmap.append(j) - tabl[j].append(i); - if line.find('#')<0: # Read the data lines - toks = line.translate(trans).split() - allZ = True # Skip lines with zeros only - for i in range(2,len(toks)): - if float(toks[i])>0.0: - allZ = False - break - if not allZ: - # A non-zero line . . . Make sure field counts are the - # same (i.e. guard against the occasional badly written - # output file - if len(toks) == len(labels): - time.append(float(toks[0])) - temp.append(float(toks[1])) - for i in range(2,len(toks)): - if i in data: - data[i].append(float(toks[i])) - else: - data[i] = [float(toks[i])] - else: - print "Bad line: toks=", len(toks), " labels=", len(labels) - - # Use circles - mk = 'o' - - # Find species fields - p = re.compile('\(\d+,\d+\)') - species = [] - for j in range(2,len(labels)): - if p.match(labels[j]): species.append(j) - - # Fields to plot - # - tm = np.array(time) - tp = np.array(temp) - - # Number of species - # - nsp = len(species) - nplt = 1 + nsp/6 - pn = 1 - - x = [] - if use_time: - x = time - for k in range(0, len(x)): x[k] *= tscale - else: - x = temp - - for i in range(nsp): - for j in range(nplt): - pn = i + 1 - 6*j - pl.subplot(2, 3, pn) - - if pn in [4,5,6]: - if use_time: pl.xlabel('Time (year)') - else: pl.xlabel('Temperature (K)') - if pn in [1,4]: pl.ylabel('Fraction') - pl.title(labels[i+2]) - pl.plot(x, data[2+i], '-o', markersize=msize) - if pn==6: pl.show() - if pn!=6: pl.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - ptime = True - msize = 4 - tscale = 1.0e5 - - header = '[T | "temp" | -m | --msize= | -t | --timescale=] ' - try: - opts, args = getopt.getopt(argv,"Tm:t:", ["temp", "msize=", "timescale=", "point"]) - except getopt.GetoptError: - print sys.argv[0], header - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print sys.argv[0], header - sys.exit() - elif opt in ("-T", "--temp"): - ptime = False - elif opt in ("-m", "--msize"): - msize = float(arg) - elif opt in ("-t", "--timescale"): - tscale = float(arg) - - suffix = ".species"; - if len(args)>0: - filename = args[0] + suffix; - else: - filename = "run" + suffix; - - plot_data(filename, ptime, msize, tscale) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/multiSpecies b/utils/Analysis/multiSpecies deleted file mode 100755 index f2208d0cb..000000000 --- a/utils/Analysis/multiSpecies +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/python - -# -*- coding: utf-8 -*- - -import os, sys, getopt -import numpy as np -import matplotlib.pylab as plt - -def plot_data(runtags, time): - """Plot the species files for the desired run tags - - Parameters: - - runtags (list of strings): input runs - - time (bool): use time rather than temperature for abscissa - - """ - - xi = 1 - if time: xi = 0 - - db = {} # Dictionary to contain all of the data - - for lab in runtags: - data = [] - name = lab + '.species' - if os.path.isfile(name): - file = open(name) - else: - print "No such file: ", name - exit() - - for line in file: - if line.find('#')<0: - data.append([float(v) for v in line.split()]) - # Pack up the data - db[lab] = np.array(data).transpose() - - - # - # First plot - # - for l in db: - plt.plot(db[l][0], db[l][1], '-', label="ion,"+l, - marker="1", linewidth=3, markersize=10) - plt.plot(db[l][0], db[l][10], '-', label="elec,"+l, - marker="2", linewidth=3, markersize=10) - - plt.legend() - plt.xlabel("Time") - plt.ylabel("Temp") - plt.show() - - # - # Second plot - # - for l in db: - plt.plot(db[l][xi], db[l][6], '-', label="He++,"+l, - marker="1", linewidth=3, markersize=10) - plt.plot(db[l][xi], db[l][5], '-', label="He+,"+l, - marker="2", linewidth=3, markersize=10) - plt.plot(db[l][xi], db[l][4], '-', label="He,"+l, - marker="3", linewidth=3, markersize=10) - plt.legend() - if xi: plt.xlabel("Temp") - else: plt.xlabel("Time") - plt.show() - - # - # Third plot - # - for l in db: - plt.plot(db[l][xi], db[l][3], '-', label="H+,"+l, - marker="1", linewidth=3, markersize=10) - plt.plot(db[l][xi], db[l][2], '-', label="H,"+l, - marker="2", linewidth=3, markersize=10) - plt.legend() - if xi: plt.xlabel("Temp") - else: plt.xlabel("Time") - plt.show() - - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - time = False - - hstr = '[-h | --help | -T | --Time] [ . . . ]' - - try: - opts, args = getopt.getopt(argv,"hT", ["help","Time"]) - except getopt.GetoptError: - print sys.argv[0], hstr - sys.exit(2) - - for opt, arg in opts: - if opt in ("-h", "--help"): - print sys.argv[0], hstr - sys.exit() - elif opt in ("-T", "--Time"): - time = True - - plot_data(args, time) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/multispecies.py b/utils/Analysis/multispecies.py deleted file mode 100755 index bb3a0b27d..000000000 --- a/utils/Analysis/multispecies.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/python - -"""Module for plotting UserTreeDSMC::CollideIon output for multiple runts""" - -import sys, getopt -import matplotlib.pyplot as plt -import getSpecies as gs - -def plot(tags, tmin, tmax, Tmin, Tmax, scale): - """ Plotting routine """ - - fig = plt.figure() - ax = plt.subplot(111) - - # Shrink current axis's height by 10% on the bottom - box = ax.get_position() - newBox = [box.x0, box.y0, 0.9*box.width, box.height] - ax.set_position(newBox) - - fields = ['Telc(1)', 'Telc(2)', 'Tion(1)', 'Tion(2)'] - d = {} - for tag in tags: - d[tag] = gs.readDB(tag) - for f in fields: - if f in d[tag]: - plt.plot(d[tag]['Time']*scale, d[tag][f], '-', label="{}: {}".format(tag[5:],f)) - - plt.xlabel('Time') - plt.ylabel('Temp') - if tmax > tmin: plt.xlim([tmin, tmax]) - if Tmax > Tmin: plt.ylim([Tmin, Tmax]) - plt.legend(prop={'size':10}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0).draggable() - plt.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - tmin = 0.0 - tmax = 0.0 - Tmin = 0.0 - Tmax = 0.0 - scale = 1.0 - - helpString = '[--tmax= | --Tmax | --tmin= | --Tmin= | --scale= | -h | --help] '; - - try: - opts, args = getopt.getopt(argv,"h", ["tmin=", "tmax=", "Tmin=", "Tmax=", "scale=", "help"]) - except getopt.GetoptError: - print sys.argv[0], helpString - sys.exit(2) - for opt, arg in opts: - if opt in ('-h', '--help'): - print sys.argv[0], helpString - sys.exit() - elif opt in ("--tmin"): - tmin = float(arg) - elif opt in ("--tmax"): - tmax = float(arg) - elif opt in ("--Tmin"): - Tmin = float(arg) - elif opt in ("--Tmax"): - Tmax = float(arg) - elif opt in ("--scale"): - scale = float(arg) - - if len(args)<=0: - print sys.argv[0], helpString - sys.exit(2) - - plot(args, tmin, tmax, Tmin, Tmax, scale) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/plotColl.py b/utils/Analysis/plotColl.py deleted file mode 100755 index ca2b16287..000000000 --- a/utils/Analysis/plotColl.py +++ /dev/null @@ -1,260 +0,0 @@ -#!/usr/bin/python - -"""Module with predefined plotting and targets for UserTreeDSMC::CollideIon output - -The main functions begin with make* function. After make* (or a -readDB() which is called by a make*), you may use showlabs() to see -the name tags for all available fields. - - readDB(tag) : read OUTLOG files and build database - - listAll() : show the fields available for plotting - - plotEnergy() : plot global energy quantities - -""" - -# For Python 3 compatibility -from __future__ import absolute_import, division, print_function, unicode_literals - -import matplotlib.pyplot as plt -import numpy as np -import os, sys, re, getopt - -species = [] -labs = [] -db = {} - -def readDB(tag): - global species, db, labs - - # leading variables and per species variables - head = 2 - stanza = 16 - - # Clear DB - db = {} - - # Open file - file = open(tag + '.ION_coll') - - # Look for species line, discarding all the user info - while True: - line = file.readline() - species = [] - if re.search('Species==>', line) is not None: - spc = re.findall('[ |]*(\([0-9]+[ ]*,[ ]*[0-9]+\))[ |]*', line) - for v in spc: species.append(v.replace(" ", "")) - break - - # Skip the separator line - line = file.readline() - - # Get all the labels - line = file.readline() - labs = re.findall('([a-zA-Z0-9()]+)', line) - - # Skip the column count line - line = file.readline() - - # Skip the separator line - line = file.readline() - - # Initialize db from labels and species info - nspc = len(species) - for i in range(head): db[labs[i]] = [] - for j in range(nspc): - indx = head + stanza*j - db[species[j]] = {} - for k in range(stanza): - db[species[j]][labs[indx+k]] = [] - indx = head + stanza*nspc - for i in range(indx,len(labs)): db[labs[i]] = [] - - # Number of fields for checking integrity of data line - # - nlab = len(labs) - - # Process the data from the file - # - for line in file: - toks = re.findall('([\+\-]*(?:inf|INF|nan|NAN|[\+\-0-9.eE]+))', line) - nvec = len(toks) - # Check number of fields with expected - # - if nvec == nlab: - for i in range(head): db[labs[i]].append(float(toks[i])) - for j in range(nspc): - indx = head + stanza*j - for k in range(stanza): - db[species[j]][labs[indx+k]].append(float(toks[indx+k])) - indx = head + stanza*nspc - for i in range(indx,len(toks)): - try: - db[labs[i]].append(float(toks[i])) - except: - print("Trouble reading float value??") - print("Toks[{}]={}".format(len(toks), toks)) - print("Line=", line) - print("labels[{}]={}".format(len(labs), labs)) - print("Attempting to read column {} out of {} expect {}".format(i, len(toks), len(labs))) - - # Convert lists to numpy arrays - # - for k in db: - if k in species: - for j in db[k]: - db[k][j] = np.array(db[k][j]) - else: - db[k] = np.array(db[k]) - -def showAll(): - for k in db: - if k in species: - print("Species {}:".format(k), end="") - cnt = 0 - for j in db[k]: - if cnt % 6 == 0: print("\n ", end="") - print("{}".format(j), end=" ") - cnt += 1 - print() - else: - print("Value: {}".format(k)) - -def listAll(): - for k in db: - if k in species: - print("{:10} [species]".format(k)) - else: - print("{:10} [value]".format(k)) - -def listGlobal(): - for k in db: - if k not in species: - print("* {}".format(k)) - -def listSpecies(): - if k in species: - print("* {}".format(k)) - -def showSpecies(v): - if v not in species: - print("No species <{}> found".format(v)) - else: - cnt = 0 - print(" ", end="") - for j in db[v]: - cnt += 1 - print("{}".format(j), end=" ") - if cnt % 6 == 0: print("\n ", end="") - print() - - -def plotEnergy(Log=False, lw=2, xcol='Time', scale=1000.0, maxT=1.0e+20, tag=''): - """ Plot critical energy conservation quantities """ - - if len(tag)>0: readDB(tag) - - x = np.copy(db[xcol]) - k = len(x) - if xcol=='Time': - x *= scale - for i in range(len(x)): - if maxT <= x[i]: - k = i - break - if Log: - plt.semilogy(x[0:k], db['Etotl'][0:k], '-o', linewidth=lw, label='E total') - plt.semilogy(x[0:k], db['ElosC'][0:k], '-o', linewidth=lw, label='E lost') - plt.semilogy(x[0:k], db['Elost'][0:k], '-o', linewidth=lw, label='dE lost') - plt.semilogy(x[0:k], db['PotI'][0:k], '-', linewidth=lw, label='Ion pot') - plt.semilogy(x[0:k], db['EkeI'][0:k] + db['EkeE'][0:k], '-', linewidth=lw, label='Tot KE') - plt.semilogy(x[0:k], db['EkeI'][0:k], '-', linewidth=lw, label='Ion KE') - plt.semilogy(x[0:k], db['EkeE'][0:k], '-', linewidth=lw, label='Elec KE') - if 'delC' in labs: - plt.semilogy(x[0:k], db['delC'][0:k], '-x', linewidth=lw, label='E excess') - else: - plt.semilogy(x[0:k], db['delI'][0:k] + db['delE'][0:k], '-x', linewidth=lw, label='E excess') - plt.semilogy(x[0:k], db['delI'][0:k], '-', linewidth=lw, label='Ion excess') - plt.semilogy(x[0:k], db['delE'][0:k], '-', linewidth=lw, label='Elec excess') - else: - plt.plot(x[0:k], db['Etotl'][0:k], '-o', linewidth=lw, label='E total') - plt.plot(x[0:k], db['ElosC'][0:k], '-', linewidth=lw, label='E lost') - plt.plot(x[0:k], db['Elost'][0:k], '-', linewidth=lw, label='dE lost') - plt.plot(x[0:k], db['PotI'][0:k], '-', linewidth=lw, label='Ion pot') - plt.plot(x[0:k], db['EkeI'][0:k] + db['EkeE'][0:k], '-', linewidth=lw, label='Tot KE') - plt.plot(x[0:k], db['EkeI'][0:k], '-', linewidth=lw, label='Ion KE') - plt.plot(x[0:k], db['EkeE'][0:k], '-', linewidth=lw, label='Elec KE') - if 'delC' in labs: - plt.plot(x[0:k], db['delC'][0:k], '-x', linewidth=lw, label='E excess') - else: - plt.plot(x[0:k], db['delE'][0:k] + db['delI'][0:k], '-x', linewidth=lw, label='E excess') - plt.plot(x[0:k], db['delI'][0:k], '-', linewidth=lw, label='Ion excess') - plt.plot(x[0:k], db['delE'][0:k], '-', linewidth=lw, label='Elec excess') - - if xcol=='Time': - plt.xlabel('Time') - else: - plt.xlabel('Temperature') - - plt.ylabel('Energy') - plt.legend().draggable() - plt.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - helpstring = \ - ' [-t | --timescale=]' + \ - ' [-T | --maxT=]' + \ - ' [--time] [--temp]' - ' ' - - energy = False - logscale = False - lw = 2.0 - xcol = 'Time' - tscale = 1000.0 - maxT = 1.0e+20 - - try: - opts, args = getopt.getopt(argv, "helw:t:T:", - ["help", "energy", "logscale", "linewidth=", "time", "temp", "timescale=", "maxT="]) - except getopt.GetoptError: - print(sys.argv[0], helpstring) - sys.exit(2) - - for opt, arg in opts: - if opt == '-h': - print(sys.argv[0], helpstring) - sys.exit() - elif opt in ("-e", "--energy"): - energy = True - elif opt in ("-l", "--logscale"): - logscale = True - elif opt in ("-w", "--linewidth"): - lw = float(arg) - elif opt in ("--time"): - xcol = 'Time' - elif opt in ("--temp"): - xcol = 'Temp' - elif opt in ("-t", "--timescale"): - tscale = float(arg) - elif opt in ("-T", "--maxT"): - maxT = float(arg) - - # - # Last argument should be filename and must exist - # - if len(args)<=0: - print("Usage: {} runtag".format(argv[0])) - exit(1) - - readDB(args[-1]) - if energy: - plotEnergy(Log=logscale, lw=lw, scale=tscale, maxT=maxT, xcol=xcol) - else: - listAll() - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/plotEcons.py b/utils/Analysis/plotEcons.py deleted file mode 100755 index 7cbb357a0..000000000 --- a/utils/Analysis/plotEcons.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/python -i - -"""Plot cumulative value of a PSP attribute column - -""" - -import matplotlib.pyplot as plt -import string -import numpy as np -import os, sys, getopt, glob, re, subprocess - -def getFiles(runtag): - """ Get list of all PSP files with given tag """ - tags = sorted(glob.glob("OUT.{}.*".format(runtag))) - return tags - -def getTime(file): - p = subprocess.Popen(['pspinfo', file], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - num = '([0-9.+\-e]+)' - tim = re.compile('Time=' + num) - res = tim.match(out) - if res is not None: - Time = float(res.group(1)) - return Time - else: - print "Error parsing: {}".format(file) - return -1.0 - -def plotData(runtag, cname, col): - t = [] - k = [] - e = [] - files = getFiles(runtag) - for file in files: - time = getTime(file) - os.system("psp2ascii {}".format(file)) - ifil = open("comp.{}".format(cname)) - line = ifil.readline() - toks = line.split() - numb = int(toks[0]) # Number of particles - iatr = int(toks[1]) # Number of integer attributes - datr = int(toks[2]) # Number of float attributes - eion = 0.0 # Ion KE - econ = 0.0 # Deferred E - # - # Parse the loop - # - for i in range(numb): - line = ifil.readline() - toks = line.split() - # Compute the KE - m = float(toks[0]) - v = [float(toks[4]), float(toks[5]), float(toks[6])] - ke = 0.0 - for u in v: ke += 0.5*m*u*u - eion += ke - # Compute the deferred energy loss - econ += float(toks[8+iatr+col]) - # Append data - t.append(time) - k.append(eion) - e.append(econ) - # Diagnostic printing - print "name={}, T={}, KE={}, EC={}".format(runtag, time, eion, econ) - - print '-'*72 - print "Time" - print '-'*72 - print t - print '-'*72 - print "Ion KE" - print '-'*72 - print k - print '-'*72 - print "E cons" - print '-'*72 - print e - print '-'*72 - # - # Make the plot - # - if len(t)>1: - plt.plot(t, e, '-') - plt.xlabel("Time") - plt.ylabel("Energy") - plt.show() - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - info = '[-r | --runtag= | -i | --col ] ' - - runtag = "run" - cname = "gas" - icol = 7 - - try: - opts, args = getopt.getopt(argv,"r:c:i:h", ["runtag=", "cname=", "col=", "help"]) - except getopt.GetoptError: - print sys.argv[0], info - sys.exit(2) - for opt, arg in opts: - if opt in ("-h", "--help"): - print sys.argv[0], info - sys.exit() - elif opt in ("-r", "--runtag"): - runtag = arg - elif opt in ("-c", "--cname"): - cname = arg - elif opt in ("-i", "--col"): - icol = int(arg) - - plotData(runtag, cname, icol) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/utils/Analysis/plotHistoE.py b/utils/Analysis/plotHistoE.py deleted file mode 100755 index 146c21c11..000000000 --- a/utils/Analysis/plotHistoE.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/python -i - -"""Module with predefined plotting and targets for pspHistoE output - -The main functions begin with make* function. After make* (or a -readDB() which is called by a make*), you may use showlabs() to see -the name tags for all available fields. - - readDB(file) : read pspHistoE output file named "file" - - showlabs() : show the fields available for plotting - - makeM(file, labs, temp) : plot fields in array "labs" from pspHistoE "file" - assuming temperature "temp" - - makeEs(file, temp) : plot ion and electron energies from pspHistoE - "file" assuming temperature "temp" - - makeEe(file, temp) : plot electron energies per specie from pspHistoE - "file" assuming temperature "temp" - - makeEi(file, temp) : plot ion energies per specie from pspHistoE - "file" assuming temperature "temp" - - makeEH(file, temp) : plot ion and electron energies for H from - pspHistoE "file" assuming temperature "temp" - - makeEHe(file, temp) : plot ion and electron energies for He from - pspHistoE "file" assuming temperature "temp" - -""" - -import matplotlib.pyplot as plt -import numpy as np -import os, sys -import astroval as C - -flab = [] - -def readDB(infile): - global flab - - flab = [] - - file = open(infile) - data = [] - line = file.readline() # Label line - flab = [v for v in line[1:].split()] - llab = len(flab) - line = file.readline() # Separators - for line in file: - t = line.split() - if len(t) == llab and t[1] != 'Overflow': - data.append([float(v) for v in t]) - - a = np.array(data).transpose() - - db = {} - icnt = 0 - for l in flab: - db[l] = a[icnt] - icnt += 1 - - return db - -def showLabs(): - icnt = 0 - for v in flab: - icnt += 1 - print "{:10s}".format(v), - if icnt % 6 == 0: print - - -def makeM(infile,labs, T): - db = readDB(infile) - - for lab in labs: - if lab not in flab: - print "No such field, available data is:" - showLabs() - return - - ymax = 1.0e-20 - for lab in labs: - ym = max(db[lab]) - if ym>0: - plt.semilogy(db['Energy'], db[lab], '-', label=lab) - ymax = max(ymax, ym) - E = ymax*np.sqrt(db['Energy'])*np.exp(-db['Energy']/(C.k_B*T/C.ev)) - plt.semilogy(db['Energy'], E, '-', label='Expected') - plt.legend() - plt.xlabel('Energy') - plt.ylabel('Counts') - plt.show() - -def makeEs(infile,T): - labs = ['Total_i','Total_e'] - makeM(infile, labs, T) - -def makeEe(infile,T): - labs = ['(1,1)_e', '(1,2)_e', '(2,1)_e', '(2,2)_e', '(2,3)_e'] - makeM(infile, labs, T) - -def makeEi(infile,T): - labs = ['(1,1)_i', '(1,2)_i', '(2,1)_i', '(2,2)_i', '(2,3)_i'] - makeM(infile, labs, T) - -def makeEH(infile,T): - labs = ['(1,1)_e', '(1,2)_e', '(1,1)_i', '(1,2)_i'] - makeM(infile, labs, T) - -def makeEHe(infile,T): - labs = ['(2,1)_e', '(2,2)_e', '(2,3)_e', '(2,1)_i', '(2,2)_i', '(2,3)_i'] - makeM(infile, labs, T) diff --git a/utils/Analysis/plotOUTLOG.py b/utils/Analysis/plotOUTLOG.py deleted file mode 100755 index dc7c0645f..000000000 --- a/utils/Analysis/plotOUTLOG.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/python -i - -"""Module with predefined plotting and targets for EXP output - -The main functions begin with make* function. After make* (or a -readDB() which is called by a make*), you may use showlabs() to see -the name tags for all available fields. - - setTags(list) : set list of run tags (default: "run") - - readDB() : read OUTLOG files and build database - - showLabs() : show the fields available for plotting - - makeM(xl, labs) : plot fields in list "labs" against field "xl" - - makeP(xl, lab) : plot field "lab" against field "xl" - - timeStep() : plot time-step CPU time against time - "file" assuming temperature "temp" - - energies() : plot energy equilibrium values - - equil() : plot virial ratio - - pos() : plot center-of-mass position values against time - - vel() : plot center-of-mass velocity values against time - - angmom() : plot total angular momentum values against time - -""" - - -import matplotlib.pyplot as plt -import string -import numpy as np -import os, sys - -tags = ["run"] -flab = [] - -def setTags(intags): - """ Set desired OUTLOG file tags for plotting""" - global tags - tags = intags - -def readDB(): - global flab - - db = {} - flab = [] - - for tag in tags: - file = open("OUTLOG." + tag) - data = [] - line = file.readline() # Label line - line = file.readline() # Separator - line = file.readline() # Labels - labs = [v.strip() for v in line[1:].split('|')] - line = file.readline() # Separator - line = file.readline() # Indices - line = file.readline() # Separator - for line in file: - data.append([float(v) for v in line.split('|')]) - - a = np.array(data).transpose() - - db[tag] = {} - - for i in range(a.shape[0]): - db[tag][labs[i]] = a[i] - - for v in labs: - if v not in flab: flab.append(v) - - return db - - -def showLabs(): - icnt = 0 - for v in flab: - icnt += 1 - print "{:10s}".format(v), - if icnt % 6 == 0: print - - -def makeM(xl, labs): - db = readDB() - - for lab in labs+[xl]: - if lab not in flab: - print "No such field, available data is:" - showLabs() - return - - for t in tags: - for lab in labs: - l = t + ":" + lab - plt.plot(db[t][xl], db[t][lab], '-', label=l) - plt.legend() - plt.xlabel(xl) - plt.ylabel(lab) - plt.show() - -def makeP(xl, lab): - db = readDB() - - for l in [xl, lab]: - if l not in flab: - print "No such field, available data is:" - showLabs() - return - - for t in tags: - plt.plot(db[t][xl], db[t][lab], '-', label=t) - plt.legend() - plt.xlabel(xl) - plt.ylabel(lab) - plt.show() - -def timeStep(): - makeP('Time', 'Clock') - -def equil(comp=''): - if type(comp) is str: comp = [comp] - lst = [] - for c in comp: - s = '' - if len(c)>0: s = c + ' ' - lst.append(s+'2T/VC') - makeM('Time', lst) - -def energies(comp=''): - s = '' - if len(comp)>0: s = comp + ' ' - makeM('Time', [s+'KE', s+'PE', s+'VC', s+'E']) - -def pos(comp=''): - s = '' - if len(comp)>0: s = comp + ' ' - makeM('Time', [s+'R(x)', s+'R(y)', s+'R(z)']) - -def vel(comp=''): - s = '' - if len(comp)>0: s = comp + ' ' - makeM('Time', [s+'V(x)', s+'V(y)', s+'V(z)']) - -def angmom(comp=''): - s = '' - if len(comp)>0: s = comp + ' ' - makeM('Time', [s+'L(x)', s+'L(y)', s+'L(z)']) - diff --git a/utils/Analysis/plotSpecies.py b/utils/Analysis/plotSpecies.py deleted file mode 100755 index 31c752193..000000000 --- a/utils/Analysis/plotSpecies.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/python -i - -"""Module with predefined plotting and targets for UserTreeDSMC::CollideIon output - -The main functions begin with make* function. After make* (or a -readDB() which is called by a make*), you may use showlabs() to see -the name tags for all available fields. - - setTag(tags) : set run tag or list of run tags (default: "run") - - readDB() : read OUTLOG files and build database - - showLabs() : show the fields available for plotting - - pview(xl, labs) : plot field(s) in "labs" against field "xl" - - pview(xl, labs, True) : plot sum of list "labs" against field "xl" - - xl may be label or a 3-tuple (label, min_value, max_value) - -""" - -# For Python 3 compatibility -from __future__ import absolute_import, division, print_function, unicode_literals - -import matplotlib.pyplot as plt -import numpy as np -import os, sys, bisect - -tags = ["run"] -flab = [] - -H_spc = ['(1,1)', '(1,2)'] -He_spc = ['(2,1)', '(2,2)', '(2,3)'] -temps = ['Tion(1)','Tion(2)','Telc(1)','Telc(2)'] -energies = ['Eion(1)','Eion(2)','Eelc(1)','Eelc(2)'] -E_cons = ['Cons_E', 'Cons_G', 'Ions_E', 'Elec_E', 'Totl_E'] -E_sum = ['Ions_E', 'Elec_E'] - -def setTag(x): - """ Set desired *.species file tag for plotting""" - global tags - - if isinstance(x, str): - tags = [x] - elif isinstance(x, list): - tags = x - else: - print("Parameter must be a string or a list of strings") - return None - -def readDB(): - global tags, flab - - db = {} - flab = [] - - for tag in tags: - file = open(tag + ".species") - data = [] - line = file.readline() # Label line - labs = [v for v in line[1:].split()] - for line in file: - if line.find('#')<0: - data.append([float(v) for v in line.split()]) - - a = np.array(data).transpose() - - db[tag] = {} - - for i in range(a.shape[0]): - db[tag][labs[i]] = a[i] - - for v in labs: - if v not in flab: flab.append(v) - - return db - - -def showLabs(): - icnt = 0 - for v in flab: - icnt += 1 - print("{:10s}".format(v), end="") - if icnt % 6 == 0: print() - if icnt % 6 != 0: print() - - -def pview(xl, x, do_sum=False, ylab=""): - if isinstance(x, str): - return makeP(xl, x) - elif isinstance(x, list): - if do_sum: - return makeS(xl, x, ylab=ylab) - else: - return makeM(xl, x, ylab=ylab) - else: - return None - - -def makeS(xL, labs, ylab=""): - - db = readDB() - xl = [] - - bound = False - if isinstance(xL, tuple): - xl = xL[0] - minv = xL[1] - maxv = xL[2] - bound = True - else: - xl = xL - - for lab in labs+[xl]: - if lab not in flab: - print("No such field, available data is:") - showLabs() - return - - # Set the figure - fig = plt.figure() - ax = plt.subplot(111) - - # Shrink current axis's width by 25% - box = ax.get_position() - ax.set_position([box.x0, box.y0, box.width * 0.75, box.height]) - - - - for t in tags: - sum = np.zeros(db[t][labs[0]].shape) - for lab in labs: sum+= db[t][lab] - if bound: - imin = bisect.bisect_left (db[t][xl], minv) - imax = bisect.bisect_right(db[t][xl], maxv) - ax.plot(db[t][xl][imin:imax], db[t][lab][imin:imax], - '-', label=t+":sum") - else: - ax.plot(db[t][xl], db[t][lab], '-', label=t+":sum") - - # Put a legend to right of plot - legend = ax.legend(loc='upper left', bbox_to_anchor=(1.01, 1.0), - fancybox=True, shadow=True, ncol=1) - - for label in legend.get_lines(): - label.set_linewidth(2.0) # the legend line width - - plt.xlabel(xl) - if len(ylab): - plt.ylabel(ylab) - else: - plt.ylabel("Sum") - plt.show() - -def makeM(xL, labs, ylab=""): - - db = readDB() - xl = [] - - bound = False - if isinstance(xL, tuple): - xl = xL[0] - minv = xL[1] - maxv = xL[2] - bound = True - else: - xl = xL - - for lab in labs+[xl]: - if lab not in flab: - print("No such field, available data is:") - showLabs() - return - - # Set the figure - fig = plt.figure() - ax = plt.subplot(111) - - # Shrink current axis's width by 25% - box = ax.get_position() - ax.set_position([box.x0, box.y0, - box.width * 0.75, box.height]) - - for t in tags: - for lab in labs: - l = t + ":" + lab - if bound: - imin = bisect.bisect_left (db[t][xl], minv) - imax = bisect.bisect_right(db[t][xl], maxv) - ax.plot(db[t][xl][imin:imax], db[t][lab][imin:imax], - '-', label=l) - else: - ax.plot(db[t][xl], db[t][lab], '-', label=l) - - # Put a legend to right of plot - legend = ax.legend(loc='upper left', bbox_to_anchor=(1.01, 1.0), - fancybox=True, shadow=True, ncol=1) - - for label in legend.get_lines(): - label.set_linewidth(2.0) # the legend line width - - ax.set_xlabel(xl) - if len(lab): - if len(ylab): - ax.set_ylabel(ylab) - else: - ax.set_ylabel("Fields") - else: - ax.set_ylabel(lab) - plt.show() - -def makeP(xL, lab): - db = readDB() - xl = [] - - bound = False - if isinstance(xL, tuple): - xl = xL[0] - minv = xL[1] - maxv = xL[2] - bound = True - else: - xl = xL - - for l in [xl, lab]: - if l not in flab: - print("No such field, available data is:") - showLabs() - return - - for t in tags: - if bound: - imin = bisect.bisect_left (db[t][xl], minv) - imax = bisect.bisect_right(db[t][xl], maxv) - plt.plot(db[t][xl][imin:imax], db[t][lab][imin:imax], '-', label=t) - else: - plt.plot(db[t][xl], db[t][lab], '-', label=t) - - plt.legend() - plt.xlabel(xl) - plt.ylabel(lab) - plt.show() diff --git a/utils/Analysis/plot_species.py b/utils/Analysis/plot_species.py deleted file mode 100755 index 32b9355dc..000000000 --- a/utils/Analysis/plot_species.py +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -"""Program to display the ionization state for atomic type -using diagnostic output from the CollideIon class in the -UserTreeDSMC module. - -Example: - - $ plot_species.py run2 - -""" - -import sys, getopt -import numpy as np -import matplotlib.pyplot as plt -import sys -import plotColl as pC - -def scanSeq(a): - """ - Attempt to put sequence in order. - Returns False if no changes are made - """ - x = a[0,0] - for i in range(1, a.shape[1]): - if a[0,i] < x: - # Find location - k = 0 - for j in range(i): - if a[0,j] > a[0,i]: - k = j - break - # print "a={}, b={}, c={}".format(x, a[0,i], a[0,k]) - return True, (k, i) - x = a[0,i] - return False, (0, a.shape[1]) - -def scanLoc(t, a): - """ - Find index k, such that a[0,k]>=t - Returns False if it does not exist. - """ - k = 0 - for i in range(1, a.shape[1]): - if t<=a[0,i] and t>a[0,i-1]: - return True, i - x = a[0,i] - return False, a.shape[1] - -def plot_data(argv): - """ - Parse and plot the *.species file - """ - - # - # Check for point type and log time - # - fmt = '-' - logX = False - logY = False - Temp = False - Tscl = 1 - Tbeg = 0.0 - Tend = -1.0 - - argc = len(argv) - - for i in range(1,argc): - if argv[i] == '-p' or argv[i] == '--points': - fmt = '-o' - if argv[i] == '--logX': - logX = True - if argv[i] == '--logY': - logY = True - if argv[i] == '-T' or argv[i] == '--temp': - Temp = True - if argv[i] == '-t' or argv[i] == '--timescale': - Tscl = float(argv[i+1]) - if argv[i] == '-b' or argv[i] == '--Tbeg': - Tbeg = float(argv[i+1]) - if argv[i] == '-e' or argv[i] == '--Tend': - Tend = float(argv[i+1]) - if argv[i] == '-h' or argv[i] == '--help': - print "Usage: {} [-p|--points] [--logX] [--logY] [-t scale|--timescale scale] [-b|--Tbeg] [-e|--Tend] [-h|--help] runtag".format(argv[0]) - return - # - # Parse data file - # - try: - file = open(argv[-1] + ".species") - except: - print "Error opening file <{}>".format(argv[-1] + ".species") - return - data = [] - labs = [] - nvec = 0 - for line in file: - if line.find('#') < 0: - pvec = [float(v) for v in line.split()] - lvec = len(pvec) - # Assign size of first data line as default - # - if nvec==0: nvec = lvec - # Enforce equal size vectors; data line integrity - # - if nvec == lvec: data.append(pvec) - elif len(labs) <= 0: - labs = [v for v in line[1:].split()] - a = np.array(data).transpose() - # - # Search for appended sequence(s) - # - print "-------- Trim statistics --------" - print "Initial shape:", a.shape - - status = True - segcnt = 0 - while status: - status, segment = scanSeq(a) - if status: - b = a[:,np.s_[0:segment[0]:]] - c = a[:,np.s_[segment[1]::]] - a = np.concatenate((b, c), axis=1) - segcnt += 1 - print "Segment [{:3d}]: ({}, {})".format(segcnt, - segment[0], - segment[1]) - if Tbeg > 0.0: - status, indx = scanLoc(Tbeg/Tscl, a) - if status: a = a[:,np.s_[indx::]] - - if Tend > Tbeg: - status, indx = scanLoc(Tend/Tscl, a) - if status: a = a[:,np.s_[:indx:]] - - - print " Final shape:", a.shape - print "---------------------------------" - - pC.readDB(argv[-1]) - pc_time = pC.db['Time'] - pc_frac = pC.db['Efrac'] - pc_len = len(pc_time) - if pc_len != len(pc_frac) or pc_len==0: pc_len = 0 - - # - # Species plot - # - x = a[0] * Tscl - if Temp: x = a[1] - if logX and not logY: - plt.semilogx(x, a[labs.index('(1,1)')], fmt, label='H') - plt.semilogx(x, a[labs.index('(1,2)')], fmt, label='H+') - plt.semilogx(x, a[labs.index('(2,1)')], fmt, label='He') - plt.semilogx(x, a[labs.index('(2,2)')], fmt, label='He+') - plt.semilogx(x, a[labs.index('(2,3)')], fmt, label='He++') - if pc_len: - plt.semilogx(pc_time*Tscl, pc_frac, fmt, label='e') - elif logY and not logX: - plt.semilogy(x, a[labs.index('(1,1)')], fmt, label='H') - plt.semilogy(x, a[labs.index('(1,2)')], fmt, label='H+') - plt.semilogy(x, a[labs.index('(2,1)')], fmt, label='He') - plt.semilogy(x, a[labs.index('(2,2)')], fmt, label='He+') - plt.semilogy(x, a[labs.index('(2,3)')], fmt, label='He++') - if pc_len: - plt.semilogy(pc_time*Tscl, pc_frac, fmt, label='e') - elif logX and logY: - plt.loglog(x, a[labs.index('(1,1)')], fmt, label='H') - plt.loglog(x, a[labs.index('(1,2)')], fmt, label='H+') - plt.loglog(x, a[labs.index('(2,1)')], fmt, label='He') - plt.loglog(x, a[labs.index('(2,2)')], fmt, label='He+') - plt.loglog(x, a[labs.index('(2,3)')], fmt, label='He++') - if pc_len: - plt.loglog(pc_time*Tscl, pc_frac, fmt, label='e') - else: - plt.plot(x, a[labs.index('(1,1)')], fmt, label='H') - plt.plot(x, a[labs.index('(1,2)')], fmt, label='H+') - plt.plot(x, a[labs.index('(2,1)')], fmt, label='He') - plt.plot(x, a[labs.index('(2,2)')], fmt, label='He+') - plt.plot(x, a[labs.index('(2,3)')], fmt, label='He++') - if pc_len: - plt.plot(pc_time*Tscl, pc_frac, fmt, label='e') - plt.legend().draggable() - if Temp: plt.xlabel('Temperature') - else: - if Tscl == 1: - plt.xlabel('Time') - else: - plt.xlabel('Time (years)') - plt.ylabel('Species') - plt.show() - # - # Temperature plot - # - if logX and not logY: - plt.semilogx(x, a[labs.index('Temp')], fmt, label='Temp') - plt.semilogx(x, a[labs.index('Temp_E')], fmt, label='Temp_e') - plt.semilogx(x, a[labs.index('Tion(1)')], fmt, label='Temp(1)_i') - plt.semilogx(x, a[labs.index('Telc(1)')], fmt, label='Temp(1)_e') - plt.semilogx(x, a[labs.index('Tion(2)')], fmt, label='Temp(2)_i') - plt.semilogx(x, a[labs.index('Telc(2)')], fmt, label='Temp(2)_e') - elif logY and not logX: - plt.semilogy(x, a[labs.index('Temp')], fmt, label='Temp') - plt.semilogy(x, a[labs.index('Temp_E')], fmt, label='Temp_e') - plt.semilogy(x, a[labs.index('Tion(1)')], fmt, label='Temp(1)_i') - plt.semilogy(x, a[labs.index('Telc(1)')], fmt, label='Temp(1)_e') - plt.semilogy(x, a[labs.index('Tion(2)')], fmt, label='Temp(2)_i') - plt.semilogy(x, a[labs.index('Telc(2)')], fmt, label='Temp(2)_e') - elif logX and logY: - plt.loglog(x, a[labs.index('Temp')], fmt, label='Temp') - plt.loglog(x, a[labs.index('Temp_E')], fmt, label='Temp_e') - plt.loglog(x, a[labs.index('Tion(1)')], fmt, label='Temp(1)_i') - plt.loglog(x, a[labs.index('Telc(1)')], fmt, label='Temp(1)_e') - plt.loglog(x, a[labs.index('Tion(2)')], fmt, label='Temp(2)_i') - plt.loglog(x, a[labs.index('Telc(2)')], fmt, label='Temp(2)_e') - else: - plt.plot(x, a[labs.index('Temp')], fmt, label='Temp') - plt.plot(x, a[labs.index('Temp_E')], fmt, label='Temp_e') - plt.plot(x, a[labs.index('Tion(1)')], fmt, label='Temp(1)_i') - plt.plot(x, a[labs.index('Telc(1)')], fmt, label='Temp(1)_e') - plt.plot(x, a[labs.index('Tion(2)')], fmt, label='Temp(2)_i') - plt.plot(x, a[labs.index('Telc(2)')], fmt, label='Temp(2)_e') - # - plt.legend().draggable() - if Temp: plt.xlabel('Temperature') - else: - if Tscl == 1: - plt.xlabel('Time') - else: - plt.xlabel('Time (years)') - - plt.ylabel('Species temperature') - plt.show() - - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - # - # Last argument should be filename and must exist - # - if len(argv) <=1: - print "Usage: {} runtag".format(argv[0]) - exit(1) - - plot_data(argv) - -if __name__ == "__main__": - main(sys.argv) diff --git a/utils/Analysis/psp_dist.py b/utils/Analysis/psp_dist.py deleted file mode 100755 index ba53963af..000000000 --- a/utils/Analysis/psp_dist.py +++ /dev/null @@ -1,386 +0,0 @@ -#!/usr/bin/python3 - -# -*- coding: utf-8 -*- - -"""Program to compute the energy distributions based on the DSMC_log -file data - -Examples: - - $ python psp_dist.py -f electron run2 - -Plots the energy distributions for the named field (in the case above, -electrons) for the run with tag "run2". Field value is "electron" by -default. Other fields are "ion" and "interact" for the electron-ion -interaction kinetic energy. - -Only for Trace method, so far. -""" - -import os, re, sys, copy, getopt, enum -import numpy as np -import matplotlib.pyplot as plt -from scipy.interpolate import interp1d -from scipy.signal import savgol_filter -from scipy.optimize import curve_fit -import psp_io - -class FitType(enum.Enum): - Analytic = 1 - AmpOnly = 2 - TempAmp = 3 - Fixed = 4 - -class PlotType(enum.Enum): - Linear = 1 - Log = 2 - Both = 3 - NoPlot = 4 - -Munit = 1.9891e33 -Lunit = 3.08568e18 -Tunit = 3.15569e10 -Vunit = Lunit/Tunit - -m_H = 1.008 -m_He = 4.002602 -X_H = 0.76 -X_He = 0.24 -Mu_i = 1.0/(X_H/m_H + X_He/m_He) -Mu_e = 0.000548579909 - -amu = 1.660539e-24 -eV = 1.60217653e-12 - -b0 = 0.0 -a0 = 1.0 - -ndatr = 6 - -def func1(x, a): - """Fit energy distribution for amplitude only""" - global b0 - if a<=0.0: return 1e30 - return a * np.sqrt(x) * np.exp(-b0 * x) / b0**1.5 - -def func2(x, a, b): - """Fit energy distribution for amplitude and temperature""" - if a<=0.0: return 1e30 - if b<=0.0: return 1e30 - return a * np.sqrt(x) * np.exp(-b * x) / b**1.5 - -def plot_data(runtag, field, defaultT, start, stop, ebeg, efin, dE, fit, pTyp, plim=15.0): - """Parse and plot the OUT psp output files - - Parameters: - - runtag (string): is the input datafile name - - field (string): is either "electron", "ion" or "interact" - - defaultT (float): is the default temperature (probably not needed - in most cases) - - start (int): initial psp index - - stop(int): final pspindex - - ebeg(float): initial energy - - efin(float): final energy - - delta(float): energy interval - - fit(FitType): how to chose comparison curve - - pTyp(PlotType): type of plots - - """ - global b0, ndatr - - slopeFac = 11604.50560112828 - slope = slopeFac/defaultT - - # - # Set up bins - # - nbin = int( (efin - ebeg)/dE ) - efin = ebeg + dE*nbin - yy = np.zeros(nbin) - oab = 0 - - # - # Loop through phase space - # - - efac = 0.5*Vunit*Vunit*amu/eV - if field=='ion': - efac *= Mu_i - elif field=='interact': - efac *= Mu_i*Mu_e/(Mu_i + Mu_e) - else: - efac *= Mu_e - - for n in range(start, stop+1): - filename = 'OUT.{}.{:05d}'.format(runtag,n) - if not os.path.isfile(filename): continue - - O = psp_io.Input(filename, comp='gas') - - exec('global xve; xve = O.d{}'.format(ndatr+6)) - exec('global yve; yve = O.d{}'.format(ndatr+7)) - exec('global zve; zve = O.d{}'.format(ndatr+8)) - - dv = np.zeros(3) - for i in range(O.mass.size): - if field=='ion': - dv[0] = O.xvel[i] - dv[1] = O.yvel[i] - dv[2] = O.zvel[i] - elif field=='interact': - dv[0] = O.xvel[i] - xve[i] - dv[1] = O.yvel[i] - yve[i] - dv[2] = O.zvel[i] - zve[i] - else: - dv[0] = xve[i] - dv[1] = yve[i] - dv[2] = zve[i] - - EE = efac * np.dot(dv, dv) - if EE=efin: - oab += 1 - else: - indx = int( (EE - ebeg)/dE) - yy[indx] += 1 - - # Compute bin centers - xx = np.zeros(nbin) - for i in range(nbin): xx[i] = ebeg + dE*(0.5+i) - - # Normalize - norm = (sum(yy) + oab)*dE - nn = copy.deepcopy(yy) - for i in range(nbin): norm += yy[i] - for i in range(nbin): yy[i] /= norm - - # Poisson error - eb = yy/np.sqrt(nn+1) - - # Fit for temperature - fc = 11604.5/defaultT # Initial guess for exponent - tt = [] - if fit == FitType.Fixed: - # Temp - bT = defaultT - # Inverse temp - b0 = fc - # - tt = np.zeros(nbin) - for i in range(nbin): tt[i] = func1(xx[i], a0) - elif fit == FitType.Analytic: - # Temp - bT = defaultT - # Inverse temp - b0 = fc - # - tt = np.zeros(nbin) - nrm = 2.0*b0**3/np.sqrt(np.pi) - for i in range(nbin): tt[i] = func1(xx[i], nrm) - elif fit == FitType.TempAmp: - p0 = [sum(yy)/len(yy)*fc**1.5,fc] # Amplitude - popt, pcov = curve_fit(func2, xx, yy, p0, sigma=eb) - # Temp - bT = slopeFac / popt[1] - for v in xx: tt.append(func2(v, popt[0], popt[1])) - print('Amplitude={} Temperature={}'.format(popt[0], bT)) - elif fit == FitType.AmpOnly: - b0 = fc - p0 = [sum(yy)/len(yy)*fc**1.5] # Amplitude - popt, pcov = curve_fit(func1, xx, yy, p0, sigma=np.sqrt(yy)+1) - # Temp - bT = defaultT - for v in xx: tt.append(func1(v, popt[0])) - print('Amplitude={} Temperature={}'.format(popt[0], bT)) - else: - print("This is impossible") - sys.exit() - - # Make curves - lt = [] - for v in tt: lt.append(np.log(v+1)) - # - ly = [] - for v in yy: ly.append(np.log(v+1)) - - if pTyp == PlotType.Both: - fig, axes = plt.subplots(nrows=2, ncols=1) - elif pTyp in [PlotType.Linear, PlotType.Log]: - fig, axes = plt.subplots(nrows=1, ncols=1) - - if pTyp.value & PlotType.Log.value: - - if pTyp == PlotType.Both: ax = axes[0] - else: ax = axes - - ax.semilogy(xx, ly, '-o') - ax.semilogy(xx, lt, '-') - - if type==FitType.TempAmp: - ax.set_title("{}: T(fit)={}".format(field,bT)) - else: - ax.set_title("{}: T={}".format(field,bT)) - - if pTyp == PlotType.Log: - ax.set_xlabel("Energy (eV)") - else: - ax.tick_params(axis='x', labelbottom='off') - ax.set_ylabel("Log(counts)") - - if pTyp.value & PlotType.Linear.value: - - if pTyp == PlotType.Both: ay = axes[1] - else: ay = axes - - ay.plot(xx, yy, '-o', label='DSMC') - # ay.plot(xx, tt, '-', label='T={}K'.format(int(defaultT))) - ay.plot(xx, tt, '-', label='T={}K'.format(int(bT))) - - ay.set_xlabel("Energy (eV)") - ay.set_ylabel("Counts") - ay.legend() - - if pTyp != PlotType.NoPlot: - fig.tight_layout() - plt.show() - - out = open(runtag + '_bins.dat', 'w') - out.write('# Temp={:16.4e}\n'.format(bT)) - for i in range(len(xx)): - out.write('{:16.4e} {:16.4e} {:16.4e} {:16.4e}\n'.format(xx[i], yy[i], tt[i], nn[i])) - dd = 100.0*(yy - tt)/tt - rr = yy/tt - - if pTyp != PlotType.NoPlot: - itp = interp1d(xx, dd, kind='linear') - wsize = 5 - dsize = int(np.sqrt(dd.shape[0])) - if dsize > wsize: - wsize = dsize - if 2*int(wsize/2) == wsize: wsize += 1 - porder = 3 - # print("wsize=", wsize) - zz = savgol_filter(itp(xx), wsize, porder) - plt.plot(xx, dd, '-*') - plt.plot(xx, zz, '-', linewidth=2) - plt.xlabel('Energy (eV)') - plt.ylabel('Relative difference (%)') - plt.ylim((-plim, plim)) - plt.grid() - plt.show() - - itr = interp1d(xx, rr, kind='linear') - wsize = 5 - dsize = int(np.sqrt(rr.shape[0])) - if dsize > wsize: - wsize = dsize - if 2*int(wsize/2) == wsize: wsize += 1 - porder = 3 - # print("wsize=", wsize) - zz = savgol_filter(itr(xx), wsize, porder) - plt.plot(xx, rr, '-*') - plt.plot(xx, zz, '-', linewidth=2) - plt.xlabel('Energy (eV)') - plt.ylabel('Ratio of electron to ion distribution') - # plt.ylim((-plim, plim)) - plt.grid() - plt.show() - - # - # List data - # - header = '{:<6s} {:<10s} {:<10s} {:<10s} {:<8s}' - datfmt = '{:<6d} {:<10.3e} {:<10.3e} {:<10.3e} {:<8.0f}' - print(header.format('bin', 'energy', 'rel dif', 'ratio', 'count')) - print(header.format('----', '------', '------', '------', '------')) - for i in range(len(dd)): - print(datfmt.format(i, xx[i], dd[i], rr[i], nn[i])) - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - global a0, ndatr - - field = "electron" - start = 0 - stop = 99999 - ebeg = 0.05 - efin = 60.0 - delta = 0.05 - defT = 100000.0 - fit = FitType.Analytic - plt = PlotType.Both - plim = 15.0 - - options = '[-f | --field= | -n | --start= | -N | --stop= | -e | --low= | -E | --high= | -d | --delta= | -T | --temp= | -t | --type | -F | --fixed | -p | --plot= | -D | --ndatr= | --plim=] ' - - try: - opts, args = getopt.getopt(argv,"hf:n:N:e:E:d:T:t:Fp:D:", ["help","field=","start=","stop=","low=", "high=", "delta=", "temp=", "type=", "fixed=", "plot=", "ndatr=", "plim="]) - except getopt.GetoptError: - print(sys.argv[0]) - sys.exit(2) - for opt, arg in opts: - if opt in ("-h", "--help"): - print(sys.argv[0], options) - sys.exit() - elif opt in ("-f", "--field"): - field = arg - elif opt in ("-n", "--start"): - start = int(arg) - elif opt in ("-N", "--stop"): - stop = int(arg) - elif opt in ("-e", "--low"): - ebeg = float(arg) - elif opt in ("-E", "--high"): - efin = float(arg) - elif opt in ("-d", "--delta"): - delta = float(arg) - elif opt in ("-T", "--temp"): - defT = float(arg) - elif opt in ("-t", "--type"): - if FitType.Analytic.name == arg: fit = FitType.Analytic - elif FitType.AmpOnly.name == arg: fit = FitType.AmpOnly - elif FitType.TempAmp.name == arg: fit = FitType.TempAmp - elif FitType.Fixed.name == arg: fit = FitType.Fixed - else: - print("No such fit type: ", arg) - print("Valid types are:") - for v in FitType: print(v.name) - sys.exit() - elif opt in ("-F", "--fixed"): - a0 = float(arg) - fit = FitType.Fixed - elif opt in ("-p", "--plot"): - if PlotType.Linear.name == arg: plt = PlotType.Linear - elif PlotType.Log.name == arg: plt = PlotType.Log - elif PlotType.Both.name == arg: plt = PlotType.Both - elif PlotType.NoPlot.name == arg: plt = PlotType.NoPlot - else: - print("No such plot type: ", arg) - print("Valid types are:") - for v in PlotType: print(v.name) - sys.exit() - elif opt in ("-D", "--ndatr"): - ndatr = int(arg) - elif opt in ("--plim"): - plim = float(arg) - - if len(args)>0: - runtag = args[0] - else: - runtag = "run" - - plot_data(runtag, field, defT, start, stop, ebeg, efin, delta, fit, plt, plim=plim) - -if __name__ == "__main__": - main(sys.argv[1:]) - diff --git a/utils/Analysis/psp_distL.py b/utils/Analysis/psp_distL.py deleted file mode 100755 index 87b940c08..000000000 --- a/utils/Analysis/psp_distL.py +++ /dev/null @@ -1,387 +0,0 @@ -#!/usr/bin/python3 - -# -*- coding: utf-8 -*- - -"""Program to compute the energy distributions based on the DSMC_log -file data - -Examples: - - $ python psp_dist.py -f electron run2 - -Plots the energy distributions for the named field (in the case above, -electrons) for the run with tag "run2". Field value is "electron" by -default. Other fields are "ion" and "interact" for the electron-ion -interaction kinetic energy. - -Only for Trace method, so far. -""" - -import os, re, sys, copy, getopt, enum -import numpy as np -import matplotlib.pyplot as plt -from scipy.interpolate import interp1d -from scipy.signal import savgol_filter -from scipy.optimize import curve_fit -import psp_io - -class FitType(enum.Enum): - Analytic = 1 - AmpOnly = 2 - TempAmp = 3 - Fixed = 4 - -class PlotType(enum.Enum): - Linear = 1 - Log = 2 - Both = 3 - -Munit = 1.9891e33 -Lunit = 3.08568e18 -Tunit = 3.15569e10 -Vunit = Lunit/Tunit - -m_H = 1.008 -m_He = 4.002602 -X_H = 0.76 -X_He = 0.24 -Mu_i = 1.0/(X_H/m_H + X_He/m_He) -Mu_e = 0.000548579909 - -amu = 1.660539e-24 -eV = 1.60217653e-12 - -b0 = 0.0 -a0 = 1.0 - -ndatr = 6 - -def func1(lx, a): - """Fit energy distribution for amplitude only""" - global b0 - if a<=0.0: return 1e30 - x = np.exp(lx) - return a * (b0 * x)**1.5 * np.exp(-b0 * x) - -def func2(lx, a, b): - """Fit energy distribution for amplitude and temperature""" - if a<=0.0: return 1e30 - if b<=0.0: return 1e30 - x = np.exp(lx) - return a * (b * x)**1.5 * np.exp(-b * x) - -def plot_data(runtag, field, defaultT, start, stop, ebeg, efin, dE, fit, pTyp, plim=15.0): - """Parse and plot the OUT psp output files - - Parameters: - - runtag (string): is the input datafile name - - field (string): is either "electron", "ion" or "interact" - - defaultT (float): is the default temperature (probably not needed - in most cases) - - start (int): initial psp index - - stop(int): final pspindex - - ebeg(float): initial energy - - efin(float): final energy - - delta(float): energy interval - - fit(FitType): how to chose comparison curve - - pTyp(PlotType): type of plots - - """ - global b0, ndatr - - slopeFac = 11604.50560112828 - slope = slopeFac/defaultT - - # - # Set up bins - # - lebeg = np.log(ebeg) - lefin = np.log(efin) - nbin = int( (lefin - lebeg)/dE ) - lefin = lebeg + dE*nbin - yy = np.zeros(nbin) - oab = 0 - - # - # Loop through phase space - # - - efac = 0.5*Vunit*Vunit*amu/eV - if field=='ion': - efac *= Mu_i - elif field=='interact': - efac *= Mu_i*Mu_e/(Mu_i + Mu_e) - else: - efac *= Mu_e - - for n in range(start, stop+1): - filename = 'OUT.{}.{:05d}'.format(runtag,n) - if not os.path.isfile(filename): continue - - O = psp_io.Input(filename, comp='gas') - - exec('global xve; xve = O.d{}'.format(ndatr+6)) - exec('global yve; yve = O.d{}'.format(ndatr+7)) - exec('global zve; zve = O.d{}'.format(ndatr+8)) - - dv = np.zeros(3) - for i in range(O.mass.size): - if field=='ion': - dv[0] = O.xvel[i] - dv[1] = O.yvel[i] - dv[2] = O.zvel[i] - elif field=='interact': - dv[0] = O.xvel[i] - xve[i] - dv[1] = O.yvel[i] - yve[i] - dv[2] = O.zvel[i] - zve[i] - else: - dv[0] = xve[i] - dv[1] = yve[i] - dv[2] = zve[i] - - EE = efac * np.dot(dv, dv) - lEE = np.log(EE) - - # Sanity check - if np.isnan(lEE): - oab += 1 - # Enforce bounds - elif lEE=lefin: - oab += 1 - else: - # Okay . . . - indx = int( (lEE - lebeg)/dE) - yy[indx] += 1 - - # Compute bin centers - xx = np.zeros(nbin) - eb = np.zeros(nbin) - lx = np.zeros(nbin) - for i in range(nbin): - lx[i] = lebeg + dE*(0.5+i) - xx[i] = np.exp(lx[i]) - - # Normalize - delE = np.exp(0.5*dE) - np.exp(-0.5*dE) - norm = 0.0 - nn = copy.deepcopy(yy) - for i in range(nbin): norm += yy[i] - for i in range(nbin): yy[i] /= norm - - # Poisson error - eb = yy/np.sqrt(nn+1) - - # Fit for temperature - fc = 11604.5/defaultT # Initial guess for exponent - nrm = 2.0/np.sqrt(np.pi) * delE - tt = [] - if fit == FitType.Fixed: - # Temp - bT = defaultT - # Inverse temp - b0 = fc - # - tt = np.zeros(nbin) - for i in range(nbin): tt[i] = func1(lx[i], a0) - elif fit == FitType.Analytic: - # Temp - bT = defaultT - # Inverse temp - b0 = fc - # - tt = np.zeros(nbin) - for i in range(nbin): tt[i] = func1(lx[i], nrm) - elif fit == FitType.TempAmp: - p0 = [nrm, fc] # Amplitude - popt, pcov = curve_fit(func2, lx, yy, p0, sigma=eb) - # popt, pcov = curve_fit(func2, lx, yy, p0, sigma=np.sqrt(yy)+1) - # Temp - bT = slopeFac / popt[1] - for v in lx: tt.append(func2(v, popt[0], popt[1])) - print('Amplitude={} Temperature={}'.format(popt[0], bT)) - print('Guess amp={}'.format(nrm)) - elif fit == FitType.AmpOnly: - b0 = fc - p0 = [nrm] # Amplitude - popt, pcov = curve_fit(func1, lx, yy, p0, sigma=eb) - # Temp - bT = defaultT - for v in lx: tt.append(func1(v, popt[0])) - print('Amplitude={} Temperature={}'.format(popt[0], bT)) - print('Guess amp={}'.format(nrm)) - else: - print("This is impossible") - sys.exit() - - # Make curves - lt = [] - for v in tt: lt.append(np.log(v+1)) - # - ly = [] - for v in yy: ly.append(np.log(v+1)) - - if pTyp == PlotType.Both: - fig, axes = plt.subplots(nrows=2, ncols=1) - else: - fig, axes = plt.subplots(nrows=1, ncols=1) - - if pTyp.value & PlotType.Log.value: - - if pTyp == PlotType.Both: ax = axes[0] - else: ax = axes - - ax.loglog(xx, ly, '-o') - ax.loglog(xx, lt, '-') - - if type==FitType.TempAmp: - ax.set_title("{}: T(fit)={}".format(field,bT)) - else: - ax.set_title("{}: T={}".format(field,bT)) - - if pTyp == PlotType.Log: - ax.set_xlabel("Energy (eV)") - else: - ax.tick_params(axis='x', labelbottom='off') - ax.set_ylabel("Log(counts)") - - if pTyp.value & PlotType.Linear.value: - - if pTyp == PlotType.Both: ay = axes[1] - else: ay = axes - - ay.semilogx(xx, yy, '-o', label='DSMC') - ay.semilogx(xx, tt, '-', label='T={}K'.format(int(bT))) - - ay.set_xlabel("Energy (eV)") - ay.set_ylabel("Counts") - ay.legend() - - fig.tight_layout() - plt.title('Run: {}'.format(runtag)) - plt.show() - - out = open(runtag + '_bins.dat', 'w') - out.write('# Temp={:16.4e}\n'.format(bT)) - for i in range(len(xx)): - out.write('{:16.4e} {:16.4e} {:16.4e}\n'.format(xx[i], yy[i], tt[i])) - dd = 100.0*(yy - tt)/tt - itp = interp1d(lx, dd, kind='linear') - wsize = 5 - dsize = int(np.sqrt(dd.shape[0])) - if dsize > wsize: - wsize = dsize - if 2*int(wsize/2) == wsize: wsize += 1 - porder = 3 - # print("wsize=", wsize) - zz = savgol_filter(itp(lx), wsize, porder) - - fig = plt.figure() - # ax = fig.add_subplot(111, xlim=(-2,2), ylim=(1,10E11)) - ax = fig.add_subplot(111, ylim=(-plim, plim)) - err = 100.0*eb/tt - ax.errorbar(xx, dd, yerr=err, fmt='-', capthick=2) - ax.plot(xx, zz, '-', linewidth=2) - ax.set_xlabel('Energy (eV)') - ax.set_ylabel('Relative difference (%)') - ax.set_xscale('log') - plt.grid() - plt.title('Run: {}'.format(runtag)) - plt.show() - # - # List rel dif plot data - # - header = '{:<6s} {:<16s} {:<16s} {:<16s} {:<8s}' - datfmt = '{:<6d} {:<16.6e} {:<16.6e} {:<16.6e} {:<8.0f}' - print(header.format('bin', 'energy', 'rel dif', 'error', 'count')) - print(header.format('----', '------', '------', '------', '------')) - for i in range(len(dd)): - print(datfmt.format(i, xx[i], dd[i], err[i], nn[i])) - -def main(argv): - """ Parse the command line and call the parsing and plotting routine """ - - global a0, ndatr - - field = "electron" - start = 0 - stop = 99999 - ebeg = 0.05 - efin = 60.0 - delta = 0.05 - defT = 100000.0 - fit = FitType.Analytic - plt = PlotType.Both - plim = 15.0 - - options = '[-f | --field= | -n | --start= | -N | --stop= | -e | --low= | -E | --high= | -d | --delta= | -T | --temp= | -t | --type | -F | --fixed | -p | --plot= | -D | --ndatr= | --plim=] ' - - try: - opts, args = getopt.getopt(argv,"hf:n:N:e:E:d:T:t:Fp:D:", ["help","field=","start=","stop=","low=", "high=", "delta=", "temp=", "type=", "fixed=", "plot=", "ndatr=", "plim="]) - except getopt.GetoptError: - print(sys.argv[0]) - sys.exit(2) - for opt, arg in opts: - if opt in ("-h", "--help"): - print(sys.argv[0], options) - sys.exit() - elif opt in ("-f", "--field"): - field = arg - elif opt in ("-n", "--start"): - start = int(arg) - elif opt in ("-N", "--stop"): - stop = int(arg) - elif opt in ("-e", "--low"): - ebeg = float(arg) - elif opt in ("-E", "--high"): - efin = float(arg) - elif opt in ("-d", "--delta"): - delta = float(arg) - elif opt in ("-T", "--temp"): - defT = float(arg) - elif opt in ("-t", "--type"): - if FitType.Analytic.name == arg: fit = FitType.Analytic - elif FitType.AmpOnly.name == arg: fit = FitType.AmpOnly - elif FitType.TempAmp.name == arg: fit = FitType.TempAmp - elif FitType.Fixed.name == arg: fit = FitType.Fixed - else: - print("No such fit type: ", arg) - print("Valid types are:") - for v in FitType: print(v.name) - sys.exit() - elif opt in ("-F", "--fixed"): - a0 = float(arg) - fit = FitType.Fixed - elif opt in ("-p", "--plot"): - if PlotType.Linear.name == arg: plt = PlotType.Linear - elif PlotType.Log.name == arg: plt = PlotType.Log - elif PlotType.Both.name == arg: plt = PlotType.Both - else: - print("No such plot type: ", arg) - print("Valid types are:") - for v in PlotType: print(v.name) - sys.exit() - elif opt in ("-D", "--ndatr"): - ndatr = int(arg) - elif opt in ("--plim"): - plim = float(arg) - - if len(args)>0: - runtag = args[0] - else: - runtag = "run" - - plot_data(runtag, field, defT, start, stop, ebeg, efin, delta, fit, plt, plim=plim) - -if __name__ == "__main__": - main(sys.argv[1:]) - diff --git a/utils/Analysis/readem.py b/utils/Analysis/readem.py deleted file mode 100755 index 2899f194c..000000000 --- a/utils/Analysis/readem.py +++ /dev/null @@ -1,61 +0,0 @@ -#! /usr/bin/python -i - -""" -This is a test -""" - -import os, sys -import numpy as np -import matplotlib.pyplot as plt - -data = {} -fields = [] -fname = "" - -def readem(*args): - global fname, data, fields - if len(args)==1: - fname = args[0] + '.ION_coll' - - for line in open(fname, 'r'): - if line.find('Time') >= 0: - line = line[1:].replace('|', '') - fields = line.split() - for v in fields: - data[v] = [] - elif line[0] != '#': - line = line[1:].replace('|', '') - vals = [float(v) for v in line.split()] - for i in range(len(fields)): - data[fields[i]].append(vals[i]) - - for f in data: - data[f] = np.array(data[f]) - -def slab(): - global fields - for i in range(len(fields)): - print '{:5d} {}'.format(i+1, fields[i]) - -def plotem(*fields, **kwargs): - global data - for f in fields: - if f in data: - plt.plot(data['Time'], data[f], '-', label=f) - else: - if f not in ['deltaKE', 'KE']: - print "No field <{}> in data".format(f) - - if 'deltaKE' in kwargs: - y = data['EkeI'] + data['EkeE'] - plt.plot(data['Time'], y-y[0], '-', label='Delta KE') - - if 'KE' in kwargs: - y = data['EkeI'] + data['EkeE'] - plt.plot(data['Time'], y, '-', label='KE') - - plt.xlabel('Time') - plt.legend().draggable() - plt.show() - -readem(sys.argv[1]) diff --git a/utils/Analysis/readsp.py b/utils/Analysis/readsp.py deleted file mode 100755 index d58c76033..000000000 --- a/utils/Analysis/readsp.py +++ /dev/null @@ -1,50 +0,0 @@ -#! /usr/bin/python -i - -""" -This is a test -""" - -import os, sys -import numpy as np -import matplotlib.pyplot as plt - -data = {} -fields = [] -fname = "" - -def readem(*args): - global fname, data, fields - if len(args)==1: - fname = args[0] + '.species' - - for line in open(fname, 'r'): - if line.find('Time') >= 0: - line = line[1:].replace('|', '') - fields = line.split() - for v in fields: - data[v] = [] - elif line[0] != '#': - line = line[1:].replace('|', '') - vals = [float(v) for v in line.split()] - for i in range(len(fields)): - data[fields[i]].append(vals[i]) - - for f in data: - data[f] = np.array(data[f]) - -def slab(): - global fields - for i in range(len(fields)): - print '{:5d} {}'.format(i+1, fields[i]) - -def plotem(*fields, **kwargs): - global data - for f in fields: - if f in data: - plt.plot(data['Time'], data[f], '-', label=f) - - plt.xlabel('Time') - plt.legend().draggable() - plt.show() - -readem(sys.argv[1]) diff --git a/utils/Analysis/recompute_temps.py b/utils/Analysis/recompute_temps.py deleted file mode 100755 index 6bc704b39..000000000 --- a/utils/Analysis/recompute_temps.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/python - -import numpy as np -import matplotlib.pyplot as plt -import os.path -import psp_io -import sys -from scipy.optimize import curve_fit - -def func(x, a, b): - """Fit energy distribution for amplitude and temperature""" - if a<=0.0: return 1e30 - if b<=0.0: return 1e30 - return a * np.sqrt(x) * np.exp(-b * x) / b**1.5 - -# -# Last argument should be filename and must exist -# - -help_string = "Usage: {} [-k n | --key n] [--beg n] [--end n] [--stride n] [-e n | --elec n] [-p | --plot] [-l | --log] [-h | --help] runtag".format(sys.argv[0]) - -argc = len(sys.argv) - -if argc<=1: - print help_string - exit(1) - -# -# Check for point type and log time -# -key = 0 -epos = 10 -nbeg = 0 -nend = -1 -strd = 1 -tstP = False -logP = False - -for i in range(1,argc): - if sys.argv[i] == '-k' or sys.argv[i] == '--key': - key = int(sys.argv[i+1]) - if sys.argv[i] == '--beg': - nbeg = int(sys.argv[i+1]) - if sys.argv[i] == '--end': - nend = int(sys.argv[i+1]) - if sys.argv[i] == '--stride': - strd = int(sys.argv[i+1]) - if sys.argv[i] == '-e' or sys.argv[i] == '--elec': - epos = int(sys.argv[i+1]) - if sys.argv[i] == '-p' or sys.argv[i] == '--plot': - tstP = True - if sys.argv[i] == '-l' or sys.argv[i] == '--log': - logP = True - if sys.argv[i] == '-h' or sys.argv[i] == '--help': - print help_string - exit(1) -# -# Parse data files -# - -filep = 'OUT.{}.{:05d}' -count = nbeg - -atomic_masses = [0.000548579909, 1.00794, 4.002602]; - -# atomic mass unit -amu = 1.660011e-24 - -# Parsec (in cm) -pc = 3.08567758e18 - -# Solar mass (in g) -msun = 1.9891e33 - -# Seconds per year -year = 365.242*24.0*3600.0 - -# electron volt in (cgs) -eV = 1.60217653e-12 - -Munit = msun*0.1 -Lunit = pc -Tunit = year*1e5 -Vunit = Lunit/Tunit - -e2ev = 0.5*amu*Vunit*Vunit/eV - -slopeFac = 11604.50560112828 - -ionsT = {} -elecT = {} - -while os.path.isfile(filep.format(sys.argv[-1], count)): - - psp = psp_io.Input(filep.format(sys.argv[-1], count), comp='gas') - s = getattr(psp, 'i{}'.format(key)) - ex = [getattr(psp, 'd{}'.format(epos+0)), getattr(psp, 'd{}'.format(epos+1)), getattr(psp, 'd{}'.format(epos+2))] - Ei = [] - Ee = [] - Nf = [] - Ms = {} - Mm = {} - for i in range(len(s)): - Ei.append(e2ev*atomic_masses[s[i]]*(psp.xvel[i]**2 + psp.yvel[i]**2 + psp.zvel[i]**2)) - Ee.append(e2ev*atomic_masses[0]*(ex[0][i]**2 + ex[1][i]**2 + ex[2][i]**2)) - Nf.append(psp.mass[i]/atomic_masses[s[i]]) - if s[i] not in Ms: - Ms[s[i]] = 0.0 - Mm[s[i]] = Nf[-1] - Ms[s[i]] += Nf[-1] - - - time = psp.ctime - for ss in Ms: - if ss not in ionsT: ionsT[ss] = [[],[]] - if ss not in elecT: elecT[ss] = [[],[]] - - # Bin distribution - for ss in Ms: - spE = [] - spI = [] - for i in range(len(s)): - if ss==s[i]: - spI.append(Ei[i]) - spE.append(Ee[i]) - - minE = min(spI) - maxE = max(spI) - delE = (maxE - minE)/100.0 - xx = np.arange(minE, maxE, delE) - yy = np.zeros(len(xx)) - for v in spI: - indx = int( (v - minE)/delE ) - if indx >= 0 and indx < len(yy): yy[indx] += 1 - - popt, pcov = curve_fit(func, xx, yy) - - ionsT[ss][0].append(time) - ionsT[ss][1].append(slopeFac/popt[1]) - - if tstP: - tt = [] - for j in range(len(xx)): tt.append(func(xx[j], popt[0], popt[1])) - if logP: - plt.semilogy(xx, yy, '-', label="data") - plt.semilogy(xx, tt, '-', label="fit") - else: - plt.plot(xx, yy, '-', label="data") - plt.plot(xx, tt, '-', label="fit") - plt.xlabel("Energy (eV)") - plt.ylabel("Counts") - plt.title("Ion (Z={}): t={} T={}".format(ss, time, slopeFac/popt[1])) - plt.legend() - plt.show() - - minE = min(spE) - maxE = max(spE) - delE = (maxE - minE)/100.0 - xx = np.arange(minE, maxE, delE) - yy = np.zeros(len(xx)) - for v in spE: - indx = int( (v - minE)/delE ) - if indx >= 0 and indx < len(yy): yy[indx] += 1 - - popt, pcov = curve_fit(func, xx, yy) - - elecT[ss][0].append(time) - elecT[ss][1].append(slopeFac/popt[1]) - - if tstP: - tt = [] - for j in range(len(xx)): tt.append(func(xx[j], popt[0], popt[1])) - if logP: - plt.semilogy(xx, yy, '-', label="data") - plt.semilogy(xx, tt, '-', label="fit") - else: - plt.plot(xx, yy, '-', label="data") - plt.plot(xx, tt, '-', label="fit") - plt.xlabel("Energy (eV)") - plt.ylabel("Counts") - plt.title("Electron (Z={}): t={} T={}".format(ss, time, slopeFac/popt[1])) - plt.legend() - plt.show() - - - count += strd - if nend>=0 and count>=nend: break - -for s in ionsT: - plt.plot(ionsT[s][0], ionsT[s][1], '-', label="Ion (Z={})".format(s)) - plt.plot(elecT[s][0], elecT[s][1], '-', label="Electron (Z={})".format(s)) - -plt.xlabel("Time") -plt.ylabel("Temperature") -plt.legend().draggable() -plt.show() diff --git a/utils/Analysis/speciesT.py b/utils/Analysis/speciesT.py deleted file mode 100755 index 816226a60..000000000 --- a/utils/Analysis/speciesT.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, os, argparse -import numpy as np -import matplotlib.pyplot as plt - -parser = argparse.ArgumentParser(description='Read DSMC species file and plot temperatures for each species') -parser.add_argument('-t', '--tscale', default=1000.0, type=float, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, type=float, help='Maximum time in years') -parser.add_argument('-w', '--lwidth', default=2.0, type=float, help='linewidth for curves') -parser.add_argument('-l', '--log', action="store_true", help='log vertical scale') -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -lw = args.lwidth - -def readDB(tag): - data = {} - if not os.path.exists(tag+'.species'): - print "Path <{}> does not exist".format(tag+'.species') - exit(-1) - file = open(tag+'.species', 'r') - labs = file.readline()[1:].split() # Labels - line = file.readline() # Indices - line = file.readline() # Separator - for v in labs: data[v] = [] - for line in file: # Parse the remaining data - for t in list(enumerate([float(v) for v in line.split()])): - data[labs[t[0]]].append(t[1]) - return data - -fields = [ ['Temp_i', 'Temp_e'], - ['(1,1)', '(1,2)', '(2,1)', '(2,2)', '(2,3)'] ] - -d = {} -for v in labs: - d[v] = readDB(v) - -f, ax = plt.subplots(2, 1, sharex='col') -plots = [0, 1] - -for x in ax: - box = x.get_position() - newBox = [box.x0, box.y0, 0.9*box.width, box.height] - x.set_position(newBox) - -for v in labs: - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - for n in plots: - for F in fields[n]: - if F in d[v]: - x = np.array(d[v]['Time'][0:indx])*args.tscale - y = np.array(d[v][F][0:indx]) - if args.log: - ax[n].semilogy(x, y, '-', linewidth=lw, label=v+':'+F) - else: - ax[n].plot(x, y, '-', linewidth=lw, label=v+':'+F) - -for n in plots: - ax[n].legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) - -ax[0].set_ylabel('Temperature') -ax[1].set_xlabel('Time') -ax[1].set_ylabel('Species density') - -plt.show() diff --git a/utils/Analysis/splitEnergy.py b/utils/Analysis/splitEnergy.py deleted file mode 100755 index 6edb8854b..000000000 --- a/utils/Analysis/splitEnergy.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, os, argparse -import numpy as np -import matplotlib.pyplot as plt -import getSpecies as gs - -parser = argparse.ArgumentParser(description='Read DSMC species file and plot energies for each species') -parser.add_argument('-t', '--tscale', default=1000.0, type=float, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, type=float, help='Maximum time in years') -parser.add_argument('-l', '--log', default=False, action="store_true", help='Logarithmic energy scale') -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -fields = [ ['Eelc(1)', 'Eion(1)', 'Selc(1)', 'Sion(1)'], \ - ['Eelc(2)', 'Eion(2)', 'Selc(2)', 'Sion(2)'] ] -labels = [ 'Hydrogen', 'Helium' ] - -d = {} -for v in labs: - d[v] = gs.readDB(v) - -f, ax = plt.subplots(2, 1, sharex='col') -for x in ax: - box = x.get_position() - newBox = [box.x0+0.05*box.width, box.y0, 0.85*box.width, box.height] - x.set_position(newBox) - -for n in range(2): - for v in labs: - for f in fields[n]: - if f in d[v]: - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - if args.log: - ax[n].semilogy(d[v]['Time'][0:indx]*args.tscale, d[v][f][0:indx], '-', label=v+':'+f) - else: - ax[n].plot(d[v]['Time'][0:indx]*args.tscale, d[v][f][0:indx], '-', label=v+':'+f) - - ax[n].legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) - if n==len(labels)-1: ax[n].set_xlabel('Time') - ax[n].set_ylabel('Energy') - ax[n].set_title(labels[n]) -plt.show() diff --git a/utils/Analysis/splitEnergy2.py b/utils/Analysis/splitEnergy2.py deleted file mode 100755 index 53c08410f..000000000 --- a/utils/Analysis/splitEnergy2.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, os, argparse -import numpy as np -import matplotlib.pyplot as plt -import getSpecies as gs - -parser = argparse.ArgumentParser(description='Read DSMC species file and plot energies for each species') -parser.add_argument('-t', '--tscale', default=1000.0, type=float, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, type=float, help='Maximum time in years') -parser.add_argument('-l', '--log', default=False, action="store_true", help='Logarithmic energy scale') -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -fields = [ [ ['Eion(1)', 'Sion(1)'], ['Eelc(1)', 'Selc(1)'] ], - [ ['Eion(2)', 'Sion(2)'], ['Eelc(2)', 'Selc(2)'] ] ] -labels = [ 'Hydrogen', 'Helium' ] - -d = {} -for v in labs: - d[v] = gs.readDB(v) - -f, ax = plt.subplots(2, 2, sharex='col') -for x in [item for sublist in ax for item in sublist]: - box = x.get_position() - newBox = [box.x0+0.05*box.width, box.y0, 0.85*box.width, box.height] - x.set_position(newBox) - -for n in range(2): - for m in range(2): - for v in labs: - for f in fields[n][m]: - if f in d[v]: - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - if args.log: - ax[n][m].semilogy(d[v]['Time'][0:indx]*args.tscale, d[v][f][0:indx], '-', label=v+':'+f) - else: - ax[n][m].plot(d[v]['Time'][0:indx]*args.tscale, d[v][f][0:indx], '-', label=v+':'+f) - - ax[n][m].legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) - if n==len(labels)-1: ax[n][m].set_xlabel('Time') - if m==0: ax[n][m].set_ylabel('Energy') - ax[n][m].set_title(labels[n]) -plt.show() diff --git a/utils/Analysis/splitEnergyN.py b/utils/Analysis/splitEnergyN.py deleted file mode 100755 index 29ba31346..000000000 --- a/utils/Analysis/splitEnergyN.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, os, argparse -import numpy as np -import matplotlib.pyplot as plt -import getSpecies as gs - -parser = argparse.ArgumentParser(description='Read DSMC species file and plot energies per particle for each species') -parser.add_argument('-t', '--tscale', default=1000.0, type=float, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, type=float, help='Maximum time in years') -parser.add_argument('-l', '--log', default=False, action="store_true", help='Logarithmic energy scale') -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -fields = [ ['Eelc(1)', 'Eion(1)'], \ - ['Eelc(2)', 'Eion(2)'] ] -nfield = [ ['Nelc(1)', 'Nion(1)'], \ - ['Nelc(2)', 'Nion(2)'] ] -labels = [ 'Hydrogen', 'Helium' ] - -atomic_masses = [0.000548579909, 1.00794, 4.002602] - -d = {} -for v in labs: - d[v] = gs.readDB(v) - -f, ax = plt.subplots(2, 1, sharex='col') -for x in ax: - box = x.get_position() - newBox = [box.x0+0.05*box.width, box.y0, 0.85*box.width, box.height] - x.set_position(newBox) - -for i in range(2): - for j in range(2): - for v in labs: - f = fields[i][j] - b = nfield[i][j] - if f in d[v]: - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - fv = d[v][f][0:indx] - bf = d[v][b][0:indx] - if f.find('elc')>=0: - bf /= atomic_masses[0] - else: - if f.find('1')>=0: - bf /= atomic_masses[1] - if f.find('2')>=0: - bf /= atomic_masses[2] - if args.log: - ax[i].semilogy(d[v]['Time'][0:indx]*args.tscale, fv/bf, '-', label=v+':'+f) - else: - ax[i].plot(d[v]['Time'][0:indx]*args.tscale, fv/bf, '-', label=v+':'+f) - if i>0: ax[i].set_xlabel('Time') - ax[i].set_ylabel('Energy') - ax[i].set_title(labels[i]) - ax[i].legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) -plt.show() diff --git a/utils/Analysis/splitTemps.py b/utils/Analysis/splitTemps.py deleted file mode 100755 index e3a7a4995..000000000 --- a/utils/Analysis/splitTemps.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, os, argparse -import numpy as np -import matplotlib.pyplot as plt -import getSpecies as gs - -parser = argparse.ArgumentParser(description='Read DSMC species file and plot temperatures for each species') -parser.add_argument('-t', '--tscale', default=1000.0, type=float, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, type=float, help='Maximum time in years') -parser.add_argument('-w', '--lwidth', default=2.0, type=float, help='linewidth for curves') -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -lw = args.lwidth - -fields = [ ['Telc(1)', 'Telc(2)'], ['Tion(1)', 'Tion(2)'] ] -titles = ['Electrons', 'Ions'] - -d = {} -for v in labs: - d[v] = gs.readDB(v) - -f, ax = plt.subplots(2, 1, sharex='col') -for x in ax: - box = x.get_position() - newBox = [box.x0, box.y0, 0.9*box.width, box.height] - x.set_position(newBox) - -cnt = 0 -for v in labs: - for n in range(2): - for F in fields[n]: - if F in d[v]: - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - ax[n].plot(d[v]['Time'][0:indx]*args.tscale, d[v][F][0:indx], '-', linewidth=lw, label=v+':'+F) - - ax[n].legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) - if n>0: ax[n].set_xlabel('Time') - ax[n].set_ylabel('Temperature') - ax[n].set_title(titles[n]) -plt.show() diff --git a/utils/Analysis/splitTemps2.py b/utils/Analysis/splitTemps2.py deleted file mode 100755 index 968a71ac2..000000000 --- a/utils/Analysis/splitTemps2.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/python - -# -*- Python -*- -# -*- coding: utf-8 -*- - -import sys, os, argparse -import numpy as np -import matplotlib.pyplot as plt -import getSpecies as gs - -parser = argparse.ArgumentParser(description='Read DSMC species file and plot temperatures for each species') -parser.add_argument('-t', '--tscale', default=1000.0, type=float, help='System time units in years') -parser.add_argument('-T', '--Tmax', default=1.0e32, type=float, help='Maximum time in years') -parser.add_argument('-w', '--lwidth', default=2.0, type=float, help='linewidth for curves') -parser.add_argument('-D', '--disp', default=False, action='store_true', help='Dispersion-based temperature only') - -parser.add_argument('tags', nargs='*', help='Files to process') - -args = parser.parse_args() - -labs = args.tags - -lw = args.lwidth - -fieldsAll = [ ['Telc(1)', 'Telc(2)', 'Relc(1)', 'Relc(2)'], - ['Tion(1)', 'Tion(2)', 'Rion(1)', 'Rion(2)'] ] -fieldsTonly = [ ['Telc(1)', 'Telc(2)'], - ['Tion(1)', 'Tion(2)'] ] -titles = ['Electrons', 'Ions'] - -fields = [] - -if args.disp: - fields = fieldsTonly -else: - fields = fieldsAll - -d = {} -for v in labs: - d[v] = gs.readDB(v) - -f, ax = plt.subplots(2, 1, sharex='col') -for x in ax: - box = x.get_position() - newBox = [box.x0, box.y0, 0.9*box.width, box.height] - x.set_position(newBox) - -cnt = 0 -for v in labs: - for n in range(2): - for F in fields[n]: - if F in d[v]: - indx = np.searchsorted(d[v]['Time'], args.Tmax/args.tscale) - ax[n].plot(d[v]['Time'][0:indx]*args.tscale, d[v][F][0:indx], '-', linewidth=lw, label=v+':'+F) - - ax[n].legend(prop={'size':8}, bbox_to_anchor=(1.02, 1), loc=2, borderaxespad=0.0) - if n>0: ax[n].set_xlabel('Time') - ax[n].set_ylabel('Temperature') - ax[n].set_title(titles[n]) -plt.show() diff --git a/utils/Analysis/uptime.py b/utils/Analysis/uptime.py deleted file mode 100755 index 59558a2c0..000000000 --- a/utils/Analysis/uptime.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python - -import os, sys, re -import subprocess - -pattern = re.compile("[0-9-]+") - -def parse(seq): - toks = seq.split(',') - n = [] - for t in toks: - if pattern.match(t): - be = t.split('-') - if len(be)==1: - n.append(int(be[0])) - if len(be)==2: - for i in range(int(be[0]), int(be[1])+1): - n.append(i) - return n - -sep = '---------+--------' - -print '{:7s} | {:5s}'.format('Host', '1 min') -print sep - -n = parse(sys.argv[1]) -for i in n: - p = subprocess.Popen(['ssh', 'node{:03d}'.format(i), 'uptime'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - start = out.find('load average:') - if start >=0: - want = out[start+len('load average:'):].split(',') - # print 'node{:03d}: {:5.2f} {:5.2f} {:5.2f}'.format(i, float(want[0]), float(want[1]), float(want[2])) - print 'node{:03d} | {:5.2f}'.format(i, float(want[0])) - -print sep diff --git a/utils/Cooling/CMakeLists.txt b/utils/Cooling/CMakeLists.txt deleted file mode 100644 index 14b834c4f..000000000 --- a/utils/Cooling/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ - -if(ENABLE_DSMC) - - set(bin_PROGRAMS hctest) - - add_executable(hctest hctest.cc) - - target_link_libraries(hctest OpenMP::OpenMP_CXX MPI::MPI_CXX - yaml-cpp exputil testdsmc ${VTK_LIBRARIES}) - - if(HAVE_CUDA) - list(APPEND common_LINKLIB CUDA::cudart CUDA::nvToolsExt) - endif() - - target_include_directories(hctest - PUBLIC $ - $ - $ - $ - ${CMAKE_BINARY_DIR} ${DEP_INC} - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/..) - -endif() - diff --git a/utils/Cooling/hctest.cc b/utils/Cooling/hctest.cc deleted file mode 100644 index df73b6c40..000000000 --- a/utils/Cooling/hctest.cc +++ /dev/null @@ -1,192 +0,0 @@ - -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -#include -#include -#include // Option parsing -#include // EXP global variables - -int main(int argc, char** argv) -{ - double Nmin=1.0e-8, Nmax=1.0e10, Tmin=1000, Tmax=1e7; - unsigned Nnum=100, Tnum=100, nnum=40, tnum=40; - string filename = "hctest.dat"; - string cachefile = ".HeatCool"; - - cxxopts::Options options("hctest", "Heating and cooling test"); - - options.add_options() - ("h,help", "print this help message") - ("1,Nmin", "minimum number density (H/cc)", - cxxopts::value(Nmin)->default_value("1.0e-08")) - ("2,Nmax", "maximum number density (H/cc)", - cxxopts::value(Nmax)->default_value("1.0e10")) - ("3,Tmin", "minimum temperature", - cxxopts::value(Tmin)->default_value("1.0e+02")) - ("4,Tmax", "maximum temperature", - cxxopts::value(Tmax)->default_value("1.0e+07")) - ("f,filename", "output filename", - cxxopts::value(filename)->default_value("hctest.dat")) - ("c,cachefile", "cache file", - cxxopts::value(cachefile)->default_value(".HeatCool")) - ("Nnum", "number of density points", - cxxopts::value(Nnum)->default_value("100")) - ("Tnum", "number of temperature points", - cxxopts::value(Tnum)->default_value("100")) - ("nnum", "number of density points for grid evaluation", - cxxopts::value(nnum)->default_value("40")) - ("Tnum", "number of temperature points grid evaluation", - cxxopts::value(tnum)->default_value("40")) - ; - - - - cxxopts::ParseResult vm; - - try { - vm = options.parse(argc, argv); - } catch (cxxopts::OptionException& e) { - std::cout << "Option error: " << e.what() << std::endl; - exit(-1); - } - - if (vm.count("help")) { - std::cout << options.help() << std::endl; - exit(-1); - } - - //===================== - // Setup grid - //===================== - - HeatCool hc(Nmin, Nmax, Tmin, Tmax, Nnum, Tnum, cachefile); - - //===================== - // Output in GP format - //===================== - - ofstream out(filename.c_str()); - if (!out) { - cerr << "Could not open <" << filename << "> for output" - << endl; - return -1; - } - - double dN = (log(Nmax) - log(Nmin))/(nnum-1); - double dT = (log(Tmax) - log(Tmin))/(tnum-1); - - double N, T; - - for (unsigned n=0; n -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "Particle.H" -#include "globalInit.H" -#include "Species.H" -#include "atomic_constants.H" -#include - -#include -#include - -// Random types -// -typedef std::shared_ptr gen_ptr; -typedef std::shared_ptr > uniform_ptr; -typedef std::shared_ptr > normal_ptr; - -// EXP library support -// -#include -#include - -// Reference to n-body globals -// -using namespace __EXP__; - -#ifdef DEBUG -#include -#include - -//=========================================== -// Handlers defined in exputil/stack.cc -//=========================================== - -extern void mpi_print_trace(const string& routine, const string& msg, - const char *file, int line); - -extern void mpi_gdb_print_trace(int sig); - -extern void mpi_gdb_wait_trace(int sig); - -//=========================================== -// A signal handler to trap invalid FP only -//=========================================== - -void set_fpu_invalid_handler(void) -{ - // Flag invalid FP results only, such as 0/0 or infinity - infinity - // or sqrt(-1). - // - feenableexcept(FE_INVALID); - // - // Print enabled flags to root node - // - if (myid==0) { - const std::list> flags = - { {FE_DIVBYZERO, "divide-by-zero"}, - {FE_INEXACT, "inexact"}, - {FE_INVALID, "invalid"}, - {FE_OVERFLOW, "overflow"}, - {FE_UNDERFLOW, "underflow"} }; - - int _flags = fegetexcept(); - std::cout << "Enabled FE flags: <"; - for (auto v : flags) { - if (v.first & _flags) std::cout << v.second << ' '; - } - std::cout << "\b>" << std::endl; - } - signal(SIGFPE, mpi_gdb_print_trace); -} - -//=========================================== -// A signal handler to produce a traceback -//=========================================== - -void set_fpu_trace_handler(void) -{ - // Flag all FP errors except inexact - // - // fedisableexcept(FE_ALL_EXCEPT & ~FE_INEXACT); - - // Flag invalid FP results only, such as 0/0 or infinity - infinity - // or sqrt(-1). - // - feenableexcept(FE_INVALID); - // - // Print enabled flags to root node - // - if (myid==0) { - const std::list> flags = - { {FE_DIVBYZERO, "divide-by-zero"}, - {FE_INEXACT, "inexact"}, - {FE_INVALID, "invalid"}, - {FE_OVERFLOW, "overflow"}, - {FE_UNDERFLOW, "underflow"} }; - - int _flags = fegetexcept(); - std::cout << "Enabled FE flags: <"; - for (auto v : flags) { - if (v.first & _flags) std::cout << v.second << ' '; - } - std::cout << "\b>" << std::endl; - } - signal(SIGFPE, mpi_gdb_print_trace); -} - -//=========================================== -// A signal handler to produce stop and wait -//=========================================== - -void set_fpu_gdb_handler(void) -{ - // Flag all FP errors except inexact - // - // fedisableexcept(FE_ALL_EXCEPT & ~FE_INEXACT); - - // Flag invalid FP results only, such as 0/0 or infinity - infinity - // or sqrt(-1). - // - feenableexcept(FE_INVALID); - // - // Print enabled flags to root node - // - if (myid==0) { - const std::list> flags = - { {FE_DIVBYZERO, "divide-by-zero"}, - {FE_INEXACT, "inexact"}, - {FE_INVALID, "invalid"}, - {FE_OVERFLOW, "overflow"}, - {FE_UNDERFLOW, "underflow"} }; - - int _flags = fegetexcept(); - std::cout << "Enabled FE flags: <"; - for (auto v : flags) { - if (v.first & _flags) std::cout << v.second << ' '; - } - std::cout << "\b>" << std::endl; - } - signal(SIGFPE, mpi_gdb_wait_trace); -} - -#endif - -//=========================================== -// Write ChiantiPy script -//=========================================== - -bool file_exists(const std::string& fileName) -{ - std::ifstream infile(fileName); - return infile.good(); -} - -void writeScript(void) -{ - const char *py = -#include "genIonization.h" - ; - - const std::string file("genIonization"); - - if (not file_exists(file)) { - std::ofstream out(file); - out << py; - if (chmod(file.c_str(), S_IWUSR | S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)) { - perror("Error in chmod:"); - } - } -} - -double Lunit; -double Tunit; -double Vunit; -double Munit; - -std::vector LL; - -PeriodicTable PT; - -// -// std RNGs -// -gen_ptr gen; -uniform_ptr uniform; -normal_ptr normal; - -// ION collide types -// -enum Itype { Hybrid, Trace, Weight, Direct }; -std::map Types -{ {"Hybrid", Hybrid}, {"Trace", Trace}, {"Weight", Weight}, {"Direct", Direct} }; - -// Use CHIANTI or ION for ionization-recombination equilibrium -// -bool use_chianti = true; -bool use_init_file = false; -bool use_yaml = true; - - -// Model types -// -enum Mtype { Uniform, Interface }; -std::map Models -{ {"Uniform", Uniform}, {"Interface", Interface} }; - - -/** - Make Uniform temperature box of gas -*/ -void InitializeUniform(std::vector& p, std::vector& mass, double molW, double Ecut, - std::vector< std::map >& T, std::vector &L, - Itype type, int sp, int ne, int ni, int nd) -{ - unsigned npart = p.size(); - double rho = mass[0]/(L[0]*L[1]*L[2]); - - std::cout << std::string(70, '-') << std::endl; - if (type == Trace) { - std::cout << "MolW: " << molW << std::endl; - std::cout << "T(ion): " << T[0][0] << " K " << std::endl; - std::cout << "T(elec): " << T[0][1] << " K " << std::endl; - } else if (T.size()>1) { - for (auto v : T[0]) { - std::ostringstream sout; - sout << "Temp " << PT[v.first]->name() << ":"; - std::cout << std::left << std::setw(13) << sout.str() << v.second - << " K " << std::endl; - } - } else { - std::cout << "Temp: " << T[0][0] << " K " << std::endl; - } - std::cout << "Number: " << npart << std::endl; - std::cout << "Length unit: " << Lunit << " cm" << std::endl; - std::cout << "Time unit: " << Tunit << " s" << std::endl; - std::cout << "Vel unit: " << Vunit << " cm/s" << std::endl; - std::cout << "Mass unit: " << Munit << " g" << std::endl; - std::cout << std::string(70, '-') << std::endl; - - /* - We want every d.o.f. to have 1/2 k_B T of energy. For classic - DSMC, every superparticle has m/mu(Z) real particles [in physical - units where mu(Z) = atomic_masses[Z]*amu]. So the velocity factor - is given by the equality: - - 3/2*N*k_B*T = 3/2*m*k_B*T/mu(Z) = 3/2*m*v^2 - - where N is the number of particles: N=m/mu(Z), or - - v^2 = k_B*T/mu(Z) - - For electrons: - - 3/2*N_e*k_B*T = 3/2*m*eta*k_B*T/mu(Z) = 3/2*m/mu(Z)*eta*m_e*v_e^2 - - v_e^2 = k_B*T/m_e - */ - - std::map varI, varE; - for (auto v : T[0]) { - unsigned char Z = v.first; - if (Z>0) { // All except TRACE - varI[Z] = sqrt((boltz*T[0][Z])/(PT[Z]->weight()*amu)) / Vunit; // Ion - varE[Z] = sqrt((boltz*T[0][Z])/(PT[0]->weight()*amu)) / Vunit; // Electron - } else { // TRACE - varI[Z] = sqrt((boltz*T[0][0])/(molW*amu)) / Vunit; // Fiducial particle - varE[Z] = sqrt((boltz*T[0][1])/(PT[0]->weight()*amu)) / Vunit; // Electrons - } - } - - double ttemp = T[0][0]; - - for (unsigned i=0; i=0) { - double totE; - do { - totE = 0.0; - for (int l=0; l<3; l++) { - double v = (*normal)(*gen); - totE += v*v; - p[i].dattrib[ne+l] = varE[0] * v; - } - } while (totE > Ecut); - } - - } else { - - KeyConvert kc(p[i].iattrib[0]); - speciesKey sKey = kc.getKey(); - unsigned short Z = sKey.first; - // unsigned short C = sKey.second; - - for (unsigned k=0; k<3; k++) { - p[i].pos[k] = L[k]*(*uniform)(*gen); - p[i].vel[k] = varI[Z] * (*normal)(*gen); - KE += p[i].vel[k] * p[i].vel[k]; - } - - if (ne>=0) { - for (int l=0; l<3; l++) { - p[i].dattrib[ne+l] = varE[Z] * (*normal)(*gen); - } - } - - ttemp = T[0][Z]; - } - - if (p[i].dattrib.size()>0) p[i].dattrib[0] = ttemp; - if (p[i].dattrib.size()>1) p[i].dattrib[1] = rho; - - if (type == Direct) { - if (p[i].dattrib.size()>2) p[i].dattrib[2] = KE; - } - } -} - -/** - Make split temperature-density box of gas -*/ -void InitializeInterface(std::vector& p, - std::vector& mass, double molW, - std::vector< std::map >& T, - std::vector &L, Itype type, - int sp, int ne, int ni, int nd) -{ - unsigned npart = p.size(); - std::vector rho - { mass[0]/(0.5*L[0]*L[1]*L[2]), mass[1]/(0.5*L[0]*L[1]*L[2]) }; - - std::cout << std::string(70, '-') << std::endl; - - for (size_t wh=0; wh<2; wh++) { - - std::cout << "Density = " << rho[wh] << std::endl; - if (type == Trace) { - std::cout << " T(ion): " << T[wh][0] << " K " << std::endl - << " T(elec): " << T[wh][1] << " K " << std::endl; - } else if (T[wh].size()>1) { - for (auto v : T[wh]) { - std::ostringstream sout; - sout << " Temp " << PT[v.first]->name() << ":"; - std::cout << std::left << std::setw(13) << sout.str() << v.second - << " K " << std::endl; - } - } else { - std::cout << " Temp: " << T[wh][0] << " K " << std::endl; - } - } - - std::cout << "Number: " << npart << std::endl; - std::cout << "Length unit: " << Lunit << " cm" << std::endl; - std::cout << "Time unit: " << Tunit << " s" << std::endl; - std::cout << "Vel unit: " << Vunit << " cm/s" << std::endl; - std::cout << "Mass unit: " << Munit << " g" << std::endl; - std::cout << std::string(70, '-') << std::endl; - - /* - We want every d.o.f. to have 1/2 k_B T of energy. For classic - DSMC, every superparticle has m/mu(Z) real particles [in physical - units where mu(Z) = atomic_masses[Z]*amu]. So the velocity factor - is given by the equality: - - 3/2*N*k_B*T = 3/2*m*k_B*T/mu(Z) = 3/2*m*v^2 - - where N is the number of particles, or - - v^2 = k_B*T/mu(Z) - */ - std::vector< std::map > varI(2), varE(2); - for (unsigned char wh=0; wh<2; wh++) { - for (auto v : T[wh]) { - if (type == Trace) { - if (v.first==0) // Ion - varI[wh][0] = sqrt((boltz*v.second)/(molW*amu)) / Vunit; - else // Electron - varE[wh][0] = sqrt((boltz*v.second)/(PT[0]->weight()*amu)) / Vunit; - } else { - unsigned char Z = v.first; - varI[wh][Z] = sqrt((boltz*T[wh][Z])/(PT[Z]->weight()*amu)) / Vunit; // Ion - varE[wh][Z] = sqrt((boltz*T[wh][Z])/(PT[0]->weight()*amu)) / Vunit; // Electron - } - } - } - - double ttemp; - - std::uniform_int_distribution<> dist(0, 1); - - for (unsigned i=0; i(dist(*gen)); - - - p[i].iattrib.resize(ni, 0); - p[i].dattrib.resize(nd, 0); - - if (type == Trace) { - - for (unsigned k=0; k<3; k++) { - if (k==0) - p[i].pos[k] = 0.5*L[k]*((*uniform)(*gen) + wh); - else - p[i].pos[k] = L[k]*(*uniform)(*gen); - p[i].vel[k] = varI[wh][0]*(*normal)(*gen); - } - - if (ne>=0) { - for (int l=0; l<3; l++) { - p[i].dattrib[ne+l] = varE[wh][0] * (*normal)(*gen); - } - } - - ttemp = T[wh][0]; - - } else { - - KeyConvert kc(p[i].iattrib[0]); - speciesKey sKey = kc.getKey(); - unsigned short Z = sKey.first; - // unsigned short C = sKey.second; - - for (unsigned k=0; k<3; k++) { - if (k==0) - p[i].pos[k] = 0.5*L[k]*((*uniform)(*gen) + wh); - else - p[i].pos[k] = L[k]*(*uniform)(*gen); - p[i].vel[k] = varI[wh][Z] * (*normal)(*gen); - KE += p[i].vel[k] * p[i].vel[k]; - } - - if (ne>=0) { - for (int l=0; l<3; l++) { - p[i].dattrib[ne+l] = varE[wh][Z] * (*normal)(*gen); - } - } - - ttemp = T[wh][Z]; - } - - if (p[i].dattrib.size()>0) p[i].dattrib[0] = ttemp; - if (p[i].dattrib.size()>1) p[i].dattrib[1] = rho[wh]; - if (p[i].dattrib.size()>2) p[i].dattrib[2] = KE; - } - -} - -void writeParticles(std::vector& particles, const string& file, Itype type, - const std::vector& sF, - const std::vector< std::vector >& sI) -{ - // For tabulating mass fractions . . . - typedef std::tuple Tspc; - typedef std::map Frac; - Frac frac; - - std::ofstream out(file.c_str()); - - out.precision(10); - - out << setw(15) << particles.size() - << setw(10) << particles[0].iattrib.size() - << setw(10) << particles[0].dattrib.size() - << std::endl; - - for (unsigned n=0; n(frac[k]) += particles[n].mass; - std::get<1>(frac[k]) ++; - } - for (unsigned k=0; k(i.second); - - double vol = 1.0; for (auto v : LL) vol *= v*Lunit; - - if ( type != Hybrid) { - std::cout << std::setw( 3) << "Z" - << std::setw( 3) << "C" - << std::setw(16) << "Mass" - << std::setw(16) << "Fraction" - << std::setw(16) << "Target" - << std::setw(12) << "Count" - << std::endl - << std::setw( 3) << "-" - << std::setw( 3) << "-" - << std::setw(16) << "--------" - << std::setw(16) << "--------" - << std::setw(16) << "--------" - << std::setw(12) << "--------" - << std::endl; - for (auto i : frac) - std::cout << std::setw( 3) << i.first.first - << std::setw( 3) << i.first.second - << std::setw(16) << std::get<0>(i.second) - << std::setw(16) << std::get<0>(i.second)/Mtot - << std::setw(16) << sF[i.first.first-1] * sI[i.first.first-1][i.first.second-1] - << std::setw(12) << std::get<1>(i.second) - << std::endl; - } else { - std::cout << std::setw( 3) << "Z" - << std::setw( 3) << "C" - << std::setw(16) << "Mass" - << std::setw(16) << "Fraction" - << std::setw(12) << "Count" - << std::setw(16) << "n_Z (#/cc)" - << std::endl - << std::setw( 3) << "-" - << std::setw( 3) << "-" - << std::setw(16) << "--------" - << std::setw(16) << "--------" - << std::setw(12) << "--------" - << std::setw(16) << "----------" - << std::endl; - for (auto i : frac) - std::cout << std::setw( 3) << i.first.first - << std::setw( 3) << i.first.second - << std::setw(16) << std::get<0>(i.second) - << std::setw(16) << std::get<0>(i.second)/Mtot - << std::setw(12) << std::get<1>(i.second) - << std::setw(16) << std::get<0>(i.second)*Munit/(PT[i.first.first]->weight()*mp*vol) - << std::endl; - } - - std::cout << std::string(70, '-') << std::endl - << "Empirical density (amu/cc) = " << Mtot*Munit/(mp*vol) - << std::endl << std::string(70, '-') << std::endl; - } -} - -void InitializeSpeciesDirect -(std::vector & particles, - std::vector& sZ, - std::vector& sF, - std::vector< std::vector >& sI, - std::vector& M, - std::vector< std::map >& T, - int sp, int ne) -{ - std::vector< std::vector > frac, cuml; - - // - // Generate the ionization-fraction input file - // - for (auto n : sZ) { - - if (use_chianti) { - - const std::string ioneq("makeIonIC.ioneq"); - std::ostringstream sout; - sout << "./genIonization" - << " -1 " << static_cast(n) - << " -2 " << static_cast(n) - << " -T " << T[0][n] << " -o " << ioneq; - - int ret = system(sout.str().c_str()); - - if (ret) { - std::cout << "System command = " << sout.str() << std::endl; - std::cout << "System ret code = " << ret << std::endl; - } - - typedef std::vector vString; - - std::string inLine; - std::ifstream sFile(ioneq.c_str()); - - if (sFile.is_open()) { - - std::getline(sFile, inLine); // Read and discard the headers - std::getline(sFile, inLine); - - { - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (auto i : s) v.push_back(::atof(i.c_str())); - frac.push_back(v); - } - - { - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (vString::iterator i=s.begin(); i!=s.end(); i++) - v.push_back(::atof(i->c_str())); - cuml.push_back(v); - } - } - } - else { - - std::ostringstream sout; - sout << "mpirun -np 1 genIonRecomb" - << " -Z " << static_cast(n) - << " -T " << T[0][n]; - - int ret = system(sout.str().c_str()); - - if (ret) { - std::cout << "System command = " << sout.str() << std::endl; - std::cout << "System ret code = " << ret << std::endl; - } - - typedef std::vector vString; - - std::string inLine; - std::string fName("IonRecombFrac.data"); - std::ifstream sFile(fName); - - if (sFile.is_open()) { - - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (auto i : s) v.push_back(::atof(i.c_str())); - frac.push_back(v); - - double norm = 0.0; - for (auto i : v) norm += i; - - if (fabs(norm - 1.0) > 1.0e-10) { - std::cout << "Normalization error: "; - for (auto i : v) std::cout << std::setw(16) << i; - std::cout << std::endl; - } - - std::vector c = v; - for (size_t i=1; i for Z=" << n << std::endl; - exit(-1); - } - } - } - - int N = particles.size(); - - // Compute cumulative species distribution - // - size_t NS = sF.size(); - - // Normalize sF - // - double norm = std::accumulate(sF.begin(), sF.end(), 0.0); - if (fabs(norm - 1.0)>1.0e-16) { - std::cout << "Normalization change: " << norm << std::endl; - } - - std::vector frcS(NS), cumS(NS); - for (size_t i=0; iweight(); - cumS[i] = frcS[i] + (i ? cumS[i-1] : 0); - } - - double normC = cumS.back(); - - for (size_t i=0; iweight() * normC; - - KeyConvert kc(speciesKey(Zi, Ci)); - particles[i].iattrib[0] = kc.getInt(); - - double KEi = 0.0, KEe = 0.0; - - for (int k=0; k<3; k++) { - KEi += particles[i].vel[k] * particles[i].vel[k]; - if (ne>=0) { - KEe += particles[i].dattrib[ne+k] * particles[i].dattrib[ne+k]; - } - } - - if (particles[i].dattrib.size()>2) particles[i].dattrib[2] = KEi; - - // Ion KE - // - tKEi += 0.5 * particles[i].mass * KEi * Eunit; - - // Electron KE - // - tKEe += 0.5 * particles[i].mass * PT[0]->weight()/PT[Zi]->weight() * KEe * Eunit; - - // Ion number - // - numbI += particles[i].mass/PT[Zi]->weight() * Munit / amu; - - // Electron number - // - numbE += particles[i].mass/PT[Zi]->weight() * Munit / amu * Ci; - } - - std::cout << "T (ion): " << tKEi/(1.5*numbI*boltz) << std::endl - << "T (elec): " << tKEe/(1.5*numbI*boltz) << std::endl - << std::string(70, '-') << std::endl; - - if (use_yaml) { - YAML::Emitter out; - - out << YAML::BeginMap - << YAML::Key << "species_map" - << YAML::BeginMap - << YAML::Key << "type" - << YAML::Value << "direct" - << YAML::Key << "elec" - << YAML::Value << sp - << YAML::Key << "elements" - << YAML::Value << YAML::BeginSeq << YAML::Flow; - for (auto v : sZ) out << v; - out << YAML::EndSeq - << YAML::EndMap - << YAML::EndMap; - - std::ofstream fout("species.yml"); - - fout << out.c_str() << std::endl; - - } else { - std::ofstream out("species.spec"); - out << "direct" << std::endl; - out << std::setw(6) << sp << std::endl; - for (size_t indx=0; indx(sZ[indx]) - << std::endl; - } - } - - sI = cuml; - for (auto &s : sI) { - double l = 0.0; - for (auto &c : s) { - double t = c; - c -= l; - l = t; - } - } -} - -void InitializeSpeciesWeight -(std::vector & particles, - std::vector& sZ, - std::vector& sF, - std::vector< std::vector >& sI, - std::vector& M, - std::vector< std::map >& T, - int sp, int ne) -{ - std::vector< std::vector > frac, cuml; - - // - // Generate the ionization-fraction input file - // - for (auto n : sZ) { - - if (use_chianti) { - - const std::string ioneq("makeIonIC.ioneq"); - std::ostringstream sout; - sout << "./genIonization" - << " -1 " << static_cast(n) - << " -2 " << static_cast(n) - << " -T " << T[0][n] << " -o n" << ioneq; - - int ret = system(sout.str().c_str()); - - if (ret) { - std::cout << "System command = " << sout.str() << std::endl; - std::cout << "System ret code = " << ret << std::endl; - } - - typedef std::vector vString; - - std::string inLine; - std::ifstream sFile(ioneq.c_str()); - - if (sFile.is_open()) { - - std::getline(sFile, inLine); // Read and discard the headers - std::getline(sFile, inLine); - - { - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (auto i : s) v.push_back(::atof(i.c_str())); - frac.push_back(v); - } - - { - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (vString::iterator i=s.begin(); i!=s.end(); i++) - v.push_back(::atof(i->c_str())); - cuml.push_back(v); - } - } - } - else { - - std::ostringstream sout; - sout << "mpirun -np 1 genIonRecomb" - << " -Z " << static_cast(n) - << " -T " << T[0][n]; - - int ret = system(sout.str().c_str()); - - if (ret) { - std::cout << "System command = " << sout.str() << std::endl; - std::cout << "System ret code = " << ret << std::endl; - } - - typedef std::vector vString; - - std::string inLine; - std::string fName("IonRecombFrac.data"); - std::ifstream sFile(fName); - - if (sFile.is_open()) { - - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (auto i : s) v.push_back(::atof(i.c_str())); - frac.push_back(v); - - double norm = 0.0; - for (auto i : v) norm += i; - - if (fabs(norm - 1.0) > 1.0e-10) { - std::cout << "Normalization error: "; - for (auto i : v) std::cout << std::setw(16) << i; - std::cout << std::endl; - } - - std::vector c = v; - for (size_t i=1; i for Z=" << n << std::endl; - exit(-1); - } - } - } - - int N = particles.size(); - // Compute cumulative species - // distribution - size_t NS = sF.size(); - // Normalize sF - // - double norm = std::accumulate(sF.begin(), sF.end(), 0.0); - if (fabs(norm - 1.0)>1.0e-16) { - std::cout << "Normalization change: " << norm << std::endl; - } - - std::vector frcS(sF), wght(NS); - double fH = sF[0], W_H = 1.0; - for (auto &v : frcS) v /= fH; - - wght[0] = W_H; - for (size_t i=1; iweight(); - - std::uniform_int_distribution<> dist(0, NS-1); - - for (int i=0; i(sZ[indx]) - << wght[indx] - << M[0]/N * sF[indx] * NS - << YAML::EndSeq; - } - out << YAML::EndSeq; - out << YAML::EndMap; - out << YAML::EndMap; - - std::ofstream fout("species.yml"); - - std::cout << out.c_str() << std::endl; - - } else { - std::ofstream out("species.spec"); - - out << "weight" << std::endl; - out << std::setw(6) << sp; - if (ne>=0) out << std::setw(6) << ne; - out << std::endl; - - for (size_t indx=0; indx(sZ[indx]) - << std::setw(16) << wght[indx] - << std::setw(16) << M[0]/N * sF[indx] * NS - << std::endl; - } - } - - sI = cuml; - for (auto &s : sI) { - double l = 0.0; - for (auto &c : s) { - double t = c; - c -= l; - l = t; - } - } -} - -void InitializeSpeciesHybrid -(std::vector & particles, - std::vector& sZ, - std::vector& sF, - std::map sC, - std::vector< std::vector >& sI, - std::vector& M, - std::vector< std::map >& T, - int sp, int ne) -{ - std::map< unsigned short, std::vector > frac; - std::vector< std::vector > cuml; - - // - // Generate the ionization-fraction input file - // - for (auto n : sZ) { - - if (use_chianti) { - - const std::string ioneq("makeIonIC.ioneq"); - std::ostringstream sout; - sout << "./genIonization" - << " -1 " << static_cast(n) - << " -2 " << static_cast(n) - << " -T " << T[0][n] << " -o " << ioneq; - - int ret = system(sout.str().c_str()); - - if (ret) { - std::cout << "System command = " << sout.str() << std::endl; - std::cout << "System ret code = " << ret << std::endl; - } - - typedef std::vector vString; - - std::string inLine; - std::ifstream sFile(ioneq.c_str()); - std::getline(sFile, inLine); // Read and discard the initial header - - if (sFile.is_open()) { - - std::getline(sFile, inLine); - // Get the atomic species - unsigned short Z; - { - std::istringstream iss(inLine); - iss >> Z; - } - // Get the ionization fractions - { - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (auto i : s) v.push_back(::atof(i.c_str())); - - if (sC.find(n) != sC.end()) { - std::vector vv; - for (int i=0; i(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (vString::iterator i=s.begin(); i!=s.end(); i++) - v.push_back(::atof(i->c_str())); - - if (sC.find(n) != sC.end()) { - std::vector vv; - for (int i=0; i(n) - << " -T " << T[0][n]; - - int ret = system(sout.str().c_str()); - - if (ret) { - std::cout << "System command = " << sout.str() << std::endl; - std::cout << "System ret code = " << ret << std::endl; - } - - typedef std::vector vString; - - std::string inLine; - std::string fName("IonRecombFrac.data"); - std::ifstream sFile(fName); - - if (sFile.is_open()) { - - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (auto i : s) v.push_back(::atof(i.c_str())); - - if (sC.find(n) != sC.end()) { - std::vector vv; - for (int i=0; i 1.0e-10) { - std::cout << "Normalization error: "; - for (auto i : v) std::cout << std::setw(16) << i; - std::cout << std::endl; - } - - std::vector c = v; - for (size_t i=1; i for Z=" << n << std::endl; - exit(-1); - } - } - } - - int N = particles.size(); - // Compute cumulative species - // distribution - size_t NS = sF.size(); - - // Normalize sF - // - double norm = std::accumulate(sF.begin(), sF.end(), 0.0); - if (fabs(norm - 1.0)>1.0e-16) { - std::cout << "Normalization change: " << norm << std::endl; - } - - std::vector frcS(sF), wght(NS); - double fH = sF[0], W_H = 1.0; - for (auto & v : frcS) v /= fH; - - wght[0] = W_H; - for (size_t i=1; iweight(); - - std::uniform_int_distribution<> dist(0, NS-1); - - for (int i=0; i=0) { - out << YAML::Key << "Elec" - << YAML::Value << ne; - } - out << YAML::Key << "elements" << YAML::BeginSeq; - for (size_t indx=0; indx(sZ[indx]) - << wght[indx] - << M[0]/N * sF[indx] * NS - << YAML::EndSeq; - } - - out << YAML::EndSeq - << YAML::EndMap - << YAML::EndMap; - - std::ofstream fout("species.yml"); - fout << out.c_str() << std::endl; - - } else { - std::ofstream out("species.spec"); - - out << "hybrid" << std::endl; - out << std::setw(6) << sp; - out << std::setw(6) << sp+1; - if (ne>=0) out << std::setw(6) << ne; - out << std::endl; - - for (size_t indx=0; indx(sZ[indx]) - << std::setw(16) << wght[indx] - << std::setw(16) << M[0]/N * sF[indx] * NS - << std::endl; - } - } - - sI = cuml; - for (auto &s : sI) { - double l = 0.0; - for (auto &c : s) { - double t = c; - c -= l; - l = t; - } - } -} - -void InitializeSpeciesTrace -(std::vector & particles, - std::vector& sZ, - std::vector& sF, - std::map sC, - std::vector& M, - std::vector< std::map >& T, - std::vector& L, - Mtype model, int sp, int ne, bool ECtype) -{ - size_t Ncomp = T.size(); - - typedef std::vector< std::vector > spA; - std::vector frac(Ncomp), cuml(Ncomp); - - if (use_init_file and Ncomp==1) { - std::ifstream sFile("IonRecombFrac.data"); - - typedef std::vector vString; - std::string inLine; - - if (sFile.is_open()) { - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (auto i : s) v.push_back(::atof(i.c_str())); - - int cnt = 0; - for (auto n : sZ) { - double sum = 0.0; - std::vector V; - int Cmax = n; - if (sC.find(n)!=sC.end()) Cmax = sC[n]-1; - for (int C=0; C 1.0e-10) { - std::cout << "Normalization error: "; - for (auto i : V) std::cout << std::setw(16) << i; - std::cout << std::endl; - } - - std::vector c = V; - for (size_t i=1; i(n) - << " -2 " << static_cast(n) - << " -T " << T[nc][1] << " -o " << ioneq; - - int ret = system(sout.str().c_str()); - - if (ret) { - std::cout << "System command = " << sout.str() << std::endl; - std::cout << "System ret code = " << ret << std::endl; - } - - typedef std::vector vString; - - std::string inLine; - std::ifstream sFile(ioneq.c_str()); - if (sFile.is_open()) { - - std::getline(sFile, inLine); // Read and discard the headers - std::getline(sFile, inLine); - - { - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (vString::iterator i=s.begin(); i!=s.end(); i++) - v.push_back(::atof(i->c_str())); - - if (sC.find(n) != sC.end()) { - std::vector vv; - for (int i=0; i(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (vString::iterator i=s.begin(); i!=s.end(); i++) - v.push_back(::atof(i->c_str())); - - if (sC.find(n) != sC.end()) { - std::vector vv; - for (int i=0; i(n) - << " -T " << T[nc][1]; - - int ret = system(sout.str().c_str()); - - if (ret) { - std::cout << "System command = " << sout.str() << std::endl; - std::cout << "System ret code = " << ret << std::endl; - } - - typedef std::vector vString; - - std::string inLine; - std::ifstream sFile("IonRecombFrac.data"); - if (sFile.is_open()) { - - vString s; - std::getline(sFile, inLine); - - std::istringstream iss(inLine); - std::copy(std::istream_iterator(iss), - std::istream_iterator(), - std::back_inserter(s)); - - std::vector v; - for (auto i : s) v.push_back(::atof(i.c_str())); - - if (sC.find(n) != sC.end()) { - std::vector vv; - for (int i=0; i 1.0e-10) { - std::cout << "Normalization error: "; - for (auto i : v) std::cout << std::setw(16) << i; - std::cout << std::endl; - } - - std::vector c = v; - for (size_t i=1; iweight(); - molW = 1.0/molW; - - // Normalize sF - // - double norm = std::accumulate(sF.begin(), sF.end(), 0.0); - if (fabs(norm - 1.0)>1.0e-16) { - std::cout << "Normalization change: " << norm << std::endl; - } - for (int i=0; i eta(Ncomp, 1.0); - - double tKEi = 0.0, tKEe = 0.0, numb = 0.0; - - for (int i=0; i 0.5*L[0]) wh = 1; - } - - particles[i].mass = M[wh]/N; - - // Sanity check - // - double test = 0.0; - - int cur = sp; - double Eta = 0.0, Mol = 0.0; - // Get the species - // - for (int indx=0; indxsecond; - - double cc = sF[indx]/PT[sZ[indx]]->weight(); - Mol += cc; - - for (int j=0; j=0) { - if (ECtype) // mean-mass correction - particles[i].dattrib[ne+k] *= 1.0/sqrt(eta[wh]); - KEe += particles[i].dattrib[ne+k] * particles[i].dattrib[ne+k]; - } - } - - if (particles[i].dattrib.size()>2) particles[i].dattrib[2] = KEi; - - // Kinetic energies - // - tKEi += 0.5*particles[i].mass * KEi; - tKEe += 0.5*particles[i].mass * KEe * PT[0]->weight() * eta[wh]/ molW; - - // Ion number - // - numb += particles[i].mass/molW * Munit / amu; - } - - if (use_yaml) { - YAML::Emitter out; - - out << YAML::BeginMap - << YAML::Key << "species_map" - << YAML::BeginMap - << YAML::Key << "type" - << YAML::Value << "trace" - << YAML::Key << "cons" - << YAML::Value << sp - 1 - << YAML::Key << "elec" - << YAML::Value << ne - << YAML::Key << "elements" - << YAML::Value << YAML::BeginSeq; - - int cntr = sp; - for (int indx=0; indxsecond; - - for (int j=0; j(sZ[indx]) - << j + 1 - << cntr++ << YAML::EndSeq; - } - } - - out << YAML::EndSeq - << YAML::EndMap - << YAML::EndMap; - - std::ofstream fout("species.yml"); - fout << out.c_str() << std::endl; - - } else { - - std::ofstream out("species.spec"); - out << "trace" << std::endl; - // Conservation position and electron position (-1 for none) - // - out << std::setw(6) << sp-1 - << std::setw(6) << ne << std::endl; - // Starting species position - // - int cntr = sp; - for (int indx=0; indxsecond; - - for (int j=0; j(sZ[indx]) - << std::setw(6) << j + 1 - << std::setw(6) << cntr++ - << std::endl; - } - } - } - - double Eunit = Munit*Vunit*Vunit; - - std::cout << std::string(70, '-') << std::endl - << "KE (ion): " << tKEi << std::endl - << "KE (elec) " << tKEe << std::endl - << std::string(70, '-') << std::endl - << "MolW: " << molW << std::endl - << "T (ion): " << tKEi*Eunit/(1.5*numb*boltz) << std::endl - << "T (elec): " << tKEe*Eunit/(1.5*numb*boltz) << std::endl - << std::string(70, '-') << std::endl; - -} // END: writeParticles - -int -main (int ac, char **av) -{ - Itype type = Direct; - Mtype model = Uniform; - bool ECtype = false; - - double D, L, R, Temp, Telec, Ecut; - std::string config; - std::string oname; - unsigned seed; - int ne = -1, ni = 2, nd = 5; - int npart; - - std::string cmd_line; - for (int i=0; i(use_chianti)->default_value("false")) - ("I,INIT", "use init file to set recombination-ionization equilibrium", - cxxopts::value(use_init_file)->default_value("false")) - ("D,dens", "density in particles per cc", - cxxopts::value(D)->default_value("1.0")) - ("T,temp", "override config file temperature for Trace, if >0", - cxxopts::value(Temp)->default_value("-1.0")) - ("Telec", "temperature for electrons, if Telec>0", - cxxopts::value(Telec)->default_value("-1.0")) - ("L,length", "box scale in system length units", - cxxopts::value(L)->default_value("1.0")) - ("l,Lunit", "length in system units", - cxxopts::value(Lunit)->default_value("1.0")) - ("t,Tunit", "time unit in years", - cxxopts::value(Tunit)->default_value("1.0e3")) - ("m,Munit", "Mass unit in solar masses", - cxxopts::value(Munit)->default_value("1.0")) - ("R,ratio", "slab length ratio (1 is cube)", - cxxopts::value(R)->default_value("1.0")) - ("i,num-int", "number of integer attributes", - cxxopts::value(ni)->default_value("2")) - ("d,num-double", "base number of double attributes", - cxxopts::value(nd)->default_value("6")) - ("N,number", "number of particles", - cxxopts::value(npart)->default_value("10000")) - ("E,Ecut", "truncation of electron tail in kT", - cxxopts::value(Ecut)->default_value("1.0e20")) - ("c,config", "element config file", - cxxopts::value(config)->default_value("makeIon.config")) - ("o,output", "output prefix", - cxxopts::value(oname)->default_value("out")) - ; - - cxxopts::ParseResult vm; - - try { - vm = options.parse(ac, av); - } catch (cxxopts::OptionException& e) { - std::cout << "Option error: " << e.what() << std::endl; - exit(-1); - } - - if (vm.count("help")) { - std::cout << options.help() << std::endl; - std::cout << "Example: " << std::endl; - std::cout << "\t" << av[0] - << "--length=0.0001 --number=1000 --dens=0.01 --electrons -c trace.config --output=out" - << std::endl; - return 1; - } - - if (vm.count("yaml")) { - use_yaml = true; - } - - if (vm.count("old")) { - use_yaml = false; - } - - if (vm.count("traceEC")) { - ECtype = true; - } - - if (myid==0) { - std::string prefix("makeIon"); - std::string cmdFile = prefix + "." + oname + ".cmd_line"; - std::ofstream out(cmdFile.c_str()); - if (!out) { - std::cerr << "makeIon: error opening <" << cmdFile - << "> for writing" << std::endl; - } else { - out << cmd_line << std::endl; - } - } - - if (use_chianti) writeScript(); - -#ifdef DEBUG // For gdb . . . - sleep(20); - // set_fpu_handler(); // Make gdb trap FPU exceptions - set_fpu_gdb_handler(); // Make gdb trap FPU exceptions -#endif - - Lunit *= pc; - Tunit *= year; - Munit *= msun; - Vunit = Lunit/Tunit; - - gen = std::make_shared(seed); - uniform = std::make_shared>(0.0, 1.0); - normal = std::make_shared>(0.0, 1.0); - - // - // Define the atomic species statistics - // - - // Element data - std::vector sZ; - std::vector sF; - - // Maximum ionization level - std::map sC; - - // Temperatures - std::vector< std::map > T; - std::vector rho; - - // Species position - int sp = -1; - - // Default molecular weight - double molW = 1.0; - - // Parse element file - { - // Load the json/yaml file - // - YAML::Node iroot = YAML::LoadFile(config); - - double norm = 0.0; - - // Look for the type - // - std::string st("Direct"); - - if (iroot["type"]) { - st = iroot["type"].as(); - - if (Types.find(st) == Types.end()) { - std::cout << "Type <" << st << "> is not valid" << std::endl - << "Valid types are:"; - for (auto v : Types) std::cout << " " << v.first; - std::cout << std::endl; - exit(-1); - } - } - - type = Types[st]; - - std::string md("Uniform"); - if (iroot["model"]) { - md = iroot["model"].as(); - - if (Models.find(md) == Models.end()) { - std::cout << "Model <" << md << "> is not valid" << std::endl - << "Valid types are:"; - for (auto v : Models) std::cout << " " << v.first; - std::cout << std::endl; - exit(-1); - } - } - - model = Models[md]; - - if (iroot["electrons"]) { - ne = 0; - } - - if (model==Interface) { - T.resize(2); - } else { - T.resize(1); - } - - double fH = 0.0; - - if (iroot["elements"]) { - - for (YAML::const_iterator it=iroot["elements"].begin(); it != iroot["elements"].end(); ++it) { - - unsigned i = it->first.as(); - - sZ.push_back(i); - - for (YAML::const_iterator jt=it->second.begin(); jt != it->second.end(); ++jt) { - std::string tag = jt->first.as(); - - if (tag == "logN") { - double val = jt->second.as(); - if (i==1) fH = val; - double wgt = pow(10.0, val - fH)*PT[i]->weight(); - sF.push_back(wgt); - norm += sF.back(); - } else if (tag == "mfrac") { - double val = jt->second.as(); - sF.push_back(val); - norm += sF.back(); - } else if (tag == "cmax") { - unsigned char cval = jt->second.as(); - sC[i] = cval; - } else { - std::cerr << "Missing element definition for atomic number " << i - << std::endl; - exit(-3); - } - } - - if (type != Trace) { - if (it->second["temp"]) { - T[0][i] = it->second["temp"].as(); - } - } - } - } - - if (norm>0.0) { - for (auto & v : sF) v /= norm; - } else { - std::cout << "Error: zero mass fraction norm" << std::endl; - exit(-1); - } - - if (type == Trace) { - molW = 0.0; - for (size_t i=0; iweight(); - molW = 1.0/molW; - } - - if (model==Interface) { - rho.resize(2); - - if (iroot["components"]) { - - // Iterate through component sequence - for (YAML::const_iterator it=iroot["components"].begin(); it != iroot["elements"].end(); ++it) { - - for (YAML::const_iterator jt=it->second.begin(); jt != it->second.end(); ++jt) { - unsigned j = jt->first.as(); // Unpack component variables - if (j>0 and j<2) { - rho[j-1] = jt->second["Density"].as(); - T[j-1][0] = jt->second["Temp" ].as(); - T[j-1][1] = jt->second["Etemp" ].as(); - } - } - - } - - } - } else { - rho.push_back(D); - if (Temp>0.0) T[0][0] = Temp; - else T[0][0] = iroot["temp"].as(); - T[0][1] = T[0][0]; - if (Telec>0.0) T[0][1] = Telec; - else T[0][1] = iroot["telc"].as(); - } - - if (type==Trace) { - nd++; // Conservation of energy position - sp = nd; - - // Augment for species number - for (size_t indx=0; indxsecond; - - for (int j=0; j=0) { - ne = nd; // Electron start position - nd += 4; // Electron velocity and conservation - } - - } - else if (type==Hybrid) { - auto it = std::max_element(std::begin(sZ), std::end(sZ)); - size_t maxSp = *it; - nd++; // Energy conservation - sp = nd++; // Species position - nd += maxSp; - if (ne>=0) { - ne = nd; // Electron start position - nd += 4; // Electron velocity and conservation - } - } - else if (type==Weight or type==Direct) { - sp = ++nd; // Energy conservation - if (ne>=0) { - ne = nd; // Electron start position - nd += 4; // Electron velocity and conservation - } - } - } - - // Ion fractions - std::vector< std::vector > sI; - - /* Additional species, e.g. - sF[2] = 0.0; sZ[2] = 3; // Li - sF[3] = 0.0; sZ[3] = 6; // C - sF[4] = 0.0; sZ[4] = 7; // N - sF[5] = 0.0 sZ[5] = 8; // O - sF[6] = 0.0; sZ[6] = 12; // Mg - */ - - // Cube axes - // - LL.resize(3, L); - LL[0] *= R; - double vol = 1.0; - for (auto v : LL) vol *= v*Lunit; - - // Mass in box in m_p - // - std::vector Mass; - - std::vector particles(npart); - - // Initialize the phase space vector - // - switch (model) { - case Interface: - Mass.push_back( mp*rho[0]*0.5*vol/Munit ); - Mass.push_back( mp*rho[1]*0.5*vol/Munit ); - InitializeInterface(particles, Mass, molW, T, LL, type, sp, ne, ni, nd); - break; - case Uniform: - default: - Mass.push_back(mp*D*vol/Munit); - InitializeUniform(particles, Mass, molW, Ecut, T, LL, type, sp, ne, ni, nd); - break; - } - - // Initialize the Z, C's - // - switch (type) { - case Hybrid: - InitializeSpeciesHybrid(particles, sZ, sF, sC, sI, Mass, T, sp, ne); - break; - case Weight: - InitializeSpeciesWeight(particles, sZ, sF, sI, Mass, T, sp, ne); - break; - case Trace: - InitializeSpeciesTrace (particles, sZ, sF, sC, Mass, T, LL, model, sp, ne, - ECtype); - // Compute molecular weight - molW = 0.0; - for (size_t k=0; kweight(); - molW = 1.0/molW; - break; - case Direct: - default: - InitializeSpeciesDirect(particles, sZ, sF, sI, Mass, T, sp, ne); - } - - // Output file - // - writeParticles(particles, oname + ".bod", type, sF, sI); - - return 0; -} diff --git a/utils/ICs/ph_conv.cc b/utils/ICs/ph_conv.cc deleted file mode 100644 index 3f472cf4f..000000000 --- a/utils/ICs/ph_conv.cc +++ /dev/null @@ -1,214 +0,0 @@ -// This method is stable for all densities, unlike ph_ion. -// - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -// Library variables - -#include - -/** - Solution for rate balance. - - n_H * f_H * alpha_H = n_e * n_H * (1 - f_H) * beta_H+ - n_He * f_He * alpha_He = n_e * n_He * f_He+ * beta_He+ - n_He * f_He+ * alpha_He+ = n_e * n_He * (1-f_He-f_He+) * beta_He++ - - LHS=rate of photoionization - RHS=rate of recombination - - Simplify to fractional rates: - - f_H * alpha_H = n_e * (1 - f_H) * beta_H+ - f_He * alpha_He = n_e * f_He+ * beta_He+ - f_He+ * alpha_He+ = n_e * (1-f_He-f_He+) * beta_He++ - - Difference in fraction in time interval h is: - - delta_H = h*[n_e * (1 - f_H) * beta_H+ - f_H * alpha_H] - delta_He = h*[n_e * f_He+ * beta_He+ - f_He * alpha_He] - delta_He+ = h*[n_e * (1-f_He-f_He+) * beta_He++ - f_He+ * alpha_He+] - - Iterative algorithm: - - f_H (n+1) = f_H (n) + delta_H - f_He (n+1) = f_He (n) + delta_He - f_He+(n+1) = f_He+(n) + delta_He+ - -*/ -int main(int argc, char**argv) -{ - double n0, tol, h, T, z; - unsigned skip; - int niter; - std::string outf; - - cxxopts::Options options(argv[0], "Compute ionization-recombination equilibrium (stable version)"); - - options.add_options() - ("h,help", "produce this help message") - ("D,density", "Density in amu/cc. Good for n0<8.5e-2", - cxxopts::value(n0)->default_value("1.0e-4")) - ("T,temp", "Density in amu/cc. Good for n0<8.5e-2", - cxxopts::value(T)->default_value("30000.0")) - ("s,skip", "Iteration step skip for diagnostic output", - cxxopts::value(skip)->default_value("10")) - ("H,step", "Time step in years", - cxxopts::value(h)->default_value("2000.0")) - ("e,tol", "error tolerance", - cxxopts::value(tol)->default_value("1.0e-10")) - ("z,redshift", "redshift", - cxxopts::value(z)->default_value("0.1")) - ("n,iter", "maximum number of iterations", - cxxopts::value(niter)->default_value("1000")) - ("o,outfile", "data file for makeIon input", - cxxopts::value(outf)->default_value("IonRecombFrac.data")) - ; - - cxxopts::ParseResult vm; - - try { - vm = options.parse(argc, argv); - } catch (cxxopts::OptionException& e) { - std::cout << "Option error: " << e.what() << std::endl; - exit(-1); - } - - if (vm.count("help")) { - std::cout << options.help() << std::endl; - return 1; - } - - - - std::map< double, std::array > alpha = { - {0.1, {6.6362e-14, 7.9561e-14, 8.2955e-15}}, - {1.1, {4.9771e-13, 5.9671e-13, 6.2216e-14}} - }; - - if (alpha.find(z) == alpha.end()) { - std::cout << "Could not find <" << z << "> in alpha" << std::endl - << "Available values are:"; - for (auto v : alpha) std::cout << " " << v.first; - std::cout << std::endl; - exit(-1); - } - - std::vector Temp; - std::vector> barray; - std::array beta; - - std::ifstream fin("coefficients.dat"); - if (fin) { - double t; - std::array v; - while (fin) { - fin >> t >> v[0] >> v[1] >> v[2]; - if (fin.good()) { - Temp.push_back(t); - barray.push_back(v); - } - } - } else { - std::cout << "Error opening " << std::endl; - exit(-1); - } - - // Interpolate - if (T<=Temp.back() and T>=Temp.front()) { - unsigned indx = 0; - for (indx = 0; indx < Temp.size(); indx++) { - if (T init {0.1, 0.1, 0.1}; - std::array curr, last, maxd {0, 0, 0}, frac {X, Y, Y}; - std::array delta; - - curr = init; - - double err = 0.0; - - for (int n=0; n(fabs(delta[j])/curr[j], maxd[j]); - curr[j] = std::min(1.0, std::max(0.0, curr[j])); - } - - if (n % skip==0) { - std::cout << std::setw(8) << n; - for (int j=0; j<3; j++) std::cout << std::setw(14) << curr[j]; - std::cout << std::setw(14) << ne/n0 << std::endl; - } - - err = 0.0; - for (int j=0; j<3; j++) { - double dif = 0.5 * (curr[j] - last[j]) / (curr[j] + last[j]); - err += dif*dif; - } - err = sqrt(err); - if (err written" << std::endl; - } else { - std::cout << "FAILURE: " - << "error opening <" << outf << "> for output" << std::endl; - } - } else { - std::cout << "FAILURE: no convergence" << std::endl; - } - std::cout << std::endl; - - return 0; -} diff --git a/utils/ICs/ph_ion.cc b/utils/ICs/ph_ion.cc deleted file mode 100644 index 1e2f1ef25..000000000 --- a/utils/ICs/ph_ion.cc +++ /dev/null @@ -1,147 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -// Library variables - -#include -#include - -int main(int argc, char**argv) -{ - double n0, tol; - unsigned T; - int niter; - std::string outf; - - cxxopts::Options options(argv[0], "Test of ionization equilibrium"); - - options.add_options() - ("h,help", "produce this help message") - ("D,density", "Density in amu/cc. Good for n0<8.5e-2", - cxxopts::value(n0)->default_value("1.0e-4")) - ("T,temp", "Temperature in Kelvin (integer value)", - cxxopts::value(T)->default_value("25000")) - ("e,tol", "error tolerance", - cxxopts::value(tol)->default_value("1.0e-10")) - ("n,iter", "maximum number of iterations", - cxxopts::value(niter)->default_value("1000")) - ("o,outfile", "data file for makeIon input", - cxxopts::value(outf)->default_value("IonRecombFrac.data")) - ; - - cxxopts::ParseResult vm; - - try { - vm = options.parse(argc, argv); - } catch (cxxopts::OptionException& e) { - std::cout << "Option error: " << e.what() << std::endl; - exit(-1); - } - - if (vm.count("help")) { - std::cout << options.help() << std::endl; - return 1; - } - - std::array alpha {4.9771e-13, 5.9671e-13, 6.2216e-14}; - - std::vector Temp; - std::vector> barray; - std::array beta; - - std::ifstream fin("coefficients.dat"); - if (fin) { - double t; - std::array v; - while (fin) { - fin >> t >> v[0] >> v[1] >> v[2]; - if (fin.good()) { - Temp.push_back(t); - barray.push_back(v); - } - } - } else { - std::cout << "Error opening " << std::endl; - exit(-1); - } - - // Interpolate - if (T<=Temp.back() and T>=Temp.front()) { - unsigned indx = 0; - for (indx = 0; indx < Temp.size(); indx++) { - if (T gamma; - std::array init {0.001, 0.001, 0.001}; - std::array curr, last; - - for (int i=0; i<3; i++) gamma[i] = beta[i]/alpha[i]; - double X = 0.76, Y = 0.24, mX = 1.0, mY = 4.0; - double mu = X/mX + Y/mY; - - curr = init; - - double err = 0.0; - - for (int n=0; n written" << std::endl; - } else { - std::cout << "FAILURE: " - << "error opening <" << outf << "> for output" << std::endl; - } - } else { - std::cout << "FAILURE: no convergence" << std::endl; - } - std::cout << std::endl; - - return 0; -} From 58343a0d339f1e4950750cc52fbb876bad52cdda Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 09:49:34 -0400 Subject: [PATCH 078/167] Moved to DSMC module --- utils/ICs/globalInit.H | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 utils/ICs/globalInit.H diff --git a/utils/ICs/globalInit.H b/utils/ICs/globalInit.H deleted file mode 100644 index 86da8922b..000000000 --- a/utils/ICs/globalInit.H +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef _Global_H -#define _Global_H - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -// Unused globals -extern unsigned multistep; - -extern double Lunit; //a box that is 1 pc per side -extern double Tunit; -extern double Vunit; -extern double Munit; //for 0.5 cm^(-3) gas in a volume of 1 pc^3 - -#endif From 418c2acac5b7e1cec8ec3641231bb2db210d0e1a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 16 Apr 2024 10:08:14 -0400 Subject: [PATCH 079/167] More support code moved to DSMC module source --- utils/ICs/genIonization.h | 74 ----------------- utils/ICs/genMakeIonconfig.py | 151 ---------------------------------- 2 files changed, 225 deletions(-) delete mode 100644 utils/ICs/genIonization.h delete mode 100644 utils/ICs/genMakeIonconfig.py diff --git a/utils/ICs/genIonization.h b/utils/ICs/genIonization.h deleted file mode 100644 index 2b496fcae..000000000 --- a/utils/ICs/genIonization.h +++ /dev/null @@ -1,74 +0,0 @@ -R"(#!/usr/bin/python3 - -import os -import re -import sys -import operator - -# Try to prevent ChiantiPy from grabbing the mouse -# -import matplotlib as mpl -mpl.use('Agg') - -from numpy import * -import ChiantiPy.core as ch -from optparse import OptionParser - -def accumulate(iterable, func=operator.add): - 'Return running totals' - # accumulate([1,2,3,4,5]) --> 1 3 6 10 15 - # accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120 - it = iter(iterable) - total = next(it) - yield total - for element in it: - total = func(total, element) - yield total - -def getIoneq(ofile, T=1.0e+04, numBeg=1, numEnd=2): - """ Use ChiantiPy to get the ionization equilibrium""" - file = open(ofile, 'w') - file.write('{0:4d} {1:4d} {2:15.6e}\n'.format(numBeg, numEnd, T)) - for n in range(numBeg, numEnd+1): - atom = ch.ioneq(n) - atom.calculate([T, T]) - f = atom.Ioneq - z = list(accumulate(f)) - z /= z[-1] # Normalization, should not be needed - file.write('{0:4d} {1:4d}\n'.format(n, len(z))) - for v in f: - file.write('{0:15.6e}'.format(v[0])) - file.write('\n') - for v in z: - file.write('{0:15.6e}'.format(v[0])) - file.write('\n') - -def main(): - """ - Get the ionization equilibrium for some number of elements - Print to a file - """ - usage = "usage: %prog [options] file" - parser = OptionParser(usage) - - parser.add_option("-1", "--nBeg", default=1, - action="store", type="int", dest="nbeg", - help="beginning atomic number") - parser.add_option("-2", "--nEnd", default=2, - action="store", type="int", dest="nend", - help="ending atomic number") - parser.add_option("-T", "--temp", default=10000.0, - action="store", type="float", dest="temp", - help="temperature") - parser.add_option("-o", "--output", default='ioneq.dat', - action="store", type="string", dest="ofile", - help="output file") - - (opt, args) = parser.parse_args() - - # Call the main routine - getIoneq(opt.ofile, T=opt.temp, numBeg=opt.nbeg, numEnd=opt.nend) - -if __name__ == "__main__": - main() -)" diff --git a/utils/ICs/genMakeIonconfig.py b/utils/ICs/genMakeIonconfig.py deleted file mode 100644 index 9874bc585..000000000 --- a/utils/ICs/genMakeIonconfig.py +++ /dev/null @@ -1,151 +0,0 @@ -import pandas as pd -import sys -import json -''' -This script generates a makeIon.config file with which to run makeIon. -Supply the elements you wish to include as command line arguments. -For example to generate a makeIon.config file which includes hydrogen, helium -and oxygen the command would be: - -python genMakeIonconfig.py H He O - -The script uses relative abundance data from Allen, C.W., 1973, Astrophysical -Quantities (see the begining of the chapter on atoms) to calculate the typical -expected mass fractions of the elements the user choses to include. These mass -fractions are outputted to the makeIon.config which can be manually edited should -the user wish to do so. - -The default particle type this file is used to generate is trace particles. This -can be changed by changing the particle_type variable in this script -''' - -# Check the user has specified at least one element to include -if len(sys.argv) < 2: - sys.exit('You must specify at least one element to include. See genMakeIonconfig.py docstring.') - -# Set the particle type to generate -particle_type = 'Trace' - -# Create a dataframe of data related to the relative abundances of different species -species_data = [[1, 'H', 1.0079, 12.00], - [2, 'He', 4.0026, 10.93], - [3, 'Li', 6.941, 1.60], - [4, 'Be', 9.0122, 2.00], - [5, 'B', 10.811, 3.50], - [6, 'C', 12.0107, 8.52], - [7, 'N', 14.0067, 7.96], - [8, 'O', 15.9994, 8.82], - [9, 'F', 18.9984, 4.60], - [10, 'Ne', 20.1797, 7.92], - [11, 'Na', 22.9897, 6.25], - [12, 'Mg', 24.305, 7.42], - [13, 'Al', 26.9815, 6.39], - [14, 'Si', 28.0855, 7.52], - [15, 'P', 30.9738, 5.52], - [16, 'S', 32.065, 7.2], - [17, 'Cl', 35.453, 5.6], - [18, 'Ar', 39.948, 6.8], - [19, 'K', 39.0983, 4.95], - [20, 'Ca', 40.078, 6.3], - [21, 'Sc', 44.9559, 3.22], - [22, 'Ti', 47.867, 5.13], - [23, 'V', 50.9415, 4.4], - [24, 'Cr', 51.9961, 5.85], - [25, 'Mn', 54.938, 5.4], - [26, 'Fe', 55.845, 7.6], - [27, 'Ni', 58.6934, 6.3], - [28, 'Co', 58.9332, 5.1], - [29, 'Cu', 63.546, 4.5], - [30, 'Zn', 65.39, 4.2], - [31, 'Ga', 69.723, 4.20], - [32, 'Ge', 72.64, 4.80], - [33, 'As', 74.9216, 4.20], - [34, 'Se', 78.96, 5.10], - [35, 'Br', 79.904, 4.50], - [36, 'Kr', 83.8, 5.10], - [37, 'Rb', 85.4678, 4.30], - [38, 'Sr', 87.62, 4.79], - [39, 'Y', 88.9059, 3.80], - [40, 'Zr', 91.224, 4.50], - [41, 'Nb', 92.9064, 4.00], - [42, 'Mo', 95.94, 3.90], - [44, 'Ru', 101.07, 3.60], - [45, 'Rh', 102.9055, 3.20], - [46, 'Pd', 106.42, 3.48], - [47, 'Ag', 107.8682, 2.83], - [48, 'Cd', 112.411, 3.80], - [49, 'In', 114.818, 3.50], - [50, 'Sn', 118.71, 3.60], - [51, 'Sb', 121.76, 3.10], - [52, 'Te', 127.6, 4.10], - [53, 'I', 126.9045, 3.50], - [54, 'Xe', 131.293, 4.10], - [55, 'Cs', 132.9055, 3.20], - [56, 'Ba', 137.327, 4.10], - [57, 'La', 138.9055, 3.70], - [58, 'Ce', 140.116, 3.95], - [59, 'Pr', 140.9077, 3.55], - [60, 'Nd', 144.24, 3.94], - [62, 'Sm', 150.36, 3.63], - [63, 'Eu', 151.964, 2.93], - [64, 'Gd', 157.25, 3.28], - [65, 'Tb', 158.9253, 2.50], - [66, 'Dy', 162.5, 3.29], - [67, 'Ho', 164.9303, 2.70], - [68, 'Er', 167.259, 3.04], - [69, 'Tm', 168.9342, 2.50], - [70, 'Yb', 173.04, 3.40], - [71, 'Lu', 174.967, 2.80], - [72, 'Hf', 178.49, 3.00], - [73, 'Ta', 180.9479, 2.60], - [74, 'W', 183.84, 3.30], - [75, 'Re', 186.207, 2.30], - [76, 'Os', 190.23, 3.20], - [77, 'Ir', 192.217, 3.10], - [78, 'Pt', 195.078, 4.20], - [79, 'Au', 196.9665, 2.89], - [80, 'Hg', 200.59, 3.20], - [81, 'Tl', 204.3833, 2.50], - [82, 'Pb', 207.2, 4.10], - [83, 'Bi', 208.9804, 3.00], - [90, 'Th', 232.0381, 3.10], - [92, 'U', 238.0289, 2.40]] - -# Make the species data into a dataframe and apply headings to it -df = pd.DataFrame(species_data, columns = ['Atomic_number', 'Element', 'Atomic_mass', 'Log_abundence']) - -# Use only the elements specified by the user -df = df.loc[df['Element'].isin(sys.argv)] - -# Convert the log abundances into abundances -df['Abundence'] = df.apply(lambda row: 10**row.Log_abundence, axis=1) - -# Get the total mass of each species in untis of atomic masses -df['Species_mass'] = df.apply(lambda row: row.Abundence * row.Atomic_mass, axis=1) - -# Get the total mass of all the species -total_mass = df['Species_mass'].sum() - -# Get the fraction of the total mass contributed by each species -df['Mass_fraction'] = df.apply(lambda row: row.Species_mass / total_mass, axis=1) - -# Create a dictonary to hold the data to output to makeIon.config -output = {} -output['type'] = particle_type -elements = {} - -# Add each element to the elements dictionary -for element in sys.argv[1:]: - - row = df.loc[df['Element'].isin([element])] - number = str(row['Atomic_number'].values[0]) - mass_frac = row['Mass_fraction'].values[0] - elements[number] = {"mfrac": mass_frac} - -# Place th elements data in the output dictionary -output['elements'] = elements - -# Write the output to makeIon.config -file = open("makeIon.config","w") -with open('makeIon.config', 'w') as f: - json.dump(output, f) From 6470a3a7932443604914d212471a10678aa46e3b Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 17 Apr 2024 10:41:50 -0400 Subject: [PATCH 080/167] Remove pHOT hash key checks (only used for debugging) from ParticleFerry as part of DSMC removal --- src/ParticleFerry.H | 3 --- src/ParticleFerry.cc | 55 -------------------------------------------- 2 files changed, 58 deletions(-) diff --git a/src/ParticleFerry.H b/src/ParticleFerry.H index d3b5c418a..bcaaecf07 100644 --- a/src/ParticleFerry.H +++ b/src/ParticleFerry.H @@ -21,14 +21,11 @@ private: unsigned bufpos, ibufcount, itotcount; unsigned _to, _from, _total; - unsigned pk_lo, pk_hi; - unsigned long key_lo, key_hi; int keypos, treepos, idxpos; void BufferSend(); void BufferRecv(); - void bufferKeyCheck(); // For debugging //! Determine size of buffer needed void particleBufInit(); diff --git a/src/ParticleFerry.cc b/src/ParticleFerry.cc index 16418dba7..810d9da36 100644 --- a/src/ParticleFerry.cc +++ b/src/ParticleFerry.cc @@ -295,18 +295,6 @@ ParticleFerry::ParticleFerry(int nimax, int ndmax) : nimax(nimax), ndmax(ndmax) bufpos = 0; ibufcount = 0; - - - // These are for key value sanity - // checks - pk_lo = 1u << (3*pkbits); - pk_hi = 1u << (3*pkbits+1); - - key_lo = 1u; - key_lo <<= (3*nbits); - key_hi = 1u; - key_hi <<= (3*nbits+1); - } // Destructor @@ -410,46 +398,3 @@ void ParticleFerry::BufferRecv() bufferKeyCheck(); #endif } - - -void ParticleFerry::bufferKeyCheck() -{ - // Sanity check for pHOT keys - unsigned long minkey = 1u; - minkey <<= sizeof(unsigned long)*8 - 1; - unsigned long maxkey = 0u; - unsigned err0 = 0; - for (unsigned n=0; n(minkey, key); - maxkey = max(maxkey, key); - if ((key < key_lo || key >= key_hi) && key > 0u) err0++; - } - - unsigned maxpexp = 1u; - maxpexp <<= (3*pkbits); - unsigned minpkey = 1u; - minpkey <<= (32 - 1); - unsigned maxpkey = 0u; - unsigned err1 = 0; - for (unsigned n=0; n(minpkey, tree); - maxpkey = max(maxpkey, tree); - if ((tree < pk_lo || tree >= pk_hi) && tree > 0u) err1++; - } - - unsigned wid = 3*nbits/4 + 3; - - if (err0) - cerr << "ParticleFerry: Key err=" << err0 << endl << hex - << "ParticleFerry: min key=" << right << setw(wid) << minkey << endl - << "ParticleFerry: max key=" << right << setw(wid) << maxkey << endl; - if (err1) - cout << "ParticleFerry: Cel err=" << dec << err1 << endl - << "ParticleFerry: min cel=" << right << setw(12) << minpkey << endl - << "ParticleFerry: max cel=" << right << setw(12) << maxpkey << endl; - - return; -} - From 0dd737fb5b5a2e51473fcf235df9ed03e70bafca Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 17 Apr 2024 10:43:17 -0400 Subject: [PATCH 081/167] Add methods to return table ranges [no ci] --- include/BiorthCyl.H | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/BiorthCyl.H b/include/BiorthCyl.H index e8700e378..b0efa6e8e 100644 --- a/include/BiorthCyl.H +++ b/include/BiorthCyl.H @@ -211,6 +211,12 @@ public: //! For pyEXP std::vector orthoCheck(); + //! Get table range bounds + double getXmin() { return xmin; } + double getXmax() { return xmax; } + double getYmin() { return ymin; } + double getYmax() { return ymax; } + }; #endif From b4664ef9748afa24bfd7122c9da5b55965c4200d Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 17 Apr 2024 10:44:10 -0400 Subject: [PATCH 082/167] Allow EmpCyl2d to provide its mapping object to a client --- include/EmpCyl2d.H | 53 ++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/include/EmpCyl2d.H b/include/EmpCyl2d.H index 256c070cf..e062644b3 100644 --- a/include/EmpCyl2d.H +++ b/include/EmpCyl2d.H @@ -46,6 +46,31 @@ public: virtual std::string ID() { return id; } }; + //! Map the radius + class Mapping + { + protected: + bool cmap; + double scale; + + public: + + //! Null constructor (for copy construct) + Mapping() {} + + //! Main constructor + Mapping(double scale, bool cmap) : scale(scale), cmap(cmap) {} + + //! From semi-infinite radius to [-1, 1] + double r_to_xi(double r); + + //! From [-1, 1] to semi-infinite radius + double xi_to_r(double xi); + + //! Jacobian + double d_xi_to_r(double xi); + }; + protected: //! Contains parameter database @@ -95,31 +120,6 @@ protected: class KuzminCyl; class ZangCyl; - //! Map the radius - class Mapping - { - protected: - bool cmap; - double scale; - - public: - - //! Null constructor (for copy construct) - Mapping() {} - - //! Main constructor - Mapping(double scale, bool cmap) : scale(scale), cmap(cmap) {} - - //! From semi-infinite radius to [-1, 1] - double r_to_xi(double r); - - //! From [-1, 1] to semi-infinite radius - double xi_to_r(double xi); - - //! Jacobian - double d_xi_to_r(double xi); - }; - //! Mapping instance Mapping map; @@ -218,6 +218,9 @@ public: return {disk->pot(r), disk->dpot(r), disk->dens(r)}; } //@} + + //! Get coordinate mapping object + Mapping getMapping() { return map; } }; From 4f125529de32ed31236b9d4c11d657f5fc2d84a1 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 17 Apr 2024 10:46:02 -0400 Subject: [PATCH 083/167] Allow AxisymmetricBasis to provide its coefficient vector and orders to client --- src/AxisymmetricBasis.H | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/AxisymmetricBasis.H b/src/AxisymmetricBasis.H index ce456d554..dfdbc232e 100644 --- a/src/AxisymmetricBasis.H +++ b/src/AxisymmetricBasis.H @@ -190,6 +190,18 @@ public: //! Set tk_type from string TKType setTK(const std::string& tk); + //! Access coefficients + VectorP getCoefs(int l) const { return expcoef[l]; } + + //@{ + //! Get angular order + int getLmax() { return Lmax; } + int getMmax() { return Mmax; } + //@{ + + //! Get radial order + int getNmax() { return nmax; } + private: int Ldim, L0; From 6804be249e3efd25228fedd42a1df26ad9aef0b2 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 17 Apr 2024 10:46:59 -0400 Subject: [PATCH 084/167] Ooops: left in some hash sanity checks, now removed [no ci] --- src/ParticleFerry.cc | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/ParticleFerry.cc b/src/ParticleFerry.cc index 810d9da36..142ca2305 100644 --- a/src/ParticleFerry.cc +++ b/src/ParticleFerry.cc @@ -169,18 +169,6 @@ void ParticleFerry::particlePack(PartPtr in, char* buffer) memcpy(&buffer[pos], &in->key, sizeof(unsigned long)); pos += sizeof(unsigned long); - // Sanity check - // - if (in->tree > 0) { - if ( (in->tree < pk_lo) || (in->tree >= pk_hi) ) { - cout << "Error!! [5], id=" << myid - << ": tree=" << in->tree - << " seq=" << in->indx - << " (x, y, z)={" << in->pos[0] << ", " << in->pos[1] - << ", " << in->pos[2] - << endl; - } - } } // Unpack the buffer into the supplied particle. Buffer is supplied @@ -267,19 +255,6 @@ void ParticleFerry::particleUnpack(PartPtr out, char* buffer) memcpy(&out->key, &buffer[pos], sizeof(unsigned long)); pos += sizeof(unsigned long); - // Sanity check - // - if (out->tree > 0) { - if ( (out->tree < pk_lo) || (out->tree >= pk_hi) ) { - cout << "Error!! [4], id=" << myid - << ": tree=" << out->tree - << " seq=" << out->indx - << " (x, y, z)={" << out->pos[0] << ", " << out->pos[1] - << ", " << out->pos[2] - << endl; - } - } - } // Constructor From 5e18ec9b20bb0e9f103bd9838c3433e169bf390f Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 17 Apr 2024 22:34:30 -0400 Subject: [PATCH 085/167] Remove parsing support for nbits and pkbits; moved to DSMC module [no ci] --- src/global.H | 6 ------ src/global.cc | 2 -- src/global_key_set.H | 2 -- src/parse.cc | 4 ---- 4 files changed, 14 deletions(-) diff --git a/src/global.H b/src/global.H index d98aa6ec6..658127f37 100644 --- a/src/global.H +++ b/src/global.H @@ -61,12 +61,6 @@ extern int nbalance; //! Load balancing threshold (larger difference initiates balancing) extern double dbthresh; -//! Number of bits per dimension for hash key -extern unsigned nbits; - -//! Number of bits per dimension for partition -extern unsigned pkbits; - //! Particle ferry buffer size extern unsigned PFbufsz; diff --git a/src/global.cc b/src/global.cc index 246e3c248..3de87a3f6 100644 --- a/src/global.cc +++ b/src/global.cc @@ -20,8 +20,6 @@ double dbthresh = 0.05; // Load balancing threshold (5% by default) double dtime = 0.1; // Default time step size double max_mindt = 0.05; // Below minimum time step threshold -unsigned nbits = 32; // Number of bits per dimension -unsigned pkbits = 6; // Number of bits for parition unsigned PFbufsz = 40000; // ParticleFerry buffer size in particles diff --git a/src/global_key_set.H b/src/global_key_set.H index 8a05d15a3..cfa4a9ef0 100644 --- a/src/global_key_set.H +++ b/src/global_key_set.H @@ -8,8 +8,6 @@ global_valid_keys = { "dbthresh", "time", "dtime", - "nbits", - "pkbits", "PFbufsz", "NICE", "VERBOSE", diff --git a/src/parse.cc b/src/parse.cc index 976da4627..e70032e08 100644 --- a/src/parse.cc +++ b/src/parse.cc @@ -96,8 +96,6 @@ void initialize(void) if (_G["time"]) tnow = _G["time"].as(); if (_G["dtime"]) dtime = _G["dtime"].as(); if (_G["maxMindt"]) max_mindt = _G["maxMindt"].as(); - if (_G["nbits"]) nbits = _G["nbits"].as(); - if (_G["pkbits"]) pkbits = _G["pkbits"].as(); if (_G["PFbufsz"]) PFbufsz = _G["PFbufsz"].as(); if (_G["NICE"]) NICE = _G["NICE"].as(); if (_G["VERBOSE"]) VERBOSE = _G["VERBOSE"].as(); @@ -349,8 +347,6 @@ void update_parm() if (not conf["time"]) conf["time"] = tnow; if (not conf["dtime"]) conf["dtime"] = dtime; - if (not conf["nbits"]) conf["nbits"] = nbits; - if (not conf["pkbits"]) conf["pkbits"] = pkbits; if (not conf["PFbufsz"]) conf["PFbufsz"] = PFbufsz; if (not conf["NICE"]) conf["NICE"] = NICE; if (not conf["VERBOSE"]) conf["VERBOSE"] = VERBOSE; From 5f6c004839879b245f050e116a7c366aec1f83c3 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 18 Apr 2024 09:59:56 -0400 Subject: [PATCH 086/167] Error message advertize its origin [no ci] --- exputil/BiorthCyl.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exputil/BiorthCyl.cc b/exputil/BiorthCyl.cc index b604ba68f..8f87e63a3 100644 --- a/exputil/BiorthCyl.cc +++ b/exputil/BiorthCyl.cc @@ -267,7 +267,7 @@ double BiorthCyl::r_to_xi(double r) if (cmapR>0) { if (r<0.0) { ostringstream msg; - msg << "radius=" << r << " < 0! [mapped]"; + msg << "BiorthCyl: radius=" << r << " < 0! [mapped]"; throw GenericError(msg.str(), __FILE__, __LINE__, 2040, true); } return (r/scale - 1.0)/(r/scale + 1.0); From ebd9fdb7dcc25bc58712323c4db48edb10fffa15 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 18 Apr 2024 10:00:40 -0400 Subject: [PATCH 087/167] Error message advertize its origin [no ci] --- exputil/EmpCylSL.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 7eba87867..170ec88fa 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -6099,7 +6099,7 @@ double EmpCylSL::r_to_xi(double r) if (CMAPR>0) { if (r<0.0) { ostringstream msg; - msg << "radius=" << r << " < 0! [mapped]"; + msg << "EmpCylSL: radius=" << r << " < 0! [mapped]"; throw GenericError(msg.str(), __FILE__, __LINE__, 1040, true); } return (r/ASCALE - 1.0)/(r/ASCALE + 1.0); From bf899a6ea2604191bcca7d57051b347a5bc65d29 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 18 Apr 2024 10:01:41 -0400 Subject: [PATCH 088/167] Per term field evaluation members need to use mapped coordinates [no ci] --- include/BiorthCyl.H | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/include/BiorthCyl.H b/include/BiorthCyl.H index b0efa6e8e..5b3f775b3 100644 --- a/include/BiorthCyl.H +++ b/include/BiorthCyl.H @@ -133,20 +133,20 @@ public: double d_yi_to_z(double y); //! Get potential for dimensionless coord with harmonic order m and radial orer n - double get_pot(double r, double z, int m, int n) - { return interp(m, n, r, z, pot); } + double get_pot(double x, double y, int m, int n) + { return interp(m, n, xi_to_r(x), yi_to_z(y), pot); } //! Get density for dimensionless coord with harmonic order l and radial orer n - double get_dens(double r, double z, int m, int n) - { return interp(m, n, r, z, dens); } + double get_dens(double x, double y, int m, int n) + { return interp(m, n, xi_to_r(x), yi_to_z(y), dens); } //! Get radial force for dimensionless coord with harmonic order l and radial orer n - double get_rforce(double r, double z, int m, int n) - { return interp(m, n, r, z, rforce); } + double get_rforce(double x, double y, int m, int n) + { return interp(m, n, xi_to_r(x), yi_to_z(y), rforce); } //! Get radial force for dimensionless coord with harmonic order l and radial orer n - double get_zforce(double r, double z, int m, int n) - { return interp(m, n, r, z, zforce, true); } + double get_zforce(double x, double y, int m, int n) + { return interp(m, n, xi_to_r(x), yi_to_z(y), zforce, true); } //! Get potential for dimensionless coord with harmonic order m and radial orer n void get_pot(Eigen::MatrixXd& p, double r, double z) From ee3e2b5bf9fbcda228e692d17a4d277d7e626dd2 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 22 Apr 2024 10:52:58 -0400 Subject: [PATCH 089/167] Add an informative README to user-modules --- extern/user-modules/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 extern/user-modules/README.md diff --git a/extern/user-modules/README.md b/extern/user-modules/README.md new file mode 100644 index 000000000..0d24864f3 --- /dev/null +++ b/extern/user-modules/README.md @@ -0,0 +1,29 @@ +# User modules + +All sub directories will be scheduled for building by CMake. + +This allows you to include your own code and especially modular force +routines into EXP simulations. We suggest following the examples +`src/user` to get started. The following steps will get you started +quickly: + +1. Make a sub directory of your own choosing and set that as your + working directory +2. Copy the `CMakeLists.txt` file from `src/user/CMakeLists.txt` +3. Pick one of the modules for your template. For example, if you are + interested a applying an external force, check out the + `UserMNdisk.cc` and header `UserMNDisk.H`. This produces the force + and potential Miyamoto-Nagai disk. Copy these to your new + sub directory. Rename them to something mnemonic and adjust they to + suit your needs. +4. Edit `CMakeLists.txt` to include the name of your new module and + source code. Don't forget to remove the original libraries from + `src/user` that you are not using. +5. Then, your next compile will compile and install your code as where + it can be found and used by EXP. + +You can add any code to the directory and it will be built and linked +to EXP libraries. This is useful for building standalone applications +that use EXP classes or pyEXP for your own intricate analysis. For +this, use the code in `utils` as a pattern for your own applications. + From 163f79659d9dc70ce633be5dc7d8efd128fd63cf Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sun, 28 Apr 2024 13:41:19 -0400 Subject: [PATCH 090/167] Version bump [no ci] --- CMakeLists.txt | 2 +- doc/exp.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cd8828311..9f1013994 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21) # Needed for CUDA, MPI, and CTest features project( EXP - VERSION "7.7.29" + VERSION "7.7.30" HOMEPAGE_URL https://github.com/EXP-code/EXP LANGUAGES C CXX Fortran) diff --git a/doc/exp.cfg b/doc/exp.cfg index b098166f9..e7245693b 100644 --- a/doc/exp.cfg +++ b/doc/exp.cfg @@ -38,7 +38,7 @@ PROJECT_NAME = "EXP" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 7.7.29 +PROJECT_NUMBER = 7.7.30 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a From 07865cba0f0c0e910907c57481446437cc03bfbe Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 30 Apr 2024 11:14:12 -0400 Subject: [PATCH 091/167] Test Orbit tree version of angle table computation for debugging [no ci] --- exputil/orbit_trans.cc | 229 ++++++++++++++++++++++++++++++++++++++--- include/orbit.H | 1 + 2 files changed, 215 insertions(+), 15 deletions(-) diff --git a/exputil/orbit_trans.cc b/exputil/orbit_trans.cc index 35edbd32f..e968b35b2 100644 --- a/exputil/orbit_trans.cc +++ b/exputil/orbit_trans.cc @@ -321,7 +321,8 @@ void SphericalOrbit::compute_freq(void) warn(rname, msg.str()); } #endif - accum2 += cost/sqrt(2.0*(energy-ur) - (jmax*jmax*kappa*kappa*s*s)); + double denom = 2.0*(energy-ur) - jmax*jmax*kappa*kappa*s*s; + if (denom>0.0) accum2 += cost/sqrt(denom); } freq[0] = M_PI/(am*accum1*dt); @@ -359,6 +360,164 @@ double rombe2(double a, double b, std::function, int n); double dtp, dtm; void SphericalOrbit::compute_angles(void) +{ + l1s = l2s = 0; + + angle_grid.t. resize(2, recs); + angle_grid.w1. resize(2, recs); + angle_grid.dw1dt.resize(2, recs); + angle_grid.f. resize(2, recs); + angle_grid.r. resize(2, recs); + + if (Gkn.n == 0) { + Gkn = LegeQuad(recs); + dtp = -0.5*M_PI; + dtm = M_PI; + } + else if (Gkn.n != recs) { + Gkn = LegeQuad(recs); + } + + double JJ = kappa * jmax; + + if (freq_defined) { + ap = 0.5*(r_apo + r_peri); + am = 0.5*(r_apo - r_peri); + sp = ap/(r_apo*r_peri); + sm = am/(r_apo*r_peri); + } + else + compute_freq(); + + + const double tol = 1.0e-8; // tolerance for radicand + + auto fw1 = [&](double t) + { + double r = ap + am*sin(t); + double ur = model->get_pot(r); + double radcand = 2.0*(energy-ur)-JJ*JJ/(r*r); + + double eps1 = fabs( 0.5*M_PI + t); + double eps2 = fabs(-0.5*M_PI + t); + + if (radcand < tol or eps1<1.0e-3 or eps2<1.0e-3) { + // value near pericenter + if (t<0) { + radcand = fabs(JJ*JJ/(r_peri*r_peri*r_peri) - model->get_dpot(r_peri)); + // radcand = JJ*JJ/(r_peri*r_peri*r_peri) - model->get_dpot(r_peri); + if (radcand < 0.0) { + std::ostringstream sout; + sout << "fw1: impossible radicand with t=" << t; + throw std::runtime_error(sout.str()); + } + return sqrt(am/radcand); + } + // value near apocenter + else { + radcand = fabs(model->get_dpot(r_apo) - JJ*JJ/(r_apo*r_apo*r_apo)); + // radcand = model->get_dpot(r_apo) - JJ*JJ/(r_apo*r_apo*r_apo); + if (radcand < 0.0) { + std::ostringstream sout; + sout << "fw1: impossible radicand with t=" << t; + throw std::runtime_error(sout.str()); + } + return sqrt(am/radcand); + } + } + + return am*cos(t)/sqrt(radcand); + }; + + auto ff = [&](double t) + { + double s = sp + sm*sin(t); + double ur = model->get_pot(1.0/s); + double radcand = 2.0*(energy-ur) - JJ*JJ*s*s; + + double eps1 = fabs( 0.5*M_PI + t); + double eps2 = fabs(-0.5*M_PI + t); + + if (radcand < tol or eps1<1.0e-3 or eps2<1.0e-3) { + // value near apocenter + if (t<0) { + radcand = fabs(model->get_dpot(r_apo) - JJ*JJ/(r_apo*r_apo*r_apo)); + // radcand = model->get_dpot(r_apo) - JJ*JJ/(r_apo*r_apo*r_apo); + if (radcand < 0.0) { + std::ostringstream sout; + sout << "ff: impossible radicand with t=" << t; + throw std::runtime_error(sout.str()); + } + return sqrt(sm/radcand)/r_apo; + } + // value near pericenter + else { + radcand = fabs(JJ*JJ/(r_peri*r_peri*r_peri) - model->get_dpot(r_peri)); + // radcand = JJ*JJ/(r_peri*r_peri*r_peri) - model->get_dpot(r_peri); + if (radcand < 0.0) { + std::ostringstream sout; + sout << "ff: impossible radicand with t=" << t; + throw std::runtime_error(sout.str()); + } + return sqrt(sm/radcand)/r_peri; + } + } + + return sm*cos(t)/sqrt(radcand); + }; + + double accum1 = 0.0; + double accum2 = 0.0; + double tl = -0.5*M_PI; + double sl = 0.5*M_PI; + + for (int i=0; i0) accum1 += rombe2(tl, t, fw1, nbsct); + + double arg = (1.0/r - sp)/sm, s; + + if (arg>1.0) s = 0.5*M_PI; + else if (arg<-1.0) s = -0.5*M_PI; + else s = asin((1.0/r - sp)/sm); + + if (i>0) accum2 += rombe2(sl, s, ff, nbsct); + + angle_grid.t(0, i) = t; + angle_grid.w1(0, i) = freq[0]*accum1; + angle_grid.dw1dt(0, i) = freq[0]*fw1(t); + angle_grid.f(0, i) = freq[1]*accum1 + JJ*accum2; + angle_grid.r(0, i) = r; + + tl = t; + sl = s; + } + + + Eigen::VectorXd work(angle_grid.t.row(0).size()); + + const double bc = 1.0e30; + + Spline(angle_grid.w1.row(0), angle_grid.t.row(0), bc, bc, work); + angle_grid.t.row(1) = work; + + Spline(angle_grid.w1.row(0), angle_grid.dw1dt.row(0), bc, bc, work); + angle_grid.dw1dt.row(1) = work; + + Spline(angle_grid.w1.row(0), angle_grid.f.row(0), bc, bc, work); + angle_grid.f.row(1) = work; + + Spline(angle_grid.w1.row(0), angle_grid.r.row(0), bc, bc, work); + angle_grid.r.row(1) = work; + + angle_grid.num = recs; + + angle_defined = true; +} + +void SphericalOrbit::compute_angles_old(void) { double accum1,accum2,r, s, t, sl, tl; int i; @@ -401,14 +560,34 @@ void SphericalOrbit::compute_angles(void) double ur = model->get_pot(r); double radcand = 2.0*(energy-ur)-JJ*JJ/(r*r); - if (radcand < tol) { - // values near turning points - double sgn = 1.0; - if (t<0) sgn = -1.0; - r = ap + sgn*am; - double dudr = model->get_dpot(r); - return sqrt( am*sgn/(dudr-JJ*JJ/(r*r*r)) ); + double eps1 = fabs( 0.5*M_PI + t); + double eps2 = fabs(-0.5*M_PI + t); + + if (radcand < tol or eps1<1.0e-3 or eps2<1.0e-3) { + // value near pericenter + if (t<0) { + radcand = fabs(JJ*JJ/(r_peri*r_peri*r_peri) - model->get_dpot(r_peri)); + // radcand = JJ*JJ/(r_peri*r_peri*r_peri) - model->get_dpot(r_peri); + if (radcand < 0.0) { + std::ostringstream sout; + sout << "fw1: impossible radicand with t=" << t; + throw std::runtime_error(sout.str()); + } + return sqrt(am/radcand); + } + // value near apocenter + else { + radcand = fabs(model->get_dpot(r_apo) - JJ*JJ/(r_apo*r_apo*r_apo)); + // radcand = model->get_dpot(r_apo) - JJ*JJ/(r_apo*r_apo*r_apo); + if (radcand < 0.0) { + std::ostringstream sout; + sout << "fw1: impossible radicand with t=" << t; + throw std::runtime_error(sout.str()); + } + return sqrt(am/radcand); + } } + return am*cos(t)/sqrt(radcand); }; @@ -418,14 +597,34 @@ void SphericalOrbit::compute_angles(void) double ur = model->get_pot(1.0/s); double radcand = 2.0*(energy-ur) - JJ*JJ*s*s; - if (radcand < tol) { - // values near turning points - double sgn = 1.0; - if (t<0) sgn = -1.0; - s = sp + sgn*sm; - double dudr = model->get_dpot(1.0/s); - return sqrt( -sm*s*s*sgn/(dudr-JJ*JJ*s*s*s) ); + double eps1 = fabs( 0.5*M_PI + t); + double eps2 = fabs(-0.5*M_PI + t); + + if (radcand < tol or eps1<1.0e-3 or eps2<1.0e-3) { + // value near apocenter + if (t<0) { + radcand = fabs(model->get_dpot(r_apo) - JJ*JJ/(r_apo*r_apo*r_apo)); + // radcand = model->get_dpot(r_apo) - JJ*JJ/(r_apo*r_apo*r_apo); + if (radcand < 0.0) { + std::ostringstream sout; + sout << "ff: impossible radicand with t=" << t; + throw std::runtime_error(sout.str()); + } + return sqrt(sm/radcand)/r_apo; + } + // value near pericenter + else { + radcand = fabs(JJ*JJ/(r_peri*r_peri*r_peri) - model->get_dpot(r_peri)); + // radcand = JJ*JJ/(r_peri*r_peri*r_peri) - model->get_dpot(r_peri); + if (radcand < 0.0) { + std::ostringstream sout; + sout << "ff: impossible radicand with t=" << t; + throw std::runtime_error(sout.str()); + } + return sqrt(sm/radcand)/r_peri; + } } + return sm*cos(t)/sqrt(radcand); }; diff --git a/include/orbit.H b/include/orbit.H index c99bf0fd3..dfdf31e82 100644 --- a/include/orbit.H +++ b/include/orbit.H @@ -69,6 +69,7 @@ private: void compute_action(void) { compute_freq(); } void compute_freq(void); void compute_angles(void); + void compute_angles_old(void); void compute_freq_epi(void); void compute_angles_epi(void); void compute_biorth(void); From fec7c44723b9c9d389e53b0aed1e76b526eb7aa3 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 30 Apr 2024 16:07:28 -0400 Subject: [PATCH 092/167] Don't install HighFive headers [no ci] --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f1013994..c105459ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -218,7 +218,7 @@ set(HIGHFIVE_BUILD_DOCS OFF CACHE BOOL "Do not build the documentation") set(HIGHFIVE_USE_BOOST OFF CACHE BOOL "Do not use Boost in HighFIve") set(H5_USE_EIGEN TRUE CACHE BOOL "Eigen3 support in HighFive") -add_subdirectory(extern/HighFive) +add_subdirectory(extern/HighFive EXCLUDE_FROM_ALL) # Configure the remaining native subdirectories add_subdirectory(exputil) From b307d7a51c6a69a71221863f147c1d2d457d6662 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 8 May 2024 14:19:43 -0400 Subject: [PATCH 093/167] Updated pybind11, HighFive, and yaml-cpp to latest upstream versions; added a HighFive template specification for the new API [no ci] --- exputil/SLGridMP2.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index c74f84115..ba0819260 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -541,8 +541,8 @@ bool SLGridSph::ReadH5Cache(void) // Table arrays will be allocated // - arrays.getDataSet("ev").read(table[l].ev); - arrays.getDataSet("ef").read(table[l].ef); + arrays.getDataSet("ev").read(table[l].ev); + arrays.getDataSet("ef").read(table[l].ef); } if (myid==0) From 8ffd856b48191f984b8d79e213ef4411c5747694 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Thu, 9 May 2024 11:19:08 +0100 Subject: [PATCH 094/167] Remove CBrock, CBrockDisk, Hernquist --- src/CBrock.H | 43 --- src/CBrock.cc | 147 -------- src/CBrockDisk.H | 140 ------- src/CBrockDisk.cc | 913 --------------------------------------------- src/CMakeLists.txt | 6 +- src/Component.H | 9 - src/Component.cc | 12 - src/Hernquist.H | 40 -- src/Hernquist.cc | 146 -------- src/TwoCenter.H | 6 +- src/TwoCenter.cc | 12 - 11 files changed, 4 insertions(+), 1470 deletions(-) delete mode 100644 src/CBrock.H delete mode 100644 src/CBrock.cc delete mode 100644 src/CBrockDisk.H delete mode 100644 src/CBrockDisk.cc delete mode 100644 src/Hernquist.H delete mode 100644 src/Hernquist.cc diff --git a/src/CBrock.H b/src/CBrock.H deleted file mode 100644 index 09926085d..000000000 --- a/src/CBrock.H +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef _CBrock_H -#define _CBrock_H - -#include - -//! Biorthonormal pairs based on Clutton-Brock's ultraspherical series -class CBrock : public SphericalBasis -{ - -private: - - Eigen::VectorXd uu, duu; - - void get_dpotl(int lmax, int nmax, double r, - Eigen::MatrixXd& p, Eigen::MatrixXd& dp, int tid); - void get_potl(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid); - void get_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid); - void get_potl_dens(int lmax, int nmax, double r, - Eigen::MatrixXd& p, Eigen::MatrixXd& d,int tid); - - void initialize(void); - - double knl(int, int); - double norm(int, int); - - double r_to_xi (double r); - double xi_to_r (double x); - double d_r_to_xi(double r); - - //! Valid keys for YAML configurations - static const std::set valid_keys; - -public: - - //! Constructor - /*! \param c0 is the instantiating component */ - /*! \param line is the parameter string */ - /*! \param m is the MixtureBasis for a multicenter expansion */ - CBrock(Component* c0, const YAML::Node& conf, MixtureBasis* m=0); - -}; - -#endif diff --git a/src/CBrock.cc b/src/CBrock.cc deleted file mode 100644 index 7de5cd0b0..000000000 --- a/src/CBrock.cc +++ /dev/null @@ -1,147 +0,0 @@ -#include "expand.H" -#include - -CBrock::CBrock(Component* c0, const YAML::Node& conf, MixtureBasis *m) : - SphericalBasis(c0, conf, m) -{ - id = "Clutton-Brock sphere"; - initialize(); - setup(); -} - -double CBrock::knl(int n, int l) -{ - return 4.0*n*(n+2*l+2) + (2*l+1)*(2*l+3); -} - -double CBrock::norm(int n, int l) -{ - return M_PI * knl(n,l) * - exp( - -log(2.0)*((double)(4*l+4)) - - lgamma((double)(1+n)) - 2.0*lgamma((double)(1+l) ) - + lgamma((double)(2*l+n+2)) - )/(double)(l+n+1); -} - - -void CBrock::initialize(void) -{ - // Do nothing -} - -void CBrock::get_dpotl(int lmax, int nmax, double r, - Eigen::MatrixXd& p, Eigen::MatrixXd& dp, int tid) -{ - double x = r_to_xi(r); - double dx = d_r_to_xi(r); - - double fac = 0.5*sqrt(1.0 - x*x); - double rfac = sqrt(0.5*(1.0 - x)); - double drfac = -0.5/(1.0 - x*x); - - for (int l=0; l<=lmax; l++) { - double dfac1 = 1.0 + x + 2.0*x*l; - double dfac2 = 2.0*(l + 1); - - get_ultra(nmax-1, (double)l, x, u[tid]); - get_ultra(nmax-1, (double)(l+1), x, du[tid]); - - for (int n=0; n=1.0) - return BIG; - else - return sqrt( (1.0+xi)/(1.0-xi) ); -} - -double CBrock::d_r_to_xi(double r) -{ - double fac; - - fac = r*r + 1.0;; - return 4.0*r/(fac*fac); -} - -double CBrock::r_to_xi(double r) -{ - return (r*r-1.0)/(r*r+1.0); -} -#undef BIG diff --git a/src/CBrockDisk.H b/src/CBrockDisk.H deleted file mode 100644 index f77ea597b..000000000 --- a/src/CBrockDisk.H +++ /dev/null @@ -1,140 +0,0 @@ -#ifndef _CBrockDisk_H -#define _CBrockDisk_H - -#include -#include -#include -#include - -#include -#include -#include - -class MixtureBasis; - -//! This routine computes the potential, acceleration and density -//! using the Clutton-Brock flat disk expansion -class CBrockDisk : public AxisymmetricBasis -{ - -private: - - MixtureBasis *mix; - pthread_mutex_t used_lock, cos_coef_lock, sin_coef_lock; - double *cylmass0; - - std::shared_ptr expcoef, expcoef1, expcoefP; - std::vector expcoef0, cc, cc1; - Eigen::MatrixXd normM, dend, work; - std::vector potd, dpot; - std::vector u, du, cosm, sinm; - - int use1, use0; - - void get_dpotl(int lmax, int nmax, double r, Eigen::MatrixXd& p, Eigen::MatrixXd& dp); - void get_potl(int lmax, int nmax, double r, Eigen::MatrixXd& p); - void get_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p); - void get_potl_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p, Eigen::MatrixXd& d); - double norm(int,int); - - void get_pot_coefs(int, const Eigen::VectorXd&, double &, double &); - void get_dens_coefs(int, const Eigen::VectorXd&, double &); - - void get_pot_coefs_safe(int, const Eigen::VectorXd&, double &, double &, Eigen::MatrixXd& p, Eigen::MatrixXd& d); - - void initialize(void); - - // These can be hidden from the interface - void determine_coefficients(); - void determine_coefficients_playback(); - void determine_coefficients_particles(); - void determine_acceleration_and_potential(); - - void * determine_coefficients_thread(void * arg); - void * determine_acceleration_and_potential_thread(void * arg); - - // Parameters - double rmax, scale; - int Lmax, nmax; - bool self_consistent; - bool NO_M0, NO_M1, EVEN_M, M0_only; - - //! Coefficient magic number - const unsigned int cmagic = 0xc0a57a4; - - //! Playback object - std::shared_ptr playback; - - /** Master node ships coefficients to hosts. True (default) implies - that only the master node caches the coefficients for playback - to save core memory. This is set in the config input using the - 'coefMaster: bool' parameter. Once I am sure that there are no - algorithmic issues, I will remove this as an option. - */ - bool coefMaster; - - //! Last playback coefficient evaluation time - double lastPlayTime; - - //! Coefficient container instance for writing HDF5 - CoefClasses::CylCoefs cylCoefs; - - //! Valid keys for YAML configurations - static const std::set valid_keys; - -public: - - //! Constructor - //! \param c0 is the instantiating caller (Component) - //! \param line is the parameter string - //! \param m is the MixtureBasis for a multicenter expansion - CBrockDisk(Component* c0, const YAML::Node& conf, MixtureBasis* m=0); - - //! Destructor - virtual ~CBrockDisk(void); - - //! Force evaluation called clients - void get_acceleration_and_potential(Component*); - - - /** - Supply density, potential and derivatives at arbitrary point - in polar coordinates. Currently this is used to implement the - required determine_fields_at_point members by - ignoring the z coordinate. - */ - void - determine_fields_at_point(double x, double y, double z, - double *tdens0, double *tpotl0, - double *tdens, double *tpotl, - double *tpotX, double *tpotY, - double *tpotZ); - - void - determine_fields_at_point_polar(double r, double phi, - double *tdens0, double *tpotl0, - double *tdens, double *tpotl, - double *tpotr, double *tpotp); - void - determine_fields_at_point_sph(double r, double theta, double phi, - double *tdens0, double *tpotl0, - double *tdens, double *tpotl, - double *tpotr, double *tpott, - double *tpotp); - - void - determine_fields_at_point_cyl(double r, double z, double phi, - double *tdens0, double *tpotl0, - double *tdens, double *tpotl, - double *tpotr, double *tpotz, - double *tpotp); - - //! Save coefficients to file (need type marker to id dump, component id?) - void dump_coefs(ostream& out); - - //! Save coefficients to HDF5 file - void dump_coefs_h5(const std::string& file); -}; - - -#endif diff --git a/src/CBrockDisk.cc b/src/CBrockDisk.cc deleted file mode 100644 index 0c2f66ecc..000000000 --- a/src/CBrockDisk.cc +++ /dev/null @@ -1,913 +0,0 @@ -#include "expand.H" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -const std::set -CBrockDisk::valid_keys = { - "rmax", - "scale", - "Lmax", - "nmax", - "self_consistent", - "playback", - "coefCompute", - "coefMaster" - "NO_M0", - "NO_M1", - "EVEN_M", - "M0_only" -}; - -CBrockDisk::CBrockDisk(Component* c0, const YAML::Node& conf, MixtureBasis* m) : AxisymmetricBasis(c0, conf) -{ - id = "Clutton-Brock two-dimensional disk"; - geometry = cylinder; - - dof = 2; // Two degrees of freedom - mix = m; - - rmax = 100.0; - scale = 1.0; - Lmax = 4; - nmax = 10; - NO_M0 = false; - NO_M1 = false; - EVEN_M = false; - M0_only = false; - - self_consistent = true; - coef_dump = true; - - initialize(); - - expcoef = std::make_shared(2*Lmax+1, nmax); - expcoef1 = std::make_shared(2*Lmax+1, nmax); - - expcoef0.resize(nthrds); - for (auto & v : expcoef0) v.resize(2*Lmax+1, nmax); - - if (pcavar) { - cc.resize((Lmax+1)*(Lmax+1)); - for (auto & v : cc) v.resize(nmax, nmax); - - cc1.resize((Lmax+1)*(Lmax+1)); - for (auto & v : cc1) v.resize(nmax, nmax); - - pthread_mutex_init(&cc_lock, NULL); - } - - // Allocate and compute normalization matrix - - normM.resize(Lmax+1, nmax); - dend .resize(Lmax+1, nmax); - work .resize(Lmax+2, nmax); - - potd.resize(nthrds); - dpot.resize(nthrds); - - // Needed for dpot recursion--------+ - // | - // v - for (auto & v : potd) v.resize(Lmax+2, nmax); - for (auto & v : dpot) v.resize(Lmax+1, nmax); - // ^ - // | - // Otherwise, we only need m<=Lmax--+ - - // Work vectors - // - u .resize(nthrds); - du.resize(nthrds); - - for (auto & v : u ) v.resize(nmax); - for (auto & v : du) v.resize(nmax); - - for (int l=0; l<=Lmax; l++) { - for (int n=0; n(); - if (conf["scale"]) scale = conf["scale"].as(); - if (conf["Lmax"]) Lmax = conf["Lmax"].as(); - if (conf["nmax"]) nmax = conf["nmax"].as(); - - if (conf["NO_M0"]) NO_M0 = conf["NO_M0"]. as(); - if (conf["NO_M1"]) NO_M1 = conf["NO_M1"]. as(); - if (conf["EVEN_M"]) EVEN_M = conf["EVEN_M"]. as(); - if (conf["M0_ONLY"]) M0_only = conf["M0_ONLY"].as(); - - if (conf["self_consistent"]) self_consistent = conf["self_consistent"].as(); - if (conf["playback"]) { - std::string file = conf["playback"].as(); - // Check the file exists - { - std::ifstream test(file); - if (not test) { - std::cerr << "CBrockDisk: process " << myid << " cannot open <" - << file << "> for reading" << std::endl; - throw std::runtime_error("CBrockDisk: file open error"); - } - } - - playback = std::make_shared(file); - - if (playback->nmax != nmax) { - if (myid==0) { - std::cerr << "CBrockDisk: nmax for playback [" << playback->nmax - << "] does not match specification [" << nmax << "]" - << std::endl; - } - throw std::runtime_error("CBrockDisk: parameter mismatch"); - } - - if (playback->Lmax != Lmax) { - if (myid==0) { - std::cerr << "CBrockDisk: Lmax for playback [" << playback->Lmax - << "] does not match specification [" << Lmax << "]" - << std::endl; - } - throw std::runtime_error("CBrockDisk: parameter mismatch"); - } - - play_back = true; - - if (conf["coefCompute"]) play_cnew = conf["coefCompute"].as(); - - if (conf["coefMaster"]) coefMaster = conf["coefMaster"].as(); - - if (myid==0) { - std::cout << "---- Playback is ON for Component " << component->name - << " using Force " << component->id << std::endl; - if (coefMaster) - std::cout << "---- Playback will use MPI master" << std::endl; - if (play_cnew) - std::cout << "---- New coefficients will be computed from particles on playback" << std::endl; - } - } - - } - catch (YAML::Exception & error) { - if (myid==0) std::cout << "Error parsing parameters in CBrockDisk: " - << error.what() << std::endl - << std::string(60, '-') << std::endl - << "Config node" << std::endl - << std::string(60, '-') << std::endl - << conf << std::endl - << std::string(60, '-') << std::endl; - throw std::runtime_error("CBrockDisk: error parsing YAML"); - } - -} - -CBrockDisk::~CBrockDisk(void) -{ - if (pcavar) { - pthread_mutex_destroy(&cc_lock); - } -} - -void CBrockDisk::get_acceleration_and_potential(Component* curComp) -{ - cC = curComp; - - //======================================== - // No coefficients for external particles - //======================================== - - if (use_external) { - - MPL_start_timer(); - determine_acceleration_and_potential(); - MPL_stop_timer(); - - use_external = false; - - return; - } - - //====================================== - // Determine potential and acceleration - //====================================== - - MPL_start_timer(); - - determine_acceleration_and_potential(); - - MPL_stop_timer(); - - // Clear external potential flag - use_external = false; -} - -void CBrockDisk::determine_coefficients(void) -{ - if (play_back) { - determine_coefficients_playback(); - if (play_cnew) determine_coefficients_particles(); - } else { - determine_coefficients_particles(); - } -} - -void CBrockDisk::determine_coefficients_playback(void) -{ - // Do we need new coefficients? - if (tnow <= lastPlayTime) return; - lastPlayTime = tnow; - - if (coefMaster) { - - if (myid==0) expcoefP = playback->interpolate(tnow); - - MPI_Bcast(expcoefP->data(), expcoefP->size(), MPI_DOUBLE, 0, MPI_COMM_WORLD); - } else { - expcoefP = playback->interpolate(tnow); - } -} - -void CBrockDisk::determine_coefficients_particles(void) -{ - int compute; - - if (!self_consistent && !initializing) return; - - if (pcavar) compute = !(this_step%npca); - - // Clean - std::fill(expcoef ->data(), expcoef ->data() + expcoef ->size(), 0.0); - std::fill(expcoef1->data(), expcoef1->data() + expcoef1->size(), 0.0); - - for (auto & v : expcoef0) v.setZero(); - if (pcavar && compute) { - for (auto & v : cc1) v.setZero(); - } - - use0 = 0; - use1 = 0; - - exp_thread_fork(true); - - for (int i=0; idata(), expcoef->data(), - expcoef->size(), MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD); - } - - if (pcavar) { - - parallel_gather_coefficients(); - - if (myid == 0) pca_hall(compute); - - parallel_distribute_coefficients(); - - } - -} - -void * CBrockDisk::determine_coefficients_thread(void * arg) -{ - double pos[3], xx, yy, zz, r, r2, phi, rs, fac1, fac2, mass; - - int nbodies = cC->Number(); - int id = *((int*)arg); - int nbeg = nbodies*id/nthrds; - int nend = nbodies*(id+1)/nthrds; - double adb = component->Adiabatic(); - - vector ctr(3, 0.0); - if (mix) mix->getCenter(ctr); - - PartMapItr it = cC->Particles().begin(); - std::advance(it, nbeg); - - for (int i=nbeg; ifirst; - - if (component->freeze(j)) // frozen particles do not respond - continue; - - mass = cC->Mass(j)* adb; - - if (mix) { - for (int k=0; k<3; k++) - pos[k] = cC->Pos(j, k, Component::Local) - ctr[k]; - } else { - for (int k=0; k<3; k++) - pos[k] = cC->Pos(j, k, Component::Local | Component::Centered); - } - - xx = pos[0]; - yy = pos[1]; - zz = pos[2]; - - r2 = (xx*xx + yy*yy); - r = sqrt(r2) + DSMALL; - - if (r<=rmax) { - use1++; - phi = atan2(yy,xx); - rs = r/scale; - - - sinecosine_R(Lmax, phi, cosm[id], sinm[id]); - get_potl(Lmax, nmax, rs, potd[id]); - - // l loop - - for (int n=0; nNumber(); - int id = *((int*)arg); - int nbeg = nbodies*id/nthrds; - int nend = nbodies*(id+1)/nthrds; - - std::vector ctr(3, 0.0); - if (mix) mix->getCenter(ctr); - - PartMapItr it=cC->Particles().begin(); - std::advance(it, nbeg); - - for (int i=nbeg; ifirst; - - if (mix) { - if (use_external) { - cC->Pos(pos, j, Component::Inertial); - component->ConvertPos(pos, Component::Local); - } else - cC->Pos(pos, j, Component::Local); - - mfactor = mix->Mixture(pos); - } else { - if (use_external) { - cC->Pos(pos, j, Component::Inertial); - component->ConvertPos(pos, Component::Local | Component::Centered); - } else - cC->Pos(pos, j, Component::Local | Component::Centered); - } - - double xx = pos[0] - ctr[0]; - double yy = pos[1] - ctr[1]; - double zz = pos[2] - ctr[2]; - - double r = sqrt(xx*xx + yy*yy) + DSMALL; - - double rs = r/scale; - double phi = atan2(yy,xx); - - sinecosine_R(Lmax, phi, cosm[id], sinm[id]); - get_dpotl(Lmax, nmax, rs, potd[id], dpot[id]); - - double potl = 0.0, potr = 0.0, pott = 0.0, potp = 0.0; - - if (not NO_M0) { - get_pot_coefs_safe(0, expcoef->row(0), p, dp, potd[id], dpot[id]); - - double potl = p; - double potr = dp; - } - - // Asymmetric terms? - // - if (not M0_only) { - - // l loop - for (int l=1; l<=Lmax; l++) { - - // Skip m=1 terms? - // - if (NO_M1 && l==1) continue; - - // Skip odd m terms? - // - if (EVEN_M && (l/2)*2 != l) continue; - - double pc, dpc, ps, dps; - - get_pot_coefs_safe(l, expcoef->row(2*l - 1), pc, dpc, potd[id], dpot[id]); - get_pot_coefs_safe(l, expcoef->row(2*l ), ps, dps, potd[id], dpot[id]); - - potl += (pc *cosm[id][l] + ps *sinm[id][l]) * M_SQRT2; - potr += (dpc*cosm[id][l] + dps*sinm[id][l]) * M_SQRT2; - potp += (-pc*sinm[id][l] + ps *cosm[id][l]) * M_SQRT2 * l; - } - } - - double fac = xx*xx + yy*yy; - - potr *= mfactor/(scale*scale); - potl *= mfactor/scale; - potp *= mfactor/scale; - - cC->AddAcc(j, 0, -potr*xx/r); - cC->AddAcc(j, 1, -potr*yy/r); - if (fac > DSMALL) { - cC->AddAcc(j, 0, potp*yy/fac); - cC->AddAcc(j, 1, -potp*xx/fac); - } - - cC->AddPot(j, potl); - - it++; - } - - - return (NULL); -} - -void -CBrockDisk::determine_fields_at_point_sph(double r, double theta, double phi, - double *tdens0, double *tpotl0, - double *tdens, double *tpotl, - double *tpotr, double *tpott, - double *tpotp) - -{ - determine_fields_at_point_polar(r, phi, tdens0, tpotl0, tdens, tpotl, tpotr, tpotp); - *tpott = 0.0; -} - - -void -CBrockDisk::determine_fields_at_point_cyl(double r, double z, double phi, - double *tdens0, double *tpotl0, - double *tdens, double *tpotl, - double *tpotr, double *tpott, - double *tpotp) - -{ - determine_fields_at_point_polar(r, phi, tdens0, tpotl0, tdens, tpotl, tpotr, tpotp); - *tpott = 0.0; -} - -void -CBrockDisk::determine_fields_at_point(double x, double y, double z, - double *tdens0, double *tpotl0, - double *tdens, double *tpotl, - double *tpotX, double *tpotY, - double *tpotZ) - -{ - double R = sqrt(x*x + y*y); - double phi = atan2(y, x); - double cph = cos(phi), sph = sin(phi); - double tpotR, tpotP; - - determine_fields_at_point_polar(R, phi, tdens0, tpotl0, tdens, tpotl, - &tpotR, &tpotP); - - *tpotZ = tpotR*cph - tpotP*sph ; - *tpotY = tpotR*sph + tpotP*cph ; - *tpotZ = 0.0; -} - - -void CBrockDisk::determine_fields_at_point_polar -( - double r, double phi, - double *tdens0, double *tpotl0, - double *tdens, double *tpotl, double *tpotr, double *tpotp - ) -{ - double p, dp, dens; - - double rs = r/scale; - - sinecosine_R(Lmax, phi, cosm[0], sinm[0]); - - get_dens (Lmax, nmax, rs, dend); - get_dpotl(Lmax, nmax, rs, potd[0], dpot[0]); - - get_dens_coefs(0, expcoef->row(0), dens); - - get_pot_coefs(0, expcoef->row(0), p, dp); - - double potl = p; - double potr = dp; - double potp = 0.0; - - *tdens0 = dens; - *tpotl0 = potl; - - // l loop - - for (int l=1; l<=Lmax; l++) { - - double pc, ps, dpc, dps; - - get_dens_coefs(l,expcoef->row(2*l - 1), pc); - get_dens_coefs(l,expcoef->row(2*l ), ps); - dens += (pc*cosm[0][l] + ps*sinm[0][l]) * M_SQRT2; - - get_pot_coefs(l,expcoef->row(2*l - 1), pc, dpc); - get_pot_coefs(l,expcoef->row(2*l ), ps, dps); - potl += (pc *cosm[0][l] + ps *sinm[0][l]) * M_SQRT2; - potr += (dpc*cosm[0][l] + dps*sinm[0][l]) * M_SQRT2; - potp += (-pc*sinm[0][l] + ps *cosm[0][l]) * M_SQRT2 * l; - } - - *tdens0 /= scale*scale*scale; - *tpotl0 /= scale; - - *tdens = dens/(scale*scale*scale); - *tpotl = potl/scale; - *tpotr = potr/(scale*scale); - *tpotp = potp/scale; -} - - -void CBrockDisk::get_pot_coefs(int l, const Eigen::VectorXd& coef, - double& p, double& dp) -{ - double pp, dpp; - - pp = dpp = 0.0; - - for (int i=0; i1) { - d(l, 1) = work(l+1, 1)*rcum; - for (int nn=1; nn1) { - d(l, 1) = work(l+1, 1)*rcumd; - for (int nn=1; nn(&cmagic), sizeof(unsigned int)); - - // Write YAML string size - // - out.write(reinterpret_cast(&hsize), sizeof(unsigned int)); - - // Write YAML string - // - out.write(reinterpret_cast(y.c_str()), hsize); - - // Write coefficient matrix - // - out.write((char *)expcoef->data(), expcoef->size()*sizeof(double)); -} - -void CBrockDisk::dump_coefs_h5(const std::string& file) -{ - // Add the current coefficients - auto cur = std::make_shared(); - - cur->time = tnow; - cur->geom = geoname[geometry]; - cur->id = id; - cur->mmax = Lmax; - cur->nmax = nmax; - - cur->allocate(); - - Eigen::VectorXd cos1(nmax), sin1(nmax); - - auto & cofs = *cur->coefs; - - for (int m=0; m<=Lmax; m++) { - for (int ir=0; irctr = component->getCenter(Component::Local | Component::Centered); - - // Check if file exists - // - if (std::filesystem::exists(file)) { - cylCoefs.clear(); - cylCoefs.add(cur); - cylCoefs.ExtendH5Coefs(file); - } else { - // Copy the YAML config. We only need this on the first call. - std::ostringstream sout; sout << conf; - cur->buf = sout.str(); // Copy to CoefStruct buffer - - // Add the name attribute. We only need this on the first call. - cylCoefs.setName(component->name); - - // Add the new coefficients and write the new HDF5 - cylCoefs.clear(); - cylCoefs.add(cur); - cylCoefs.WriteH5Coefs(file); - } -} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a88847a5f..04bd285ae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,9 +7,9 @@ if (ENABLE_CUDA) cudaBiorthCyl.cu cudaCube.cu cudaSlabSL.cu) endif() -set(exp_SOURCES Basis.cc Bessel.cc CBrock.cc Component.cc - CBrockDisk.cc Cube.cc Cylinder.cc ExternalCollection.cc - ExternalForce.cc Hernquist.cc Orient.cc PotAccel.cc ScatterMFP.cc +set(exp_SOURCES Basis.cc Bessel.cc Component.cc + Cube.cc Cylinder.cc ExternalCollection.cc + ExternalForce.cc Orient.cc PotAccel.cc ScatterMFP.cc PeriodicBC.cc SphericalBasis.cc AxisymmetricBasis.cc Sphere.cc TwoDCoefs.cc TwoCenter.cc EJcom.cc global.cc begin.cc ddplgndr.cc Direct.cc Shells.cc NoForce.cc end.cc OutputContainer.cc OutPS.cc diff --git a/src/Component.H b/src/Component.H index fd4339ca4..5dfc1ceb5 100644 --- a/src/Component.H +++ b/src/Component.H @@ -50,15 +50,6 @@ struct loadb_datum \par bessel %Bessel function basis, see Bessel - \par c_brock - Clutton-Brock's spherical basis, see CBrock - - \par c_brock_disk - Clutton-Brock's two-dimensional disk basis, see CBrockDisk - - \par hernq - %Hernquist's spherical basis, see Hernquist - \par sphereSL Weinberg's adaptive spherical basis derived by numerical solution to the Sturm-Liouville equation, see Sphere diff --git a/src/Component.cc b/src/Component.cc index 147ba4f75..d696a43da 100644 --- a/src/Component.cc +++ b/src/Component.cc @@ -9,9 +9,6 @@ #include #include -#include -#include -#include #include #include #include @@ -868,15 +865,6 @@ void Component::configure(void) if ( !id.compare("bessel") ) { force = new Bessel(this, fconf); } - else if ( !id.compare("c_brock") ) { - force = new CBrock(this, fconf); - } - else if ( !id.compare("c_brock_disk") ) { - force = new CBrockDisk(this, fconf); - } - else if ( !id.compare("hernq") ) { - force = new Hernquist(this, fconf); - } else if ( !id.compare("sphereSL") ) { force = new Sphere(this, fconf); } diff --git a/src/Hernquist.H b/src/Hernquist.H deleted file mode 100644 index 193f88453..000000000 --- a/src/Hernquist.H +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef _Hernquist_H -#define _Hernquist_H - -#include - -class MixtureBasis; - -//! Biorthonormal pairs for the Hernquist model -class Hernquist : public SphericalBasis -{ - -private: - - void get_dpotl(int lmax, int nmax, double r, - Eigen::MatrixXd& p, Eigen::MatrixXd& dp, int tid); - void get_potl(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid); - void get_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid); - void get_potl_dens(int lmax, int nmax, double r, - Eigen::MatrixXd& p, Eigen::MatrixXd& d, int tid); - - double norm(int, int); - double knl(int, int); - - double xi_to_r (double x); - double d_r_to_xi(double r); - double r_to_xi (double r); - - void initialize(void); - - //! Valid keys for YAML configurations - static const std::set valid_keys; - -public: - - //! Constructor - Hernquist(Component* c0, const YAML::Node& conf, MixtureBasis* m=0); - -}; - -#endif diff --git a/src/Hernquist.cc b/src/Hernquist.cc deleted file mode 100644 index a365ab0b8..000000000 --- a/src/Hernquist.cc +++ /dev/null @@ -1,146 +0,0 @@ -#include -#include "expand.H" -#include - -Hernquist::Hernquist(Component* c0, const YAML::Node& conf, MixtureBasis* m) : - SphericalBasis(c0, conf, m) -{ - id = "Hernquist sphere"; - initialize(); - setup(); -} - -void Hernquist::initialize(void) -{ - // Do nothing . . . -} - -double Hernquist::knl(int n, int l) -{ - return 0.5*n*(n+4*l+3) + (l+1)*(2*l+1); -} - -double Hernquist::norm(int n, int l) -{ - extern double dgammln(double); - return M_PI * knl(n, l) * - exp( - -log(2.0)*((double)(8*l+4)) - - lgamma((double)(1+n)) - 2.0*lgamma((double)(1.5+2.0*l)) - + lgamma((double)(4*l+n+3)) - )/(double)(2*l+n+1.5); -} - -void Hernquist::get_dpotl(int lmax, int nmax, double r, - Eigen::MatrixXd& p, Eigen::MatrixXd& dp, - int tid) -{ - double x = r_to_xi(r); - double dx = d_r_to_xi(r); - double fac = 0.25*(1.0 - x*x); - double rfac = 0.5*(1.0 - x); - double drfac = -1.0/(1.0 - x*x); - - for (int l=0; l<=lmax; l++) { - double dfac1 = 1.0 + x + 2.0*x*l; - double dfac2 = 4.0*l + 3.0; - - get_ultra(nmax-1, 2.0*l+0.5, x, u[tid]); - get_ultra(nmax-1, 2.0*l+1.5, x, du[tid]); - - for (int n=0; n=1.0) - return BIG; - else - return (1.0+x)/(1.0-x); -} - -double Hernquist::d_r_to_xi(double r) -{ - double fac; - - fac = r + 1.0; - return 2.0/(fac*fac); -} - -double Hernquist::r_to_xi(double r) -{ - return (r-1.0)/(r+1.0); -} -#undef BIG diff --git a/src/TwoCenter.H b/src/TwoCenter.H index d2aa27387..aa1a96b52 100644 --- a/src/TwoCenter.H +++ b/src/TwoCenter.H @@ -3,11 +3,8 @@ #include -#include #include #include -#include -#include #include class MixtureBasis; @@ -31,8 +28,7 @@ class MixtureBasis; \param basis is the name of the force method to use for the two-center expansion. Available types are: "bessel" (Bessel), - "c_brock" (CBrock), "c_brock_disk" (CBrockDisk), "hernq" - (Hernquist), "sphereSL" (Sphere), "cylinder" (Cylinder). + "sphereSL" (Sphere), "cylinder" (Cylinder). */ class TwoCenter : public PotAccel { diff --git a/src/TwoCenter.cc b/src/TwoCenter.cc index 93b0aedc0..7a7e33e39 100644 --- a/src/TwoCenter.cc +++ b/src/TwoCenter.cc @@ -49,18 +49,6 @@ TwoCenter::TwoCenter(Component* c0, const YAML::Node& conf) : PotAccel(c0, conf) exp_in = new Bessel(c0, conf, mix_in ); exp_out = new Bessel(c0, conf, mix_out); } - else if ( !basis.compare("c_brock") ) { - exp_in = new CBrock(c0, conf, mix_in ); - exp_out = new CBrock(c0, conf, mix_out); - } - else if ( !basis.compare("c_brock_disk") ) { - exp_in = new CBrockDisk(c0, conf, mix_in ); - exp_out = new CBrockDisk(c0, conf, mix_out); - } - else if ( !basis.compare("hernq") ) { - exp_in = new Hernquist(c0, conf, mix_in ); - exp_out = new Hernquist(c0, conf, mix_out); - } else if ( !basis.compare("sphereSL") ) { exp_in = new Sphere(c0, conf, mix_in ); exp_out = new Sphere(c0, conf, mix_out); From 03300c68de6ed6607c1a6d286011139e9380d193 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 9 May 2024 12:09:32 -0400 Subject: [PATCH 095/167] Added a 'look alive' time-step marker for interactive use --- src/step.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/step.cc b/src/step.cc index e17db5cd1..c5d562f1d 100644 --- a/src/step.cc +++ b/src/step.cc @@ -340,6 +340,9 @@ void do_step(int n) // Stop the total step timer if (step_timing) timer_tot.stop(); + if (VERBOSE==2 and myid==0) // Time step marker + std::cout << << std::endl << ">>>" << this_step << "<<<" << std::endl; + // Timer output if (step_timing && this_step!=0 && (this_step % tskip) == 0) { if (myid==0) { From 4bd74a02c231770aad6696c358e41a935b2139c1 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 9 May 2024 13:24:42 -0400 Subject: [PATCH 096/167] Fixes for FieldGenerator (eval functions not following the correct field-ordering convention) [no ci] --- expui/BiorthBasis.cc | 57 ++++++++++++++++++++++---------------------- src/step.cc | 2 +- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index b13e39e56..68897a4cc 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -531,15 +531,15 @@ namespace BasisClasses double potlfac = 1.0/scale; return - {den0 * densfac, - den1 * densfac, - (den0 + den1) * densfac, - pot0 * potlfac, - pot1 * potlfac, - (pot0 + pot1) * densfac, - potr * (-potlfac)/scale, - pott * (-potlfac), - potp * (-potlfac)}; + {den0 * densfac, // 0 + den1 * densfac, // 1 + (den0 + den1) * densfac, // 2 + pot0 * potlfac, // 3 + pot1 * potlfac, // 4 + (pot0 + pot1) * densfac, // 5 + potr * (-potlfac)/scale, // 6 + pott * (-potlfac), // 7 + potp * (-potlfac)}; // 8 // ^ // | // Return force not potential gradient @@ -554,10 +554,10 @@ namespace BasisClasses auto v = sph_eval(r, costh, phi); - double potR = v[4]*sinth + v[5]*costh; - double potz = v[4]*costh - v[5]*sinth; + double potR = v[6]*sinth + v[7]*costh; + double potz = v[6]*costh - v[7]*sinth; - return {v[0], v[1], v[2], v[3], potR, potz, v[6]}; + return {v[0], v[1], v[2], v[3], v[4], v[5], potR, potz, v[8]}; } @@ -571,12 +571,10 @@ namespace BasisClasses auto v = cyl_eval(R, z, phi); - //tdens0, tdens, tpotl0, tpotl, tpotR, tpotz, tpotp + double tpotx = v[6]*x/R - v[8]*y/R ; + double tpoty = v[6]*y/R + v[8]*x/R ; - double tpotx = v[4]*x/R - v[6]*y/R ; - double tpoty = v[4]*y/R + v[6]*x/R ; - - return {v[0], v[1], v[2], v[3], tpotx, tpoty, v[5]}; + return {v[0], v[1], v[2], v[3], v[4], v[5], tpotx, tpoty, v[7]}; } @@ -1277,14 +1275,15 @@ namespace BasisClasses if (midplane) { height = sl->accumulated_midplane_eval(R, -colh*hcyl, colh*hcyl, phi); - return {tdens0, tdens - tdens0, tdens, - tpotl0, tpotl - tpotl0, tpotl, tpotR, tpotz, tpotp, height}; + tpotl0, tpotl - tpotl0, tpotl, + tpotR, tpotz, tpotp, height}; } else { return {tdens0, tdens - tdens0, tdens, - tpotl0, tpotl - tpotl0, tpotl, tpotR, tpotz, tpotp}; + tpotl0, tpotl - tpotl0, tpotl, + tpotR, tpotz, tpotp}; } } @@ -1741,7 +1740,7 @@ namespace BasisClasses rpot = -totalMass*R/(r*r2 + 10.0*std::numeric_limits::min()); zpot = -totalMass*z/(r*r2 + 10.0*std::numeric_limits::min()); - return {den0, den1, pot0, pot1, rpot, zpot, ppot}; + return {den0, den1, den0+den1, pot0, pot1, pot0+pot1, rpot, zpot, ppot}; } // Get the basis fields @@ -1827,16 +1826,16 @@ namespace BasisClasses // Cylindrical coords // double sinth = sqrt(fabs(1.0 - costh*costh)); - double R = r*sinth, z = r*costh, potR, potz; + double R = r*sinth, z = r*costh; auto v = cyl_eval(R, z, phi); // Spherical force element converstion // - double potr = potR*sinth + potz*costh; - double pott = potR*costh - potz*sinth; + double potr = v[6]*sinth + v[7]*costh; + double pott = v[6]*costh - v[7]*sinth; - return {v[0], v[1], v[2], v[3], potr, pott, v[6]}; + return {v[0], v[1], v[2], v[3], v[4], v[5], potr, pott, v[8]}; } std::vector FlatDisk::crt_eval(double x, double y, double z) @@ -1848,10 +1847,10 @@ namespace BasisClasses auto v = cyl_eval(R, z, phi); - double potx = v[4]*x/R - v[6]*y/R; - double poty = v[4]*y/R + v[6]*x/R; + double potx = v[4]*x/R - v[8]*y/R; + double poty = v[4]*y/R + v[8]*x/R; - return {v[0], v[1], v[2], v[3], potx, poty, v[5]}; + return {v[0], v[1], v[2], v[3], v[4], v[5], potx, poty, v[7]}; } std::vector FlatDisk::orthoCheck() @@ -2582,7 +2581,7 @@ namespace BasisClasses double frcy = -frc(1).real(); double frcz = -frc(2).real(); - return {0, den1, 0, pot1, frcx, frcy, frcz}; + return {0, den1, den1, 0, pot1, pot1, frcx, frcy, frcz}; } std::vector Cube::cyl_eval(double R, double z, double phi) diff --git a/src/step.cc b/src/step.cc index c5d562f1d..a057ec247 100644 --- a/src/step.cc +++ b/src/step.cc @@ -341,7 +341,7 @@ void do_step(int n) if (step_timing) timer_tot.stop(); if (VERBOSE==2 and myid==0) // Time step marker - std::cout << << std::endl << ">>>" << this_step << "<<<" << std::endl; + std::cout << std::endl << ">>>" << this_step << "<<<" << std::endl; // Timer output if (step_timing && this_step!=0 && (this_step % tskip) == 0) { From 3ec4c56c83d673d019aa18dbde6f3485d11e49af Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 10 May 2024 17:54:18 -0400 Subject: [PATCH 097/167] Generalized KD for carrying velocity data [no ci] --- include/KDtree.H | 66 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/include/KDtree.H b/include/KDtree.H index 57a35bc00..40a845b32 100644 --- a/include/KDtree.H +++ b/include/KDtree.H @@ -40,20 +40,39 @@ public: point() {} //! Constructor - point(std::array c, double m=1) : coords_(c), mass_(m) {} + point(std::array c, double m=1, + unsigned long indx=0) : coords_(c), mass_(m), indx_(indx) {} + + point(std::array c, + std::array v, + double m=1, unsigned long indx=0) : coords_(c), vels_(v), + mass_(m), indx_(indx) {} //! List constructor - point(std::initializer_list list, double m=1) : mass_(m) + point(std::initializer_list list, double m=1, + unsigned long indx=0) : mass_(m), indx_(indx) + { + size_t n = std::min(dimensions, list.size()); + std::copy_n(list.begin(), n, coords_.begin()); + } + + point(std::initializer_list list, + std::initializer_list vlst, + double m=1, + unsigned long indx=0) : mass_(m), indx_(indx) { size_t n = std::min(dimensions, list.size()); std::copy_n(list.begin(), n, coords_.begin()); + std::copy_n(vlst.begin(), n, vels_.begin()); } //! Copy constructor point(const point& p) { for (size_t n=0; n coords_; + std::array vels_; double mass_; + unsigned long indx_; }; //! For iostream printing of points @@ -163,7 +212,7 @@ private: size_t n = begin + (end - begin)/2; std::nth_element(&nodes_[begin], &nodes_[n], &nodes_[end], node_cmp(index)); index = (index + 1) % dimensions; - nodes_[n].left_ = make_tree(begin, n, index); + nodes_[n].left_ = make_tree(begin, n, index); nodes_[n].right_ = make_tree(n + 1, end, index); return &nodes_[n]; } @@ -175,19 +224,20 @@ private: ++visited_; double d = root->distance(point); - if (best_.size()==0 || d < best_.rbegin()->first) { + if (best_.size()first) { best_.add(d, root); } - // This is only correct is the test point is never in the data set . . . + // This is only correct if the test point is never in the data set . . . // if (best_.begin()->first == 0) return; double dx = root->get(index) - point.get(index); index = (index + 1) % dimensions; - nearestN(dx > 0 ? root->left_ : root->right_, point, index, N); - if (dx * dx >= best_.rbegin()->first) return; - nearestN(dx > 0 ? root->right_ : root->left_, point, index, N); + nearestN(dx > 0 ? root->left_ : root->right_, point, index, N); + + if (best_.size()>=N and dx * dx >= best_.rbegin()->first) return; + nearestN(dx > 0 ? root->right_ : root->left_, point, index, N); } public: From 19ae54824e7d254507de2a42610bd7c3ca436e9d Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 11 May 2024 12:09:40 -0400 Subject: [PATCH 098/167] Add basis cache version - Create version string variable for all HighFive-generated HDF5 caches - This will effectively expire all existing caches - Emit a message that the cache is rebuiling for an API change --- exputil/BiorthCyl.cc | 19 +++++++++++++++++++ exputil/EmpCyl2d.cc | 41 ++++++++++++++++++++++++++++------------- exputil/EmpCylSL.cc | 15 ++++++++++++++- exputil/SLGridMP2.cc | 13 +++++++++++++ include/BiorthCyl.H | 3 +++ include/EmpCyl2d.H | 3 +++ include/EmpCylSL.H | 3 +++ include/SLGridMP2.H | 3 +++ 8 files changed, 86 insertions(+), 14 deletions(-) diff --git a/exputil/BiorthCyl.cc b/exputil/BiorthCyl.cc index 8f87e63a3..3509cc50b 100644 --- a/exputil/BiorthCyl.cc +++ b/exputil/BiorthCyl.cc @@ -27,6 +27,9 @@ #include using namespace __EXP__; // For reference to n-body globals +// Cache version +std::string BiorthCyl::Version = "1.0"; + // Constructor BiorthCyl::BiorthCyl(const YAML::Node& conf) : conf(conf) { @@ -547,6 +550,10 @@ void BiorthCyl::WriteH5Cache() // file.createAttribute("forceID", HighFive::DataSpace::From(forceID)).write(forceID); + // Cache version + // + file.createAttribute("Version", HighFive::DataSpace::From(Version)).write(Version); + // Stash the basis configuration (this is not yet implemented in EXP) // std::ostringstream sout; sout << conf; @@ -631,6 +638,18 @@ bool BiorthCyl::ReadH5Cache() if (not checkStr(geometry, "geometry")) return false; if (not checkStr(forceID, "forceID")) return false; + // Version check + // + if (h5file.hasAttribute("Version")) { + if (not checkStr(Version, "Version")) return false; + } else { + if (myid==0) + std::cout << "---- BiorthCyl::ReadH5Cache: " + << "recomputing cache for HighFive API change" + << std::endl; + return false; + } + // Parameter check // if (not checkInt(mmax, "mmax")) return false; diff --git a/exputil/EmpCyl2d.cc b/exputil/EmpCyl2d.cc index fd3408eaa..9164af353 100644 --- a/exputil/EmpCyl2d.cc +++ b/exputil/EmpCyl2d.cc @@ -997,6 +997,8 @@ void EmpCyl2d::WriteH5Cache() // HighFive::File file(cache_name_2d, HighFive::File::Overwrite); + + // Workaround for lack of HighFive boolean support int ilogr = 0, icmap = 0; if (logr) ilogr = 1; @@ -1008,19 +1010,20 @@ void EmpCyl2d::WriteH5Cache() // Parameters // - file.createAttribute ("mmax", HighFive::DataSpace::From(mmax)). write(mmax); - file.createAttribute ("nmaxfid", HighFive::DataSpace::From(nmaxfid)). write(nmaxfid); - file.createAttribute ("nmax", HighFive::DataSpace::From(nmax)).write(nmax); - file.createAttribute ("numr", HighFive::DataSpace::From(numr)). write(numr); - file.createAttribute ("knots", HighFive::DataSpace::From(knots)). write(knots); - file.createAttribute ("ilogr", HighFive::DataSpace::From(ilogr)). write(ilogr); - file.createAttribute ("icmap", HighFive::DataSpace::From(icmap)). write(icmap); - file.createAttribute ("rmin", HighFive::DataSpace::From(rmin)). write(rmin); - file.createAttribute ("rmax", HighFive::DataSpace::From(rmax)). write(rmax); - file.createAttribute ("scale", HighFive::DataSpace::From(scale)). write(scale); - file.createAttribute("params", HighFive::DataSpace::From(params)).write(params); - file.createAttribute("model", HighFive::DataSpace::From(model)). write(model); - file.createAttribute("biorth", HighFive::DataSpace::From(biorth)).write(biorth); + file.createAttribute("Version", HighFive::DataSpace::From(Version)).write(Version); + file.createAttribute ("mmax", HighFive::DataSpace::From(mmax)). write(mmax); + file.createAttribute ("nmaxfid", HighFive::DataSpace::From(nmaxfid)).write(nmaxfid); + file.createAttribute ("nmax", HighFive::DataSpace::From(nmax)). write(nmax); + file.createAttribute ("numr", HighFive::DataSpace::From(numr)). write(numr); + file.createAttribute ("knots", HighFive::DataSpace::From(knots)). write(knots); + file.createAttribute ("ilogr", HighFive::DataSpace::From(ilogr)). write(ilogr); + file.createAttribute ("icmap", HighFive::DataSpace::From(icmap)). write(icmap); + file.createAttribute ("rmin", HighFive::DataSpace::From(rmin)). write(rmin); + file.createAttribute ("rmax", HighFive::DataSpace::From(rmax)). write(rmax); + file.createAttribute ("scale", HighFive::DataSpace::From(scale)). write(scale); + file.createAttribute("params", HighFive::DataSpace::From(params)). write(params); + file.createAttribute("model", HighFive::DataSpace::From(model)). write(model); + file.createAttribute("biorth", HighFive::DataSpace::From(biorth)). write(biorth); // Arrays // @@ -1080,6 +1083,18 @@ bool EmpCyl2d::ReadH5Cache() // + // Version check + // + if (file.hasAttribute("Version")) { + if (not checkStr(Version, "Version")) return false; + } else { + if (myid==0) + std::cout << "---- EmpCyl2d::ReadH5Cache: " + << "recomputing cache for HighFive API change" + << std::endl; + return false; + } + // Serialize the config and make a string for checking YAML::Emitter y; y << Params; std::string params(y.c_str()); diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 170ec88fa..9dc4860cd 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -7040,7 +7040,8 @@ void EmpCylSL::WriteH5Cache() std::string model = EmpModelLabs[mtype]; file.createAttribute("geometry", HighFive::DataSpace::From(geometry)).write(geometry); - file.createAttribute("forceID", HighFive::DataSpace::From(forceID)).write(forceID); + file.createAttribute("forceID", HighFive::DataSpace::From(forceID)).write(forceID); + file.createAttribute("Version", HighFive::DataSpace::From(Version)).write(Version); // Write the specific parameters // @@ -7161,6 +7162,18 @@ bool EmpCylSL::ReadH5Cache() return false; }; + // Version check + // + if (file.hasAttribute("Version")) { + if (not checkStr(Version, "Version")) return false; + } else { + if (myid==0) + std::cout << "---- EmpCylSL::ReadH5Cache: " + << "recomputing cache for HighFive API change" + << std::endl; + return false; + } + if (not checkStr(geometry, "geometry")) return false; if (not checkStr(forceID, "forceID")) return false; if (not checkInt(MMAX, "mmax")) return false; diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index ba0819260..7db5367d7 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -506,6 +506,18 @@ bool SLGridSph::ReadH5Cache(void) if (not checkStr(geometry, "geometry")) return false; if (not checkStr(forceID, "forceID")) return false; + // Version check + // + if (h5file.hasAttribute("Version")) { + if (not checkStr(Version, "Version")) return false; + } else { + if (myid==0) + std::cout << "---- SLGridSph::ReadH5Cache: " + << "recomputing cache for HighFive API change" + << std::endl; + return false; + } + // Parameter check // if (not checkStr(modl, "model")) return false; @@ -604,6 +616,7 @@ void SLGridSph::WriteH5Cache(void) file.createAttribute("geometry", HighFive::DataSpace::From(geometry)).write(geometry); file.createAttribute("forceID", HighFive::DataSpace::From(forceID)).write(forceID); + file.createAttribute("Version", HighFive::DataSpace::From(Version)).write(Version); // Write parameters // diff --git a/include/BiorthCyl.H b/include/BiorthCyl.H index 5b3f775b3..b052c7979 100644 --- a/include/BiorthCyl.H +++ b/include/BiorthCyl.H @@ -100,6 +100,9 @@ protected: //! Read the HDF5 cache virtual bool ReadH5Cache(); + //! Cache versioning + static std::string Version; + public: //! Flag for MPI enabled (default: 0=off) diff --git a/include/EmpCyl2d.H b/include/EmpCyl2d.H index e062644b3..440ca3667 100644 --- a/include/EmpCyl2d.H +++ b/include/EmpCyl2d.H @@ -127,6 +127,9 @@ protected: bool ReadH5Cache(); void WriteH5Cache(); + //! Cache versioning + inline static const std::string Version = "1.0"; + //! Basis magic number inline static const unsigned int hmagic = 0xc0a57a1; diff --git a/include/EmpCylSL.H b/include/EmpCylSL.H index 0f36e026e..30a4a6587 100644 --- a/include/EmpCylSL.H +++ b/include/EmpCylSL.H @@ -231,6 +231,9 @@ protected: //! Read HDF5 cache bool ReadH5Cache(); + //! Cache versioning + inline static const std::string Version = "1.0"; + //! The cache file name std::string cachefile; // 1=write, 0=read diff --git a/include/SLGridMP2.H b/include/SLGridMP2.H index 7581c56ff..95a9bf6b7 100644 --- a/include/SLGridMP2.H +++ b/include/SLGridMP2.H @@ -91,6 +91,9 @@ private: //! Read HDF5 cache bool ReadH5Cache(); + //! Cache versioning + inline static const std::string Version = "1.0"; + public: //! Flag for MPI enabled (default: 0=off) From f7ba79b86028a1287bb64e372ae929785c7961a0 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 11 May 2024 16:24:06 -0400 Subject: [PATCH 099/167] Missing submodule updates; missing API updates as a consequence --- expui/Coefficients.cc | 10 +++------- expui/expMSSA.cc | 8 ++++---- exputil/BiorthCyl.cc | 2 +- exputil/EmpCyl2d.cc | 8 ++++---- exputil/EmpCylSL.cc | 23 ++++++++++------------- exputil/SLGridMP2.cc | 6 ++---- extern/HighFive | 2 +- extern/yaml-cpp | 2 +- include/EmpCyl2d.H | 6 ++---- 9 files changed, 28 insertions(+), 39 deletions(-) diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 452ebecba..5385be271 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -116,9 +116,7 @@ namespace CoefClasses if (Time < Tmin or Time > Tmax) continue; - int ldim = (Lmax+1)*(Lmax+2)/2; - Eigen::MatrixXcd in(ldim, Nmax); - stanza.getDataSet("coefficients").read(in); + auto in = stanza.getDataSet("coefficients").read(); // Pack the data into the coefficient variable // @@ -286,8 +284,7 @@ namespace CoefClasses std::array shape; stanza.getAttribute("shape").read(shape); - Eigen::VectorXcd in(shape[0]*shape[1]*shape[2]); - stanza.getDataSet("coefficients").read(in); + auto in = stanza.getDataSet("coefficients").read(); // Pack the data into the coefficient variable // @@ -378,8 +375,7 @@ namespace CoefClasses std::array shape; stanza.getAttribute("shape").read(shape); - Eigen::VectorXcd in(shape[0]*shape[1]*shape[2]); - stanza.getDataSet("coefficients").read(in); + auto in = stanza.getDataSet("coefficients").read(); // Pack the data into the coefficient variable // diff --git a/expui/expMSSA.cc b/expui/expMSSA.cc index 342f6d110..713d34ecb 100644 --- a/expui/expMSSA.cc +++ b/expui/expMSSA.cc @@ -1513,10 +1513,10 @@ namespace MSSA { auto analysis = h5file.getGroup("mssa_analysis"); - analysis.getDataSet("Y" ).read(Y ); - analysis.getDataSet("S" ).read(S ); - analysis.getDataSet("U" ).read(U ); - analysis.getDataSet("PC").read(PC); + Y = analysis.getDataSet("Y" ).read(); + S = analysis.getDataSet("S" ).read(); + U = analysis.getDataSet("U" ).read(); + PC = analysis.getDataSet("PC").read(); numK = numT - numW + 1; // Recompute numK, needed for // reconstruction diff --git a/exputil/BiorthCyl.cc b/exputil/BiorthCyl.cc index 3509cc50b..cd4777cb8 100644 --- a/exputil/BiorthCyl.cc +++ b/exputil/BiorthCyl.cc @@ -504,7 +504,7 @@ void BiorthCyl::WriteH5Arrays(HighFive::Group& harmonic) sout << n; auto arrays = order.createGroup(sout.str()); - HighFive::DataSet ds1 = arrays.createDataSet("density", dens [m][n]); + HighFive::DataSet ds1 = arrays.createDataSet("density", dens [m][n]); HighFive::DataSet ds2 = arrays.createDataSet("potential", pot [m][n]); HighFive::DataSet ds3 = arrays.createDataSet("rforce", rforce[m][n]); HighFive::DataSet ds4 = arrays.createDataSet("zforce", zforce[m][n]); diff --git a/exputil/EmpCyl2d.cc b/exputil/EmpCyl2d.cc index 9164af353..af0901e4e 100644 --- a/exputil/EmpCyl2d.cc +++ b/exputil/EmpCyl2d.cc @@ -1146,10 +1146,10 @@ bool EmpCyl2d::ReadH5Cache() sout << m; auto harmonic = file.getGroup(sout.str()); - harmonic.getDataSet("potl").read(potl_array[m]); - harmonic.getDataSet("dens").read(dens_array[m]); - harmonic.getDataSet("dpot").read(dpot_array[m]); - harmonic.getDataSet("rot" ).read(rot_matrix[m]); + potl_array[m] = harmonic.getDataSet("potl").read(); + dens_array[m] = harmonic.getDataSet("dens").read(); + dpot_array[m] = harmonic.getDataSet("dpot").read(); + rot_matrix[m] = harmonic.getDataSet("rot" ).read(); } } catch (HighFive::Exception& err) { diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 9dc4860cd..23d579c0f 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -26,11 +26,8 @@ // For reading and writing HDF5 cache files // -#include -#include -#include -#include - +#include +#include #ifdef HAVE_OMP_H #include // For multithreading basis construction @@ -7244,10 +7241,10 @@ bool EmpCylSL::ReadH5Cache() sout << n; auto order = harmonic.getGroup(sout.str()); - order.getDataSet("potC") .read(potC [m][n]); - order.getDataSet("rforceC").read(rforceC[m][n]); - order.getDataSet("zforceC").read(zforceC[m][n]); - order.getDataSet("densC") .read(densC[m][n]); + potC [m][n] = order.getDataSet("potC") .read(); + rforceC[m][n] = order.getDataSet("rforceC").read(); + zforceC[m][n] = order.getDataSet("zforceC").read(); + densC [m][n] = order.getDataSet("densC") .read(); } } @@ -7267,10 +7264,10 @@ bool EmpCylSL::ReadH5Cache() sout << n; auto order = harmonic.getGroup(sout.str()); - order.getDataSet("potS") .read(potS [m][n]); - order.getDataSet("rforceS").read(rforceS[m][n]); - order.getDataSet("zforceS").read(zforceS[m][n]); - order.getDataSet("densS") .read(densS[m][n]); + potS [m][n] = order.getDataSet("potS") .read(); + rforceS[m][n] = order.getDataSet("rforceS").read(); + zforceS[m][n] = order.getDataSet("zforceS").read(); + densS [m][n] = order.getDataSet("densS") .read(); } } diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 7db5367d7..0984fff00 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -30,10 +30,8 @@ // For reading and writing cache file // -#include -#include -#include -#include +#include +#include // For fortran call // (This should work both 32-bit and 64-bit . . . ) diff --git a/extern/HighFive b/extern/HighFive index a794a4d65..12064079d 160000 --- a/extern/HighFive +++ b/extern/HighFive @@ -1 +1 @@ -Subproject commit a794a4d651b309069fec5c628d0fb82c2be92662 +Subproject commit 12064079d9533c0636310f25606571b3dbb977cf diff --git a/extern/yaml-cpp b/extern/yaml-cpp index 0579ae3d9..1d8ca1f35 160000 --- a/extern/yaml-cpp +++ b/extern/yaml-cpp @@ -1 +1 @@ -Subproject commit 0579ae3d976091d7d664aa9d2527e0d0cff25763 +Subproject commit 1d8ca1f35eb3a9c9142462b28282a848e5d29a91 diff --git a/include/EmpCyl2d.H b/include/EmpCyl2d.H index 440ca3667..703e8e610 100644 --- a/include/EmpCyl2d.H +++ b/include/EmpCyl2d.H @@ -17,10 +17,8 @@ // For reading and writing cache file // -#include -#include -#include -#include +#include +#include /** A class that implements most of the members for an Exp force routine From cd1de795d268ac23f4a717c2910709ef5eafd73d Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 11 May 2024 21:58:25 -0400 Subject: [PATCH 100/167] Missing updates for HighFive 3.0.0 --- expui/Coefficients.cc | 15 +++++---------- expui/Koopman.cc | 24 +++++++++++------------- expui/expMSSA.cc | 8 +++----- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 5385be271..17f642933 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -12,10 +12,8 @@ #include #include -#include -#include -#include -#include +#include +#include #include @@ -802,8 +800,7 @@ namespace CoefClasses if (Time < Tmin or Time > Tmax) continue; - Eigen::MatrixXcd in(Mmax+1, Nmax); - stanza.getDataSet("coefficients").read(in); + auto in = stanza.getDataSet("coefficients").read(); // Work around for previous unitiaized data bug; enforces real data // @@ -1173,8 +1170,7 @@ namespace CoefClasses if (Time < Tmin or Time > Tmax) continue; - Eigen::VectorXcd in; - stanza.getDataSet("coefficients").read(in); + auto in = stanza.getDataSet("coefficients").read(); Eigen::TensorMap dat(in.data(), 2*NmaxX+1, 2*NmaxY+1, NmaxZ); @@ -1526,8 +1522,7 @@ namespace CoefClasses if (Time < Tmin or Time > Tmax) continue; - Eigen::VectorXcd in; - stanza.getDataSet("coefficients").read(in); + auto in = stanza.getDataSet("coefficients").read(); Eigen::TensorMap dat(in.data(), 2*NmaxX+1, 2*NmaxY+1, 2*NmaxZ+1); diff --git a/expui/Koopman.cc b/expui/Koopman.cc index 14f14cf94..b32f00b92 100644 --- a/expui/Koopman.cc +++ b/expui/Koopman.cc @@ -31,10 +31,8 @@ #include -#include -#include -#include -#include +#include +#include /* For debugging #undef eigen_assert @@ -795,15 +793,15 @@ namespace MSSA { auto analysis = h5file.getGroup("koopman_analysis"); - analysis.getDataSet("Phi").read(Phi); - analysis.getDataSet("X0" ).read(X0 ); - analysis.getDataSet("X1" ).read(X1 ); - analysis.getDataSet("U" ).read(U ); - analysis.getDataSet("V" ).read(V ); - analysis.getDataSet("A" ).read(A ); - analysis.getDataSet("L" ).read(L ); - analysis.getDataSet("W" ).read(W ); - analysis.getDataSet("Y" ).read(Y ); + Phi = analysis.getDataSet("Phi").read(); + X0 = analysis.getDataSet("X0" ).read(); + X1 = analysis.getDataSet("X1" ).read(); + U = analysis.getDataSet("U" ).read(); + V = analysis.getDataSet("V" ).read(); + A = analysis.getDataSet("A" ).read(); + L = analysis.getDataSet("L" ).read(); + W = analysis.getDataSet("W" ).read(); + Y = analysis.getDataSet("Y" ).read(); computed = true; diff --git a/expui/expMSSA.cc b/expui/expMSSA.cc index 713d34ecb..0aca81d71 100644 --- a/expui/expMSSA.cc +++ b/expui/expMSSA.cc @@ -24,10 +24,8 @@ #include -#include -#include -#include -#include +#include +#include /* For debugging #undef eigen_assert @@ -1532,7 +1530,7 @@ namespace MSSA { for (int n=0; n(); } reconstructed = true; From 92d9492e4f3518f9f8756ae6eb1ded5df7d9da3f Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sun, 12 May 2024 12:32:19 -0400 Subject: [PATCH 101/167] Minor code clean-up and fixed a bunch of linger fence-post issues (that I already fixed?) --- exputil/sbessz.cc | 15 ++----- src/Bessel.H | 2 +- src/Bessel.cc | 104 ++++++++++++++++++++-------------------------- 3 files changed, 50 insertions(+), 71 deletions(-) diff --git a/exputil/sbessz.cc b/exputil/sbessz.cc index d8be4065e..91abe4478 100644 --- a/exputil/sbessz.cc +++ b/exputil/sbessz.cc @@ -16,33 +16,26 @@ #define STEPS 6 #define TOL 1.0e-7 -static int NN; - -static double zbess(double z) -{ - return EXPmath::sph_bessel(NN, z); -} - Eigen::VectorXd sbessjz(int n, int m) { Eigen::VectorXd a(m); - auto zfunc = [n](double z) { return EXPmath::cyl_bessel_j(n, z); }; + auto zfunc = [n](double z) { return EXPmath::sph_bessel(n, z); }; double dz = M_PI/STEPS; double z = 0.5+fabs((double)n); double zl = z, fl, f; for (int i=0; i0) { zl = z; fl = f; z += dz; - f = EXPmath::sph_bessel(n,z); + f = EXPmath::sph_bessel(n, z); } - a[i] = zbrent(zbess, zl, z, TOL); + a[i] = zbrent(zfunc, zl, z, TOL); zl = z; fl = f; } diff --git a/src/Bessel.H b/src/Bessel.H index c768347d7..56bb00028 100644 --- a/src/Bessel.H +++ b/src/Bessel.H @@ -56,7 +56,7 @@ private: Eigen::VectorXd a; Roots(int L, int nmax) : l(L), n(nmax) { - a = sbessjz(l-1, n); + a = sbessjz(l, n); } ~Roots() {} diff --git a/src/Bessel.cc b/src/Bessel.cc index 1889b7acd..fdf41b919 100644 --- a/src/Bessel.cc +++ b/src/Bessel.cc @@ -24,24 +24,20 @@ Bessel::Bessel(Component* c0, const YAML::Node& conf, MixtureBasis* m) : Spheric void Bessel::get_dpotl(int lmax, int nmax, double r, Eigen::MatrixXd& p, Eigen::MatrixXd& dp, int tid) { - double a,aa,aaa,b,bb,bbb; - int klo, khi; - int l, n; + int klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; + if (klo < 0) klo = 0; + if (klo > RNUM - 2) klo = RNUM - 2; + int khi = klo + 1; - klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; - if (klo < 1) klo = 1; - if (klo >= RNUM) klo = RNUM - 1; - khi = klo + 1; - - a = (r_grid[khi] - r)/r_grid_del; - b = (r - r_grid[klo])/r_grid_del; - - aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; - bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; - aaa = -(3.0*a*a - 1.0)*r_grid_del/6.0; - bbb = (3.0*b*b - 1.0)*r_grid_del/6.0; + double a = (r_grid[khi] - r)/r_grid_del; + double b = (r - r_grid[klo])/r_grid_del; + + double aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; + double bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; + double aaa = -(3.0*a*a - 1.0)*r_grid_del/6.0; + double bbb = (3.0*b*b - 1.0)*r_grid_del/6.0; - for (l=0; l<=lmax; l++) { + for (int l=0; l<=lmax; l++) { for (int n=0; n RNUM - 2) klo = RNUM - 2; + int khi = klo + 1; - klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; - if (klo < 1) klo = 1; - if (klo >= RNUM) klo = RNUM - 1; - khi = klo + 1; + double a = (r_grid[khi] - r)/r_grid_del; + double b = (r - r_grid[klo])/r_grid_del; - a = (r_grid[khi] - r)/r_grid_del; - b = (r - r_grid[klo])/r_grid_del; - - aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; - bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; + double aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; + double bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; for (int l=0; l<=lmax; l++) { for (int n=0; n= RNUM) klo = RNUM - 1; - khi = klo + 1; + int klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; + if (klo < 0) klo = 0; + if (klo > RNUM - 2) klo = RNUM - 2; + int khi = klo + 1; - a = (r_grid[khi] - r)/r_grid_del; - b = (r - r_grid[klo])/r_grid_del; + double a = (r_grid[khi] - r)/r_grid_del; + double b = (r - r_grid[klo])/r_grid_del; - aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; - bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; + double aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; + double bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; for (int l=0; l<=lmax; l++) { - for (int n=1; n<=nmax; n++) { + for (int n=0; n RNUM - 2) klo = RNUM - 2; + int khi = klo + 1; - klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; - if (klo < 1) klo = 1; - if (klo >= RNUM) klo = RNUM - 1; - khi = klo + 1; + double a = (r_grid[khi] - r)/r_grid_del; + double b = (r - r_grid[klo])/r_grid_del; - a = (r_grid[khi] - r)/r_grid_del; - b = (r - r_grid[klo])/r_grid_del; - - aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; - bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; + double aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; + double bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; for (int l=0; l<=lmax; l++) { for (int n=0; na[n]; - return alpha*M_SQRT2/fabs(EXPmath::sph_bessel(p->l, alpha)) * pow(rmax,-2.5) * + return alpha*M_SQRT2/fabs(EXPmath::sph_bessel(p->l+1, alpha)) * pow(rmax,-2.5) * EXPmath::sph_bessel(p->l, alpha*r/rmax); } @@ -148,7 +134,7 @@ double Bessel::potl(double r, int n) throw GenericError("Routine potl() called with n out of bounds", __FILE__, __LINE__, 1002, true); alpha = p->a[n]; - return M_SQRT2/fabs(alpha*EXPmath::sph_bessel(p->l,alpha)) * pow(rmax,-0.5) * + return M_SQRT2/fabs(alpha*EXPmath::sph_bessel(p->l+1,alpha)) * pow(rmax,-0.5) * EXPmath::sph_bessel(p->l,alpha*r/rmax); } @@ -174,9 +160,9 @@ void Bessel::make_grid(double rmin, double rmax, int lmax, int nmax) for (int n=0; n Date: Sun, 12 May 2024 13:01:34 -0400 Subject: [PATCH 102/167] Add a parsing stanza and let RNUM be a configurable parameter [no ci] --- src/Bessel.H | 20 ++++++++++++++++---- src/Bessel.cc | 29 ++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/Bessel.H b/src/Bessel.H index 56bb00028..1f21f5709 100644 --- a/src/Bessel.H +++ b/src/Bessel.H @@ -17,6 +17,8 @@ class Bessel : public SphericalBasis private: + //@{ + //! Required members for Spherical Basis void get_pot_coefs(int l, double *coef, double *p, double *dp); void get_pot_coefs_safe(int l, double *coef, double *p, double *dp, double **potd1, double **dpot1); @@ -26,8 +28,10 @@ private: void get_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid); void get_potl_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p, Eigen::MatrixXd& d,int tid); double get_dens(double r, int l, double *coef); + //@} - void initialize() {} + //! Initialize parameters from YAML + void initialize(); bool firstime_coef; bool firstime_accel; @@ -41,9 +45,12 @@ private: int nmax; }; + //@{ + //! Grid storage and parameters std::vector dens_grid, potl_grid; Eigen::VectorXd r_grid; double r_grid_del; + //@} //! Cache roots for spherical Bessel functions class Roots @@ -62,21 +69,26 @@ private: ~Roots() {} }; + //! Root database isntance std::shared_ptr p; + //@{ + //! Density and potential members double dens(double r, int n); double potl(double r, int n); + //@} + //! Make the density and potential grid void make_grid(double rmin, double rmax, int lmax, int nmax); //! Valid keys for YAML configurations static const std::set valid_keys; + //! Number of entries in the fixed table + int RNUM; + public: - //! Number of entries in fixed table (static variable) - static int RNUM; - //! Constructor Bessel(Component* c0, const YAML::Node& conf, MixtureBasis* m=0); diff --git a/src/Bessel.cc b/src/Bessel.cc index fdf41b919..657a9d1b4 100644 --- a/src/Bessel.cc +++ b/src/Bessel.cc @@ -6,7 +6,34 @@ #include #include -int Bessel::RNUM = 1000; +const std::set +Bessel::valid_keys = { + "rnum" +}; + +void Bessel::initialize() +{ + // Remove matched keys + // + for (auto v : valid_keys) current_keys.erase(v); + + // Assign values from YAML + // + try { + if (conf["rnum"]) RNUM = conf["rnum"].as(); + else RNUM = 1000; + } + catch (YAML::Exception & error) { + if (myid==0) std::cout << "Error parsing parameters in Sphere: " + << error.what() << std::endl + << std::string(60, '-') << std::endl + << "Config node" << std::endl + << std::string(60, '-') << std::endl + << conf << std::endl + << std::string(60, '-') << std::endl; + throw std::runtime_error("Sphere::initialze: error parsing YAML"); + } +} Bessel::Bessel(Component* c0, const YAML::Node& conf, MixtureBasis* m) : SphericalBasis(c0, conf, m) { From 1f09674115ad661a096cd671103222d7376a51e9 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sun, 12 May 2024 14:00:25 -0400 Subject: [PATCH 103/167] Add the git submodule commands to the CMake workflow --- CMakeLists.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c105459ef..598bf4ad9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -187,6 +187,19 @@ execute_process( OUTPUT_STRIP_TRAILING_WHITESPACE ) +# Git submodule updates +execute_process( + COMMAND git submodule update --init --recursive + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT +) + +if(NOT GIT_SUBMOD_RESULT EQUAL "0") + message(FATAL_ERROR "git submodule update --init --recursive failed ${GIT_SUBMOD_RESULT}, please checkout submodules") +else() + message(STATUS "Submodules updated successfully - good") +endif() + # Get the latest abbreviated commit hash of the working branch execute_process( COMMAND git rev-parse HEAD From 254df3d0761ee6a04d75f102e7e9e38e37252b08 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sun, 12 May 2024 22:00:26 -0400 Subject: [PATCH 104/167] Preliminary implementation of Bessel basis for pyEXP --- expui/BasisFactory.cc | 3 + expui/BiorthBasis.H | 226 ++++++++++++++++++++------- expui/BiorthBasis.cc | 246 +++++++++++++++++++++--------- expui/CMakeLists.txt | 2 +- pyEXP/BasisWrappers.cc | 339 ++++++++++++++++++++++++++++------------- src/Bessel.cc | 8 +- 6 files changed, 587 insertions(+), 237 deletions(-) diff --git a/expui/BasisFactory.cc b/expui/BasisFactory.cc index 599a0eafb..5b574a8e2 100644 --- a/expui/BasisFactory.cc +++ b/expui/BasisFactory.cc @@ -175,6 +175,9 @@ namespace BasisClasses if ( !name.compare("sphereSL") ) { basis = std::make_shared(conf); } + else if ( !name.compare("bessel") ) { + basis = std::make_shared(conf); + } else if ( !name.compare("cylinder") ) { basis = std::make_shared(conf); } diff --git a/expui/BiorthBasis.H b/expui/BiorthBasis.H index ce88f02f7..a4af27c1a 100644 --- a/expui/BiorthBasis.H +++ b/expui/BiorthBasis.H @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -60,7 +61,6 @@ namespace BasisClasses virtual std::vector sph_eval(double r, double costh, double phi) = 0; - //! Evaluate fields in cylindrical coordinates in centered coordinate system virtual std::vector cyl_eval(double r, double costh, double phi) = 0; @@ -164,10 +164,10 @@ namespace BasisClasses }; /** - Uses SLGridSph basis to evaluate expansion coeffients and provide - potential and density basis fields + An abstract spherical basis to evaluate expansion coeffients and + provide potential and density basis fields */ - class SphericalSL : public BiorthBasis + class Spherical : public BiorthBasis { public: @@ -175,45 +175,13 @@ namespace BasisClasses using BasisMap = std::map; using BasisArray = std::vector>; - private: + protected: //! Helper for constructor void initialize(); - std::shared_ptr sl; - std::shared_ptr mod; - - std::string model_file; - int lmax, nmax, cmap, numr; - double rmin, rmax, rmap; - - bool NO_L0, NO_L1, EVEN_L, EVEN_M, M0_only; - - std::vector potd, dpot, dpt2, dend; - std::vector legs, dlegs, d2legs; - - Eigen::MatrixXd factorial; - Eigen::MatrixXd expcoef; - double scale; - int N1, N2; - int used; - - using matT = std::vector; - using vecT = std::vector; - - double totalMass; - int npart; - - Eigen::VectorXd work; - - //! For coefficient writing - typedef Eigen::Matrix - EigenColMajor; - - protected: - - //! Load coefficients into the new CoefStruct - virtual void load_coefs(CoefClasses::CoefStrPtr coefs, double time); + //! Load coefficients for a particular time + virtual void load_coefs(CoefClasses::CoefStrPtr coef, double time); //! Set coefficients virtual void set_coefs(CoefClasses::CoefStrPtr coefs); @@ -222,7 +190,7 @@ namespace BasisClasses static const std::set valid_keys; //! Return readable class name - virtual const std::string classname() { return "SphericalSL";} + virtual const std::string classname() { return "Spherical";} //! Subspace index virtual const std::string harmonic() { return "l";} @@ -239,16 +207,60 @@ namespace BasisClasses virtual std::vector cyl_eval(double R, double z, double phi); + //@{ + //! Required basis members + + //! Get potential + virtual void get_pot(Eigen::MatrixXd& tab, double x) = 0; + + //! Get density + virtual void get_dens(Eigen::MatrixXd& tab, double x) = 0; + + //! Get force + virtual void get_force(Eigen::MatrixXd& tab, double x) = 0; + + //@} + + //@{ + //! Internal parameters and storage + int lmax, nmax, cmap, numr; + double rmin, rmax, rmap; + + bool NO_L0, NO_L1, EVEN_L, EVEN_M, M0_only; + + std::vector potd, dpot, dpt2, dend; + std::vector legs, dlegs, d2legs; + + Eigen::MatrixXd factorial; + Eigen::MatrixXd expcoef; + double scale; + int N1, N2; + int used; + + using matT = std::vector; + using vecT = std::vector; + + double totalMass; + int npart; + + Eigen::VectorXd work; + + //! For coefficient writing + typedef Eigen::Matrix + EigenColMajor; + + //@} + public: //! Constructor from YAML node - SphericalSL(const YAML::Node& conf); + Spherical(const YAML::Node& conf, const std::string& forceID); //! Constructor from YAML string - SphericalSL(const std::string& confstr); + Spherical(const std::string& confstr, const std::string& forceID); //! Destructor - virtual ~SphericalSL(void) {} + virtual ~Spherical(void) {} //! Print and return the cache parameters static std::map @@ -276,29 +288,134 @@ namespace BasisClasses int getNmax() { return nmax; } //! Return potential-density pair of a vector of a vector of 1d - //! basis-function grids for SphericalSL, logarithmically spaced - //! between [logxmin, logxmax] (base 10). - BasisArray getBasis - (double logxmin=-3.0, double logxmax=0.5, int numgrid=2000); + //! basis-function grids for Spherical [rmin, rmax] + virtual BasisArray getBasis + (double rmin=0.0, double rax=1.0, int numgrid=2000); //! Compute the orthogonality of the basis by returning inner //! produce matrices - std::vector orthoCheck(int knots=40) - { - return sl->orthoCheck(knots); - } + virtual std::vector orthoCheck(int knots) = 0; //! Biorthogonality sanity check bool orthoTest(int knots=100) { - auto [ret, worst, lworst] = orthoCompute(sl->orthoCheck(knots)); + auto [ret, worst, lworst] = orthoCompute(orthoCheck(knots)); // For the CTest log - std::cout << "SphericalSL::orthoTest: worst=" << worst << std::endl; + std::cout << "Spherical::orthoTest: worst=" << worst << std::endl; return ret; } }; + + /** + Uses SLGridSph basis to evaluate expansion coeffients and provide + potential and density basis fields + */ + class SphericalSL : public Spherical + { + + protected: + + //! Helper for constructor + void initialize(); + + static const std::set valid_keys; + + std::shared_ptr sl; + std::shared_ptr mod; + + std::string model_file; + + //! Return readable class name + const std::string classname() { return "SphericalSL";} + + // Get potential + void get_pot(Eigen::MatrixXd& tab, double r) + { sl->get_pot(tab, r); } + + // Get density + void get_dens(Eigen::MatrixXd& tab, double r) + { sl->get_dens(tab, r); } + + // Get force + void get_force(Eigen::MatrixXd& tab, double r) + { sl->get_force(tab, r); } + + public: + + //! Constructor from YAML node + SphericalSL(const YAML::Node& conf); + + //! Constructor from YAML string + SphericalSL(const std::string& confstr); + + //! Destructor + virtual ~SphericalSL(void) {} + + //! Return potential-density pair of a vector of a vector of 1d + //! basis-function grids for SphericalSL, logarithmically spaced + //! between [logxmin, logxmax] (base 10). + BasisArray getBasis(double logxmin=-3.0, double logxmax=0.5, int numgrid=2000); + + //! Compute the orthogonality of the basis by returning inner + //! produce matrices + std::vector orthoCheck(int knots=40) + { return sl->orthoCheck(knots); } + }; + + /** + Uses Bessel basis to evaluate expansion coeffients and provide + potential and density basis fields + */ + class Bessel : public Spherical + { + + protected: + + //! Helper for constructor + void initialize(); + + static const std::set valid_keys; + + //! Return readable class name + const std::string classname() { return "Bessel";} + + //! Grid size for Bessel function table + int rnum = 2000; + + //! Biorthgonal Bessel function generator + std::shared_ptr bess; + + // Get potential + void get_pot(Eigen::MatrixXd& tab, double r) + { bess->get_potl(r, tab); } + + // Get density + void get_dens(Eigen::MatrixXd& tab, double r) + { bess->get_dens(r, tab); } + + // Get force + void get_force(Eigen::MatrixXd& tab, double r) + { bess->get_dpotl(r, tab); } + + public: + + //! Constructor from YAML node + Bessel(const YAML::Node& conf); + + //! Constructor from YAML string + Bessel(const std::string& confstr); + + //! Destructor + virtual ~Bessel(void) {} + + //! Compute the orthogonality of the basis by returning inner + //! produce matrices + std::vector orthoCheck(int knots=40) + { return bess->orthoCheck(knots); } + }; + /** Uses the BiorthCyl basis to evaluate expansion coeffients and provide potential and density basis fields @@ -408,8 +525,7 @@ namespace BasisClasses //! Return a vector of a vector of 1d basis-function grids for //! FlatDisk, logarithmically spaced between [logxmin, logxmax] //! (base 10). - BasisArray getBasis - (double logxmin=-3.0, double logxmax=0.5, int numgrid=2000); + BasisArray getBasis(double logxmin=-3.0, double logxmax=0.5, int numgrid=2000); //! Compute the orthogonality of the basis by returning inner //! produce matrices diff --git a/expui/BiorthBasis.cc b/expui/BiorthBasis.cc index 68897a4cc..9625e4f2d 100644 --- a/expui/BiorthBasis.cc +++ b/expui/BiorthBasis.cc @@ -14,7 +14,7 @@ namespace BasisClasses { const std::set - SphericalSL::valid_keys = { + Spherical::valid_keys = { "rmapping", "cmap", "Lmax", @@ -32,7 +32,6 @@ namespace BasisClasses "tkcum", "tk_type", "nmax", - "modelname", "scale", "rmin", "rmax", @@ -59,7 +58,9 @@ namespace BasisClasses "logr", "plummer", "self_consistent", - "cachename" + "cachename", + "modelname", + "rnum" }; std::vector BiorthBasis::getFieldLabels(const Coord ctype) @@ -90,19 +91,39 @@ namespace BasisClasses return labels; } - SphericalSL::SphericalSL(const YAML::Node& CONF) : - BiorthBasis(CONF, "sphereSL") + Spherical::Spherical(const YAML::Node& CONF, const std::string& forceID) : + BiorthBasis(CONF, forceID) { initialize(); } - SphericalSL::SphericalSL(const std::string& confstr) : - BiorthBasis(confstr, "sphereSL") + Spherical::Spherical(const std::string& confstr, const std::string& forceID) : + BiorthBasis(confstr, forceID) { initialize(); } - void SphericalSL::initialize() + SphericalSL::SphericalSL(const YAML::Node& CONF) : Spherical(CONF, "sphereSL") + { + initialize(); + } + + SphericalSL::SphericalSL(const std::string& confstr) : Spherical(confstr, "sphereSL") + { + initialize(); + } + + Bessel::Bessel(const YAML::Node& CONF) : Spherical(CONF, "Bessel") + { + initialize(); + } + + Bessel::Bessel(const std::string& confstr) : Spherical(confstr, "Bessel") + { + initialize(); + } + + void Spherical::initialize() { // Assign some defaults @@ -110,27 +131,17 @@ namespace BasisClasses cmap = 1; lmax = 6; nmax = 18; - model_file = "SLGridSph.model"; // Check for unmatched keys // auto unmatched = YamlCheck(conf, valid_keys); if (unmatched.size()) - throw YamlConfigError("Basis::Basis::SphericalSL", "parameter", unmatched, __FILE__, __LINE__); + throw YamlConfigError("Basis::Basis::Spherical", "parameter", unmatched, __FILE__, __LINE__); - // Default cachename, empty by default - // - std::string cachename; - - // Assign values from YAML - // - double rmap = 1.0; - try { if (conf["cmap"]) cmap = conf["cmap"].as(); if (conf["Lmax"]) lmax = conf["Lmax"].as(); if (conf["nmax"]) nmax = conf["nmax"].as(); - if (conf["modelname"]) model_file = conf["modelname"].as(); if (conf["rmapping"]) rmap = conf["rmapping"].as(); @@ -168,7 +179,6 @@ namespace BasisClasses if (conf["EVEN_L"]) EVEN_L = conf["EVEN_L"].as(); if (conf["EVEN_M"]) EVEN_M = conf["EVEN_M"].as(); if (conf["M0_ONLY"]) M0_only = conf["M0_ONLY"].as(); - if (conf["cachename"]) cachename = conf["cachename"].as(); } catch (YAML::Exception & error) { if (myid==0) std::cout << "Error parsing parameter stanza for <" @@ -178,43 +188,9 @@ namespace BasisClasses << conf << std::endl << std::string(60, '-') << std::endl; - throw std::runtime_error("SphericalSL: error parsing YAML"); - } - - // Check for non-null cache file name. This must be specified - // to prevent recomputation and unexpected behavior. - // - if (cachename.size() == 0) { - throw std::runtime_error - ("SphericalSL requires a specified cachename in your YAML config\n" - "for consistency with previous invocations and existing coefficient\n" - "sets. Please add explicitly add 'cachename: name' to your config\n" - "with new 'name' for creating a basis or an existing 'name' for\n" - "reading a previously generated basis cache\n"); + throw std::runtime_error("Spherical: error parsing YAML"); } - // Set MPI flag in SLGridSph from MPI_Initialized - SLGridSph::mpi = use_mpi ? 1 : 0; - - // Instantiate to get min/max radius from the model - mod = std::make_shared(model_file); - - // Set rmin to a sane value if not specified - if (not conf["rmin"] or rmin < mod->get_min_radius()) - rmin = mod->get_min_radius(); - - // Set rmax to a sane value if not specified - if (not conf["rmax"] or rmax > mod->get_max_radius()) - rmax = mod->get_max_radius()*0.99; - - // Finally, make the Sturm-Lioville basis... - sl = std::make_shared - (model_file, lmax, nmax, numr, rmin, rmax, true, cmap, rmap, - 0, 1, cachename); - - // Test basis for consistency - orthoTest(200); - // Number of possible threads int nthrds = omp_get_max_threads(); @@ -258,7 +234,94 @@ namespace BasisClasses coordinates = Coord::Spherical; } - void SphericalSL::reset_coefs(void) + void SphericalSL::initialize() + { + + // Assign some defaults + // + model_file = "SLGridSph.model"; + + // Default cachename, empty by default + // + std::string cachename; + + try { + if (conf["modelname"]) model_file = conf["modelname"].as(); + if (conf["cachename"]) cachename = conf["cachename"].as(); + } + catch (YAML::Exception & error) { + if (myid==0) std::cout << "Error parsing parameter stanza for <" + << name << ">: " + << error.what() << std::endl + << std::string(60, '-') << std::endl + << conf << std::endl + << std::string(60, '-') << std::endl; + + throw std::runtime_error("SphericalSL: error parsing YAML"); + } + + // Check for non-null cache file name. This must be specified + // to prevent recomputation and unexpected behavior. + // + if (cachename.size() == 0) { + throw std::runtime_error + ("SphericalSL requires a specified cachename in your YAML config\n" + "for consistency with previous invocations and existing coefficient\n" + "sets. Please add explicitly add 'cachename: name' to your config\n" + "with new 'name' for creating a basis or an existing 'name' for\n" + "reading a previously generated basis cache\n"); + } + + // Set MPI flag in SLGridSph from MPI_Initialized + SLGridSph::mpi = use_mpi ? 1 : 0; + + // Instantiate to get min/max radius from the model + mod = std::make_shared(model_file); + + // Set rmin to a sane value if not specified + if (not conf["rmin"] or rmin < mod->get_min_radius()) + rmin = mod->get_min_radius(); + + // Set rmax to a sane value if not specified + if (not conf["rmax"] or rmax > mod->get_max_radius()) + rmax = mod->get_max_radius()*0.99; + + // Finally, make the Sturm-Lioville basis... + sl = std::make_shared + (model_file, lmax, nmax, numr, rmin, rmax, true, cmap, rmap, + 0, 1, cachename); + + // Test basis for consistency + orthoTest(200); + } + + void Bessel::initialize() + { + try { + if (conf["rnum"]) + rnum = conf["rnum"].as(); + else + rnum = 2000; + } + catch (YAML::Exception & error) { + if (myid==0) std::cout << "Error parsing parameter stanza for <" + << name << ">: " + << error.what() << std::endl + << std::string(60, '-') << std::endl + << conf << std::endl + << std::string(60, '-') << std::endl; + + throw std::runtime_error("SphericalSL: error parsing YAML"); + } + + // Finally, make the Sturm-Lioville basis... + bess = std::make_shared(rmax, lmax, nmax, rnum); + + // Test basis for consistency + orthoTest(200); + } + + void Spherical::reset_coefs(void) { if (expcoef.rows()>0 && expcoef.cols()>0) expcoef.setZero(); totalMass = 0.0; @@ -266,7 +329,7 @@ namespace BasisClasses } - void SphericalSL::load_coefs(CoefClasses::CoefStrPtr coef, double time) + void Spherical::load_coefs(CoefClasses::CoefStrPtr coef, double time) { CoefClasses::SphStruct* cf = dynamic_cast(coef.get()); @@ -301,12 +364,12 @@ namespace BasisClasses } } - void SphericalSL::set_coefs(CoefClasses::CoefStrPtr coef) + void Spherical::set_coefs(CoefClasses::CoefStrPtr coef) { // Sanity check on derived class type // if (typeid(*coef) != typeid(CoefClasses::SphStruct)) - throw std::runtime_error("SphericalSL::set_coefs: you must pass a CoefClasses::SphStruct"); + throw std::runtime_error("Spherical::set_coefs: you must pass a CoefClasses::SphStruct"); // Sanity check on dimensionality // @@ -318,7 +381,7 @@ namespace BasisClasses int rexp = (lmax+1)*(lmax+2)/2; if (rows != rexp or cols != nmax) { std::ostringstream sout; - sout << "SphericalSL::set_coefs: the basis has (lmax, nmax)=(" + sout << "Spherical::set_coefs: the basis has (lmax, nmax)=(" << lmax << ", " << nmax << ") and the dimensions must be (rows, cols)=(" << rexp << ", " << nmax @@ -357,7 +420,7 @@ namespace BasisClasses coefctr = {0.0, 0.0, 0.0}; } - void SphericalSL::accumulate(double x, double y, double z, double mass) + void Spherical::accumulate(double x, double y, double z, double mass) { double fac, fac1, fac2, fac4; double norm = -4.0*M_PI; @@ -381,7 +444,7 @@ namespace BasisClasses used++; totalMass += mass; - sl->get_pot(potd[tid], rs); + get_pot(potd[tid], rs); legendre_R(lmax, costh, legs[tid]); @@ -420,7 +483,7 @@ namespace BasisClasses } - void SphericalSL::make_coefs() + void Spherical::make_coefs() { if (use_mpi) { @@ -437,7 +500,7 @@ namespace BasisClasses } std::vector - SphericalSL::sph_eval(double r, double costh, double phi) + Spherical::sph_eval(double r, double costh, double phi) { // Get thread id int tid = omp_get_thread_num(); @@ -447,9 +510,9 @@ namespace BasisClasses fac1 = factorial(0, 0); - sl->get_dens (dend[tid], r/scale); - sl->get_pot (potd[tid], r/scale); - sl->get_force(dpot[tid], r/scale); + get_dens (dend[tid], r/scale); + get_pot (potd[tid], r/scale); + get_force(dpot[tid], r/scale); legendre_R(lmax, costh, legs[tid], dlegs[tid]); @@ -547,7 +610,7 @@ namespace BasisClasses std::vector - SphericalSL::cyl_eval(double R, double z, double phi) + Spherical::cyl_eval(double R, double z, double phi) { double r = sqrt(R*R + z*z) + 1.0e-18; double costh = z/r, sinth = R/r; @@ -563,7 +626,7 @@ namespace BasisClasses // Evaluate in cartesian coordinates std::vector - SphericalSL::crt_eval + Spherical::crt_eval (double x, double y, double z) { double R = sqrt(x*x + y*y); @@ -578,7 +641,7 @@ namespace BasisClasses } - SphericalSL::BasisArray SphericalSL::getBasis + Spherical::BasisArray SphericalSL::getBasis (double logxmin, double logxmax, int numgrid) { // Assing return storage @@ -599,9 +662,45 @@ namespace BasisClasses Eigen::MatrixXd tabpot, tabden, tabfrc; for (int i=0; iget_pot (tabpot, pow(10.0, logxmin + dx*i)); - sl->get_dens (tabden, pow(10.0, logxmin + dx*i)); - sl->get_force(tabfrc, pow(10.0, logxmin + dx*i)); + get_pot (tabpot, pow(10.0, logxmin + dx*i)); + get_dens (tabden, pow(10.0, logxmin + dx*i)); + get_force(tabfrc, pow(10.0, logxmin + dx*i)); + for (int l=0; l<=lmax; l++) { + for (int n=0; n getFields(double x, double y, double z) override { - PYBIND11_OVERRIDE(std::vector, SphericalSL, getFields, x, y, z); + PYBIND11_OVERRIDE(std::vector, Spherical, getFields, x, y, z); } void accumulate(double x, double y, double z, double mass) override { - PYBIND11_OVERRIDE(void, SphericalSL, accumulate, x, y, z, mass); + PYBIND11_OVERRIDE(void, Spherical, accumulate, x, y, z, mass); } void reset_coefs(void) override { - PYBIND11_OVERRIDE(void, SphericalSL, reset_coefs,); + PYBIND11_OVERRIDE(void, Spherical, reset_coefs,); } void make_coefs(void) override { - PYBIND11_OVERRIDE(void, SphericalSL, make_coefs,); + PYBIND11_OVERRIDE(void, Spherical, make_coefs,); + } + + std::vector orthoCheck(int knots) override { + PYBIND11_OVERRIDE_PURE(std::vector, Spherical, orthoCheck, knots); } }; + class PyCylindrical : public Cylindrical { protected: @@ -809,7 +830,7 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); Returns ------- - CoefStructure + CoefStruct the coefficient structure created from the particles See also @@ -1042,7 +1063,7 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); R"( Set the coordinate system for force evaluations. The natural coordinates for the basis class are the default; spherical - coordinates for SphericalSL, cylindrical coordinates for + coordinates for SphericalSL and Bessel, cylindrical coordinates for Cylindrical and FlatDisk, and Cartesian coordinates for the Slab and Cube. This member function can be used to override the default. The available coorindates are: 'spherical', 'cylindrical', @@ -1131,37 +1152,135 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); None )", py::arg("coefs")); - py::class_, PySphericalSL, BasisClasses::BiorthBasis>(m, "SphericalSL") - .def(py::init(), + py::class_, PyCylindrical, BasisClasses::BiorthBasis>(m, "Cylindrical") + .def(py::init(), R"( - Create a spherical Sturm-Liouville basis + Create a cylindrical EOF basis Parameters ---------- YAMLstring : str - The YAML configuration for the spherical basis + The YAML configuration for the cylindrical basis Returns ------- - SphericalSL + Cylindrical the new instance )", py::arg("YAMLstring")) + .def("getBasis", &BasisClasses::Cylindrical::getBasis, + R"( - .def("getBasis", &BasisClasses::SphericalSL::getBasis, + Evaluate basis on grid for visualization + + Evaluate the potential-density basis functions on a linearly spaced + 2d-grid for inspection. The structure is a two-grid of dimension + lmax by nmax each pointing to a dictionary of 2-d arrays ('potential', + 'density', 'rforce', 'zforce') of dimension numr X numz. + + Parameters + ---------- + xmin : float, default=0.0 + minimum value in mapped radius + xmax : float, default=1.0 + maximum value in mapped radius + numr : int, default=40 + number of linearly-space evaluation points in radius + zmin : float, default=-0.1 + minimum value in vertical height + zmax : float, default=0.1 + maximum value in vertical height + numz : int, default=40 + number of linearly-space evaluation points in height + linear : bool, default=True + use linear spacing + + Returns + ------- + list(list(dict)) + dictionaries of basis functions as lists indexed by m, n + )", + py::arg("xmin")=0.0, + py::arg("xmax")=1.0, + py::arg("numr")=40, + py::arg("zmin")=-0.1, + py::arg("zmax")=0.1, + py::arg("numz")=40, + py::arg("linear")=true) + // The following member needs to be a lambda capture because + // orthoCheck is not in the base class and needs to have different + // parameters depending on the basis type. Here, the quadrature + // is determined by the scale of the meridional grid. + .def("orthoCheck", [](BasisClasses::Cylindrical& A) + { + return A.orthoCheck(); + }, + R"( + Check orthgonality of basis functions by quadrature + + Inner-product matrix of Sturm-Liouville solutions indexed by + harmonic order used to assess fidelity. + + Parameters + ---------- + knots : int + Number of quadrature knots + + Returns + ------- + list(numpy.ndarray) + list of numpy.ndarrays from [0, ... , Mmax] + )") + .def_static("cacheInfo", [](std::string cachefile) + { + return BasisClasses::Cylindrical::cacheInfo(cachefile); + }, + R"( + Report the parameters in a basis cache file and return a dictionary + + Parameters + ---------- + cachefile : str + name of cache file + + Returns + ------- + dict({tag: value},...) + cache parameters + )", + py::arg("cachefile")); + + py::class_, PySpherical, BasisClasses::BiorthBasis>(m, "Spherical") + .def(py::init(), + R"( + Create a spherical basis + + Parameters + ---------- + YAMLstring : str + The YAML configuration for the spherical basis + ForceID : str + The string identifier for this force type + + Returns + ------- + Spherical + the new instance + )", py::arg("YAMLstring"), py::arg("ForceID")) + .def("getBasis", &BasisClasses::Spherical::getBasis, R"( Get basis functions - Evaluate the potential-density basis functions on a logarithmically + Evaluate the potential-density basis functions on a linearly spaced grid for inspection. The structure is a two-grid of dimension lmax by nmax each pointing to a dictionary of 1-d arrays ('potential', 'density', 'rforce') of dimension numr. Parameters ---------- - logxmin : float, default=-3.0 - minimum mapped radius in log10 units - logxmax : float, default=0.5 - maximum mapped radius in log10 units + rmin : float, default=0.0 + minimum radius + rmax : float, default=1.0 + maximum radius numr : int, default=400 number of equally spaced output points @@ -1170,14 +1289,14 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); list(list(dict)) dictionaries of basis functions as lists indexed by l, n )", - py::arg("logxmin")=-3.0, - py::arg("logxmax")=0.5, + py::arg("rmin")=0.0, + py::arg("rmax")=1.0, py::arg("numr")=400) // The following member needs to be a lambda capture because // orthoCheck is not in the base class and needs to have // different parameters depending on the basis type. Here the // user can and will often need to specify a quadrature value. - .def("orthoCheck", [](BasisClasses::SphericalSL& A, int knots) + .def("orthoCheck", [](BasisClasses::Spherical& A, int knots) { return A.orthoCheck(knots); }, @@ -1200,7 +1319,7 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); py::arg("knots")=40) .def_static("cacheInfo", [](std::string cachefile) { - return BasisClasses::SphericalSL::cacheInfo(cachefile); + return BasisClasses::Spherical::cacheInfo(cachefile); }, R"( Report the parameters in a basis cache file and return a dictionary @@ -1261,68 +1380,98 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); )", py::arg("I")); - py::class_, PyCylindrical, BasisClasses::BiorthBasis>(m, "Cylindrical") - .def(py::init(), + py::class_, BasisClasses::Spherical>(m, "SphericalSL") + .def(py::init(), R"( - Create a cylindrical EOF basis + Create a spherical Sturm-Liouville basis Parameters ---------- YAMLstring : str - The YAML configuration for the cylindrical basis + The YAML configuration for the spherical basis Returns ------- - Cylindrical + SphericalSL the new instance )", py::arg("YAMLstring")) - .def("getBasis", &BasisClasses::Cylindrical::getBasis, - R"( - Evaluate basis on grid for visualization + .def("getBasis", &BasisClasses::SphericalSL::getBasis, + R"( + Get basis functions - Evaluate the potential-density basis functions on a linearly spaced - 2d-grid for inspection. The structure is a two-grid of dimension - lmax by nmax each pointing to a dictionary of 2-d arrays ('potential', - 'density', 'rforce', 'zforce') of dimension numr X numz. + Evaluate the potential-density basis functions on a logarithmically + spaced grid for inspection. The structure is a two-grid of dimension + lmax by nmax each pointing to a dictionary of 1-d arrays ('potential', + 'density', 'rforce') of dimension numr. + + Parameters + ---------- + logxmin : float, default=-3.0 + minimum mapped radius in log10 units + logxmax : float, default=0.5 + maximum mapped radius in log10 units + numr : int, default=400 + number of equally spaced output points + + Returns + ------- + list(list(dict)) + dictionaries of basis functions as lists indexed by l, n + )", + py::arg("logxmin")=-3.0, + py::arg("logxmax")=0.5, + py::arg("numr")=400) + // The following member needs to be a lambda capture because + // orthoCheck is not in the base class and needs to have + // different parameters depending on the basis type. Here the + // user can and will often need to specify a quadrature value. + .def("orthoCheck", [](BasisClasses::SphericalSL& A, int knots) + { + return A.orthoCheck(knots); + }, + R"( + Check orthgonality of basis functions by quadrature + + Inner-product matrix of Sturm-Liouville solutions indexed by + harmonic order used to assess fidelity. + + Parameters + ---------- + knots : int, default=40 + Number of quadrature knots + + Returns + ------- + list(numpy.ndarray) + list of numpy.ndarrays from [0, ... , Lmax] + )", + py::arg("knots")=40); + + + py::class_, BasisClasses::Spherical>(m, "Bessel") + .def(py::init(), + R"( + Create a spherical Bessel-function basis Parameters ---------- - xmin : float, default=0.0 - minimum value in mapped radius - xmax : float, default=1.0 - maximum value in mapped radius - numr : int, default=40 - number of linearly-space evaluation points in radius - zmin : float, default=-0.1 - minimum value in vertical height - zmax : float, default=0.1 - maximum value in vertical height - numz : int, default=40 - number of linearly-space evaluation points in height - linear : bool, default=True - use linear spacing + YAMLstring : str + The YAML configuration for the spherical Bessel-function basis Returns ------- - list(list(dict)) - dictionaries of basis functions as lists indexed by m, n - )", - py::arg("xmin")=0.0, - py::arg("xmax")=1.0, - py::arg("numr")=40, - py::arg("zmin")=-0.1, - py::arg("zmax")=0.1, - py::arg("numz")=40, - py::arg("linear")=true) - // The following member needs to be a lambda capture because - // orthoCheck is not in the base class and needs to have different - // parameters depending on the basis type. Here, the quadrature - // is determined by the scale of the meridional grid. - .def("orthoCheck", [](BasisClasses::Cylindrical& A) - { - return A.orthoCheck(); - }, + Bessel + the new instance + )", py::arg("YAMLstring")) + // The following member needs to be a lambda capture because + // orthoCheck is not in the base class and needs to have + // different parameters depending on the basis type. Here the + // user can and will often need to specify a quadrature value. + .def("orthoCheck", [](BasisClasses::Bessel& A, int knots) + { + return A.orthoCheck(knots); + }, R"( Check orthgonality of basis functions by quadrature @@ -1331,32 +1480,15 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); Parameters ---------- - knots : int + knots : int, default=40 Number of quadrature knots Returns ------- list(numpy.ndarray) - list of numpy.ndarrays from [0, ... , Mmax] - )") - .def_static("cacheInfo", [](std::string cachefile) - { - return BasisClasses::Cylindrical::cacheInfo(cachefile); - }, - R"( - Report the parameters in a basis cache file and return a dictionary - - Parameters - ---------- - cachefile : str - name of cache file - - Returns - ------- - dict({tag: value},...) - cache parameters - )", - py::arg("cachefile")); + list of numpy.ndarrays from [0, ... , Lmax] + )", + py::arg("knots")=40); py::class_, PyFlatDisk, BasisClasses::BiorthBasis>(m, "FlatDisk") .def(py::init(), @@ -1714,7 +1846,7 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); Returns ------- - CoefStructure + CoefStruct the coefficient structure created from the particles See also @@ -1920,5 +2052,4 @@ PYBIND11_OVERRIDE_PURE(void, Basis, addFromArray, m, p, roundrobin, posvelrows); py::arg("tinit"), py::arg("tfinal"), py::arg("h"), py::arg("ps"), py::arg("basiscoef"), py::arg("func"), py::arg("nout")=std::numeric_limits::max()); - } diff --git a/src/Bessel.cc b/src/Bessel.cc index 657a9d1b4..eedf4592f 100644 --- a/src/Bessel.cc +++ b/src/Bessel.cc @@ -51,7 +51,7 @@ Bessel::Bessel(Component* c0, const YAML::Node& conf, MixtureBasis* m) : Spheric void Bessel::get_dpotl(int lmax, int nmax, double r, Eigen::MatrixXd& p, Eigen::MatrixXd& dp, int tid) { - int klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; + int klo = (int)( (r-r_grid[0])/r_grid_del ); if (klo < 0) klo = 0; if (klo > RNUM - 2) klo = RNUM - 2; int khi = klo + 1; @@ -76,7 +76,7 @@ void Bessel::get_dpotl(int lmax, int nmax, double r, void Bessel::get_potl(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid) { - int klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; + int klo = (int)( (r-r_grid[0])/r_grid_del ); if (klo < 0) klo = 0; if (klo > RNUM - 2) klo = RNUM - 2; int khi = klo + 1; @@ -97,7 +97,7 @@ void Bessel::get_potl(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid) void Bessel::get_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid) { - int klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; + int klo = (int)( (r-r_grid[0])/r_grid_del ); if (klo < 0) klo = 0; if (klo > RNUM - 2) klo = RNUM - 2; int khi = klo + 1; @@ -120,7 +120,7 @@ void Bessel::get_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p, int tid) void Bessel::get_potl_dens(int lmax, int nmax, double r, Eigen::MatrixXd& p, Eigen::MatrixXd& d, int tid) { - int klo = (int)( (r-r_grid[1])/r_grid_del ) + 1; + int klo = (int)( (r-r_grid[0])/r_grid_del ); if (klo < 0) klo = 0; if (klo > RNUM - 2) klo = RNUM - 2; int khi = klo + 1; From 10c01fe7cafe1678a0876bf6d12f15c6237eb5fa Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sun, 12 May 2024 22:23:18 -0400 Subject: [PATCH 105/167] Check in missing Bessel implementation --- expui/BiorthBess.H | 93 ++++++++++++++++ expui/BiorthBess.cc | 251 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 expui/BiorthBess.H create mode 100644 expui/BiorthBess.cc diff --git a/expui/BiorthBess.H b/expui/BiorthBess.H new file mode 100644 index 000000000..ed0dcb57e --- /dev/null +++ b/expui/BiorthBess.H @@ -0,0 +1,93 @@ +#ifndef _BiorthBess_H_ +#define _BiorthBess_H_ + +#include +#include +#include + +#include + +Eigen::VectorXd sbessjz(int n, int m); + +class BiorthBess +{ +private: + + //! Grid to hold tabulated basis + class RGrid + { + public: + Eigen::MatrixXd rw; + Eigen::MatrixXd rw2; + int nmax; + }; + + //@{ + //! Grid storage and parameters + std::vector dens_grid, potl_grid; + Eigen::VectorXd r_grid; + double r_grid_del; + //@} + + //! Cache roots for spherical Bessel functions + class Roots + { + public: + + int l; + int n; + + Eigen::VectorXd a; + + Roots(int L, int nmax) : l(L), n(nmax) { + a = sbessjz(l, n); + } + + ~Roots() {} + }; + + //! Root database isntance + std::shared_ptr p; + + //@{ + //! Density and potential members + double dens(double r, int n); + double potl(double r, int n); + //@} + + //@{ + //! Parameters + double rmin=0.0, rmax; + int lmax, nmax, RNUM=2000; + //@} + + //! Make the density and potential grid + void make_grid(); + +public: + + //! Constructor + BiorthBess(double rmax, int lmax, int nmax, int rnum); + + //! Destructor + virtual ~BiorthBess() {} + + //! Potential evaluation + void get_potl(double r, Eigen::MatrixXd& p); + + //! Density evaluation + void get_dens(double r, Eigen::MatrixXd& p); + + //! Force evaluation + void get_dpotl(double r, Eigen::MatrixXd& p); + + //! Potential and density evaluation + void get_potl_dens(double r, Eigen::MatrixXd& p, Eigen::MatrixXd& d); + + //! Compute the orthogonality of the basis by returning inner + //! produce matrices + std::vector orthoCheck(int knots=40); +}; + +#endif + diff --git a/expui/BiorthBess.cc b/expui/BiorthBess.cc new file mode 100644 index 000000000..6114da63c --- /dev/null +++ b/expui/BiorthBess.cc @@ -0,0 +1,251 @@ +#include +#include +#include +#include +#include + + +BiorthBess::BiorthBess(double rmax, int lmax, int nmax, int RNUM) : + rmax(rmax), lmax(lmax), nmax(nmax), RNUM(RNUM) +{ + // Initialize radial grids + make_grid(); +} + +// Get potential functions by from table +void BiorthBess::get_dpotl(double r, Eigen::MatrixXd& p) +{ + int klo = (int)( (r-r_grid[0])/r_grid_del ); + if (klo < 0) klo = 0; + if (klo > RNUM - 2) klo = RNUM - 2; + int khi = klo + 1; + + double a = (r_grid[khi] - r)/r_grid_del; + double b = (r - r_grid[klo])/r_grid_del; + + double aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; + double bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; + double aaa = -(3.0*a*a - 1.0)*r_grid_del/6.0; + double bbb = (3.0*b*b - 1.0)*r_grid_del/6.0; + + p.resize(lmax+1, nmax); + + for (int l=0; l<=lmax; l++) { + for (int n=0; n RNUM - 2) klo = RNUM - 2; + int khi = klo + 1; + + double a = (r_grid[khi] - r)/r_grid_del; + double b = (r - r_grid[klo])/r_grid_del; + + double aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; + double bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; + + p.resize(lmax+1, nmax); + + for (int l=0; l<=lmax; l++) { + for (int n=0; n RNUM - 2) klo = RNUM - 2; + int khi = klo + 1; + + double a = (r_grid[khi] - r)/r_grid_del; + double b = (r - r_grid[klo])/r_grid_del; + + double aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; + double bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; + + p.resize(lmax+1, nmax); + + for (int l=0; l<=lmax; l++) { + for (int n=0; n RNUM - 2) klo = RNUM - 2; + int khi = klo + 1; + + double a = (r_grid[khi] - r)/r_grid_del; + double b = (r - r_grid[klo])/r_grid_del; + + double aa = a*(a*a-1.0)*r_grid_del*r_grid_del/6.0; + double bb = b*(b*b-1.0)*r_grid_del*r_grid_del/6.0; + + p.resize(lmax+1, nmax); + d.resize(lmax+1, nmax); + + for (int l=0; l<=lmax; l++) { + for (int n=0; np->n) + throw GenericError("Routine dens() called with n out of bounds", __FILE__, __LINE__, 1001, true); + + alpha = p->a[n]; + return alpha*M_SQRT2/fabs(EXPmath::sph_bessel(p->l+1, alpha)) * pow(rmax,-2.5) * + EXPmath::sph_bessel(p->l, alpha*r/rmax); +} + +double BiorthBess::potl(double r, int n) +{ + double alpha; + + if (n>p->n) + throw GenericError("Routine potl() called with n out of bounds", __FILE__, __LINE__, 1002, true); + + alpha = p->a[n]; + return M_SQRT2/fabs(alpha*EXPmath::sph_bessel(p->l+1,alpha)) * pow(rmax,-0.5) * + EXPmath::sph_bessel(p->l,alpha*r/rmax); +} + +void BiorthBess::make_grid() +{ + + potl_grid.resize(lmax+1); + dens_grid.resize(lmax+1); + + r_grid.resize(RNUM); + + r_grid_del = rmax/(double)(RNUM-1); + double r = 0.0; + for (int ir=0; ir(l, nmax); + + for (int n=0; n BiorthBess::orthoCheck(int num) +{ + // Allocate storage + // + std::vector one(lmax+1); + for (auto & u : one) { + u.resize(nmax, nmax); + u.setZero(); + } + + // Number of knots + // + LegeQuad wk(num); + + // Radial range + // + double dr = rmax - rmin; + + Eigen::MatrixXd p(lmax+1, nmax), d(lmax+1, nmax); + + + // Biorthogonal integral loop + // + for (int i=0; i Date: Mon, 13 May 2024 07:34:59 -0400 Subject: [PATCH 106/167] Revert the density sign change because it breaks the orthoTest [no ci] --- expui/BiorthBess.cc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/expui/BiorthBess.cc b/expui/BiorthBess.cc index 6114da63c..82a0c7593 100644 --- a/expui/BiorthBess.cc +++ b/expui/BiorthBess.cc @@ -82,8 +82,6 @@ void BiorthBess::get_dens(double r, Eigen::MatrixXd& p) aa*dens_grid[l].rw2(n, klo) + bb*dens_grid[l].rw2(n, khi); } } - - p *= -1.0; } @@ -111,8 +109,6 @@ void BiorthBess::get_potl_dens(double r, Eigen::MatrixXd& p, Eigen::MatrixXd& d) aa*dens_grid[l].rw2(n, klo) + bb*dens_grid[l].rw2(n, khi); } } - - d *= -1.0; } double BiorthBess::dens(double r, int n) From 2c79f85d14d67b3f1c218bda21047e334ba69b9e Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 14 May 2024 22:39:43 -0400 Subject: [PATCH 107/167] Enforce a minimum small integer step into sorted particle array to choose inner and outer bin edges to prevent empty bins at small and large radii [no ci] --- src/Sphere.H | 1 + src/Sphere.cc | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Sphere.H b/src/Sphere.H index 1178b996b..0ec8e9c7a 100644 --- a/src/Sphere.H +++ b/src/Sphere.H @@ -92,6 +92,7 @@ private: double tnext, dtime; int numr; int nums; + int noff; int cmap; int diverge; double dfac; diff --git a/src/Sphere.cc b/src/Sphere.cc index df11809e9..6c3fda20b 100644 --- a/src/Sphere.cc +++ b/src/Sphere.cc @@ -14,6 +14,7 @@ Sphere::valid_keys = { "rmapping", "numr", "nums", + "noff", "cmap", "diverge", "dfac", @@ -32,6 +33,7 @@ Sphere::Sphere(Component* c0, const YAML::Node& conf, MixtureBasis* m) : rmap = 0.067*rmax; numr = 2000; nums = 2000; + noff = 32; cmap = 1; diverge = 0; dfac = 1.0; @@ -108,6 +110,7 @@ void Sphere::initialize() if (conf["rmapping"]) rmap = conf["rmapping"].as(); if (conf["numr"]) numr = conf["numr"].as(); if (conf["nums"]) nums = conf["nums"].as(); + if (conf["noff"]) noff = conf["noff"].as(); if (conf["cmap"]) cmap = conf["cmap"].as(); if (conf["diverge"]) diverge = conf["diverge"].as(); if (conf["dfac"]) dfac = conf["dfac"].as(); @@ -251,8 +254,18 @@ void Sphere::make_model_bin() // Make mass array // int numR = std::min(sqrt(Ntot), nums); - double Rmin = std::max(RM.begin()->first, rmin); - double Rmax = RM.rbegin()->first; + + // Compute offsets into mass array for bin edges + // + int nstep = std::max(noff, std::floor(0.5*Ntot/numR)); + + auto ibeg = RM.begin(); + auto iend = RM.rbegin(); + std::advance(ibeg, nstep); + std::advance(iend, nstep); + + double Rmin = std::max(ibeg->first, rmin); + double Rmax = iend->first; bool logR = false; if (logr and Rmin>0.0) { From 80cfca0ec9efb04dc54096dfe6e44cf431ece686 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 16 May 2024 13:41:19 -0400 Subject: [PATCH 108/167] Add runtag to model-build diagnostics [no ci] --- src/Sphere.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Sphere.cc b/src/Sphere.cc index 6c3fda20b..cf1dc514e 100644 --- a/src/Sphere.cc +++ b/src/Sphere.cc @@ -316,7 +316,8 @@ void Sphere::make_model_bin() // if (myid==0) { static int cnt = 0; - std::ostringstream sout; sout << "SphereMassModel." << cnt++; + std::ostringstream sout; + sout << runtag << ".SphereMassModel." << cnt++; std::ofstream dbg(sout.str()); if (dbg.good()) { for (int i=0; i(mod, Lmax, nmax, numR, Rmin, Rmax, false, 1, 1.0, cachename); // Test for basis consistency (will generate an exception if maximum From 3fb2284760093c6921944511ef9380c57bf61ed9 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 16 May 2024 13:42:28 -0400 Subject: [PATCH 109/167] Add additional diagnostic output; fix use_mpi logic to prevent MPI_Finalize calls [no ci] --- utils/SL/slcheck.cc | 68 +++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/utils/SL/slcheck.cc b/utils/SL/slcheck.cc index 4a28596e8..f182b8d8e 100644 --- a/utils/SL/slcheck.cc +++ b/utils/SL/slcheck.cc @@ -15,8 +15,8 @@ int main(int argc, char** argv) { - bool use_mpi, use_logr; - double rmin, rmax, rs, dfac; + bool use_mpi, use_logr, diag; + double rmin, rmax, rmapping, dfac; int numr, cmap, diverge, Lmax, nmax; string filename, cachefile; @@ -27,43 +27,38 @@ int main(int argc, char** argv) cxxopts::Options options(argv[0], "Check the consistency a spherical SL basis"); options.add_options() - ("h,help", "Print this help message") - ("mpi", "using parallel computation", + ("h,help", "Print this help message") + ("d,diag", "Turn on Sledge diagnostics for SL computation", + cxxopts::value(diag)->default_value("false")) + ("mpi", "using parallel computation", cxxopts::value(use_mpi)->default_value("false")) - ("logr", "logarithmic spacing for orthogonality check", + ("logr", "logarithmic spacing for orthogonality check", cxxopts::value(use_logr)->default_value("false")) - ("cmap", "coordinates in SphereSL: use mapped (1) or linear(0) coordinates", + ("cmap", "coordinates in SphereSL: use mapped (1) or linear(0) coordinates", cxxopts::value(cmap)->default_value("1")) - ("Lmax", "maximum number of angular harmonics in the expansion", + ("Lmax", "maximum number of angular harmonics in the expansion", cxxopts::value(Lmax)->default_value("2")) - ("nmax", "maximum number of radial harmonics in the expansion", + ("nmax", "maximum number of radial harmonics in the expansion", cxxopts::value(nmax)->default_value("10")) - ("numr", "radial knots for the SL grid", + ("numr", "radial knots for the SL grid", cxxopts::value(numr)->default_value("1000")) - ("rmin", "minimum radius for the SL grid", + ("rmin", "minimum radius for the SL grid", cxxopts::value(rmin)->default_value("-1.0")) - ("rmax", "maximum radius for the SL grid", + ("rmax", "maximum radius for the SL grid", cxxopts::value(rmax)->default_value("-1.0")) - ("rs", "cmap scale factor", - cxxopts::value(rs)->default_value("0.067")) - ("diverge", "cusp divergence for spherical model", + ("rmapping", "cmap scale factor", + cxxopts::value(rmapping)->default_value("0.067")) + ("diverge", "cusp divergence for spherical model", cxxopts::value(diverge)->default_value("0")) - ("dfac", "cusp divergence exponent for spherical model", + ("dfac", "cusp divergence exponent for spherical model", cxxopts::value(dfac)->default_value("1.0")) - ("filename", "model file", + ("filename", "model file", cxxopts::value(filename)->default_value("SLGridSph.model")) - ("cache", "cache file", + ("cache", "cache file", cxxopts::value(cachefile)->default_value(".slgrid_sph_cache")) ; - //=================== - // MPI preliminaries - //=================== - if (use_mpi) { - local_init_mpi(argc, argv); - } - //=================== // Parse options //=================== @@ -78,6 +73,13 @@ int main(int argc, char** argv) return 2; } + //=================== + // MPI preliminaries + //=================== + if (use_mpi) { + local_init_mpi(argc, argv); + } + // Print help message and exit // if (vm.count("help")) { @@ -109,14 +111,14 @@ int main(int argc, char** argv) // try { ortho = std::make_shared(filename, Lmax, nmax, numr, rmin, rmax, - true, cmap, rs, 0, 1.0, cachefile, false); - // ^ ^ ^ - // | | | - // Use cache file----------------------+ | | - // | | - // Cusp extrapolation----------------------------------+ | - // | - // Turn on diagnostic output in SL creation-------------------------------+ + true, cmap, rmapping, 0, 1.0, cachefile, diag); + // ^ ^ ^ + // | | | + // Use cache file----------------------+ | | + // | | + // Cusp extrapolation----------------------------------------+ | + // | + // Turn on diagnostic output in SL creation-------------------------------------+ } catch (const EXPException& err) { if (myid==0) { @@ -131,7 +133,7 @@ int main(int argc, char** argv) } if (bad) { - MPI_Finalize(); + if (use_mpi) MPI_Finalize(); exit(0); } // Do what? From a9ef74b9a68ddb741e2cb98ef78908660d114d2a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 16 May 2024 13:42:50 -0400 Subject: [PATCH 110/167] Fix fencepost error in grid generation assignment [no ci] --- exputil/realize_model.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/exputil/realize_model.cc b/exputil/realize_model.cc index ab3b0264f..0adfc7b0a 100644 --- a/exputil/realize_model.cc +++ b/exputil/realize_model.cc @@ -1093,6 +1093,7 @@ Eigen::VectorXd SphericalModelMulti::gen_point(int& ierr) ibeg[n] = dN*n; iend[n] = dN*(n+1); } + iend[numprocs-1] = gen_N; std::vector gen_emax(gen_N, 0.0), gen_vmax(gen_N, 0.0); From 1fcc289605f890bf87473eed194c6418dff06faa Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 17 May 2024 17:52:10 -0400 Subject: [PATCH 111/167] Fix grid asymmetry error; very minor [no ci] --- expui/FieldGenerator.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/FieldGenerator.cc b/expui/FieldGenerator.cc index 3f1631a0b..5e00e1c82 100644 --- a/expui/FieldGenerator.cc +++ b/expui/FieldGenerator.cc @@ -93,7 +93,7 @@ namespace Field // Compute the probe length // std::vector dd(3); - for (int k=0; k<3; k++) dd[k] = (end[k] - beg[k])/num; + for (int k=0; k<3; k++) dd[k] = (end[k] - beg[k])/(num-1); double dlen = sqrt(dd[0]*dd[0] + dd[1]*dd[1] + dd[2]*dd[2]); for (int icnt=0; icnt Date: Mon, 20 May 2024 14:50:31 -0400 Subject: [PATCH 112/167] Fixed mistake in exception that prevented message from showing; fixed mistake in default value for Coef::EvenOddPower() [no ci] --- expui/Coefficients.cc | 18 +++++++++++------- pyEXP/CoefWrappers.cc | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 17f642933..78c3f2827 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -1085,17 +1085,21 @@ namespace CoefClasses node = YAML::Load(getYAML()); } catch (const std::runtime_error& error) { - std::cout << "CylCoefs::EvenOddPower: found a problem while loading " - << "the YAML config" << std::endl; - throw; + throw std::runtime_error + ( + "CylCoefs::EvenOddPower: found a problem while loading the " + "YAML config" + ); } if (node["ncylodd"]) nodd = node["ncylodd"].as(); else { - std::cout << "CylCoefs::EvenOddPower: ncylodd is not in the YAML " - << "config stanza. Please specify this explicitly as " - << "the first argument to EvenOddPower()" << std::endl; - throw; + throw std::runtime_error + ( + "CylCoefs::EvenOddPower: ncylodd is not in the YAML config " + "stanza. Please specify this explicitly as the first argument " + "to EvenOddPower()" + ); } } diff --git a/pyEXP/CoefWrappers.cc b/pyEXP/CoefWrappers.cc index 477718c79..b1ef1d91e 100644 --- a/pyEXP/CoefWrappers.cc +++ b/pyEXP/CoefWrappers.cc @@ -1302,7 +1302,7 @@ void CoefficientClasses(py::module &m) { configuration. If in doubt, use the default. )", py::arg("nodd")=-1, py::arg("min")=0, - py::arg("max") = std::numeric_limits::max()); + py::arg("max")=std::numeric_limits::max()); py::class_, CoefClasses::Coefs> From be9aaee58b9c467a4a5c6bcca58e08b2598ae093 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 22 May 2024 12:20:37 -0400 Subject: [PATCH 113/167] Add a version tag to HDF5 coefficient sets and use that to toggle backward compatibility with the old (buggy) HighFive Eigen wrappers --- expui/Coefficients.H | 9 +++++++++ expui/Coefficients.cc | 36 ++++++++++++++++++++++++++++++++++++ pyEXP/CoefWrappers.cc | 13 +++++++++++++ 3 files changed, 58 insertions(+) diff --git a/expui/Coefficients.H b/expui/Coefficients.H index 6cc6c0df8..737881916 100644 --- a/expui/Coefficients.H +++ b/expui/Coefficients.H @@ -54,6 +54,9 @@ namespace CoefClasses //! Verbose debugging output bool verbose; + //! Backward compatibility flag for HighFive + static bool H5BackCompat; + //! Time vector std::vector times; @@ -64,6 +67,9 @@ namespace CoefClasses //! Get the YAML config for the basis (to be implemented by EXP) virtual std::string getYAML() = 0; + //! Coefficient file versioning + inline static const std::string Version = "1.0"; + //! Write parameter attributes (needed for derived classes) virtual void WriteH5Params(HighFive::File& file) = 0; @@ -195,6 +201,9 @@ namespace CoefClasses //! Set maximum grid interpolation offset void setDeltaT(double dT) { deltaT = dT; } + //! Override backward compatibility for HighFive + static void setNewH5() { H5BackCompat = false; } + class CoefsError : public std::runtime_error { public: diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 78c3f2827..227125751 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -20,6 +20,8 @@ namespace CoefClasses { + bool Coefs::H5BackCompat = true; + void Coefs::copyfields(std::shared_ptr p) { // These variables will copy data, not pointers @@ -91,6 +93,9 @@ namespace CoefClasses file.getAttribute("geometry").read(geometry); file.getAttribute("forceID" ).read(forceID ); + bool H5back = true; + if (file.hasAttribute("Version")) H5back = false; + // Open the snapshot group // auto snaps = file.getGroup("snapshots"); @@ -115,6 +120,18 @@ namespace CoefClasses if (Time < Tmin or Time > Tmax) continue; auto in = stanza.getDataSet("coefficients").read(); + + if (H5back and H5BackCompat) { + + auto in2 = stanza.getDataSet("coefficients").read(); + in2.transposeInPlace(); + + for (size_t c=0, n=0; c Tmax) continue; auto in = stanza.getDataSet("coefficients").read(); + + if (H5back and H5BackCompat) { + + auto in2 = stanza.getDataSet("coefficients").read(); + in2.transposeInPlace(); + + for (size_t c=0, n=0; c("Version", HighFive::DataSpace::From(Version)).write(Version); + // We write the coefficient file geometry // file.createAttribute("geometry", HighFive::DataSpace::From(geometry)).write(geometry); diff --git a/pyEXP/CoefWrappers.cc b/pyEXP/CoefWrappers.cc index b1ef1d91e..f5736a3f7 100644 --- a/pyEXP/CoefWrappers.cc +++ b/pyEXP/CoefWrappers.cc @@ -867,6 +867,19 @@ void CoefficientClasses(py::module &m) { are found at the requested time )", py::arg("time")) + .def("newH5", + &CoefClasses::Coefs::setNewH5, + R"( + Override backwards compatibility flag for HighFive API change + + Parameters + ---------- + None + + Returns + ------- + None + )") .def("setData", &CoefClasses::Coefs::setData, R"( From 5a6e3ebc59cf80c4adb3dd54b1169dbbc57b5c89 Mon Sep 17 00:00:00 2001 From: Martin Weinberg Date: Wed, 22 May 2024 17:42:35 -0400 Subject: [PATCH 114/167] Update expui/Coefficients.cc Co-authored-by: Michael Petersen --- expui/Coefficients.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 227125751..1c73c5207 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -821,7 +821,7 @@ namespace CoefClasses if (Time < Tmin or Time > Tmax) continue; auto in = stanza.getDataSet("coefficients").read(); - + // If an older version of the coefficients and backwards compatibility is desired, re-order the coefficients to match the cache. if (H5back and H5BackCompat) { auto in2 = stanza.getDataSet("coefficients").read(); From 17ea22ce7495f0ee68032e92eb7ce12929b181ce Mon Sep 17 00:00:00 2001 From: Martin Weinberg Date: Wed, 22 May 2024 17:42:52 -0400 Subject: [PATCH 115/167] Update expui/Coefficients.cc Co-authored-by: Michael Petersen --- expui/Coefficients.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 1c73c5207..158e9f8bb 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -795,7 +795,7 @@ namespace CoefClasses file.getDataSet ("count" ).read(count ); bool H5back = true; - if (file.hasAttribute("Version")) H5back = false; + if (file.hasAttribute("CoefficientOutputVersion")) H5back = false; // Open the snapshot group // From c48a11a76e3846a28191adb1fa019cd460684240 Mon Sep 17 00:00:00 2001 From: Martin Weinberg Date: Wed, 22 May 2024 17:43:09 -0400 Subject: [PATCH 116/167] Update expui/Coefficients.cc Co-authored-by: Michael Petersen --- expui/Coefficients.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 158e9f8bb..59a2321a0 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -2287,7 +2287,7 @@ namespace CoefClasses // Write the Version string // - file.createAttribute("Version", HighFive::DataSpace::From(Version)).write(Version); + file.createAttribute("CoefficientOutputVersion", HighFive::DataSpace::From(CoefficientOutputVersion)).write(CoefficientOutputVersion); // We write the coefficient file geometry // From a46897a8fe34efa253c834ac5075965510f38ddc Mon Sep 17 00:00:00 2001 From: Martin Weinberg Date: Wed, 22 May 2024 17:43:16 -0400 Subject: [PATCH 117/167] Update expui/Coefficients.H Co-authored-by: Michael Petersen --- expui/Coefficients.H | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/Coefficients.H b/expui/Coefficients.H index 737881916..8615c22aa 100644 --- a/expui/Coefficients.H +++ b/expui/Coefficients.H @@ -68,7 +68,7 @@ namespace CoefClasses virtual std::string getYAML() = 0; //! Coefficient file versioning - inline static const std::string Version = "1.0"; + inline static const std::string CoefficientOutputVersion = "1.0"; //! Write parameter attributes (needed for derived classes) virtual void WriteH5Params(HighFive::File& file) = 0; From aef851a552718ef5233076294b7e8853cca0b453 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 22 May 2024 17:56:52 -0400 Subject: [PATCH 118/167] Some fixes for Mike's suggested code changes --- expui/Coefficients.H | 6 ------ expui/Coefficients.cc | 22 ++++++++++++++++------ pyEXP/CoefWrappers.cc | 13 ------------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/expui/Coefficients.H b/expui/Coefficients.H index 8615c22aa..d6a121815 100644 --- a/expui/Coefficients.H +++ b/expui/Coefficients.H @@ -54,9 +54,6 @@ namespace CoefClasses //! Verbose debugging output bool verbose; - //! Backward compatibility flag for HighFive - static bool H5BackCompat; - //! Time vector std::vector times; @@ -201,9 +198,6 @@ namespace CoefClasses //! Set maximum grid interpolation offset void setDeltaT(double dT) { deltaT = dT; } - //! Override backward compatibility for HighFive - static void setNewH5() { H5BackCompat = false; } - class CoefsError : public std::runtime_error { public: diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 59a2321a0..230291ec7 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -20,8 +20,6 @@ namespace CoefClasses { - bool Coefs::H5BackCompat = true; - void Coefs::copyfields(std::shared_ptr p) { // These variables will copy data, not pointers @@ -93,8 +91,11 @@ namespace CoefClasses file.getAttribute("geometry").read(geometry); file.getAttribute("forceID" ).read(forceID ); + // Look for Coef output version to toggle backward compatibility + // with legacy storage order + // bool H5back = true; - if (file.hasAttribute("Version")) H5back = false; + if (file.hasAttribute("CoefficientOutputVersion")) H5back = false; // Open the snapshot group // @@ -121,7 +122,10 @@ namespace CoefClasses auto in = stanza.getDataSet("coefficients").read(); - if (H5back and H5BackCompat) { + // If we have a legacy set of coefficients, re-order the + // coefficients to match the new HighFive/Eigen ordering + // + if (H5back) { auto in2 = stanza.getDataSet("coefficients").read(); in2.transposeInPlace(); @@ -794,6 +798,9 @@ namespace CoefClasses file.getAttribute("config" ).read(config); file.getDataSet ("count" ).read(count ); + // Look for Coef output version to toggle backward compatibility + // with legacy storage order + // bool H5back = true; if (file.hasAttribute("CoefficientOutputVersion")) H5back = false; @@ -821,8 +828,11 @@ namespace CoefClasses if (Time < Tmin or Time > Tmax) continue; auto in = stanza.getDataSet("coefficients").read(); - // If an older version of the coefficients and backwards compatibility is desired, re-order the coefficients to match the cache. - if (H5back and H5BackCompat) { + + // If we have a legacy set of coefficients, re-order the + // coefficients to match the new HighFive/Eigen ordering + // + if (H5back) { auto in2 = stanza.getDataSet("coefficients").read(); in2.transposeInPlace(); diff --git a/pyEXP/CoefWrappers.cc b/pyEXP/CoefWrappers.cc index f5736a3f7..b1ef1d91e 100644 --- a/pyEXP/CoefWrappers.cc +++ b/pyEXP/CoefWrappers.cc @@ -867,19 +867,6 @@ void CoefficientClasses(py::module &m) { are found at the requested time )", py::arg("time")) - .def("newH5", - &CoefClasses::Coefs::setNewH5, - R"( - Override backwards compatibility flag for HighFive API change - - Parameters - ---------- - None - - Returns - ------- - None - )") .def("setData", &CoefClasses::Coefs::setData, R"( From dc8e8b99f10ac7d0f2c359aa593d716e6d36eede Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 11 Jun 2024 10:23:54 -0700 Subject: [PATCH 119/167] Upgrade config for current version of Doxygen --- doc/exp.cfg | 844 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 563 insertions(+), 281 deletions(-) diff --git a/doc/exp.cfg b/doc/exp.cfg index e7245693b..b7ccb4716 100644 --- a/doc/exp.cfg +++ b/doc/exp.cfg @@ -1,4 +1,4 @@ -# Doxyfile 1.8.17 +# Doxyfile 1.9.8 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -12,6 +12,16 @@ # For lists, items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] #--------------------------------------------------------------------------- # Project related configuration options @@ -32,7 +42,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = "EXP" +PROJECT_NAME = EXP # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version @@ -60,16 +70,28 @@ PROJECT_LOGO = exp_logo.png OUTPUT_DIRECTORY = . -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -# directories (in 2 levels) under the output directory of each output format and -# will distribute the generated files over these directories. Enabling this +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this # option can be useful when feeding doxygen a huge amount of source files, where # putting all generated files in the same directory would otherwise causes -# performance problems for the file system. +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. # The default value is: NO. CREATE_SUBDIRS = NO +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# number of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + # If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII # characters to appear in the names of generated files. If set to NO, non-ASCII # characters will be escaped, for example _xE3_x81_x84 will be used for Unicode @@ -81,26 +103,18 @@ ALLOW_UNICODE_NAMES = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. -# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -# Ukrainian and Vietnamese. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. # The default value is: English. OUTPUT_LANGUAGE = English -# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all generated output in the proper direction. -# Possible values are: None, LTR, RTL and Context. -# The default value is: None. - -OUTPUT_TEXT_DIRECTION = None - # If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member # descriptions after the members that are listed in the file and class # documentation (similar to Javadoc). Set to NO to disable this. @@ -227,6 +241,14 @@ QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO +# By default Python docstrings are displayed as preformatted text and doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + # If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the # documentation from any documented member that it re-implements. # The default value is: YES. @@ -250,25 +272,19 @@ TAB_SIZE = 8 # the documentation. An alias has the form: # name=value # For example adding -# "sideeffect=@par Side Effects:\n" +# "sideeffect=@par Side Effects:^^" # will allow you to put the command \sideeffect (or @sideeffect) in the # documentation, which will result in a user-defined paragraph with heading -# "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines (in the resulting output). You can put ^^ in the value part of an -# alias to insert a newline as if a physical newline was in the original file. -# When you need a literal { or } or , in the value part of an alias you have to -# escape them by means of a backslash (\), this can lead to conflicts with the -# commands \{ and \} for these it is advised to use the version @{ and @} or use -# a double escape (\\{ and \\}) +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) ALIASES = -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For # instance, some of the names that are used will be different. The list of all @@ -310,18 +326,21 @@ OPTIMIZE_OUTPUT_SLICE = NO # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and # language is one of the parsers supported by doxygen: IDL, Java, JavaScript, -# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, -# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: # FortranFree, unknown formatted Fortran: Fortran. In the later case the parser # tries to guess whether the code is fixed or free formatted code, this is the -# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat -# .inc files as Fortran files (default is PHP), and .f files as C (default is -# Fortran), use: inc=Fortran f=C. +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. # # Note: For files without extension you can use no_extension as a placeholder. # # Note that for custom extensions you also need to set FILE_PATTERNS otherwise -# the files are not read by doxygen. +# the files are not read by doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. EXTENSION_MAPPING = @@ -344,6 +363,17 @@ MARKDOWN_SUPPORT = YES TOC_INCLUDE_HEADINGS = 5 +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or @@ -455,6 +485,27 @@ TYPEDEF_HIDES_STRUCT = NO LOOKUP_CACHE_SIZE = 0 +# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = YES + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- @@ -518,6 +569,13 @@ EXTRACT_LOCAL_METHODS = YES EXTRACT_ANON_NSPACES = NO +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + # If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all # undocumented members inside documented classes or files. If set to NO these # members will be included in the various overviews, but no documentation @@ -529,7 +587,8 @@ HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. If set # to NO, these classes will be included in the various overviews. This option -# has no effect if EXTRACT_ALL is enabled. +# will also hide undocumented C++ concepts if enabled. This option has no effect +# if EXTRACT_ALL is enabled. # The default value is: NO. HIDE_UNDOC_CLASSES = NO @@ -555,12 +614,20 @@ HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = NO -# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file -# names in lower-case letters. If set to YES, upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# (including Cygwin) ands Mac users are advised to set this option to NO. -# The default value is: system dependent. +# With the correct setting of option CASE_SENSE_NAMES doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and MacOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. CASE_SENSE_NAMES = NO @@ -578,6 +645,12 @@ HIDE_SCOPE_NAMES = NO HIDE_COMPOUND_REFERENCE= NO +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + # If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of # the files that are included by a file in the documentation of that file. # The default value is: YES. @@ -735,7 +808,8 @@ FILE_VERSION_FILTER = # output files in an output format independent way. To create the layout file # that represents doxygen's defaults, run doxygen with the -l option. You can # optionally specify a file name after the option, if omitted DoxygenLayout.xml -# will be used as the name of the layout file. +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. # # Note that if you run doxygen from a directory containing a file called # DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE @@ -781,24 +855,50 @@ WARNINGS = YES WARN_IF_UNDOCUMENTED = NO # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some parameters -# in a documented function, or documenting parameters that don't exist or using -# markup commands wrongly. +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. # The default value is: YES. WARN_IF_DOC_ERROR = NO +# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete +# function parameter documentation. If set to NO, doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that # are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong or incomplete -# parameter documentation, but not about the absence of documentation. If -# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# value. If set to NO, doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC # The default value is: NO. WARN_NO_PARAMDOC = NO +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about +# undocumented enumeration values. If set to NO, doxygen will accept +# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: NO. + +WARN_IF_UNDOC_ENUM_VAL = NO + # If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the doxygen process doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. WARN_AS_ERROR = NO @@ -809,13 +909,27 @@ WARN_AS_ERROR = NO # and the warning text. Optionally the format may contain $version, which will # be replaced by the version of the file (if it could be obtained via # FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT # The default value is: $file:$line: $text. WARN_FORMAT = "$file:$line: $text" +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + # The WARN_LOGFILE tag can be used to specify a file to which warning and error # messages should be written. If left blank the output is written to standard -# error (stderr). +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). WARN_LOGFILE = debug.txt @@ -857,12 +971,23 @@ INPUT = ./intro.doc \ # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses # libiconv (or the iconv built into libc) for the transcoding. See the libiconv -# documentation (see: https://www.gnu.org/software/libiconv/) for the list of -# possible encodings. +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# See also: INPUT_FILE_ENCODING # The default value is: UTF-8. INPUT_ENCODING = UTF-8 +# This tag can be used to specify the character encoding of the source files +# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify +# character encoding on a per file pattern basis. Doxygen will compare the file +# name with each pattern and apply the encoding instead of the default +# INPUT_ENCODING) if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding +# "INPUT_ENCODING" for further information on supported encodings. + +INPUT_FILE_ENCODING = + # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and # *.h) to filter out the source-files in the directories. @@ -871,13 +996,15 @@ INPUT_ENCODING = UTF-8 # need to set EXTENSION_MAPPING for the extension otherwise the files are not # read by doxygen. # -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), -# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen -# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f, *.for, *.tcl, *.vhd, -# *.vhdl, *.ucf, *.qsf and *.ice. +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, +# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php, +# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be +# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.doc \ *.dox \ @@ -920,10 +1047,7 @@ EXCLUDE_PATTERNS = _* # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* +# ANamespace::AClass, ANamespace::*Test EXCLUDE_SYMBOLS = _* @@ -969,6 +1093,11 @@ IMAGE_PATH = . # code is scanned, but not when the output code is generated. If lines are added # or removed, the anchors will not be placed correctly. # +# Note that doxygen will use the data processed and written to standard output +# for further processing, therefore nothing else, like debug statements or used +# commands (so in case of a Windows batch file always use @echo OFF), should be +# written to standard output. +# # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by doxygen. @@ -1010,6 +1139,15 @@ FILTER_SOURCE_PATTERNS = USE_MDFILE_AS_MAINPAGE = +# The Fortran standard specifies that for fixed formatted Fortran code all +# characters from position 72 are to be considered as comment. A common +# extension is to allow longer lines before the automatic comment starts. The +# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can +# be processed before the automatic comment starts. +# Minimum value: 7, maximum value: 10000, default value: 72. + +FORTRAN_COMMENT_AFTER = 72 + #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- @@ -1097,16 +1235,24 @@ USE_HTAGS = NO VERBATIM_HEADERS = YES # If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the -# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the -# cost of reduced performance. This can be particularly helpful with template -# rich C++ code for which doxygen's built-in parser lacks the necessary type -# information. +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which doxygen's built-in parser lacks the necessary type information. # Note: The availability of this option depends on whether or not doxygen was # generated with the -Duse_libclang=ON option for CMake. # The default value is: NO. CLANG_ASSISTED_PARSING = NO +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + # If clang assisted parsing is enabled you can provide the compiler with command # line options that you would normally use when invoking the compiler. Note that # the include paths will already be set by doxygen for the files and directories @@ -1116,10 +1262,13 @@ CLANG_ASSISTED_PARSING = NO CLANG_OPTIONS = # If clang assisted parsing is enabled you can provide the clang parser with the -# path to the compilation database (see: -# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) used when the files -# were built. This is equivalent to specifying the "-p" option to a clang tool, -# such as clang-check. These options will then be passed to the parser. +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. # Note: The availability of this option depends on whether or not doxygen was # generated with the -Duse_libclang=ON option for CMake. @@ -1136,17 +1285,11 @@ CLANG_DATABASE_PATH = ALPHABETICAL_INDEX = YES -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -COLS_IN_ALPHA_INDEX = 5 - -# In case all classes in a project start with a common prefix, all classes will -# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag -# can be used to specify a prefix (or a list of prefixes) that should be ignored -# while generating the index headers. +# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) +# that should be ignored while generating the index headers. The IGNORE_PREFIX +# tag works for classes, function and member names. The entity will be placed in +# the alphabetical list under the first letter of the entity name that remains +# after removing the prefix. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. IGNORE_PREFIX = @@ -1225,10 +1368,16 @@ HTML_STYLESHEET = ./Tools/Documentation/doxygen.css # Doxygen will copy the style sheet files to the output directory. # Note: The order of the extra style sheet files is of importance (e.g. the last # style sheet in the list overrules the setting of the previous ones in the -# list). For an example see the documentation. +# list). +# Note: Since the styling of scrollbars can currently not be overruled in +# Webkit/Chromium, the styling will be left out of the default doxygen.css if +# one or more extra stylesheets have been specified. So if scrollbar +# customization is desired it has to be added explicitly. For an example see the +# documentation. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_EXTRA_STYLESHEET = newcustom.css custom.css +HTML_EXTRA_STYLESHEET = newcustom.css \ + custom.css # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note @@ -1240,9 +1389,22 @@ HTML_EXTRA_STYLESHEET = newcustom.css custom.css HTML_EXTRA_FILES = +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. +# Possible values are: LIGHT always generate light mode output, DARK always +# generate dark mode output, AUTO_LIGHT automatically set the mode according to +# the user preference, use light mode if no preference is set (the default), +# AUTO_DARK automatically set the mode according to the user preference, use +# dark mode if no preference is set and TOGGLE allow to user to switch between +# light and dark mode via a button. +# The default value is: AUTO_LIGHT. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE = AUTO_LIGHT + # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # will adjust the colors in the style sheet and background images according to -# this color. Hue is specified as an angle on a colorwheel, see +# this color. Hue is specified as an angle on a color-wheel, see # https://en.wikipedia.org/wiki/Hue for more information. For instance the value # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 # purple, and 360 is red again. @@ -1252,7 +1414,7 @@ HTML_EXTRA_FILES = HTML_COLORSTYLE_HUE = 209 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -# in the HTML output. For a value of 0 the output will use grayscales only. A +# in the HTML output. For a value of 0 the output will use gray-scales only. A # value of 255 will produce the most vivid colors. # Minimum value: 0, maximum value: 255, default value: 100. # This tag requires that the tag GENERATE_HTML is set to YES. @@ -1270,15 +1432,6 @@ HTML_COLORSTYLE_SAT = 255 HTML_COLORSTYLE_GAMMA = 113 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = YES - # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that # are dynamically created via JavaScript. If disabled, the navigation index will @@ -1298,6 +1451,13 @@ HTML_DYNAMIC_MENUS = YES HTML_DYNAMIC_SECTIONS = YES +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1313,10 +1473,11 @@ HTML_INDEX_NUM_ENTRIES = 100 # If the GENERATE_DOCSET tag is set to YES, additional index files will be # generated that can be used as input for Apple's Xcode 3 integrated development -# environment (see: https://developer.apple.com/xcode/), introduced with OSX -# 10.5 (Leopard). To create a documentation set, doxygen will generate a -# Makefile in the HTML output directory. Running make will produce the docset in -# that directory and running make install will install the docset in +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at # startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy # genXcode/_index.html for more information. @@ -1333,6 +1494,13 @@ GENERATE_DOCSET = NO DOCSET_FEEDNAME = "Doxygen generated docs" +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + # This tag specifies a string that should uniquely identify the documentation # set bundle. This should be a reverse domain-name style string, e.g. # com.mycompany.MyDocSet. Doxygen will append .docset to the name. @@ -1358,8 +1526,12 @@ DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three # additional HTML index files: index.hhp, index.hhc, and index.hhk. The # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on -# Windows. +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline the HTML help workshop was already many years +# in maintenance mode). You can download the HTML help workshop from the web +# archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). # # The HTML Help Workshop contains a compiler that can convert all HTML output # generated by doxygen into a single compiled HTML file (.chm). Compiled HTML @@ -1389,7 +1561,7 @@ CHM_FILE = HHC_LOCATION = # The GENERATE_CHI flag controls if a separate .chi index file is generated -# (YES) or that it should be included in the master .chm file (NO). +# (YES) or that it should be included in the main .chm file (NO). # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. @@ -1416,6 +1588,16 @@ BINARY_TOC = NO TOC_EXPAND = NO +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help @@ -1434,7 +1616,8 @@ QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help # Project output. For more information please see Qt Help Project / Namespace -# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1442,8 +1625,8 @@ QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt # Help Project output. For more information please see Qt Help Project / Virtual -# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- -# folders). +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). # The default value is: doc. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1451,16 +1634,16 @@ QHP_VIRTUAL_FOLDER = doc # If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom # filter to add. For more information please see Qt Help Project / Custom -# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- -# filters). +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_NAME = # The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see Qt Help Project / Custom -# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- -# filters). +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_ATTRS = @@ -1472,9 +1655,9 @@ QHP_CUST_FILTER_ATTRS = QHP_SECT_FILTER_ATTRS = -# The QHG_LOCATION tag can be used to specify the location of Qt's -# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the -# generated .qhp file. +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to +# run qhelpgenerator on the generated .qhp file. # This tag requires that the tag GENERATE_QHP is set to YES. QHG_LOCATION = @@ -1517,16 +1700,28 @@ DISABLE_INDEX = NO # to work a browser that supports JavaScript, DHTML, CSS and frames is required # (i.e. any modern browser). Windows users are probably better off using the # HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -# further fine-tune the look of the index. As an example, the default style -# sheet generated by doxygen has an example that shows how to put an image at -# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -# the same information as the tab index, you could consider setting -# DISABLE_INDEX to YES when enabling this option. +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_TREEVIEW = YES +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that # doxygen will group on one line in the generated HTML documentation. # @@ -1551,6 +1746,24 @@ TREEVIEW_WIDTH = 250 EXT_LINKS_IN_WINDOW = NO +# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + # Use this tag to change the font size of LaTeX formulas included as images in # the HTML documentation. When you change the font size after a successful # doxygen run you need to manually remove any form_*.png images from the HTML @@ -1560,17 +1773,6 @@ EXT_LINKS_IN_WINDOW = NO FORMULA_FONTSIZE = 10 -# Use the FORMULA_TRANSPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_TRANSPARENT = YES - # The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands # to create new LaTeX commands to be used in formulas as building blocks. See # the section "Including formulas" for details. @@ -1588,11 +1790,29 @@ FORMULA_MACROFILE = USE_MATHJAX = NO +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + # When MathJax is enabled you can set the default output format to be used for -# the MathJax output. See the MathJax site (see: -# http://docs.mathjax.org/en/latest/output.html) for more details. +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). # Possible values are: HTML-CSS (which is slower, but has the best -# compatibility), NativeMML (i.e. MathML) and SVG. +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. # The default value is: HTML-CSS. # This tag requires that the tag USE_MATHJAX is set to YES. @@ -1605,22 +1825,29 @@ MATHJAX_FORMAT = HTML-CSS # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax # Content Delivery Network so you can quickly see the result without installing # MathJax. However, it is strongly recommended to install a local copy of -# MathJax from https://www.mathjax.org before deployment. -# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/. +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_EXTENSIONS = # The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces # of code that will be used on startup of the MathJax code. See the MathJax site -# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an # example see the documentation. # This tag requires that the tag USE_MATHJAX is set to YES. @@ -1667,7 +1894,8 @@ SERVER_BASED_SEARCH = NO # # Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: https://xapian.org/). +# Xapian (see: +# https://xapian.org/). # # See the section "External Indexing and Searching" for details. # The default value is: NO. @@ -1680,8 +1908,9 @@ EXTERNAL_SEARCH = NO # # Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: https://xapian.org/). See the section "External Indexing and -# Searching" for details. +# Xapian (see: +# https://xapian.org/). See the section "External Indexing and Searching" for +# details. # This tag requires that the tag SEARCHENGINE is set to YES. SEARCHENGINE_URL = @@ -1790,29 +2019,31 @@ PAPER_TYPE = letter EXTRA_PACKAGES = -# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the -# generated LaTeX document. The header should contain everything until the first -# chapter. If it is left blank doxygen will generate a standard header. See -# section "Doxygen usage" for information on how to let doxygen write the -# default header to a separate file. +# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for +# the generated LaTeX document. The header should contain everything until the +# first chapter. If it is left blank doxygen will generate a standard header. It +# is highly recommended to start with a default header using +# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty +# and then modify the file new_header.tex. See also section "Doxygen usage" for +# information on how to generate the default header that doxygen normally uses. # -# Note: Only use a user-defined header if you know what you are doing! The -# following commands have a special meaning inside the header: $title, -# $datetime, $date, $doxygenversion, $projectname, $projectnumber, -# $projectbrief, $projectlogo. Doxygen will replace $title with the empty -# string, for the replacement values of the other commands the user is referred -# to HTML_HEADER. +# Note: Only use a user-defined header if you know what you are doing! +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. The following +# commands have a special meaning inside the header (and footer): For a +# description of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_HEADER = -# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the -# generated LaTeX document. The footer should contain everything after the last -# chapter. If it is left blank doxygen will generate a standard footer. See +# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for +# the generated LaTeX document. The footer should contain everything after the +# last chapter. If it is left blank doxygen will generate a standard footer. See # LATEX_HEADER for more information on how to generate a default footer and what -# special commands can be used inside the footer. -# -# Note: Only use a user-defined footer if you know what you are doing! +# special commands can be used inside the footer. See also section "Doxygen +# usage" for information on how to generate the default footer that doxygen +# normally uses. Note: Only use a user-defined footer if you know what you are +# doing! # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_FOOTER = @@ -1845,18 +2076,26 @@ LATEX_EXTRA_FILES = PDF_HYPERLINKS = YES -# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate -# the PDF file directly from the LaTeX files. Set this option to YES, to get a -# higher quality PDF documentation. +# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as +# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX +# files. Set this option to YES, to get a higher quality PDF documentation. +# +# See also section LATEX_CMD_NAME for selecting the engine. # The default value is: YES. # This tag requires that the tag GENERATE_LATEX is set to YES. USE_PDFLATEX = YES -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. This option is also used -# when generating formulas in HTML. +# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. +# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch +# mode nothing is printed on the terminal, errors are scrolled as if is +# hit at every error; missing files that TeX tries to input or request from +# keyboard input (\read on a not open input stream) cause the job to abort, +# NON_STOP In nonstop mode the diagnostic message will appear on the terminal, +# but there is no possibility of user interaction just like in batch mode, +# SCROLL In scroll mode, TeX will stop only for missing files to input or if +# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at +# each error, asking for user intervention. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -1869,16 +2108,6 @@ LATEX_BATCHMODE = NO LATEX_HIDE_INDICES = NO -# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source -# code with syntax highlighting in the LaTeX output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_SOURCE_CODE = NO - # The LATEX_BIB_STYLE tag can be used to specify the style to use for the # bibliography, e.g. plainnat, or ieeetr. See # https://en.wikipedia.org/wiki/BibTeX and \cite for more info. @@ -1887,14 +2116,6 @@ LATEX_SOURCE_CODE = NO LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, # it will be relative to the LATEX_OUTPUT directory. If left blank the @@ -1959,16 +2180,6 @@ RTF_STYLESHEET_FILE = RTF_EXTENSIONS_FILE = -# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code -# with syntax highlighting in the RTF output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_RTF is set to YES. - -RTF_SOURCE_CODE = NO - #--------------------------------------------------------------------------- # Configuration options related to the man page output #--------------------------------------------------------------------------- @@ -2065,27 +2276,44 @@ GENERATE_DOCBOOK = NO DOCBOOK_OUTPUT = docbook -# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the -# program listings (including syntax highlighting and cross-referencing -# information) to the DOCBOOK output. Note that enabling this will significantly -# increase the size of the DOCBOOK output. -# The default value is: NO. -# This tag requires that the tag GENERATE_DOCBOOK is set to YES. - -DOCBOOK_PROGRAMLISTING = NO - #--------------------------------------------------------------------------- # Configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # the structure of the code including all documentation. Note that this feature # is still experimental and incomplete at the moment. # The default value is: NO. GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# Configuration options related to Sqlite3 output +#--------------------------------------------------------------------------- + +# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3 +# database with symbols found by doxygen stored in tables. +# The default value is: NO. + +GENERATE_SQLITE3 = NO + +# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be +# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put +# in front of it. +# The default directory is: sqlite3. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_OUTPUT = sqlite3 + +# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each doxygen run. If set to NO, doxygen +# will warn if an a database file is already found and not modify it. +# The default value is: YES. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_RECREATE_DB = YES + #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- @@ -2160,7 +2388,8 @@ SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by the -# preprocessor. +# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of +# RECURSIVE has no effect here. # This tag requires that the tag SEARCH_INCLUDES is set to YES. INCLUDE_PATH = @@ -2227,15 +2456,15 @@ TAGFILES = GENERATE_TAGFILE = -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. +# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces +# will be listed in the class and namespace index. If set to NO, only the +# inherited external classes will be listed. # The default value is: NO. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be +# in the topic index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. @@ -2249,25 +2478,9 @@ EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- -# Configuration options related to the dot tool +# Configuration options related to diagram generator tools #--------------------------------------------------------------------------- -# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram -# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to -# NO turns the diagrams off. Note that this option also works with HAVE_DOT -# disabled, but it is recommended to install and use dot, since it yields more -# powerful graphs. -# The default value is: YES. - -CLASS_DIAGRAMS = YES - -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - # If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. @@ -2276,7 +2489,7 @@ HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO # The default value is: YES. @@ -2293,49 +2506,73 @@ HAVE_DOT = YES DOT_NUM_THREADS = 0 -# When you want a differently looking font in the dot files that doxygen -# generates you can specify the font name using DOT_FONTNAME. You need to make -# sure dot is able to find the font, which can be done by putting it in a -# standard location or by setting the DOTFONTPATH environment variable or by -# setting DOT_FONTPATH to the directory containing the font. -# The default value is: Helvetica. +# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of +# subgraphs. When you want a differently looking font in the dot files that +# doxygen generates you can specify fontname, fontcolor and fontsize attributes. +# For details please see Node, +# Edge and Graph Attributes specification You need to make sure dot is able +# to find the font, which can be done by putting it in a standard location or by +# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. Default graphviz fontsize is 14. +# The default value is: fontname=Helvetica,fontsize=10. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTNAME = +DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10" -# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of -# dot graphs. -# Minimum value: 4, maximum value: 24, default value: 10. +# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can +# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. Complete documentation about +# arrows shapes. +# The default value is: labelfontname=Helvetica,labelfontsize=10. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTSIZE = 10 +DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10" -# By default doxygen will tell dot to use the default font as specified with -# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set -# the path where dot can find it using this tag. +# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes +# around nodes set 'shape=plain' or 'shape=plaintext' Shapes specification +# The default value is: shape=box,height=0.2,width=0.4. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" + +# You can set the path where dot can find font specified with fontname in +# DOT_COMMON_ATTR and others dot attributes. # This tag requires that the tag HAVE_DOT is set to YES. DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for -# each documented class showing the direct and indirect inheritance relations. -# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will +# generate a graph for each documented class showing the direct and indirect +# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and +# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case +# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the +# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. +# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance +# relations will be shown as texts / links. +# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. CLASS_GRAPH = YES # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. +# class with other documented classes. Explicit enabling a collaboration graph, +# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the +# command \collaborationgraph. Disabling a collaboration graph can be +# accomplished by means of the command \hidecollaborationgraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. COLLABORATION_GRAPH = NO # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. +# groups, showing the direct groups dependencies. Explicit enabling a group +# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means +# of the command \groupgraph. Disabling a directory graph can be accomplished by +# means of the command \hidegroupgraph. See also the chapter Grouping in the +# manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2358,10 +2595,32 @@ UML_LOOK = NO # but if the number exceeds 15, the total amount of fields shown is limited to # 10. # Minimum value: 0, maximum value: 100, default value: 10. -# This tag requires that the tag HAVE_DOT is set to YES. +# This tag requires that the tag UML_LOOK is set to YES. UML_LIMIT_NUM_FIELDS = 10 +# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and +# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS +# tag is set to YES, doxygen will add type and arguments for attributes and +# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen +# will not generate fields with class member information in the UML graphs. The +# class diagrams will look similar to the default class diagrams but using UML +# notation for the relationships. +# Possible values are: NO, YES and NONE. +# The default value is: NO. +# This tag requires that the tag UML_LOOK is set to YES. + +DOT_UML_DETAILS = NO + +# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters +# to display on a single line. If the actual line length exceeds this threshold +# significantly it will wrapped across multiple lines. Some heuristics are apply +# to avoid ugly line breaks. +# Minimum value: 0, maximum value: 1000, default value: 17. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_WRAP_THRESHOLD = 17 + # If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and # collaboration graphs will show the relations between templates and their # instances. @@ -2373,7 +2632,9 @@ TEMPLATE_RELATIONS = NO # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # YES then doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, +# can be accomplished by means of the command \includegraph. Disabling an +# include graph can be accomplished by means of the command \hideincludegraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2382,7 +2643,10 @@ INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # set to YES then doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set +# to NO, can be accomplished by means of the command \includedbygraph. Disabling +# an included by graph can be accomplished by means of the command +# \hideincludedbygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2422,23 +2686,32 @@ GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the -# files in the directories. +# files in the directories. Explicit enabling a directory graph, when +# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command +# \directorygraph. Disabling a directory graph can be accomplished by means of +# the command \hidedirectorygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. DIRECTORY_GRAPH = YES +# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels +# of child directories generated in directory dependency graphs by dot. +# Minimum value: 1, maximum value: 25, default value: 1. +# This tag requires that the tag DIRECTORY_GRAPH is set to YES. + +DIR_GRAPH_MAX_DEPTH = 1 + # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). +# https://www.graphviz.org/)). # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # to make the SVG files visible in IE 9+ (other browsers do not have this # requirement). -# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd, -# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo, -# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo, -# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and +# Possible values are: png, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, +# gif, gif:cairo, gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, +# png:cairo, png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and # png:gdiplus:gdiplus. # The default value is: png. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2470,11 +2743,12 @@ DOT_PATH = DOTFILE_DIRS = -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. -MSCFILE_DIRS = +DIA_PATH = # The DIAFILE_DIRS tag can be used to specify one or more directories that # contain dia files that are included in the documentation (see the \diafile @@ -2483,10 +2757,10 @@ MSCFILE_DIRS = DIAFILE_DIRS = # When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the -# path where java can find the plantuml.jar file. If left blank, it is assumed -# PlantUML is not used or called during a preprocessing step. Doxygen will -# generate a warning when it encounters a \startuml command in this case and -# will not generate output for the diagram. +# path where java can find the plantuml.jar file or to the filename of jar file +# to be used. If left blank, it is assumed PlantUML is not used or called during +# a preprocessing step. Doxygen will generate a warning when it encounters a +# \startuml command in this case and will not generate output for the diagram. PLANTUML_JAR_PATH = @@ -2524,18 +2798,6 @@ DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, because dot on Windows does not seem -# to support this out of the box. -# -# Warning: Depending on the platform used, enabling this option may lead to -# badly anti-aliased labels on the edges of a graph (i.e. they become hard to -# read). -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_TRANSPARENT = NO - # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) support @@ -2548,14 +2810,34 @@ DOT_MULTI_TARGETS = NO # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page # explaining the meaning of the various boxes and arrows in the dot generated # graphs. +# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal +# graphical representation for inheritance and collaboration diagrams is used. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. GENERATE_LEGEND = YES -# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot +# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate # files that are used to generate the various graphs. +# +# Note: This setting is not only used for dot files but also for msc temporary +# files. # The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. DOT_CLEANUP = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will +# use a built-in version of mscgen tool to produce the charts. Alternatively, +# the MSCGEN_TOOL tag can also specify the name an external tool. For instance, +# specifying prog as the value, doxygen will call the tool as prog -T +# -o . The external tool should support +# output file formats "png", "eps", "svg", and "ismap". + +MSCGEN_TOOL = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = From 343fdad64615daea6c5cd16d7e92ae58f4c2c7c1 Mon Sep 17 00:00:00 2001 From: mdw Date: Mon, 17 Jun 2024 19:45:47 -0400 Subject: [PATCH 120/167] Add 'diskconf' stanza to 'Disk2d' config --- utils/ICs/Disk2d.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/ICs/Disk2d.cc b/utils/ICs/Disk2d.cc index c5cccb23b..f1df721a6 100644 --- a/utils/ICs/Disk2d.cc +++ b/utils/ICs/Disk2d.cc @@ -4,6 +4,7 @@ // Valid key list for Disk2d including BiorthCyl keys // std::set Disk2d::valid_keys = { + "diskconf", "acyltbl", "rcylmin", "rcylmax", From 5277a132592ebb7c49cc222004dea029571570ea Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 17 Jun 2024 16:47:59 -0700 Subject: [PATCH 121/167] Update for new 'diskconf' configuration stanza --- utils/ICs/initial2d.cc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/utils/ICs/initial2d.cc b/utils/ICs/initial2d.cc index 7573039dc..b892d7172 100644 --- a/utils/ICs/initial2d.cc +++ b/utils/ICs/initial2d.cc @@ -561,6 +561,8 @@ main(int ac, char **av) // Create YAML db YAML::Emitter yml; + + // Begin the configuration map yml << YAML::BeginMap; yml << YAML::Key << "acyltbl" << YAML::Value << ACYL; yml << YAML::Key << "rcylmin" << YAML::Value << RCYLMIN; @@ -578,6 +580,17 @@ main(int ac, char **av) yml << YAML::Key << "cachename" << YAML::Value << cachefile; if (vm.count("report")) yml << YAML::Key << "verbose" << YAML::Value << true; + + // Build the diskconf stanza + yml << YAML::Key << "diskconf" << YAML::BeginMap + << YAML::Key << "name" << YAML::Value << "expon" + << YAML::Key << "parameters" + << YAML::BeginMap + << YAML::Key << "aexp" << YAML::Value << ACYL + << YAML::EndMap + << YAML::EndMap; + + // End the configuration map yml << YAML::EndMap; // Create expansion only if needed . . . From f345935fb82ee5d3ed10f101273a56ad25f9e95a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 20 Jun 2024 10:43:07 -0700 Subject: [PATCH 122/167] getAllCoefs should use roundTime() for map access --- expui/Coefficients.cc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 230291ec7..818623760 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -509,7 +509,7 @@ namespace CoefClasses ret.resize((Lmax+1)*(Lmax+2)/2, Nmax, ntim); for (int t=0; tcoefs); + auto & cof = *(coefs[roundTime(times[t])]->coefs); for (int l=0; l<(Lmax+2)*(Lmax+1)/2; l++) { for (int n=0; ncoefs; + auto & cof = *coefs[roundTime(times[t])]->coefs; for (int m=0; mcoefs)(c).real(); } @@ -2498,7 +2498,7 @@ namespace CoefClasses ret.resize(Nfld, (Lmax+1)*(Lmax+2)/2, Nmax, ntim); for (int t=0; tcoefs); + auto & cof = *(coefs[roundTime(times[t])]->coefs); for (int i=0; i<4; i++) { for (int l=0; l<(Lmax+2)*(Lmax+1)/2; l++) { for (int n=0; ncoefs); + auto & cof = *(coefs[roundTime(times[t])]->coefs); for (int i=0; i Date: Mon, 24 Jun 2024 23:25:00 -0700 Subject: [PATCH 123/167] Add new construction to create instance from a cachefile [no ci] --- exputil/EmpCylSL.cc | 118 +++++++++++++++++++++++++++++++++++++++++++- include/EmpCylSL.H | 3 ++ 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 23d579c0f..7ded57acd 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -216,6 +216,122 @@ EmpCylSL::EmpCylSL(int nmax, int lmax, int mmax, int nord, maxSNR = 0.0; } +template +U getH5(std::string name, HighFive::File& file) +{ + if (file.hasAttribute(name)) { + U v; + HighFive::Attribute vv = file.getAttribute(name); + vv.read(v); + return v; + } else { + std::ostringstream sout; + sout << "EmpCylSL: could not find <" << name << ">"; + throw std::runtime_error(sout.str()); + } +} + +EmpCylSL::EmpCylSL(int mlim, std::string cachename) +{ + // Use default name? + // + if (cachename.size()==0) + throw std::runtime_error("EmpCylSL: you must specify a cachename"); + + // Open and read the cache file to get the needed input parameters + // + + try { + // Silence the HDF5 error stack + // + HighFive::SilenceHDF5 quiet; + + // Open the hdf5 file + // + HighFive::File file(cachefile, HighFive::File::ReadOnly); + + // For basis ID + std::string forceID("Cylinder"), geometry("cylinder"); + std::string model = EmpModelLabs[mtype]; + + // Version check + // + if (file.hasAttribute("Version")) { + auto ver = getH5(std::string("Version"), file); + if (ver.compare(Version)) + throw std::runtime_error("EmpCylSL: version mismatch"); + } else { + throw std::runtime_error("EmpCylSL: outdated cache"); + } + + NMAX = getH5("nmaxfid", file); + MMAX = getH5("mmax", file); + LMAX = getH5("lmax", file); + NORDER = getH5("nmax", file); + MMIN = 0; + MLIM = std::min(mlim, MMAX); + NMIN = 0; + NLIM = std::numeric_limits::max(); + Neven = getH5("neven", file); + Nodd = getH5("nodd", file); + ASCALE = getH5("ascl", file); + HSCALE = getH5("hscl", file); + } + catch (YAML::Exception& error) { + std::ostringstream sout; + sout << "EmpCylSL::getHeader: invalid cache file <" << cachefile << ">. "; + sout << "YAML error in getHeader: " << error.what() << std::endl; + throw GenericError(sout.str(), __FILE__, __LINE__, 1038, false); + } + + // Set EvenOdd if values seem sane + // + EvenOdd = false; + if (Nodd>=0 and Nodd<=NORDER and Nodd+Neven==NORDER) { + EvenOdd = true; + } + + pfac = 1.0/sqrt(ASCALE); + ffac = pfac/ASCALE; + dfac = ffac/ASCALE; + + EVEN_M = false; + + // Check whether MPI is initialized + // + int flag; + MPI_Initialized(&flag); + if (flag) use_mpi = true; + else use_mpi = false; + + // Enable MPI code in SLGridSph + // + if (use_mpi and numprocs>1) SLGridSph::mpi = 1; + + // Choose table dimension + // + MPItable = 4; + + // Initialize storage and values + // + coefs_made.resize(multistep+1); + std::fill(coefs_made.begin(), coefs_made.end(), false); + + eof_made = false; + + sampT = 1; + defSampT = 1; + tk_type = None; + + cylmass = 0.0; + cylmass1 = std::vector(nthrds); + cylmass_made = false; + + hallfile = ""; + minSNR = std::numeric_limits::max(); + maxSNR = 0.0; +} + void EmpCylSL::reset(int numr, int lmax, int mmax, int nord, double ascale, double hscale, int nodd, @@ -815,7 +931,7 @@ std::string compare_out(std::string str, U one, U two) return sout.str(); } -int EmpCylSL::cache_grid(int readwrite, string cachename) +int EmpCylSL::cache_grid(int readwrite, std::string cachename) { // Option to reset cache file name diff --git a/include/EmpCylSL.H b/include/EmpCylSL.H index 30a4a6587..233039ef7 100644 --- a/include/EmpCylSL.H +++ b/include/EmpCylSL.H @@ -501,6 +501,9 @@ public: double ascale, double hscale, int Nodd, std::string cachename); + //! Construct from cache file + EmpCylSL(int mlim, const std::string cache); + //! Destructor ~EmpCylSL(void); From 9cdc6e009243ff742d396096ad801ef85fa37dc6 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 25 Jun 2024 14:56:55 -0700 Subject: [PATCH 124/167] Fix for cache-only constructor [no ci] --- exputil/EmpCylSL.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index 7ded57acd..f24bb9fc7 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -248,7 +248,7 @@ EmpCylSL::EmpCylSL(int mlim, std::string cachename) // Open the hdf5 file // - HighFive::File file(cachefile, HighFive::File::ReadOnly); + HighFive::File file(cachename, HighFive::File::ReadOnly); // For basis ID std::string forceID("Cylinder"), geometry("cylinder"); @@ -266,7 +266,7 @@ EmpCylSL::EmpCylSL(int mlim, std::string cachename) NMAX = getH5("nmaxfid", file); MMAX = getH5("mmax", file); - LMAX = getH5("lmax", file); + LMAX = getH5("lmaxfid", file); NORDER = getH5("nmax", file); MMIN = 0; MLIM = std::min(mlim, MMAX); From ecd09f2307da2575a251c8e7517c51405344623b Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 25 Jun 2024 17:26:17 -0700 Subject: [PATCH 125/167] Missing initialization of accumulation structures [no ci] --- exputil/EmpCylSL.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index f24bb9fc7..d3d414f42 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -330,6 +330,8 @@ EmpCylSL::EmpCylSL(int mlim, std::string cachename) hallfile = ""; minSNR = std::numeric_limits::max(); maxSNR = 0.0; + + setup_accumulation(); } From 532c267ff4a341ca0d13d0d2e08a1246028303c4 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 25 Jun 2024 21:22:10 -0700 Subject: [PATCH 126/167] Call read_cache() to set up grids [no ci] --- exputil/EmpCylSL.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index d3d414f42..a28022364 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -238,6 +238,8 @@ EmpCylSL::EmpCylSL(int mlim, std::string cachename) if (cachename.size()==0) throw std::runtime_error("EmpCylSL: you must specify a cachename"); + cachefile = cachename; + // Open and read the cache file to get the needed input parameters // @@ -331,7 +333,7 @@ EmpCylSL::EmpCylSL(int mlim, std::string cachename) minSNR = std::numeric_limits::max(); maxSNR = 0.0; - setup_accumulation(); + read_cache(); } From c7c631101c41ed97ac1e951af9d6c4bc8d3229a1 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 29 Jun 2024 14:52:21 -0700 Subject: [PATCH 127/167] Added constructor from cache file for SLGridSph [no ci] --- expui/CMakeLists.txt | 2 +- expui/Koopman.H | 2 +- expui/Koopman.cc | 2 +- expui/expMSSA.H | 15 +++ expui/expMSSA.cc | 276 +++++++++++++++++++++++++++++++++++++++++- exputil/EmpCylSL.cc | 2 + exputil/SLGridMP2.cc | 36 ++++++ include/SLGridMP2.H | 9 ++ pyEXP/MSSAWrappers.cc | 43 ++++++- 9 files changed, 380 insertions(+), 7 deletions(-) diff --git a/expui/CMakeLists.txt b/expui/CMakeLists.txt index 8690817f7..d506bea8d 100644 --- a/expui/CMakeLists.txt +++ b/expui/CMakeLists.txt @@ -34,7 +34,7 @@ endif() set(expui_SOURCES BasisFactory.cc BiorthBasis.cc FieldBasis.cc CoefContainer.cc CoefStruct.cc FieldGenerator.cc expMSSA.cc Coefficients.cc KMeans.cc Centering.cc ParticleIterator.cc - Koopman.cc BiorthBess.cc) + Koopman.cc BiorthBess.cc SGSmooth.cc) add_library(expui ${expui_SOURCES}) set_target_properties(expui PROPERTIES OUTPUT_NAME expui) target_include_directories(expui PUBLIC ${common_INCLUDE}) diff --git a/expui/Koopman.H b/expui/Koopman.H index a84b31dc7..921ac4089 100644 --- a/expui/Koopman.H +++ b/expui/Koopman.H @@ -119,7 +119,7 @@ namespace MSSA return L; } - //! Return the EDMD modes + //! Return the EDMD modes, an approximation to the Koopman eigenfunctions Eigen::MatrixXcd getModes() { if (not computed) koopman_analysis(); diff --git a/expui/Koopman.cc b/expui/Koopman.cc index b32f00b92..cc85cca1f 100644 --- a/expui/Koopman.cc +++ b/expui/Koopman.cc @@ -194,7 +194,7 @@ namespace MSSA { Eigen::VectorXcd B = Phi.inverse() * xx; Eigen::MatrixXcd LL = I.asDiagonal(); - // Propate the solution with the operator + // Propagate the solution using the approximate Koopman operator // for (int i=0; i + getKoopmanModes(const double tol, int window, bool debug); + + //! Return the reconstructed Koopman modes + std::map getReconstructedKoopman(int mode); + }; diff --git a/expui/expMSSA.cc b/expui/expMSSA.cc index 0aca81d71..3ff1c71c7 100644 --- a/expui/expMSSA.cc +++ b/expui/expMSSA.cc @@ -1536,13 +1536,283 @@ namespace MSSA { reconstructed = true; } - } catch (HighFive::Exception& err) { + } catch (HighFive::Exception& err) { // Number of channels + // + nkeys = mean.size(); + + // Make sure parameters are sane + // + if (numW<=0) numW = numT/2; + if (numW > numT/2) numW = numT/2; + + numK = numT - numW + 1; + + std::cerr << "**** Error opening or reading H5 file ****" << std::endl; throw; } } + std::tuple + expMSSA::getKoopmanModes(double tol, int D, bool debug) + { + bool use_fullKh = true; // Use the non-reduced computation of + // Koopman/eDMD + // Number of channels + // + nkeys = mean.size(); + + // Make sure parameters are sane + // + if (numW<=0) numW = numT/2; + if (numW > numT/2) numW = numT/2; + + numK = numT - numW + 1; + + Eigen::VectorXd S1; + Eigen::MatrixXd Y1; + Eigen::MatrixXd V1; + Eigen::MatrixXd VT1; + Eigen::MatrixXd VT2; + + // Make a new trajetory matrix with smoothing + // + Y1.resize(numK, numW*nkeys + D*(nkeys-1)); + + size_t n=0, offset=0; + for (auto k : mean) { + for (int i=0; i 0) { + // Back blending + for (int j=0; j(D-j)/D; + } + } + // Main series + for (int j=0; j(D-j)/D; + } + } + } + offset += numW + D; + n++; + } + + double Scale = Y1.norm(); + + // auto YY = Y1/Scale; + auto YY = Y1; + + // Use one of the built-in Eigen3 algorithms + // + /* + if (params["Jacobi"]) { + // -->Using Jacobi + Eigen::JacobiSVD + svd(YY, Eigen::ComputeThinU | Eigen::ComputeThinV); + S1 = svd.singularValues(); + V1 = svd.matrixV(); + } else if (params["BDCSVD"]) { + */ + // -->Using BDC + Eigen::BDCSVD + svd(YY, Eigen::ComputeFullU | Eigen::ComputeFullV); + // svd(YY, Eigen::ComputeThinU | Eigen::ComputeThinV); + S1 = svd.singularValues(); + V1 = svd.matrixV(); + /* + } else { + // -->Use Random approximation algorithm from Halko, Martinsson, + // and Tropp + int srank = std::min(YY.cols(), YY.rows()); + RedSVD::RedSVD svd(YY, srank); + S1 = svd.singularValues(); + V1 = svd.matrixV(); + } + */ + + std::cout << "shape V1 = " << V1.rows() << " x " + << V1.cols() << std::endl; + + std::cout << "shape Y1 = " << Y1.rows() << " x " + << Y1.cols() << std::endl; + + int lags = V1.rows(); + int rank = V1.cols(); + + std::ofstream out; + if (debug) out.open("debug.txt"); + + if (out) out << "rank=" << rank << " lags=" << lags << std::endl; + + VT1.resize(rank, lags-1); + VT2.resize(rank, lags-1); + + for (int j=0; j uu; + for (int i=0; iUsing Jacobi + Eigen::JacobiSVD + // svd(VT1, Eigen::ComputeThinU | Eigen::ComputeThinV); + svd(VT1, Eigen::ComputeFullU | Eigen::ComputeFullV); + SS = svd.singularValues(); + UU = svd.matrixU(); + VV = svd.matrixV(); + } else if (params["BDCSVD"]) { + */ + { + // -->Using BDC + Eigen::BDCSVD + // svd(VT1, Eigen::ComputeThinU | Eigen::ComputeThinV); + svd(VT1, Eigen::ComputeFullU | Eigen::ComputeFullV); + SS = svd.singularValues(); + UU = svd.matrixU(); + VV = svd.matrixV(); + } + /* + } else { + // -->Use Random approximation algorithm from Halko, Martinsson, + // and Tropp + // RedSVD::RedSVD svd(VT1, std::min(rank, numK-1)); + RedSVD::RedSVD svd(VT1, std::max(VT1.rows(), VT2.cols())); + SS = svd.singularValues(); + UU = svd.matrixU(); + VV = svd.matrixV(); + } + */ + + if (out) out << "Singular values" << std::endl << SS << std::endl; + + // Compute inverse + for (int i=0; i tol) SS(i) = 1.0/SS(i); + else SS(i) = 0.0; + } + + // Compute full Koopman operator + if (use_fullKh) { + + Eigen::MatrixXd DD(VV.cols(), UU.cols()); + DD.setZero(); + for (int i=0; i es(AT); + + L = es.eigenvalues(); + Phi = es.eigenvectors(); + + if (out) { + out << std::endl << "Eigenvalues" << std::endl << L << std::endl + << std::endl << "Eigenvectors" << std::endl << Phi << std::endl; + } + + } + // Compute the reduced Koopman operator + else { + + Eigen::MatrixXd AT = UU.transpose() * (VT2 * VV) * SS.asDiagonal(); + + // Compute spectrum + Eigen::EigenSolver es(AT, true); + + L = es.eigenvalues(); + auto W = es.eigenvectors(); + + // Compute the EDMD modes + // + Eigen::VectorXcd Linv(L); + for (int i=0; i tol) Linv(i) = 1.0/Linv(i); + else Linv(i) = 0.0; + } + + Phi = VT2 * VV * SS.asDiagonal() * W * Linv.asDiagonal(); + + if (out) { + out << std::endl << "Eigenvalues" << std::endl << L << std::endl + << std::endl << "Eigenvectors" << std::endl << Phi << std::endl; + } + } + + // Cache window size + // + window = D; + + return {L, Phi}; + } + + std::map + expMSSA::getReconstructedKoopman(int mode) + { + // Copy the original map for return + // + auto newdata = data; + + size_t n=0, offset=0; + + for (auto u : mean) { + + double disp = totVar; + if (type == TrendType::totPow) disp = totPow; + if (disp==0.0) disp = var[u.first]; + + std::complex phase = 1.0; + for (int i=0; i(); - std::cout << "Trajectory is " << std::boolalpha << trajectory - << std::endl; + // std::cout << "Trajectory is " << std::boolalpha << trajectory + // << std::endl; // Eigen OpenMP reporting // diff --git a/exputil/EmpCylSL.cc b/exputil/EmpCylSL.cc index a28022364..e69be02aa 100644 --- a/exputil/EmpCylSL.cc +++ b/exputil/EmpCylSL.cc @@ -270,6 +270,8 @@ EmpCylSL::EmpCylSL(int mlim, std::string cachename) MMAX = getH5("mmax", file); LMAX = getH5("lmaxfid", file); NORDER = getH5("nmax", file); + CMAPR = getH5("cmapr", file); + CMAPZ = getH5("cmapz", file); MMIN = 0; MLIM = std::min(mlim, MMAX); NMIN = 0; diff --git a/exputil/SLGridMP2.cc b/exputil/SLGridMP2.cc index 0984fff00..7924551b7 100644 --- a/exputil/SLGridMP2.cc +++ b/exputil/SLGridMP2.cc @@ -168,6 +168,42 @@ SLGridSph::SLGridSph(std::shared_ptr mod, } +SLGridSph::SLGridSph(std::string cachename) +{ + if (cachename.size()) sph_cache_name = cachename; + else throw std::runtime_error("SLGridSph: you must specify a cachename"); + + tbdbg = false; + + int LMAX, NMAX, NUMR, CMAP, DIVERGE=0; + double RMIN, RMAX, RMAP, DFAC=1.0; + + try { + + auto node = getHeader(cachename); + + LMAX = node["lmax"].as(); + NMAX = node["nmax"].as(); + NUMR = node["numr"].as(); + CMAP = node["cmap"].as(); + RMIN = node["rmin"].as(); + RMAX = node["rmax"].as(); + RMAP = node["rmapping"].as(); + + model_file_name = node["model"].as(); + model = SphModTblPtr(new SphericalModelTable(model_file_name, diverge, dfac)); + } + catch (YAML::Exception& error) { + std::ostringstream sout; + sout << "SLGridMP2: error parsing parameters from getHeader: " + << error.what(); + throw GenericError(sout.str(), __FILE__, __LINE__, 1039, false); + } + + initialize(LMAX, NMAX, NUMR, RMIN, RMAX, false, CMAP, RMAP); +} + + std::map SLGridSph::cacheInfo(const std::string& cachefile, bool verbose) { diff --git a/include/SLGridMP2.H b/include/SLGridMP2.H index 95a9bf6b7..f83d98fe9 100644 --- a/include/SLGridMP2.H +++ b/include/SLGridMP2.H @@ -116,6 +116,10 @@ public: std::string cachename=".slgrid_sph_cache", bool Verbose=false); + //! Constructor (from cache file) + SLGridSph(std::string cachename); + + //! Destructor virtual ~SLGridSph(); @@ -216,6 +220,11 @@ public: double getRmax() { return rmax; } //@} + //@{ + //! Get expansion limits + int getLmax() { return lmax; } + int getNmax() { return nmax; } + //@} }; diff --git a/pyEXP/MSSAWrappers.cc b/pyEXP/MSSAWrappers.cc index 9fdc5658f..aa5ef5c57 100644 --- a/pyEXP/MSSAWrappers.cc +++ b/pyEXP/MSSAWrappers.cc @@ -326,7 +326,6 @@ void MSSAtoolkitClasses(py::module &m) { dict({id: Coefs},...) reconstructed time series in the original coefficient form - Notes ----- The reconstructed data will overwrite the memory of the original coefficient @@ -543,6 +542,48 @@ void MSSAtoolkitClasses(py::module &m) { )"); + f.def("getKoopmanModes", &expMSSA::getKoopmanModes, + R"( + Compute the Koopman mode estimate from the right-singular vectors + + Uses eDMD to estimate the modes + + Parameters + ---------- + tol : double + singular value truncation level + window: int + Smoothing between serialized channels (0 for no smoothing) + debug : bool + flag indicating whether to print debug information + + Notes + ----- + Use getReconstructedKoopman() to copy the reconstruction for a + particular mode back to the coefficient db + + Returns + ------- + tuple(numpy.ndarray, numpy.ndarray) + vector of eigenvalues and modes + )", py::arg("tol")=1.0e-12, py::arg("window")=0, py::arg("debug")=false); + + f.def("getReconstructedKoopman", &expMSSA::getReconstructedKoopman, + R"( + Reconstruct the coefficients for a particular Koopman mode + + Parameters + ---------- + mode: int + The index of the mode to be reconstructed + + Returns + ------- + dict({id: Coefs},...) + reconstructed time series in the original coefficient form + + )", py::arg("mode")); + f.def("getRC", &expMSSA::getRC, R"( Access the detrended reconstructed channel series by internal key From 60957b5ac0c761ee5b1144920970b97b9fe37b4d Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 29 Jun 2024 14:55:48 -0700 Subject: [PATCH 128/167] Added Savitsky-Golay smoothing class [no ci] --- expui/SGSmooth.H | 13 ++ expui/SGSmooth.cc | 549 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 562 insertions(+) create mode 100644 expui/SGSmooth.H create mode 100644 expui/SGSmooth.cc diff --git a/expui/SGSmooth.H b/expui/SGSmooth.H new file mode 100644 index 000000000..6615bffa2 --- /dev/null +++ b/expui/SGSmooth.H @@ -0,0 +1,13 @@ +#ifndef __SGSMOOTH_HPP__ +#define __SGSMOOTH_HPP__ + +#include + +//! savitzky golay smoothing. +std::vector sg_smooth(const std::vector &v, const int w, const int deg); + +//! numerical derivative based on savitzky golay smoothing. +std::vector sg_derivative(const std::vector &v, const int w, + const int deg, const double h=1.0); + +#endif // __SGSMOOTH_HPP__ diff --git a/expui/SGSmooth.cc b/expui/SGSmooth.cc new file mode 100644 index 000000000..1b5150b53 --- /dev/null +++ b/expui/SGSmooth.cc @@ -0,0 +1,549 @@ +//! +// Sliding window signal processing (and linear algebra toolkit). +// +// supported operations: +//
    +//
  • Savitzky-Golay smoothing. +//
  • computing a numerical derivative based of Savitzky-Golay smoothing. +//
  • required linear algebra support for SG smoothing using STL based +// vector/matrix classes +//
+// +// \brief Linear Algebra "Toolkit". +// +// modified by Rob Patro, 2016 +// modified by MDW, 2024 + +// system headers +#include +#include +#include // for size_t +#include // for fabs +#include + +//! default convergence +static const double TINY_FLOAT = 1.0e-300; + +//! comfortable array of doubles +using float_vect = std::vector; +//! comfortable array of ints; +using int_vect = std::vector; + +/*! matrix class. + * + * This is a matrix class derived from a vector of float_vects. Note that + * the matrix elements indexed [row][column] with indices starting at 0 (c + * style). Also note that because of its design looping through rows should + * be faster than looping through columns. + * + * \brief two dimensional floating point array + */ +class float_mat : public std::vector { +private: + //! disable the default constructor + explicit float_mat() {}; + //! disable assignment operator until it is implemented. + float_mat &operator =(const float_mat &) { return *this; }; +public: + //! constructor with sizes + float_mat(const size_t rows, const size_t cols, const double def=0.0); + //! copy constructor for matrix + float_mat(const float_mat &m); + //! copy constructor for vector + float_mat(const float_vect &v); + + //! use default destructor + // ~float_mat() {}; + + //! get size + size_t nr_rows(void) const { return size(); }; + //! get size + size_t nr_cols(void) const { return front().size(); }; +}; + + + +// constructor with sizes +float_mat::float_mat(const size_t rows,const size_t cols,const double defval) + : std::vector(rows) { + int i; + for (i = 0; i < rows; ++i) { + (*this)[i].resize(cols, defval); + } + if ((rows < 1) || (cols < 1)) { + std::ostringstream msg; + msg << "cannot build matrix with " << rows << " rows and " << cols + << "columns"; + throw std::runtime_error(msg.str()); + } +} + +// copy constructor for matrix +float_mat::float_mat(const float_mat &m) : std::vector(m.size()) { + + float_mat::iterator inew = begin(); + float_mat::const_iterator iold = m.begin(); + for (/* empty */; iold < m.end(); ++inew, ++iold) { + const size_t oldsz = iold->size(); + inew->resize(oldsz); + const float_vect oldvec(*iold); + *inew = oldvec; + } +} + +// copy constructor for vector +float_mat::float_mat(const float_vect &v) + : std::vector(1) { + + const size_t oldsz = v.size(); + front().resize(oldsz); + front() = v; +} + +////////////////////// +// Helper functions // +////////////////////// + +//! permute() orders the rows of A to match the integers in the index array. +void permute(float_mat &A, int_vect &idx) +{ + int_vect i(idx.size()); + int j,k; + + for (j = 0; j < A.nr_rows(); ++j) { + i[j] = j; + } + + // loop over permuted indices + for (j = 0; j < A.nr_rows(); ++j) { + if (i[j] != idx[j]) { + + // search only the remaining indices + for (k = j+1; k < A.nr_rows(); ++k) { + if (i[k] ==idx[j]) { + std::swap(A[j],A[k]); // swap the rows and + i[k] = i[j]; // the elements of + i[j] = idx[j]; // the ordered index. + break; // next j + } + } + } + } +} + +/*! \brief Implicit partial pivoting. + * + * The function looks for pivot element only in rows below the current + * element, A[idx[row]][column], then swaps that row with the current one in + * the index map. The algorithm is for implicit pivoting (i.e., the pivot is + * chosen as if the max coefficient in each row is set to 1) based on the + * scaling information in the vector scale. The map of swapped indices is + * recorded in swp. The return value is +1 or -1 depending on whether the + * number of row swaps was even or odd respectively. */ +static int partial_pivot(float_mat &A, const size_t row, const size_t col, + float_vect &scale, int_vect &idx, double tol) +{ + if (tol <= 0.0) + tol = TINY_FLOAT; + + int swapNum = 1; + + // default pivot is the current position, [row,col] + int pivot = row; + double piv_elem = fabs(A[idx[row]][col]) * scale[idx[row]]; + + // loop over possible pivots below current + int j; + for (j = row + 1; j < A.nr_rows(); ++j) { + + const double tmp = fabs(A[idx[j]][col]) * scale[idx[j]]; + + // if this elem is larger, then it becomes the pivot + if (tmp > piv_elem) { + pivot = j; + piv_elem = tmp; + } + } + +#if 0 + if (piv_elem < tol) { + throw std::runtime_error("partial_pivot(): Zero pivot encountered."); +#endif + + if(pivot > row) { // bring the pivot to the diagonal + j = idx[row]; // reorder swap array + idx[row] = idx[pivot]; + idx[pivot] = j; + swapNum = -swapNum; // keeping track of odd or even swap + } + return swapNum; +} + +/*! \brief Perform backward substitution. + * + * Solves the system of equations A*b=a, ASSUMING that A is upper + * triangular. If diag==1, then the diagonal elements are additionally + * assumed to be 1. Note that the lower triangular elements are never + * checked, so this function is valid to use after a LU-decomposition in + * place. A is not modified, and the solution, b, is returned in a. */ +static void lu_backsubst(float_mat &A, float_mat &a, bool diag=false) +{ + int r,c,k; + + for (r = (A.nr_rows() - 1); r >= 0; --r) { + for (c = (A.nr_cols() - 1); c > r; --c) { + for (k = 0; k < A.nr_cols(); ++k) { + a[r][k] -= A[r][c] * a[c][k]; + } + } + if(!diag) { + for (k = 0; k < A.nr_cols(); ++k) { + a[r][k] /= A[r][r]; + } + } + } +} + +/*! \brief Perform forward substitution. + * + * Solves the system of equations A*b=a, ASSUMING that A is lower + * triangular. If diag==1, then the diagonal elements are additionally + * assumed to be 1. Note that the upper triangular elements are never + * checked, so this function is valid to use after a LU-decomposition in + * place. A is not modified, and the solution, b, is returned in a. */ +static void lu_forwsubst(float_mat &A, float_mat &a, bool diag=true) +{ + int r,k,c; + for (r = 0;r < A.nr_rows(); ++r) { + for(c = 0; c < r; ++c) { + for (k = 0; k < A.nr_cols(); ++k) { + a[r][k] -= A[r][c] * a[c][k]; + } + } + if(!diag) { + for (k = 0; k < A.nr_cols(); ++k) { + a[r][k] /= A[r][r]; + } + } + } +} + +/*! \brief Performs LU factorization in place. + * + * This is Crout's algorithm (cf., Num. Rec. in C, Section 2.3). The map of + * swapped indeces is recorded in idx. The return value is +1 or -1 + * depending on whether the number of row swaps was even or odd + * respectively. idx must be preinitialized to a valid set of indices + * (e.g., {1,2, ... ,A.nr_rows()}). */ +static int lu_factorize(float_mat &A, int_vect &idx, double tol=TINY_FLOAT) +{ + if ( tol <= 0.0) + tol = TINY_FLOAT; + + if ((A.nr_rows() == 0) || (A.nr_rows() != A.nr_cols())) { + throw std::runtime_error("lu_factorize(): cannot handle empty " + "or nonsquare matrices."); + } + + float_vect scale(A.nr_rows()); // implicit pivot scaling + int i,j; + for (i = 0; i < A.nr_rows(); ++i) { + double maxval = 0.0; + for (j = 0; j < A.nr_cols(); ++j) { + if (fabs(A[i][j]) > maxval) + maxval = fabs(A[i][j]); + } + if (maxval == 0.0) { + throw std::runtime_error("lu_factorize(): zero pivot found."); + } + scale[i] = 1.0 / maxval; + } + + int swapNum = 1; + int c,r; + for (c = 0; c < A.nr_cols() ; ++c) { // loop over columns + swapNum *= partial_pivot(A, c, c, scale, idx, tol); // bring pivot to diagonal + for(r = 0; r < A.nr_rows(); ++r) { // loop over rows + int lim = (r < c) ? r : c; + for (j = 0; j < lim; ++j) { + A[idx[r]][c] -= A[idx[r]][j] * A[idx[j]][c]; + } + if (r > c) + A[idx[r]][c] /= A[idx[c]][c]; + } + } + permute(A,idx); + return swapNum; +} + +/*! \brief Solve a system of linear equations. + * Solves the inhomogeneous matrix problem with lu-decomposition. Note that + * inversion may be accomplished by setting a to the identity_matrix. */ +static float_mat lin_solve(const float_mat &A, const float_mat &a, + double tol=TINY_FLOAT) +{ + float_mat B(A); + float_mat b(a); + int_vect idx(B.nr_rows()); + int j; + + for (j = 0; j < B.nr_rows(); ++j) { + idx[j] = j; // init row swap label array + } + lu_factorize(B,idx,tol); // get the lu-decomp. + permute(b,idx); // sort the inhomogeneity to match the lu-decomp + lu_forwsubst(B,b); // solve the forward problem + lu_backsubst(B,b); // solve the backward problem + return b; +} + +/////////////////////// +// related functions // +/////////////////////// + +//! Returns the inverse of a matrix using LU-decomposition. +static float_mat invert(const float_mat &A) +{ + const int n = A.size(); + float_mat E(n, n, 0.0); + float_mat B(A); + int i; + + for (i = 0; i < n; ++i) { + E[i][i] = 1.0; + } + + return lin_solve(B, E); +} + +//! returns the transposed matrix. +static float_mat transpose(const float_mat &a) +{ + float_mat res(a.nr_cols(), a.nr_rows()); + int i,j; + + for (i = 0; i < a.nr_rows(); ++i) { + for (j = 0; j < a.nr_cols(); ++j) { + res[j][i] = a[i][j]; + } + } + return res; +} + +//! matrix multiplication. +float_mat operator *(const float_mat &a, const float_mat &b) +{ + float_mat res(a.nr_rows(), b.nr_cols()); + if (a.nr_cols() != b.nr_rows()) { + throw std::runtime_error("incompatible matrices in multiplication"); + } + + int i,j,k; + + for (i = 0; i < a.nr_rows(); ++i) { + for (j = 0; j < b.nr_cols(); ++j) { + double sum(0.0); + for (k = 0; k < a.nr_cols(); ++k) { + sum += a[i][k] * b[k][j]; + } + res[i][j] = sum; + } + } + return res; +} + + +//! calculate savitzky golay coefficients. +static float_vect sg_coeff(const float_vect &b, const size_t deg) +{ + const size_t rows(b.size()); + const size_t cols(deg + 1); + float_mat A(rows, cols); + float_vect res(rows); + + // generate input matrix for least squares fit + int i,j; + for (i = 0; i < rows; ++i) { + for (j = 0; j < cols; ++j) { + A[i][j] = pow(double(i), double(j)); + } + } + + float_mat c(invert(transpose(A) * A) * (transpose(A) * transpose(b))); + + for (i = 0; i < b.size(); ++i) { + res[i] = c[0][0]; + for (j = 1; j <= deg; ++j) { + res[i] += c[j][0] * pow(double(i), double(j)); + } + } + return res; +} + +/*! \brief savitzky golay smoothing. + * + * This method means fitting a polynome of degree 'deg' to a sliding window + * of width 2w+1 throughout the data. The needed coefficients are + * generated dynamically by doing a least squares fit on a "symmetric" unit + * vector of size 2w+1, e.g. for w=2 b=(0,0,1,0,0). evaluating the polynome + * yields the sg-coefficients. at the border non symmectric vectors b are + * used. */ +float_vect sg_smooth(const float_vect &v, const int width, const int deg) +{ + float_vect res(v.size(), 0.0); + if ((width < 1) || (deg < 0) || (v.size() < (2 * width + 2))) { + throw std::runtime_error("sgsmooth: parameter error."); + } + + const int window = 2 * width + 1; + const int endidx = v.size() - 1; + + // do a regular sliding window average + int i,j; + if (deg == 0) { + // handle border cases first because we need different coefficients +#if defined(_OPENMP) +#pragma omp parallel for private(i,j) schedule(static) +#endif + for (i = 0; i < width; ++i) { + const double scale = 1.0/double(i+1); + const float_vect c1(width, scale); + for (j = 0; j <= i; ++j) { + res[i] += c1[j] * v[j]; + res[endidx - i] += c1[j] * v[endidx - j]; + } + } + + // now loop over rest of data. reusing the "symmetric" coefficients. + const double scale = 1.0/double(window); + const float_vect c2(window, scale); +#if defined(_OPENMP) +#pragma omp parallel for private(i,j) schedule(static) +#endif + for (i = 0; i <= (v.size() - window); ++i) { + for (j = 0; j < window; ++j) { + res[i + width] += c2[j] * v[i + j]; + } + } + return res; + } + + // handle border cases first because we need different coefficients +#if defined(_OPENMP) +#pragma omp parallel for private(i,j) schedule(static) +#endif + for (i = 0; i < width; ++i) { + float_vect b1(window, 0.0); + b1[i] = 1.0; + + const float_vect c1(sg_coeff(b1, deg)); + for (j = 0; j < window; ++j) { + res[i] += c1[j] * v[j]; + res[endidx - i] += c1[j] * v[endidx - j]; + } + } + + // now loop over rest of data. reusing the "symmetric" coefficients. + float_vect b2(window, 0.0); + b2[width] = 1.0; + const float_vect c2(sg_coeff(b2, deg)); + +#if defined(_OPENMP) +#pragma omp parallel for private(i,j) schedule(static) +#endif + for (i = 0; i <= (v.size() - window); ++i) { + for (j = 0; j < window; ++j) { + res[i + width] += c2[j] * v[i + j]; + } + } + return res; +} + +/*! least squares fit a polynome of degree 'deg' to data in 'b'. + * then calculate the first derivative and return it. */ +static float_vect lsqr_fprime(const float_vect &b, const int deg) +{ + const int rows(b.size()); + const int cols(deg + 1); + float_mat A(rows, cols); + float_vect res(rows); + + // generate input matrix for least squares fit + int i,j; + for (i = 0; i < rows; ++i) { + for (j = 0; j < cols; ++j) { + A[i][j] = pow(double(i), double(j)); + } + } + + float_mat c(invert(transpose(A) * A) * (transpose(A) * transpose(b))); + + for (i = 0; i < b.size(); ++i) { + res[i] = c[1][0]; + for (j = 1; j < deg; ++j) { + res[i] += c[j + 1][0] * double(j+1) + * pow(double(i), double(j)); + } + } + return res; +} + +/*! \brief savitzky golay smoothed numerical derivative. + * + * This method means fitting a polynome of degree 'deg' to a sliding window + * of width 2w+1 throughout the data. + * + * In contrast to the sg_smooth function we do a brute force attempt by + * always fitting the data to a polynome of degree 'deg' and using the + * result. */ +float_vect sg_derivative(const float_vect &v, const int width, + const int deg, const double h) +{ + float_vect res(v.size(), 0.0); + if ((width < 1) || (deg < 1) || (v.size() < (2 * width + 2))) { + throw std::runtime_error("sgsderiv: parameter error"); + } + + const int window = 2 * width + 1; + + // handle border cases first because we do not repeat the fit + // lower part + float_vect b(window, 0.0); + int i,j; + + for (i = 0; i < window; ++i) { + b[i] = v[i] / h; + } + const float_vect c(lsqr_fprime(b, deg)); + for (j = 0; j <= width; ++j) { + res[j] = c[j]; + } + // upper part. direction of fit is reversed + for (i = 0; i < window; ++i) { + b[i] = v[v.size() - 1 - i] / h; + } + const float_vect d(lsqr_fprime(b, deg)); + for (i = 0; i <= width; ++i) { + res[v.size() - 1 - i] = -d[i]; + } + + // now loop over rest of data. wasting a lot of least squares calcs + // since we only use the middle value. +#if defined(_OPENMP) +#pragma omp parallel for private(i,j) schedule(static) +#endif + for (i = 1; i < (v.size() - window); ++i) { + for (j = 0; j < window; ++j) { + b[j] = v[i + j] / h; + } + res[i + width] = lsqr_fprime(b, deg)[width]; + } + return res; +} + +// Local Variables: +// mode: c++ +// c-basic-offset: 4 +// fill-column: 76 +// indent-tabs-mode: nil +// End: From 59a509d2d95569394b60d4c28650cc509d9951e8 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 2 Jul 2024 14:55:25 -0700 Subject: [PATCH 129/167] Added JOSS paper to source tree --- Paper/README.md | 20 + Paper/paper/examplefunctions.png | Bin 0 -> 208688 bytes Paper/paper/jats/examplefunctions.png | Bin 0 -> 208688 bytes Paper/paper/jats/paper.jats | 714 +++++++++++++++++++++++++ Paper/paper/media/examplefunctions.png | Bin 0 -> 208688 bytes Paper/paper/paper.bib | 322 +++++++++++ Paper/paper/paper.jats | 697 ++++++++++++++++++++++++ Paper/paper/paper.md | 211 ++++++++ Paper/paper/paper.pdf | Bin 0 -> 358193 bytes 9 files changed, 1964 insertions(+) create mode 100644 Paper/README.md create mode 100644 Paper/paper/examplefunctions.png create mode 100644 Paper/paper/jats/examplefunctions.png create mode 100644 Paper/paper/jats/paper.jats create mode 100644 Paper/paper/media/examplefunctions.png create mode 100644 Paper/paper/paper.bib create mode 100644 Paper/paper/paper.jats create mode 100644 Paper/paper/paper.md create mode 100644 Paper/paper/paper.pdf diff --git a/Paper/README.md b/Paper/README.md new file mode 100644 index 000000000..b29b1397b --- /dev/null +++ b/Paper/README.md @@ -0,0 +1,20 @@ +# Paper + +This is the JOSS abstract/paper describing the EXP release + +## Contents + +All of the text is in the `paper` directory. Contents are: + +| File | Description | +| --- | --- | +| paper.md | The primary markdown text file | +| paper.bib | The bibliography file | +| paper.pdf | The compiled PDF file | + + +## Compile to PDF with Docker as follows + +``` +docker run --rm --volume $PWD/paper:/data --user $(id -u):$(id -g) --env JOURNAL=joss openjournals/inara +``` diff --git a/Paper/paper/examplefunctions.png b/Paper/paper/examplefunctions.png new file mode 100644 index 0000000000000000000000000000000000000000..b04493bbf1e40a9e93aba4ffefb3d9ecfc550497 GIT binary patch literal 208688 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 literal 0 HcmV?d00001 diff --git a/Paper/paper/jats/examplefunctions.png b/Paper/paper/jats/examplefunctions.png new file mode 100644 index 0000000000000000000000000000000000000000..b04493bbf1e40a9e93aba4ffefb3d9ecfc550497 GIT binary patch literal 208688 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 literal 0 HcmV?d00001 diff --git a/Paper/paper/jats/paper.jats b/Paper/paper/jats/paper.jats new file mode 100644 index 000000000..e8d31d229 --- /dev/null +++ b/Paper/paper/jats/paper.jats @@ -0,0 +1,714 @@ + + +
+ + + + +Journal of Open Source Software +JOSS + +2475-9066 + +Open Journals + + + +0 +N/A + +EXP: a Python/C++ package for basis function expansion +methods in galactic dynamics + + + +https://orcid.org/0000-0003-1517-3935 + +Petersen +Michael S. + + + + +https://orcid.org/0000-0003-2660-2889 + +Weinberg +Martin D. + + + + + +University of Edinburgh, UK + + + + +University of Massachusetts/Amherst, USA + + + + +1 +6 +2024 + +¿VOL? +¿ISSUE? +¿PAGE? + +Authors of papers retain copyright and release the +work under a Creative Commons Attribution 4.0 International License (CC +BY 4.0) +2022 +The article authors + +Authors of papers retain copyright and release the work under +a Creative Commons Attribution 4.0 International License (CC BY +4.0) + + + +C++ +Python +astronomy +dynamics +galactic dynamics +Milky Way + + + + + + Summary +

Galaxies are ensembles of dark and baryonic matter confined by + their mutual gravitational attraction. The dynamics of galactic + evolution have been studied with a combination of numerical + simulations and analytical methods + (Binney + & Tremaine, 2008) for decades. Recently, data describing + the positions and motions of billions of stars from the Gaia + satellite + (Gaia + Collaboration, 2016, + 2018) + depict a Milky Way (our home galaxy) much farther from equilibrium + than we imagined and beyond the range of many analytic models. + Simulations that allow collections of bodies to evolve under their + mutual gravity are capable of reproducing such complexities but robust + links to fundamental theoretical explanations are still missing.

+

Basis Function Expansions (BFE) represent fields as a linear + combination of orthogonal functions. BFEs are particularly well-suited + for studies of perturbations from equilibrium, such as the evolution + of a galaxy. For any galaxy simulation, a biorthogonal BFE can fully + represent the density, potential and forces by time series of + coefficients. The coefficients have physical meaning: they represent + the gravitational potential energy in a given function. The variation + the function coefficients in time encodes the dynamical evolution. The + representation of simulation data by BFE results in huge compression + of the information in the dynamical fields; for example, 1.5 TB of + phase space data enumerating the positions and velocities of millions + of particles becomes 200 MB of coefficient data!

+

For optimal representation, the lowest-order basis function should + be similar to the mean or equilibrium profile of the galaxy. This + allows for use of the fewest number of terms in the expansion. For + example, the often-used basis set from Hernquist & Ostriker + (1992) + matches the Hernquist + (1990) + dark matter halo profile, yet this basis set is inappropriate for + representing the cosmologically-motivated Navarro et al. + (1997) profile. + The EXP software package implements the + adaptive empirical orthogonal function (EOF; see + [fig:examplecylinder]) + basis strategy originally described in Weinberg + (1999) + that matches any physical system close to an equilibrium model. The + package includes both a high performance N-body simulation toolkit + with computational effort scaling linearly with N + (Petersen + et al., 2022), and a user-friendly Python interface called + pyEXP that enables BFE and time-series analysis + of any N-body simulation dataset.

+
+ + Statement of need +

The need for methodology that seamlessly connects theoretical + descriptions of dynamics, N-body simulations, and compact descriptions + of observed data gave rise to EXP. This package + provides recent developments from applied mathematics and numerical + computation to represent complete series of Basis Function + Expansions that describe the variation of + any field in space. In the context of galactic + dynamics, these fields may be density, potential, force, velocity + fields or any intrinsic field produced by simulations such as + chemistry data. By combining the coefficient information through time + using multichannel singular spectral analysis (mSSA; Golyandina et al. + (2001)), a + non-parametric spectral technique, EXP can + deepen our understanding by discovering the dynamics of galaxy + evolution directly from simulated, and by analogy, observed data.

+

EXP decomposes a galaxy into multiple bases + for a variety of scales and geometries and is thus able to represent + arbitrarily complex simulation with many components (e.g., disk, + bulge, dark matter halo, satellites). EXP is + able to efficiently summarize the degree and nature of asymmetries + through coefficient amplitudes tracked through time and provide + details at multiple scales. The amplitudes themselves enable + ex-post-facto dynamical discovery. + EXP is a collection of object-oriented C++ + libraries with an associated modular N-body code and a suite of + stand-alone analysis applications.

+

pyEXP provides a full Python interface to + the EXP libraries, implemented with + pybind11 + (Jakob + et al., 2017), which provides full interoperability with major + astronomical packages including Astropy + (Astropy + Collaboration, 2013) and Gala + (Price-Whelan, + 2017). Example workflows based on previously published work are + available and distributed as accompanying + examples + and tutorials. The examples and tutorials flatten the + learning curve for adopting BFE tools to generate and analyze the + significance of coefficients and discover dynamical relationships + using time series analysis such as mSSA. We provide a + full + online manual hosted by ReadTheDocs.

+

The software package brings published – but difficult to implement + – applied-math technologies into the astronomical mainstream. + EXP and the associated Python interface + pyEXP accomplish this by providing tools + integrated with the Python ecosystem, and in particular are + well-suited for interactive Python + (Pérez + & Granger, 2007) use through (e.g.) Jupyter notebooks + (Kluyver + et al., 2016). EXP serves as the + scaffolding for new imaginative applications in galactic dynamics, + providing a common dynamical language for simulations and analytic + theory.

+
+ + Features and workflow +

The core EXP library is built around methods + to build the best basis function expansion for an arbitrary data set + in galactic dynamics. The table below lists some of the available + basis functions. All computed bases and resulting coefficient data are + stored in HDF5 + (The + HDF Group, 2000-2010) format.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
sphereSLSturm-Liouville basis function solutions to Poisson’s + equation for any arbitrary input spherical density
besselBasis constructed from eigenfunctions of the spherical + Laplacian
cylinderEOF solutions tabulated on the meridional plane for + distributions with cylindrical geometries
flatdiskEOF basis solutions for the three-dimensional + gravitational field of a razor-thin disk
cubeTrigonometric basis solution for expansions in a cube with + boundary criteria
fieldGeneral-purpose EOF solution for scalar profiles
velocityEOF solution for velocity flow coefficients
+
+ +

Example cylinder basis functions, where the color + encodes the amplitude of the function, for an exponential disk with + a scalelength of 3 and a scaleheight of 0.3 in arbitrary units. We + select three functions at low, medium, and higher order + (corresponding to the number of nodes). The color scale has been + normalised such that the largest amplitude is unity in each panel. +

+ +
+ + N-body simulation +

Our design includes a wide choice of run-time summary + diagnostics, phase-space output formats, dynamically loadable user + libraries, and easy extensibility. Stand-alone routines include the + EOF and mSSA methods described above, and the modular software + architecture of EXP enables users to easily build and maintain + extensions. The EXP code base is described in + published papers + (Petersen + et al., 2022; + Weinberg, + 2023) and has been used, enhanced, and rigorously tested for + nearly two decades.

+

The design and implementation of the N-body tools allows for + execution on a wide variety of hardware, from personal laptops to + high performance computing centers, with communication between + processes handled by MPI + (Message + Passing Interface Forum, 2023) and GPU implementations in + CUDA + (NVIDIA + et al., 2020). Owing to the linear scaling of computational + effort with N and the novel GPU implementation, the N-body methods + in EXP deliver performance in collisionless + N-body simulations previously only accessible with large dedicated + CPU clusters.

+

The flexible N-body software design allows users to write their + own modules for on-the-fly execution during N-body integration. + Modules enable powerful and intricate dynamical experiments in + N-body simulations, further reducing the gap between numerical + simulations and analytic dynamics. The package ships with several + examples, including imposed external potentials, as well as a basic + example that can be extended by users.

+
+ + Using pyEXP to represent simulations +

pyEXP provides an interface to many of the + classes in the EXP C++ library, allowing for + both the generation of all bases listed in the table above as well + as coefficients for an input data set. Each of these tools are + Python classes that accept numpy + (Harris + et al., 2020) arrays for immediate interoperability with + matplotlib + (Hunter, + 2007) and Astropy. We include a verified set of stand-alone + routines that read phase-space files from many major cosmological + tree codes and produce BFE-based analyses. The code suite includes + adapters for reading and writing phase space for many of the widely + used cosmology codes, with a base class for developing new ones. + There are multiple ways to use the versatile and modular tools in + pyEXP, and we anticipate pipelines that we + have not yet imagined.

+
+ + Using pyEXP to analyze time series +

The EXP library includes multiple time + series analysis tools, documented in the manual. Here, we briefly + highlight one technique that we have already used in published work: + mSSA + (Johnson + et al., 2023; + Weinberg + & Petersen, 2021). Beginning with coefficient series from + the previous tools, mSSA summarizes signals in time + that describes dynamically correlated responses and patterns. + Essentially, this is BFE in time and space. These temporal and + spatial patterns allow users to better identify dynamical mechanisms + and enable intercomparisons and filtering for features in simulation + suites; e.g. computing the fraction galaxies with grand design + structure or hosting bars. Random-matrix techniques for + singular-value decomposition ensure that analyses of large data sets + is possible. All mSSA decompositions are saved in HDF5 format for + reuse.

+
+
+ + Acknowledgements +

We acknowledge the ongoing support of the + B-BFE + collaboration. We also acknowledge the support of the + Center for Computational Astrophysics (CCA). The CCA is part of the + Flatiron Institute, funded by the Simons Foundation. We thank Robert + Blackwell for invaluable help with HPC best practices.

+
+ + + + + + + BinneyJ. + TremaineS. + + Galactic Dynamics: Second Edition + Princeton University Press + 2008 + http://adsabs.harvard.edu/abs/2008gady.book.....B + + + + + + Gaia Collaboration + + The Gaia mission + Astronomy and Astrophysics + 201611 + 595 + http://adsabs.harvard.edu/abs/2016A%26A...595A...1G + 10.1051/0004-6361/201629272 + + + + + + Gaia Collaboration + + Gaia Data Release 2. Mapping the Milky Way disc kinematics + A&Ap + 201808 + 616 + https://arxiv.org/abs/1804.09380 + 10.1051/0004-6361/201832865 + A11 + + + + + + + Astropy Collaboration + + Astropy: A community Python package for astronomy + Astronomy and Astrophysics + 201310 + 558 + http://adsabs.harvard.edu/abs/2013A%26A...558A..33A + 10.1051/0004-6361/201322068 + + + + + + Price-WhelanAdrian M. + + Gala: A python package for galactic dynamics + The Journal of Open Source Software + The Open Journal + 201710 + 2 + 18 + https://doi.org/10.21105%2Fjoss.00388 + 10.21105/joss.00388 + + + + + + The HDF Group + + Hierarchical data format version 5 + http://www.hdfgroup.org/HDF5 + + + + + + JakobWenzel + RhinelanderJason + MoldovanDean + + pybind11 – seamless operability between c++11 and python + 2017 + + + + + + WeinbergMartin D. + + New dipole instabilities in spherical stellar systems + MNRAS + 202311 + 525 + 4 + https://arxiv.org/abs/2209.06846 + 10.1093/mnras/stad2591 + 4962 + 4975 + + + + + + WeinbergMartin D. + PetersenMichael S. + + Using multichannel singular spectrum analysis to study galaxy dynamics + MNRAS + 202103 + 501 + 4 + https://arxiv.org/abs/2009.07870 + 10.1093/mnras/staa3997 + 5408 + 5423 + + + + + + PetersenMichael S. + WeinbergMartin D. + KatzNeal + + EXP: N-body integration using basis function expansions + + 202203 + 510 + 4 + https://arxiv.org/abs/2104.14577 + 10.1093/mnras/stab3639 + 6201 + 6217 + + + + + + JohnsonAlexander C. + PetersenMichael S. + JohnstonKathryn V. + WeinbergMartin D. + + Dynamical data mining captures disc-halo couplings that structure galaxies + MNRAS + 202305 + 521 + 2 + https://arxiv.org/abs/2301.02256 + 10.1093/mnras/stad485 + 1757 + 1774 + + + + + + GolyandinaNina + NekrutkinVladimir + ZhigljavskyAnatoly A + + Analysis of time series structure: SSA and related techniques + CRC press + 2001 + + + + + + HarrisCharles R. + MillmanK. Jarrod + WaltStéfan J. van der + GommersRalf + VirtanenPauli + CournapeauDavid + WieserEric + TaylorJulian + BergSebastian + SmithNathaniel J. + KernRobert + PicusMatti + HoyerStephan + KerkwijkMarten H. van + BrettMatthew + HaldaneAllan + RíoJaime Fernández del + WiebeMark + PetersonPearu + Gérard-MarchantPierre + SheppardKevin + ReddyTyler + WeckesserWarren + AbbasiHameer + GohlkeChristoph + OliphantTravis E. + + Array programming with NumPy + Nature + Springer Science; Business Media LLC + 202009 + 585 + 7825 + https://doi.org/10.1038/s41586-020-2649-2 + 10.1038/s41586-020-2649-2 + 357 + 362 + + + + + + HunterJ. D. + + Matplotlib: A 2D graphics environment + Computing in Science & Engineering + IEEE COMPUTER SOC + 2007 + 9 + 3 + 10.1109/MCSE.2007.55 + 90 + 95 + + + + + + WeinbergMartin D. + + An Adaptive Algorithm for N-Body Field Expansions + + 199901 + 117 + 1 + https://arxiv.org/abs/astro-ph/9805357 + 10.1086/300669 + 629 + 637 + + + + + + PérezFernando + GrangerBrian E. + + IPython: A system for interactive scientific computing + Computing in Science and Engineering + IEEE Computer Society + 200705 + 9 + 3 + 1521-9615 + https://ipython.org + 10.1109/MCSE.2007.53 + 21 + 29 + + + + + + KluyverThomas + Ragan-KelleyBenjain + PérezFernando + GrangerBrian + BussonnierMatthias + FredericJonathan + KelleyKyle + HamrickJessica + GroutJason + CorlaySylvain + IvanovPaul + AvilaDamián + AbdallaSafia + WillingCarol + Jupyter Development Team + + Jupyter Notebooksa publishing format for reproducible computational workflows + IOS press + 2016 + 10.3233/978-1-61499-649-1-87 + 87 + 90 + + + + + + NVIDIA + VingelmannPéter + FitzekFrank H. P. + + CUDA, release: 10.2.89 + 2020 + https://developer.nvidia.com/cuda-toolkit + + + + + + Message Passing Interface Forum + + MPI: A message-passing interface standard version 4.1 + 202311 + https://www.mpi-forum.org/docs/mpi-4.1/mpi41-report.pdf + + + + + + HernquistLars + + An Analytical Model for Spherical Galaxies and Bulges + + 199006 + 356 + 10.1086/168845 + 359 + + + + + + + HernquistLars + OstrikerJeremiah P. + + A Self-consistent Field Method for Galactic Dynamics + + 199202 + 386 + 10.1086/171025 + 375 + + + + + + + NavarroJulio F. + FrenkCarlos S. + WhiteSimon D. M. + + A Universal Density Profile from Hierarchical Clustering + + 199712 + 490 + 2 + https://arxiv.org/abs/astro-ph/9611107 + 10.1086/304888 + 493 + 508 + + + + +
diff --git a/Paper/paper/media/examplefunctions.png b/Paper/paper/media/examplefunctions.png new file mode 100644 index 0000000000000000000000000000000000000000..b04493bbf1e40a9e93aba4ffefb3d9ecfc550497 GIT binary patch literal 208688 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 literal 0 HcmV?d00001 diff --git a/Paper/paper/paper.bib b/Paper/paper/paper.bib new file mode 100644 index 000000000..f6dedcf60 --- /dev/null +++ b/Paper/paper/paper.bib @@ -0,0 +1,322 @@ +@book{Binney:2008, + url = {http://adsabs.harvard.edu/abs/2008gady.book.....B}, + Author = {{Binney}, J. and {Tremaine}, S.}, + Booktitle = {Galactic Dynamics: Second Edition, by James Binney and Scott Tremaine.~ISBN 978-0-691-13026-2 (HB).~Published by Princeton University Press, Princeton, NJ USA, 2008.}, + Publisher = {Princeton University Press}, + Title = {{Galactic Dynamics: Second Edition}}, + Year = 2008 +} + +@article{gaia, + author = {{Gaia Collaboration}}, + title = "{The Gaia mission}", + journal = {Astronomy and Astrophysics}, + archivePrefix = "arXiv", + eprint = {1609.04153}, + primaryClass = "astro-ph.IM", + keywords = {space vehicles: instruments, Galaxy: structure, astrometry, parallaxes, proper motions, telescopes}, + year = 2016, + month = nov, + volume = 595, + doi = {10.1051/0004-6361/201629272}, + url = {http://adsabs.harvard.edu/abs/2016A%26A...595A...1G}, +} + +@article{gaia_DR2_disk, + Title = {{Gaia Data Release 2. Mapping the Milky Way disc kinematics}}, + Author = {{Gaia Collaboration}}, + Doi = {10.1051/0004-6361/201832865}, + Eid = {A11}, + Eprint = {1804.09380}, + Journal = {A\&Ap}, + Month = {Aug}, + Pages = {A11}, + Primaryclass = {astro-ph.GA}, + Volume = {616}, + Year = {2018}, + Archiveprefix = {arXiv} +} + +@article{astropy, + author = {{Astropy Collaboration}}, + title = "{Astropy: A community Python package for astronomy}", + journal = {Astronomy and Astrophysics}, + archivePrefix = "arXiv", + eprint = {1307.6212}, + primaryClass = "astro-ph.IM", + keywords = {methods: data analysis, methods: miscellaneous, virtual observatory tools}, + year = 2013, + month = oct, + volume = 558, + doi = {10.1051/0004-6361/201322068}, + url = {http://adsabs.harvard.edu/abs/2013A%26A...558A..33A} +} + +@article{gala, + doi = {10.21105/joss.00388}, + url = {https://doi.org/10.21105%2Fjoss.00388}, + year = 2017, + month = {oct}, + publisher = {The Open Journal}, + volume = {2}, + number = {18}, + author = {Adrian M. Price-Whelan}, + title = {Gala: A Python package for galactic dynamics}, + journal = {The Journal of Open Source Software}} + + +@misc{hdf5, + author = {{The HDF Group}}, + title = "{Hierarchical data format version 5}", + year = {2000-2010}, + howpublished = {http://www.hdfgroup.org/HDF5} +} + +@misc{pybind11, + author = {Wenzel Jakob and Jason Rhinelander and Dean Moldovan}, + year = {2017}, + note = {https://github.com/pybind/pybind11}, + title = {pybind11 -- Seamless operability between C++11 and Python} +} + +@ARTICLE{Weinberg:23, + author = {{Weinberg}, Martin D.}, + title = "{New dipole instabilities in spherical stellar systems}", + journal = {MNRAS}, + year = 2023, + month = nov, + volume = {525}, + number = {4}, + pages = {4962-4975}, + doi = {10.1093/mnras/stad2591}, +archivePrefix = {arXiv}, + eprint = {2209.06846}, + primaryClass = {astro-ph.GA} +} + +@ARTICLE{Weinberg:21, + author = {{Weinberg}, Martin D. and {Petersen}, Michael S.}, + title = "{Using multichannel singular spectrum analysis to study galaxy dynamics}", + journal = {MNRAS}, + year = 2021, + month = mar, + volume = {501}, + number = {4}, + pages = {5408-5423}, + doi = {10.1093/mnras/staa3997}, +archivePrefix = {arXiv}, + eprint = {2009.07870}, + primaryClass = {astro-ph.GA} +} + +@ARTICLE{Petersen:22, + author = {{Petersen}, Michael S. and {Weinberg}, Martin D. and {Katz}, Neal}, + title = "{EXP: N-body integration using basis function expansions}", + journal = {\mnras}, + keywords = {methods: numerical, Galaxy: halo, galaxies: haloes, galaxies: kinematics and dynamics, galaxies: structure, Astrophysics - Astrophysics of Galaxies, Astrophysics - Instrumentation and Methods for Astrophysics}, + year = 2022, + month = mar, + volume = {510}, + number = {4}, + pages = {6201-6217}, + doi = {10.1093/mnras/stab3639}, +archivePrefix = {arXiv}, + eprint = {2104.14577}, + primaryClass = {astro-ph.GA} +} + +@ARTICLE{Johnson:23, + author = {{Johnson}, Alexander C. and {Petersen}, Michael S. and {Johnston}, Kathryn V. and {Weinberg}, Martin D.}, + title = "{Dynamical data mining captures disc-halo couplings that structure galaxies}", + journal = {MNRAS}, + keywords = {galaxies: disc, galaxies: haloes, galaxies: structure, Astrophysics - Astrophysics of Galaxies, Astrophysics - Instrumentation and Methods for Astrophysics}, + year = 2023, + month = may, + volume = {521}, + number = {2}, + pages = {1757-1774}, + doi = {10.1093/mnras/stad485}, +archivePrefix = {arXiv}, + eprint = {2301.02256}, + primaryClass = {astro-ph.GA} +} + +@book{SSA, + title={Analysis of time series structure: SSA and related techniques}, + author={Golyandina, Nina and Nekrutkin, Vladimir and Zhigljavsky, Anatoly A}, + year={2001}, + publisher={CRC press} +} + +@Article{numpy, + title = {Array programming with {NumPy}}, + author = {Charles R. Harris and K. Jarrod Millman and St{\'{e}}fan J. + van der Walt and Ralf Gommers and Pauli Virtanen and David + Cournapeau and Eric Wieser and Julian Taylor and Sebastian + Berg and Nathaniel J. Smith and Robert Kern and Matti Picus + and Stephan Hoyer and Marten H. van Kerkwijk and Matthew + Brett and Allan Haldane and Jaime Fern{\'{a}}ndez del + R{\'{i}}o and Mark Wiebe and Pearu Peterson and Pierre + G{\'{e}}rard-Marchant and Kevin Sheppard and Tyler Reddy and + Warren Weckesser and Hameer Abbasi and Christoph Gohlke and + Travis E. Oliphant}, + year = {2020}, + month = sep, + journal = {Nature}, + volume = {585}, + number = {7825}, + pages = {357--362}, + doi = {10.1038/s41586-020-2649-2}, + publisher = {Springer Science and Business Media {LLC}}, + url = {https://doi.org/10.1038/s41586-020-2649-2} +} +@Article{matplotlib, + Author = {Hunter, J. D.}, + Title = {Matplotlib: A 2D graphics environment}, + Journal = {Computing in Science \& Engineering}, + Volume = {9}, + Number = {3}, + Pages = {90--95}, + abstract = {Matplotlib is a 2D graphics package used for Python for + application development, interactive scripting, and publication-quality + image generation across user interfaces and operating systems.}, + publisher = {IEEE COMPUTER SOC}, + doi = {10.1109/MCSE.2007.55}, + year = 2007 +} + +@ARTICLE{Weinberg:99, + author = {{Weinberg}, Martin D.}, + title = "{An Adaptive Algorithm for N-Body Field Expansions}", + journal = {\aj}, + keywords = {CELESTIAL MECHANICS, STELLAR DYNAMICS, GALAXIES: STRUCTURE, GALAXY: STRUCTURE, METHODS: NUMERICAL, Astrophysics}, + year = 1999, + month = jan, + volume = {117}, + number = {1}, + pages = {629-637}, + doi = {10.1086/300669}, +archivePrefix = {arXiv}, + eprint = {astro-ph/9805357}, + primaryClass = {astro-ph}, + adsurl = {https://ui.adsabs.harvard.edu/abs/1999AJ....117..629W}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} + + @MISC{Eigen, + author = {Ga\"{e}l Guennebaud and Beno\^{i}t Jacob and others}, + title = {Eigen v3}, + howpublished = {http://eigen.tuxfamily.org}, + year = {2010} + } + +@Article{iPython, + Author = {P\'erez, Fernando and Granger, Brian E.}, + Title = {{IP}ython: a System for Interactive Scientific Computing}, + Journal = {Computing in Science and Engineering}, + Volume = {9}, + Number = {3}, + Pages = {21--29}, + month = may, + year = 2007, + url = "https://ipython.org", + ISSN = "1521-9615", + doi = {10.1109/MCSE.2007.53}, + publisher = {IEEE Computer Society}, +} + +@INCOLLECTION{jupyter, + author = {{Kluyver}, Thomas and {Ragan-Kelley}, Benjain and {P{\'e}rez}, Fernando and {Granger}, Brian and {Bussonnier}, Matthias and {Frederic}, Jonathan and {Kelley}, Kyle and {Hamrick}, Jessica and {Grout}, Jason and {Corlay}, Sylvain and {Ivanov}, Paul and {Avila}, Dami{\'a}n and {Abdalla}, Safia and {Willing}, Carol and {Jupyter Development Team}}, + title = "{Jupyter Notebooks{\textemdash}a publishing format for reproducible computational workflows}", + keywords = {Notebook, reproducibility, research code}, + booktitle = {IOS Press}, + year = 2016, + pages = {87-90}, + doi = {10.3233/978-1-61499-649-1-87}, + adsurl = {https://ui.adsabs.harvard.edu/abs/2016ppap.book...87K}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} + +@misc{cuda, + author={NVIDIA and Vingelmann, Péter and Fitzek, Frank H.P.}, + title={CUDA, release: 10.2.89}, + year={2020}, + url={https://developer.nvidia.com/cuda-toolkit}, +} + +@manual{mpi41, + author = "{Message Passing Interface Forum}", + title = "{MPI}: A Message-Passing Interface Standard Version 4.1", + url = "https://www.mpi-forum.org/docs/mpi-4.1/mpi41-report.pdf", + year = 2023, + month = nov +} + + +@ARTICLE{GaravitoCamargo:21, + author = {{Garavito-Camargo}, Nicol{\'a}s and {Besla}, Gurtina and {Laporte}, Chervin F.~P. and {Price-Whelan}, Adrian M. and {Cunningham}, Emily C. and {Johnston}, Kathryn V. and {Weinberg}, Martin and {G{\'o}mez}, Facundo A.}, + title = "{Quantifying the Impact of the Large Magellanic Cloud on the Structure of the Milky Way's Dark Matter Halo Using Basis Function Expansions}", + journal = {\apj}, + keywords = {Milky Way dynamics, Large Magellanic Cloud, Milky Way dark matter halo, 1051, 903, 1049, Astrophysics - Astrophysics of Galaxies}, + year = 2021, + month = oct, + volume = {919}, + number = {2}, + eid = {109}, + pages = {109}, + doi = {10.3847/1538-4357/ac0b44}, +archivePrefix = {arXiv}, + eprint = {2010.00816}, + primaryClass = {astro-ph.GA}, + adsurl = {https://ui.adsabs.harvard.edu/abs/2021ApJ...919..109G}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} + +@ARTICLE{Hernquist:90, + author = {{Hernquist}, Lars}, + title = "{An Analytical Model for Spherical Galaxies and Bulges}", + journal = {\apj}, + keywords = {Computational Astrophysics, Elliptical Galaxies, Galactic Bulge, Galactic Structure, Astronomical Models, Astronomical Photometry, Brightness Distribution, Distribution Functions, Astrophysics, GALAXIES: PHOTOMETRY, GALAXIES: STRUCTURE}, + year = 1990, + month = jun, + volume = {356}, + pages = {359}, + doi = {10.1086/168845}, + adsurl = {https://ui.adsabs.harvard.edu/abs/1990ApJ...356..359H}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} + + + +@ARTICLE{Hernquist:92, + author = {{Hernquist}, Lars and {Ostriker}, Jeremiah P.}, + title = "{A Self-consistent Field Method for Galactic Dynamics}", + journal = {\apj}, + keywords = {Celestial Mechanics, Computational Astrophysics, Galaxies, Stellar Motions, Algorithms, Astronomical Models, Dynamical Systems, Numerical Analysis, Astrophysics, CELESTIAL MECHANICS, STELLAR DYNAMICS, METHODS: NUMERICAL}, + year = 1992, + month = feb, + volume = {386}, + pages = {375}, + doi = {10.1086/171025}, + adsurl = {https://ui.adsabs.harvard.edu/abs/1992ApJ...386..375H}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} + +@ARTICLE{NFW, + author = {{Navarro}, Julio F. and {Frenk}, Carlos S. and {White}, Simon D.~M.}, + title = "{A Universal Density Profile from Hierarchical Clustering}", + journal = {\apj}, + keywords = {Cosmology: Theory, Cosmology: Dark Matter, Galaxies: Halos, Methods: Numerical, Astrophysics}, + year = 1997, + month = dec, + volume = {490}, + number = {2}, + pages = {493-508}, + doi = {10.1086/304888}, +archivePrefix = {arXiv}, + eprint = {astro-ph/9611107}, + primaryClass = {astro-ph}, + adsurl = {https://ui.adsabs.harvard.edu/abs/1997ApJ...490..493N}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} + diff --git a/Paper/paper/paper.jats b/Paper/paper/paper.jats new file mode 100644 index 000000000..194915004 --- /dev/null +++ b/Paper/paper/paper.jats @@ -0,0 +1,697 @@ + + +
+ + + + +Journal of Open Source Software +JOSS + +2475-9066 + +Open Journals + + + +0 +N/A + +EXP: a Python/C++ package for basis function expansion +methods in galactic dynamics + + + +https://orcid.org/0000-0003-1517-3935 + +Petersen +Michael S. + + + + +https://orcid.org/0000-0003-2660-2889 + +Weinberg +Martin D. + + + + + +University of Edinburgh, UK + + + + +University of Massachusetts/Amherst, USA + + + + +1 +6 +2024 + +¿VOL? +¿ISSUE? +¿PAGE? + +Authors of papers retain copyright and release the +work under a Creative Commons Attribution 4.0 International License (CC +BY 4.0) +2022 +The article authors + +Authors of papers retain copyright and release the work under +a Creative Commons Attribution 4.0 International License (CC BY +4.0) + + + +C++ +Python +astronomy +dynamics +galactic dynamics +Milky Way + + + + + + Summary +

Galaxies are nearly equilibrium ensembles of dark and baryonic + matter confined by their mutual gravitational attraction. Over many + decades of rich development, the dynamics of galactic evolution have + been studied with a combination of numerical simulations and + analytical methods + (Binney + & Tremaine, 2008). Recently, data describing the positions + and motions of billions of stars from the Gaia + satellite + (Gaia + Collaboration, 2016, + 2018) + depict a Milky Way (our home galaxy) much farther from equilibrium + that we imagined and beyond the range of many analytic models. N-body + simulations, tracking the evolution of N bodies under their mutual + gravity, are capable of capturing such complexities, but robust links + to theoretical descriptions are still missing.

+

Basis Function Expansions (BFE) represent fields as a linear + combination of orthogonal functions. BFEs are particularly well-suited + for studies of perturbations around equilibria, such as the evolution + of a galaxy. If the BFE is used to describe the evolution of a galaxy + with time, then one can also study the time series of function + coefficients. For any galaxy simulation, a biorthogonal BFE can fully + represent the density, potential and forces by time series of + coefficients. This results in huge compression of the information in + the dynamical fields; for example, 1.5 TB of phase space data becomes + 200 MB of coefficient data!

+

For optimal representation, the lowest-order basis function should + be similar to the mean or equilibrium profile of the galaxy. This + allows for use of the fewest number of terms in the expansion. For + example, the often-used basis set from Hernquist & Ostriker + (1992) + matches the Hernquist + (1990) + dark matter halo profile, yet this basis set is inappropriate for + representing the cosmologically-motivated Navarro et al. + (1997) profile. + The EXP software package implements the + adaptive empirical orthogonal function (EOF) basis strategies + originally described in Weinberg + (1999) + that match any physical system close to an equilibrium model. The + package includes both a high performance N-body simulation toolkit + with computational effort scaling linearly with N + (Petersen + et al., 2022), and a user-friendly Python interface called + pyEXP that enables BFE and time-series analysis + of any N-body simulation dataset.

+
+ + Statement of need +

The need for methodology that seamlessly connects theoretical + descriptions of dynamics, N-body simulations, and compact descriptions + of observed data gave rise to EXP. This package + provides recent developments from applied mathematics and numerical + computation to represent complete series of Basis Function + Expansions that describe the variation of + any field in space. In the context of galactic + dynamics, these fields may be density, potential, force, or even + velocity fields or chemical tags. By combining the information through + time using SSA + (Golyandina + et al., 2001), a non-parametric spectral technique, + EXP can deepen our understanding by discovering + the dynamics galaxy evolution directly from simulations and + observations.

+

EXP is a collection of object-oriented C++ + libraries with an associated modular N-body code and a suite of + stand-alone analysis applications. The library includes many of the + published recursion relations for specific basis sets; we have + demonstrated that the adaptive method can reproduce all of them to + machine accuracy. By combining multiple bases for different scales and + geometries, EXP, can decompose a galaxy model + based on the geometry and symmetry for any number of components (disk, + bulge, dark-matter halo, satellites). The run of coefficient + amplitudes through time efficiently summarize the degree and nature of + asymmetries and provides detail at multiple scales and enable + ex-post-facto discovery.

+

pyEXP provides full Python interface, + implemented with pybind11 + (Jakob + et al., 2017), that provides full interoperability with major + astronomical packages including Astropy + (Astropy + Collaboration, 2013) and Gala + (Price-Whelan, + 2017). Example workflows based on previously published work are + available and distributed as accompanying + examples + and tutorials. The examples and tutorials flatten the + learning curve for adopting BFE tools for generating and analyzing the + significance of coefficients and discovering dynamical relationships + using time series analysis such as mSSA. We provide a + full + online manual hosted by ReadTheDocs.

+

The software package brings published – but difficult to implement + – applied-math technologies into the astronomical mainstream. + EXP and particularly the Python interface + pyEXP accomplish this by providing tools + integrated with the Python ecosystem, and in particular are + well-suited for interactive Python + (Pérez + & Granger, 2007) use through (e.g.) Jupyter notebooks + (Kluyver + et al., 2016). We anticipate that EXP we + serve as the scaffolding for new imaginative applications in galactic + dynamics, providing a common dynamical language for simulations and + analytic theory.

+
+ + Features and workflow +

The core EXP library is built around methods + to build the best basis function expansion for an arbitrary data set + in galactic dynamics. The table below lists available basis functions. + All computed bases and resulting coefficient data are stored in HDF5 + (The + HDF Group, 2000-2010) format.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
sphereSLSturm-Liouville computed basis function solutions to + Poisson’s equation for any arbitrary input spherical + density
cylinderEOF solutions tabulated on the meridional plane for + distributions with cylindrical geometries
flatdiskEOF basis solutions for the three-dimensional + gravitational field of a razor-thin disk
cubeTrigonometric basis solution for expansions in a cube with + boundary criteria
fieldGeneral-purpose EOF solution for scalar profiles
velocityEOF solution for velocity flow coefficients
+
+

A picture of some basis functions will be trialled + here

+ + N-body simulation +

Our design includes a wide choice of run-time summary + diagnostics, phase-space output formats, dynamically loadable user + libraries, and easy extensibility. Stand-alone routines include the + EOF and mSSA methods described above. EXP has + been used, enhanced, and tested for nearly twenty years. Its modular + software architecture makes it extensible and maintainable. This + code base has been tested rigorously over more than a decade and + described in published papers + (Petersen + et al., 2022; + Weinberg, + 2023).

+

The design and implementation of the N-body tools allows for + execution on a wide variety of hardware, from personal laptops to + high performance computing centers, with communication between + processes handled by MPI + (Message + Passing Interface Forum, 2023) and GPU implementations in + CUDA + (NVIDIA + et al., 2020). Owing to the linear scaling of computational + effort with N, the N-body methods in EXP + deliver performance in N-body simulations previously only accessible + with large dedicated CPU clusters.

+

The flexible N-body software design allows users to write their + own modules for on-the-fly execution during N-body integration. Such + modules can be powerful methods to design dynamical experiments in + N-body simulations, further reducing the gap between numerical + simulations and analytic deynamics. The package ships with several + examples, including imposed external potentials, as well as a basic + example that can be extended by users.

+
+ + Using pyEXP to represent simulations +

pyEXP provides an interface to many of the + classes in the EXP C++ library, allowing for + both the generation of all bases listed in the table above as well + as coefficients for an input data set. Each of these tools are + Python classes that accept numpy + (Harris + et al., 2020) arrays for immediate interoperability with + matplotlib + (Hunter, + 2007) and Astropy. We include a verified set of stand-alone + routines that read phase-space files from many major cosmological + tree codes and produce BFE-based analyses. The code suite includes + adapters for reading and writing phase space for many of the widely + used cosmology codes, with a base class for developing new ones. + There are multiple way to use the versatile and modular tools in + pyEXP, and we anticipate pipelines that we + have not yet imagined.

+
+ + Using pyEXP to analyze time series +

The EXP library includes multiple + different time series analysis tools, documented in the manual. + Here, we briefly highlight one technique that we have already used + in published work: mSSA + (Johnson + et al., 2023; + Weinberg + & Petersen, 2021). Beginning with coefficient series from + the previous tools, mSSA summarizes signals in time + that describes dynamically correlated responses and patterns. + Essentially, this is BFE in time and space. These temporal and + spatial patterns allow users to better identify dynamical mechanism + and enable intercomparisons and filtering for features in simulation + suites; e.g. computing the fraction of grand design or bar + excitation. Options for eigenanalysis ensure that decomposition of + large data sets is possible. All mSSA decompositions are saved in + HDF5 format for reuse.

+
+
+ + Acknowledgements +

We acknowledge the ongoing support of the + B-BFE + collaboration. We also acknowledge the support of the + Center for Computational Astrophysics (CCA). The CCA is part of the + Flatiron Institute, funded by the Simons Foundation. We thank Robert + Blackwell for invaluable help with HPC best practices.

+
+ + + + + + + BinneyJ. + TremaineS. + + Galactic Dynamics: Second Edition + Princeton University Press + 2008 + http://adsabs.harvard.edu/abs/2008gady.book.....B + + + + + + Gaia Collaboration + + The Gaia mission + Astronomy and Astrophysics + 201611 + 595 + http://adsabs.harvard.edu/abs/2016A%26A...595A...1G + 10.1051/0004-6361/201629272 + + + + + + Gaia Collaboration + + Gaia Data Release 2. Mapping the Milky Way disc kinematics + A&Ap + 201808 + 616 + https://arxiv.org/abs/1804.09380 + 10.1051/0004-6361/201832865 + A11 + + + + + + + Astropy Collaboration + + Astropy: A community Python package for astronomy + Astronomy and Astrophysics + 201310 + 558 + http://adsabs.harvard.edu/abs/2013A%26A...558A..33A + 10.1051/0004-6361/201322068 + + + + + + Price-WhelanAdrian M. + + Gala: A python package for galactic dynamics + The Journal of Open Source Software + The Open Journal + 201710 + 2 + 18 + https://doi.org/10.21105%2Fjoss.00388 + 10.21105/joss.00388 + + + + + + The HDF Group + + Hierarchical data format version 5 + http://www.hdfgroup.org/HDF5 + + + + + + JakobWenzel + RhinelanderJason + MoldovanDean + + pybind11 – seamless operability between c++11 and python + 2017 + + + + + + WeinbergMartin D. + + New dipole instabilities in spherical stellar systems + MNRAS + 202311 + 525 + 4 + https://arxiv.org/abs/2209.06846 + 10.1093/mnras/stad2591 + 4962 + 4975 + + + + + + WeinbergMartin D. + PetersenMichael S. + + Using multichannel singular spectrum analysis to study galaxy dynamics + MNRAS + 202103 + 501 + 4 + https://arxiv.org/abs/2009.07870 + 10.1093/mnras/staa3997 + 5408 + 5423 + + + + + + PetersenMichael S. + WeinbergMartin D. + KatzNeal + + EXP: N-body integration using basis function expansions + + 202203 + 510 + 4 + https://arxiv.org/abs/2104.14577 + 10.1093/mnras/stab3639 + 6201 + 6217 + + + + + + JohnsonAlexander C. + PetersenMichael S. + JohnstonKathryn V. + WeinbergMartin D. + + Dynamical data mining captures disc-halo couplings that structure galaxies + MNRAS + 202305 + 521 + 2 + https://arxiv.org/abs/2301.02256 + 10.1093/mnras/stad485 + 1757 + 1774 + + + + + + GolyandinaNina + NekrutkinVladimir + ZhigljavskyAnatoly A + + Analysis of time series structure: SSA and related techniques + CRC press + 2001 + + + + + + HarrisCharles R. + MillmanK. Jarrod + WaltStéfan J. van der + GommersRalf + VirtanenPauli + CournapeauDavid + WieserEric + TaylorJulian + BergSebastian + SmithNathaniel J. + KernRobert + PicusMatti + HoyerStephan + KerkwijkMarten H. van + BrettMatthew + HaldaneAllan + RíoJaime Fernández del + WiebeMark + PetersonPearu + Gérard-MarchantPierre + SheppardKevin + ReddyTyler + WeckesserWarren + AbbasiHameer + GohlkeChristoph + OliphantTravis E. + + Array programming with NumPy + Nature + Springer Science; Business Media LLC + 202009 + 585 + 7825 + https://doi.org/10.1038/s41586-020-2649-2 + 10.1038/s41586-020-2649-2 + 357 + 362 + + + + + + HunterJ. D. + + Matplotlib: A 2D graphics environment + Computing in Science & Engineering + IEEE COMPUTER SOC + 2007 + 9 + 3 + 10.1109/MCSE.2007.55 + 90 + 95 + + + + + + WeinbergMartin D. + + An Adaptive Algorithm for N-Body Field Expansions + + 199901 + 117 + 1 + https://arxiv.org/abs/astro-ph/9805357 + 10.1086/300669 + 629 + 637 + + + + + + PérezFernando + GrangerBrian E. + + IPython: A system for interactive scientific computing + Computing in Science and Engineering + IEEE Computer Society + 200705 + 9 + 3 + 1521-9615 + https://ipython.org + 10.1109/MCSE.2007.53 + 21 + 29 + + + + + + KluyverThomas + Ragan-KelleyBenjain + PérezFernando + GrangerBrian + BussonnierMatthias + FredericJonathan + KelleyKyle + HamrickJessica + GroutJason + CorlaySylvain + IvanovPaul + AvilaDamián + AbdallaSafia + WillingCarol + Jupyter Development Team + + Jupyter Notebooksa publishing format for reproducible computational workflows + IOS press + 2016 + 10.3233/978-1-61499-649-1-87 + 87 + 90 + + + + + + NVIDIA + VingelmannPéter + FitzekFrank H. P. + + CUDA, release: 10.2.89 + 2020 + https://developer.nvidia.com/cuda-toolkit + + + + + + Message Passing Interface Forum + + MPI: A message-passing interface standard version 4.1 + 202311 + https://www.mpi-forum.org/docs/mpi-4.1/mpi41-report.pdf + + + + + + HernquistLars + + An Analytical Model for Spherical Galaxies and Bulges + + 199006 + 356 + 10.1086/168845 + 359 + + + + + + + HernquistLars + OstrikerJeremiah P. + + A Self-consistent Field Method for Galactic Dynamics + + 199202 + 386 + 10.1086/171025 + 375 + + + + + + + NavarroJulio F. + FrenkCarlos S. + WhiteSimon D. M. + + A Universal Density Profile from Hierarchical Clustering + + 199712 + 490 + 2 + https://arxiv.org/abs/astro-ph/9611107 + 10.1086/304888 + 493 + 508 + + + + +
diff --git a/Paper/paper/paper.md b/Paper/paper/paper.md new file mode 100644 index 000000000..a593490bb --- /dev/null +++ b/Paper/paper/paper.md @@ -0,0 +1,211 @@ +--- +title: 'EXP: a Python/C++ package for basis + function expansion methods in galactic dynamics' +tags: + - C++ + - Python + - astronomy + - dynamics + - galactic dynamics + - Milky Way +authors: + - name: Michael S. Petersen + orcid: 0000-0003-1517-3935 + affiliation: 1 + - name: Martin D. Weinberg + orcid: 0000-0003-2660-2889 + affiliation: 2 +affiliations: + - name: University of Edinburgh, UK + index: 1 + - name: University of Massachusetts/Amherst, USA + index: 2 +date: 01 June 2024 +bibliography: paper.bib + +--- + +# Summary + +Galaxies are ensembles of dark and baryonic matter confined by their +mutual gravitational attraction. The dynamics of galactic evolution +have been studied with a combination of numerical simulations and +analytical methods [@Binney:2008] for decades. Recently, data +describing the positions and motions of billions of stars from the +_Gaia satellite_ [@gaia; @gaia_DR2_disk] depict a Milky Way (our home +galaxy) much farther from equilibrium than we imagined and beyond the +range of many analytic models. Simulations that allow collections of +bodies to evolve under their mutual gravity are capable of reproducing +such complexities but robust links to fundamental theoretical +explanations are still missing. + +Basis Function Expansions (BFE) represent fields as a linear +combination of orthogonal functions. BFEs are particularly well-suited +for studies of perturbations from equilibrium, such as the evolution +of a galaxy. For any galaxy simulation, a biorthogonal BFE can fully +represent the density, potential and forces by time series of +coefficients. The coefficients have physical meaning: they represent +the gravitational potential energy in a given function. The variation +the function coefficients in time encodes the dynamical evolution. +The representation of simulation data by BFE results in huge +compression of the information in the dynamical fields; for example, +1.5 TB of phase space data enumerating the positions and velocities of +millions of particles becomes 200 MB of coefficient data! + +For optimal representation, the lowest-order basis function should be +similar to the mean or equilibrium profile of the galaxy. This allows +for use of the fewest number of terms in the expansion. For example, +the often-used basis set from @Hernquist:92 matches the @Hernquist:90 +dark matter halo profile, yet this basis set is inappropriate for +representing the cosmologically-motivated @NFW profile. The `EXP` +software package implements the adaptive empirical orthogonal function +(EOF; see \autoref{fig:examplecylinder}) basis strategy originally +described in @Weinberg:99 that matches any physical system close to an +equilibrium model. The package includes both a high performance N-body +simulation toolkit with computational effort scaling linearly with N +[@Petersen:22], and a user-friendly Python interface called `pyEXP` +that enables BFE and time-series analysis of any N-body simulation +dataset. + +# Statement of need + +The need for methodology that seamlessly connects theoretical +descriptions of dynamics, N-body simulations, and compact descriptions +of observed data gave rise to `EXP`. This package provides recent +developments from applied mathematics and numerical computation to +represent complete series of _Basis Function Expansions_ that describe +the variation of _any_ field in space. In the context of galactic +dynamics, these fields may be density, potential, force, velocity +fields or any intrinsic field produced by simulations such as +chemistry data. By combining the coefficient information through time +using multichannel singular spectral analysis (mSSA; @SSA), a +non-parametric spectral technique, `EXP` can deepen our understanding +by discovering the dynamics of galaxy evolution directly from +simulated, and by analogy, observed data. + +`EXP` decomposes a galaxy into multiple bases for a variety of scales +and geometries and is thus able to represent arbitrarily complex +simulation with many components (e.g., disk, bulge, dark matter halo, +satellites). `EXP` is able to efficiently summarize the degree and +nature of asymmetries through coefficient amplitudes tracked through +time and provide details at multiple scales. The +amplitudes themselves enable ex-post-facto dynamical discovery. +`EXP` is a collection of object-oriented C++ libraries with an +associated modular N-body code and a suite of stand-alone analysis +applications. + +`pyEXP` provides a full Python interface to the `EXP` libraries, +implemented with `pybind11` [@pybind11], which provides full +interoperability with major astronomical packages including Astropy +[@astropy] and `Gala` [@gala]. Example workflows based on previously +published work are available and distributed as accompanying [examples +and tutorials](https://github.com/EXP-code/pyEXP-examples). The +examples and tutorials flatten the learning curve for +adopting BFE tools to generate and analyze the significance of +coefficients and discover dynamical relationships using time series +analysis such as mSSA. We provide a [full online +manual](https://exp-docs.readthedocs.io) hosted by ReadTheDocs. + +The software package brings published -- but difficult to implement -- +applied-math technologies into the astronomical mainstream. `EXP` and +the associated Python interface `pyEXP` accomplish this by providing +tools integrated with the Python ecosystem, and in particular are +well-suited for interactive Python [@iPython] use through (e.g.) +Jupyter notebooks [@jupyter]. `EXP` serves as the +scaffolding for new imaginative applications in galactic dynamics, +providing a common dynamical language for simulations and analytic +theory. + +# Features and workflow + +The core `EXP` library is built around methods to build the best basis +function expansion for an arbitrary data set in galactic dynamics. +The table below lists some of the available basis functions. All +computed bases and resulting coefficient data are stored in HDF5 +[@hdf5] format. + +| Name | Description | +| ----------- | ------------- | +| sphereSL | Sturm-Liouville basis function solutions to Poisson's equation for any arbitrary input spherical density | +| bessel | Basis constructed from eigenfunctions of the spherical Laplacian | +| cylinder | EOF solutions tabulated on the meridional plane for distributions with cylindrical geometries | +| flatdisk | EOF basis solutions for the three-dimensional gravitational field of a razor-thin disk | +| cube | Trigonometric basis solution for expansions in a cube with boundary criteria | +| field | General-purpose EOF solution for scalar profiles | +| velocity | EOF solution for velocity flow coefficients | + +![Example cylinder basis functions, where the color encodes the amplitude of the function, for an exponential disk with a scalelength of 3 and a scaleheight of 0.3 in arbitrary units. We select three functions at low, medium, and higher order (corresponding to the number of nodes). The color scale has been normalised such that the largest amplitude is unity in each panel. \label{fig:examplecylinder}](examplefunctions.png) + + +## N-body simulation + +Our design includes a wide choice of run-time summary diagnostics, +phase-space output formats, dynamically loadable user libraries, and +easy extensibility. Stand-alone routines include the EOF and mSSA +methods described above, and the modular software architecture of +EXP enables users to easily build and maintain extensions. The `EXP` +code base is described in published papers [@Petersen:22; @Weinberg:23] +and has been used, enhanced, and rigorously tested for nearly two +decades. + + +The design and implementation of the N-body tools allows for execution +on a wide variety of hardware, from personal laptops to high +performance computing centers, with communication between processes +handled by MPI [@mpi41] and GPU implementations in CUDA [@cuda]. Owing +to the linear scaling of computational effort with N and the novel +GPU implementation, the N-body +methods in `EXP` deliver performance in collisionless N-body simulations +previously only accessible with large dedicated CPU clusters. + +The flexible N-body software design allows users to write their own +modules for on-the-fly execution during N-body integration. Modules +enable powerful and intricate dynamical experiments in N-body +simulations, further reducing the gap between numerical simulations +and analytic dynamics. The package ships with several examples, +including imposed external potentials, as well as a basic example that +can be extended by users. + +## Using pyEXP to represent simulations + +`pyEXP` provides an interface to many of the classes in the `EXP` C++ +library, allowing for both the generation of all bases listed in the +table above as well as coefficients for an input data set. Each of +these tools are Python classes that accept `numpy` [@numpy] arrays for +immediate interoperability with `matplotlib` [@matplotlib] and +Astropy. We include a verified set of stand-alone routines that read +phase-space files from many major cosmological tree codes and produce +BFE-based analyses. The code suite includes adapters for reading and +writing phase space for many of the widely used cosmology codes, with +a base class for developing new ones. There are multiple ways to use +the versatile and modular tools in `pyEXP`, and we anticipate +pipelines that we have not yet imagined. + + +## Using pyEXP to analyze time series + +The `EXP` library includes multiple time series analysis tools, +documented in the manual. Here, we briefly highlight one technique +that we have already used in published work: mSSA [@Weinberg:21; +@Johnson:23]. Beginning with coefficient series from the previous +tools, mSSA summarizes signals _in time_ that describes dynamically +correlated responses and patterns. Essentially, this is BFE in time +and space. These temporal and spatial patterns allow users to better +identify dynamical mechanisms and enable intercomparisons and +filtering for features in simulation suites; e.g. computing the +fraction galaxies with grand design structure or hosting +bars. Random-matrix techniques for singular-value decomposition ensure +that analyses of large data sets is possible. All mSSA decompositions +are saved in HDF5 format for reuse. + +# Acknowledgements + +We acknowledge the ongoing support of the [B-BFE +collaboration](https://b-bfe.org). We also acknowledge the support of +the Center for Computational Astrophysics (CCA). The CCA is part of +the Flatiron Institute, funded by the Simons Foundation. We thank +Robert Blackwell for invaluable help with HPC best practices. + + +# References + diff --git a/Paper/paper/paper.pdf b/Paper/paper/paper.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1f8072ec72e69ab4af5c85d18482b255a54432ef GIT binary patch literal 358193 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
8`Xz&}IkHzgwSYmu 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%0AUWofk$TakGvKxkPQ*F9 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�$m~speSbXf+g&ge{kePZtM%C&8}lI$Bs` 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 D8K}jD literal 0 HcmV?d00001 From 6825d70bed149663d0e3b8738bd23c5e28e9f9fc Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 2 Jul 2024 16:54:25 -0700 Subject: [PATCH 130/167] A draft CONTRIBUTING document for the EXP source tree ported from the readthedocs version --- CONTRIBUTING.md | 240 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..3e5c6ac52 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,240 @@ +# Contributing to EXP + +There are many ways to contribute to EXP. Here are some of them: + +- Blog about EXP. Cite the EXP published papers. Tell the world how + you're using EXP. This will help newcomers with more examples and + will help the EXP project to increase its visibility. +- Report bugs and request features in the [issue + tracker](https://github.com/EXP-code/EXP/issues), trying to follow + the guidelines detailed in **Reporting bugs** below. +- Submit new examples to the [EXP examples + repo](https://github.com/EXP-code/EXP-examples) or the [pyEXP + examples repo](https://github.com/EXP-code/pyEXP-examples). + +# Stable and development branches + +Our current branch policy changed as of May 1, 2023. Rather than a +`main` branch for a stable EXP/pyEXP release with development taking +place on the `devel` branch, the only official EXP branch will be +`main`. All development will take place through a [pull +request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) +to `main`. All other branches are considered to be temporary. The +current stable release will be tagged in the GitHub release menu. The +HEAD of `main` will contain the latest new features and fixes. + +# Reporting bugs + +Well-written bug reports are very helpful, so keep in mind the following +guidelines when you're going to report a new bug. + +- check the [FAQ](https://exp-docs.readthedocs.io) first to see if + your issue is addressed in a well-known question +- check the [open issues](https://github.com/EXP-code/EXP/issues) + to see if the issue has already been reported. If it has, don't + dismiss the report, but check the ticket history and comments. If + you have additional useful information, please leave a comment, or + consider sending a pull request with a fix. +- write **complete, reproducible, specific bug reports**. The smaller + the test case, the better. Remember that other developers won't + have your project to reproduce the bug, so please include all + relevant files required to reproduce it. +- the most awesome way to provide a complete reproducible example is + to send a pull request which adds a failing test case to the EXP + testing suite. This is helpful even if you don't have an + intention to fix the issue yourselves. +- include the output of `exp -v` so developers working on your bug + know exactly which version and platform it occurred on, which is + often very helpful for reproducing it, or knowing if it was already + fixed. + +# Contributing to code development + +We are now using the [pull +request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) +method for all EXP development, in including the code authors and +maintainers. In essence, a pull request is code patch that allows us +to easily review, test, and discuss the proposed change. The better a +patch is written, the higher the chances that it'll get accepted and +the sooner it will be merged. + +Well-written patches should: + +- contain the minimum amount of code required for the specific change. + Small patches are easier to review and merge. So, if you're doing + more than one change (or bug fix), please consider submitting one + patch per change. Do not collapse multiple changes into a single + patch. For big changes consider using a patch queue. +- pass all EXP basic example tests. See **Running tests** below. +- if you're contributing a feature, especially one that changes a + public (documented) API, please include the documentation changes + in the same patch. See **Documentation strategy** below. + +# Submitting patches + +The best way to submit a patch is to issue a [pull +request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) +on GitHub. The work flow for this is: + +1. Fork the [EXP-code/EXP](https://github.com/EXP-code/EXP) repo in GitHub +2. Create a branch with the proposed change +3. Compile and test your changes +4. Once you are satisfied, create the [pull +request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) +on the GitHub EXP code repo + +Remember to explain what was fixed or the new functionality (what it +is, why it's needed, etc). The more info you include, the easier will +be for core developers to understand and accept your patch. + +You can also discuss the new functionality (or bug fix) before creating +the patch, but it's always good to have a patch ready to illustrate +your arguments and show that you have put some additional thought into +the subject. A good starting point is to send a pull request on GitHub. +It can be simple enough to illustrate your idea, and leave +documentation/tests for later, after the idea has been validated and +proven useful. All functionality (including new features and bug fixes) +must include a test case to check that it works as expected, so please +include tests for your patches if you want them to get accepted sooner. + +There might be an existing pull request for the problem you'd like to +solve, so please do take a look at the existing pull request list. For +example, a pull request might be a good start, but changes are +requested by EXP maintainers, and the original pull request author +hasn't had time to address them. In this case consider picking up this +pull request: open a new pull request with all commits from the +original pull request, as well as additional changes to address the +raised issues. Doing so helps us a lot; it is not considered rude as +long as the original author is acknowledged by keeping his/her +commits. + +You can pull an existing pull request to a local branch by running +`git fetch https://github.com/EXP-code/code pull/$PR_NUMBER/head:$BRANCH_NAME_TO_CREATE` +(replace `$PR_NUMBER` with an ID of the pull request, and +`$BRANCH_NAME_TO_CREATE` with a name of the branch you want to create +locally). See also: +. + +When writing GitHub pull requests, try to keep titles short but +descriptive. Complete titles make it easy to skim through the issue +tracker. + +Finally, we all appreciate improving the code's readability and though +formatting and improved comments. But please, try to keep aesthetic +changes in separate commits from functional changes. This will make pull +requests easier to review and more likely to get merged. + +## Adding a Git Commit + +All code updates are done by pull request (PR) as described above. +The GitHub EXP-code includes Continuous Integration (CI) support which +can get very chatty and take up unnecessary compute resources. When +your changes only affect documentation (e.g. docstring additions or +software comments) or provide code snippets that you know still need +work, you may add a [no ci] in your commit message. For example: bash + +``` +> git commit -m "Updated some docstrings [no ci]" +``` + +When this commit is pushed out to your fork and branch associated with a +pull request, all CI will be skipped. + +# Documentation strategy + +EXP and pyEXP has three types of documentation: + +1. Inline Doxygen comments in C/C++ class headers. If you have not + used [Doxgyen](http://doxygen.org) in the past, we recommend that + you simply copy the style in existing headers in the `include` + directory in the EXP source tree. In short, Doxygen uses stylized + comments to extract documentation. Lines that start with `//!` or + blocks that start with `/**` and end with `*/` will end up + describing the immediately following class or member function. +2. Python wrapper code has embedded Python docstrings. For some + examples, check the C++ code in the `pyEXP`. +3. The ReadTheDocs manual that you are currently reading is designed to + provide an overview and tutorial for using EXP/pyEXP. You can fork + and issue pull requests against the `EXP-code/docs` repo just for + the EXP source code as described in + **Submitting-patches** above. + +## Contributing Documentation + +Our goal is a set of consistent documentation that provides users and +developers a shallow learning curve for using and adding to EXP. For end +users, we strive to write simple step-by-step instructions for common +tasks and give a clear description of the features and capabilities of +the EXP software. + +However, it *is* hard to know what everyone needs. As you work with this +package, we would love help with this and encourage all of your +contributions. + +This section is an attempt to provide some stylistic guidelines for +documentation writers and developers. For EXP and the documentation +overall, we hope that the existing documentation is a good starting +point. For internal Python documentation in pyEXP, we are trying to +follow the now familiar style of code documentation of both the Astropy +and Numpy projects. In particular, we have adopted the Numpy style +guidelines +[here](https://numpy.org/doc/devdocs/docs/howto_document.html). + +## Adding pyEXP docstrings + +pyEXP is a set of bindings implemented by +[pybind11](https://pybind11.readthedocs.io/) and a small amount of +native Python code. It has a full set of docstrings to provide users +with easy access to interactive tips and call signatures. If you would +like to contribute new code, please try to follow the following +guidelines: + +- Please write docstrings for all public classes, methods, and + functions +- Please consult the + [numpydoc](https://numpy.org/doc/devdocs/docs/howto_document.html) + format from the link above whenever possible +- We would like to encourage references to internal EXP project links + in docstrings when useful. This is still a work in progress. +- Examples and/or tutorials are strongly encouraged for typical + use-cases of a particular module or class. These can be included in + the [EXP examples + repository](https://github.com/EXP-code/EXP-examples) or the [pyEXP + examples repository](https://github.com/EXP-code/pyEXP-examples)) as + appropriate. + +# Writing examples + +We strongly encourage to contribute interesting examples and workflows +to one of the two example repositories: + +1. Use the [EXP examples + repo](https://github.com/EXP-code/EXP-examples) to illustrate + simulations. Check the existing ones for guidance. It's best if + your examples contain sample configuration files and phase-space + files, or possibly instructions for computing the phase-space files + if they are large. +2. Use the [pyEXP examples + repo](https://github.com/EXP-code/pyEXP-examples). Either documented + Python or Jupyter notebooks are ideal. + +# Running tests + +Please check any bug fixes and proposed new functionality works with +existing examples. Here are some suggested guidelines for EXP and pyEXP +changes, respectively: + +1. For EXP, clone the [EXP examples + repo](https://github.com/EXP-code/EXP-examples) to test changes to + the EXP simulation code. At the very least, please try the `Nbody` + example, but please try as many as you can to make sure that your + change will not break an existing use case for someone else. If your + change introduces a feature, please try to devise and contribute + example that demonstrates the new feature. That way, your new + feature will be tested by all future proposed changes and will help + others understand how to use your new feature. +2. The drill for pyEXP is similar. Clone [pyEXP examples + repo](https://github.com/EXP-code/pyEXP-examples) to test changes to + the pyEXP N-body analysis code. There are many work flows here, we + don't expect anyone to try all of them. But please use your best + judgment to try those affected by your proposed change. From c7948acb021a85743703631c2510019ada63aa2c Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Wed, 3 Jul 2024 21:55:40 -0700 Subject: [PATCH 131/167] Basis expects a node with the 'parameters' key for parsing [no ci] --- src/OutVel.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/OutVel.cc b/src/OutVel.cc index 78225f9d2..a10a732ce 100644 --- a/src/OutVel.cc +++ b/src/OutVel.cc @@ -62,7 +62,10 @@ OutVel::OutVel(const YAML::Node& conf) : Output(conf) // Create the basis // - basis = std::make_shared(conf); + YAML::Node node; + node["parameters"] = conf; + + basis = std::make_shared(node); // Create the coefficient container based on the dimensionality. // Currently, these are spherical and polar grids. From b70207ff314ffe54110ef4cbbb075941ee7f1fdd Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 4 Jul 2024 13:15:58 -0700 Subject: [PATCH 132/167] Fixes for HDF5 libraries with CMake --- utils/ICs/CMakeLists.txt | 3 ++- utils/PhaseSpace/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/utils/ICs/CMakeLists.txt b/utils/ICs/CMakeLists.txt index b0ec76de6..06cbb8b18 100644 --- a/utils/ICs/CMakeLists.txt +++ b/utils/ICs/CMakeLists.txt @@ -5,7 +5,8 @@ set(bin_PROGRAMS shrinkics gensph gendisk gendisk2d gsphere pstmod cubeics zangics slabics) set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX yaml-cpp exputil - ${VTK_LIBRARIES} ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES}) + ${VTK_LIBRARIES} ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES} + ${HDF5_CXX_LIBRARIES}) if(ENABLE_CUDA) list(APPEND common_LINKLIB CUDA::cudart CUDA::nvToolsExt) diff --git a/utils/PhaseSpace/CMakeLists.txt b/utils/PhaseSpace/CMakeLists.txt index 3076d0b12..71d1c2141 100644 --- a/utils/PhaseSpace/CMakeLists.txt +++ b/utils/PhaseSpace/CMakeLists.txt @@ -14,7 +14,7 @@ if(HAVE_XDR) endif() set(common_LINKLIB OpenMP::OpenMP_CXX MPI::MPI_CXX yaml-cpp expui - exputil ${VTK_LIBRARIES} ${HDF5_LIBRARIES}) + exputil ${VTK_LIBRARIES} ${HDF5_LIBRARIES} ${HDF5_CXX_LIBRARIES}) if(PNG_FOUND) list(APPEND common_LINKLIB PNG::PNG) From 77a1ba953f7adf2d193b521ff9a9f7f1ad841ed3 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 4 Jul 2024 14:50:28 -0700 Subject: [PATCH 133/167] Fixes for velocity field analysis --- expui/CoefContainer.H | 13 +++ expui/CoefContainer.cc | 182 ++++++++++++++++++++++++++++++++ expui/FieldBasis.cc | 8 +- utils/ICs/CMakeLists.txt | 3 +- utils/PhaseSpace/CMakeLists.txt | 2 +- 5 files changed, 202 insertions(+), 6 deletions(-) diff --git a/expui/CoefContainer.H b/expui/CoefContainer.H index 66a3c9951..0c8bf39d0 100644 --- a/expui/CoefContainer.H +++ b/expui/CoefContainer.H @@ -91,6 +91,7 @@ namespace MSSA void pack_sphere(); void unpack_sphere(); void restore_background_sphere(); + //@} //@{ //! Cylindrical coefficients @@ -99,6 +100,18 @@ namespace MSSA void restore_background_cylinder(); //@} + //@{ + //! Spherical field coefficients + void pack_sphfld(); + void unpack_sphfld(); + //@} + + //@{ + //! Cylindrical coefficients + void pack_cylfld(); + void unpack_cylfld(); + //@} + //@{ //! Slab coefficients void pack_slab(); diff --git a/expui/CoefContainer.cc b/expui/CoefContainer.cc index 03af71d92..a0ec98abe 100644 --- a/expui/CoefContainer.cc +++ b/expui/CoefContainer.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -64,6 +65,12 @@ namespace MSSA else if (dynamic_cast(coefs.get())) { unpack_table(); } + else if (dynamic_cast(coefs.get())) { + unpack_sphfld(); + } + else if (dynamic_cast(coefs.get())) { + unpack_cylfld(); + } else { throw std::runtime_error("CoefDB::unpack_channels(): can not reflect coefficient type"); } @@ -79,6 +86,10 @@ namespace MSSA restore_background_cylinder(); else if (dynamic_cast(coefs.get())) { } // Do nothing + else if (dynamic_cast(coefs.get())) + { } // Do nothing + else if (dynamic_cast(coefs.get())) + { } // Do nothing else { throw std::runtime_error("CoefDB::background(): can not reflect coefficient type"); } @@ -86,6 +97,8 @@ namespace MSSA void CoefDB::pack_channels() { + std::cout << "Type: " << typeid(*coefs.get()).name() << std::endl; + if (dynamic_cast(coefs.get())) pack_sphere(); else if (dynamic_cast(coefs.get())) @@ -96,6 +109,10 @@ namespace MSSA pack_cube(); else if (dynamic_cast(coefs.get())) pack_table(); + else if (dynamic_cast(coefs.get())) + pack_sphfld(); + else if (dynamic_cast(coefs.get())) + pack_cylfld(); else { throw std::runtime_error("CoefDB::pack_channels(): can not reflect coefficient type"); } @@ -201,6 +218,85 @@ namespace MSSA // END time loop } + void CoefDB::pack_cylfld() + { + auto cur = dynamic_cast(coefs.get()); + + times = cur->Times(); + complexKey = true; + + auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + + int nfld = cf->nfld; + int mmax = cf->mmax; + int nmax = cf->nmax; + int ntimes = times.size(); + + // Promote desired keys into c/s pairs + // + keys.clear(); + for (auto v : keys0) { + // Sanity check rank + // + if (v.size() != 3) { + std::ostringstream sout; + sout << "CoefDB::pack_cylfld: key vector should have rank 3; " + << "found rank " << v.size() << " instead"; + throw std::runtime_error(sout.str()); + } + // Sanity check values + // + else if (v[0] >= 0 and v[0] < nfld and + v[1] >= 0 and v[1] <= mmax and + v[2] >= 0 and v[2] <= nmax ) { + auto c = v, s = v; + c.push_back(0); + s.push_back(1); + keys.push_back(c); + if (v[0]) keys.push_back(s); + } else { + throw std::runtime_error("CoefDB::pack_cylfld: key is out of bounds"); + } + } + + bkeys.clear(); // No background fields + + // Only pack the keys in the list + // + for (auto k : keys) { + data[k].resize(ntimes); + } + + for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { + if (k[3]==0) + data[k][t] = (*cf->coefs)(k[0], k[1], k[2]).real(); + else + data[k][t] = (*cf->coefs)(k[0], k[1], k[3]).imag(); + } + } + } + + void CoefDB::unpack_cylfld() + { + for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + + for (auto k : keys0) { + auto c = k, s = k; + c.push_back(0); s.push_back(1); + + int f = k[1], m = k[1], n = k[2]; + + if (m==0) (*cf->coefs)(f, m, n) = {data[c][i], 0.0}; + else (*cf->coefs)(f, m, n) = {data[c][i], data[s][i]}; + } + // END key loop + } + // END time loop + } + void CoefDB::pack_sphere() { auto cur = dynamic_cast(coefs.get()); @@ -309,6 +405,92 @@ namespace MSSA } // END time loop } + + void CoefDB::pack_sphfld() + { + auto cur = dynamic_cast(coefs.get()); + + times = cur->Times(); + complexKey = true; + + auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + + int nfld = cf->nfld; + int lmax = cf->lmax; + int nmax = cf->nmax; + int ntimes = times.size(); + + // Make extended key list + // + keys.clear(); + for (auto k : keys0) { + // Sanity check rank + // + if (k.size() != 4) { + std::ostringstream sout; + sout << "CoefDB::pack_sphfld: key vector should have rank 4; " + << "found rank " << k.size() << " instead"; + throw std::runtime_error(sout.str()); + } + // Sanity check values + // + else if (k[0] < nfld and k[0] >= 0 and + k[1] <= lmax and k[1] >= 0 and + k[2] <= k[1] and k[2] >= 0 and + k[3] <= nmax and k[3] >= 0 ) { + + auto v = k; + v.push_back(0); + keys.push_back(v); + data[v].resize(ntimes); + + if (k[2]>0) { + v[4] = 1; + keys.push_back(v); + data[v].resize(ntimes); + } + } + else { + throw std::runtime_error("CoefDB::pack_sphfld: key is out of bounds"); + } + } + + bkeys.clear(); // No background for field quantities + + auto I = [](const Key& k) { return k[1]*(k[1]+1)/2 + k[2]; }; + + for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { + auto c = (*cf->coefs)(k[0], I(k), k[3]); + data[k][t] = c.real(); + if (k[4]) data[k][t] = c.imag(); + } + } + } + + void CoefDB::unpack_sphfld() + { + auto I = [](const Key& k) { return k[1]*(k[1]+1)/2 + k[2]; }; + + for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + + for (auto k : keys0) { + auto c = k, s = k; + c.push_back(0); + s.push_back(1); + + int f = k[0], l = k[1], m = k[2], n = k[3]; + + if (m==0) (*cf->coefs)(f, I(k), n) = {data[c][i], 0.0 }; + else (*cf->coefs)(f, I(k), n) = {data[c][i], data[s][i]}; + } + // END key loop + } + // END time loop + } void CoefDB::pack_slab() { diff --git a/expui/FieldBasis.cc b/expui/FieldBasis.cc index eecdcc520..ae0a46278 100644 --- a/expui/FieldBasis.cc +++ b/expui/FieldBasis.cc @@ -304,15 +304,15 @@ namespace BasisClasses (*coefs[tid])(0, 0, 0) += mass*p(0); - for (int l=0, k=0; l<=lmax; l++) { - for (int m=0; m<=l; m++, k++) { + for (int l=0, lm=0; l<=lmax; l++) { + for (int m=0; m<=l; m++, lm++) { std::complex P = std::exp(I*(phi*m))*Ylm01(l, m)*plgndr(l, m, cth); for (int n=0; n Date: Thu, 4 Jul 2024 16:15:39 -0700 Subject: [PATCH 134/167] Fix HighFive type mismatch that was causing an exception in writing the mSSA cache --- expui/expMSSA.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/expMSSA.cc b/expui/expMSSA.cc index 0aca81d71..5adf38f57 100644 --- a/expui/expMSSA.cc +++ b/expui/expMSSA.cc @@ -1339,7 +1339,7 @@ namespace MSSA { // Save trend state // int trend = static_cast::type>(type); - file.createAttribute("trendType", HighFive::DataSpace::From(trend)).write(trend); + file.createAttribute("trendType", HighFive::DataSpace::From(trend)).write(trend); // Save the key list // From 3f8e317cf8b75486b95c311903d2a277e5858d5a Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 4 Jul 2024 22:52:03 -0700 Subject: [PATCH 135/167] Fix H5 type for correct cache writing --- expui/expMSSA.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/expMSSA.cc b/expui/expMSSA.cc index 3ff1c71c7..7a19c6f6d 100644 --- a/expui/expMSSA.cc +++ b/expui/expMSSA.cc @@ -1339,7 +1339,7 @@ namespace MSSA { // Save trend state // int trend = static_cast::type>(type); - file.createAttribute("trendType", HighFive::DataSpace::From(trend)).write(trend); + file.createAttribute("trendType", HighFive::DataSpace::From(trend)).write(trend); // Save the key list // From aef82b5ac3539d0c7f1482d791a93bc4fae7ab34 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Thu, 4 Jul 2024 22:52:36 -0700 Subject: [PATCH 136/167] Implement proper coefficient parsing for velocity field basis --- expui/CoefContainer.cc | 85 ++++++++++++++++++++++++++++-------------- expui/FieldBasis.cc | 7 ++++ 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/expui/CoefContainer.cc b/expui/CoefContainer.cc index a0ec98abe..d5deb13da 100644 --- a/expui/CoefContainer.cc +++ b/expui/CoefContainer.cc @@ -72,7 +72,7 @@ namespace MSSA unpack_cylfld(); } else { - throw std::runtime_error("CoefDB::unpack_channels(): can not reflect coefficient type"); + throw std::runtime_error(std::string("CoefDB::unpack_channels(): can not reflect coefficient type=") + typeid(*coefs.get()).name()); } return coefs; @@ -91,14 +91,12 @@ namespace MSSA else if (dynamic_cast(coefs.get())) { } // Do nothing else { - throw std::runtime_error("CoefDB::background(): can not reflect coefficient type"); + throw std::runtime_error(std::string("CoefDB::background(): can not reflect coefficient type=") + typeid(*coefs.get()).name()); } } void CoefDB::pack_channels() { - std::cout << "Type: " << typeid(*coefs.get()).name() << std::endl; - if (dynamic_cast(coefs.get())) pack_sphere(); else if (dynamic_cast(coefs.get())) @@ -114,7 +112,7 @@ namespace MSSA else if (dynamic_cast(coefs.get())) pack_cylfld(); else { - throw std::runtime_error("CoefDB::pack_channels(): can not reflect coefficient type"); + throw std::runtime_error(std::string("CoefDB::pack_channels(): can not reflect coefficient type=") + typeid(*coefs.get()).name()); } } @@ -125,7 +123,8 @@ namespace MSSA times = cur->Times(); complexKey = true; - auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + auto cf = dynamic_cast + ( cur->getCoefStruct(times[0]).get() ); int mmax = cf->mmax; int nmax = cf->nmax; @@ -182,7 +181,9 @@ namespace MSSA } for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + cf = dynamic_cast + ( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { if (k[2]==0) data[k][t] = (*cf->coefs)(k[0], k[1]).real(); @@ -202,7 +203,8 @@ namespace MSSA void CoefDB::unpack_cylinder() { for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + auto cf = dynamic_cast + ( coefs->getCoefStruct(times[i]).get() ); for (auto k : keys0) { auto c = k, s = k; @@ -225,7 +227,8 @@ namespace MSSA times = cur->Times(); complexKey = true; - auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + auto cf = dynamic_cast + ( cur->getCoefStruct(times[0]).get() ); int nfld = cf->nfld; int mmax = cf->mmax; @@ -268,7 +271,9 @@ namespace MSSA } for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + cf = dynamic_cast + ( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { if (k[3]==0) data[k][t] = (*cf->coefs)(k[0], k[1], k[2]).real(); @@ -281,7 +286,8 @@ namespace MSSA void CoefDB::unpack_cylfld() { for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + auto cf = dynamic_cast + ( coefs->getCoefStruct(times[i]).get() ); for (auto k : keys0) { auto c = k, s = k; @@ -304,7 +310,8 @@ namespace MSSA times = cur->Times(); complexKey = true; - auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + auto cf = dynamic_cast + ( cur->getCoefStruct(times[0]).get() ); int lmax = cf->lmax; int nmax = cf->nmax; @@ -368,7 +375,9 @@ namespace MSSA auto I = [](const Key& k) { return k[0]*(k[0]+1)/2 + k[1]; }; for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + cf = dynamic_cast + ( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { auto c = (*cf->coefs)(I(k), k[2]); data[k][t] = c.real(); @@ -389,7 +398,8 @@ namespace MSSA for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + auto cf = dynamic_cast + ( coefs->getCoefStruct(times[i]).get() ); for (auto k : keys0) { auto c = k, s = k; @@ -413,7 +423,8 @@ namespace MSSA times = cur->Times(); complexKey = true; - auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + auto cf = dynamic_cast + ( cur->getCoefStruct(times[0]).get() ); int nfld = cf->nfld; int lmax = cf->lmax; @@ -437,7 +448,7 @@ namespace MSSA else if (k[0] < nfld and k[0] >= 0 and k[1] <= lmax and k[1] >= 0 and k[2] <= k[1] and k[2] >= 0 and - k[3] <= nmax and k[3] >= 0 ) { + k[3] < nmax and k[3] >= 0 ) { auto v = k; v.push_back(0); @@ -460,7 +471,10 @@ namespace MSSA auto I = [](const Key& k) { return k[1]*(k[1]+1)/2 + k[2]; }; for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + + cf = dynamic_cast + ( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { auto c = (*cf->coefs)(k[0], I(k), k[3]); data[k][t] = c.real(); @@ -475,10 +489,13 @@ namespace MSSA for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + auto cf = dynamic_cast + ( coefs->getCoefStruct(times[i]).get() ); for (auto k : keys0) { + auto c = k, s = k; + c.push_back(0); s.push_back(1); @@ -499,7 +516,8 @@ namespace MSSA times = cur->Times(); complexKey = true; - auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + auto cf = dynamic_cast + ( cur->getCoefStruct(times[0]).get() ); int nmaxx = cf->nmaxx; int nmaxy = cf->nmaxy; @@ -558,7 +576,9 @@ namespace MSSA } for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + cf = dynamic_cast + ( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { auto c = (*cf->coefs)(k[0], k[1], k[2]); if (k[3]) data[k][t] = c.imag(); @@ -580,7 +600,8 @@ namespace MSSA times = cur->Times(); complexKey = true; - auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + auto cf = dynamic_cast + ( cur->getCoefStruct(times[0]).get() ); int nmaxx = cf->nmaxx; int nmaxy = cf->nmaxy; @@ -639,7 +660,9 @@ namespace MSSA } for (int t=0; t( cur->getCoefStruct(times[t]).get() ); + cf = dynamic_cast + ( cur->getCoefStruct(times[t]).get() ); + for (auto k : keys) { auto c = (*cf->coefs)(k[0], k[1], k[2]); if (k[3]) data[k][t] = c.imag(); @@ -658,7 +681,8 @@ namespace MSSA { for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + auto cf = dynamic_cast + ( coefs->getCoefStruct(times[i]).get() ); for (auto k : keys0) { auto c = k, s = k; @@ -676,7 +700,8 @@ namespace MSSA { for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + auto cf = dynamic_cast + ( coefs->getCoefStruct(times[i]).get() ); for (auto k : keys0) { auto c = k, s = k; @@ -697,7 +722,8 @@ namespace MSSA times = cur->Times(); complexKey = false; - auto cf = dynamic_cast( cur->getCoefStruct(times[0]).get() ); + auto cf = dynamic_cast + ( cur->getCoefStruct(times[0]).get() ); int cols = cf->cols; int ntimes = times.size(); @@ -734,7 +760,9 @@ namespace MSSA for (unsigned c=0; c( cur->getCoefStruct(times[t]).get() ); + cf = dynamic_cast + ( cur->getCoefStruct(times[t]).get() ); + data[key][t] = (*cf->coefs)(c).real(); } } @@ -744,7 +772,8 @@ namespace MSSA { for (int i=0; i( coefs->getCoefStruct(times[i]).get() ); + auto cf = dynamic_cast + ( coefs->getCoefStruct(times[i]).get() ); int cols = cf->cols; @@ -862,6 +891,7 @@ namespace MSSA auto I = [](const Key& k) { return k[0]*(k[0]+1)/2 + k[1]; }; for (int t=0; t (cur->getCoefStruct(times[t]).get()); @@ -882,6 +912,7 @@ namespace MSSA auto cur = dynamic_cast(coefs.get()); for (int t=0; t (cur->getCoefStruct(times[t]).get()); diff --git a/expui/FieldBasis.cc b/expui/FieldBasis.cc index ae0a46278..35a0089a5 100644 --- a/expui/FieldBasis.cc +++ b/expui/FieldBasis.cc @@ -290,9 +290,13 @@ namespace BasisClasses (*coefs[tid])(0, 0, 0) += mass*p(0); for (int m=0; m<=lmax; m++) { + std::complex P = std::exp(I*(phi*m)); + for (int n=0; n P = std::exp(I*(phi*m))*Ylm01(l, m)*plgndr(l, m, cth); for (int n=0; n Date: Fri, 5 Jul 2024 14:54:55 -0700 Subject: [PATCH 137/167] Parameter name changes for consistency with main N-body code --- expui/BasisFactory.cc | 2 + expui/FieldBasis.H | 8 ++-- expui/FieldBasis.cc | 103 ++++++++++++++++++++++++++++-------------- src/OutVel.H | 6 +-- src/OutVel.cc | 8 ++-- 5 files changed, 82 insertions(+), 45 deletions(-) diff --git a/expui/BasisFactory.cc b/expui/BasisFactory.cc index 5b574a8e2..ee6d53d4d 100644 --- a/expui/BasisFactory.cc +++ b/expui/BasisFactory.cc @@ -84,6 +84,8 @@ namespace BasisClasses // try { conf = node["parameters"]; + YAML::Emitter y; y << conf; + std::cout << "YAML in Basis: " << y.c_str() << std::endl; } catch (YAML::Exception & error) { if (myid==0) std::cout << "Error parsing Basis parameters for <" diff --git a/expui/FieldBasis.H b/expui/FieldBasis.H index d405c3179..25ef5dbe3 100644 --- a/expui/FieldBasis.H +++ b/expui/FieldBasis.H @@ -53,9 +53,9 @@ namespace BasisClasses //@{ //! Parameters - std::string model, filename; + std::string model, modelname; int lmax, nmax; - double rmin, rmax, ascl, scale, delta; + double rmin, rmax, ascl, rmapping, delta; //@} //@{ @@ -116,8 +116,8 @@ namespace BasisClasses //! Register phase-space functionoid void addPSFunction(PSFunction func, std::vector& labels); - //! Prescaling factor - void set_scale(const double scl) { scale = scl; } + //! Coordinate mapping factor + void set_scale(const double scl) { rmapping = scl; } //! Zero out coefficients to prepare for a new expansion void reset_coefs(void); diff --git a/expui/FieldBasis.cc b/expui/FieldBasis.cc index 35a0089a5..75c6d0a23 100644 --- a/expui/FieldBasis.cc +++ b/expui/FieldBasis.cc @@ -11,16 +11,23 @@ #include #endif -extern double Ylm01(int ll, int mm); extern double plgndr(int l, int m, double x); +static double Ylm(int l, int m) +{ + m = abs(m); + return sqrt( (2.0*l+1)/(4.0*M_PI) ) * + exp(0.5*(lgamma(1.0+l-m) - lgamma(1.0+l+m))); +} + + namespace BasisClasses { const std::set FieldBasis::valid_keys = { - "filename", + "modelname", "dof", - "scale", + "rmapping", "rmin", "rmax", "ascl", @@ -60,18 +67,18 @@ namespace BasisClasses // void FieldBasis::configure() { - nfld = 2; // Weight and density fields by default - lmax = 4; - nmax = 10; - rmin = 1.0e-4; - rmax = 2.0; - ascl = 0.01; - delta = 0.005; - scale = 0.05; - dof = 3; - model = "file"; - name = "field"; - filename = "SLGridSph.model"; + nfld = 2; // Weight and density fields by default + lmax = 4; + nmax = 10; + rmin = 1.0e-4; + rmax = 2.0; + ascl = 0.01; + delta = 0.005; + rmapping = 0.05; + dof = 3; + model = "file"; + name = "field"; + modelname = "SLGridSph.model"; initialize(); @@ -111,8 +118,8 @@ namespace BasisClasses // if (model == "file") { std::vector r, d; - std::ifstream in(filename); - if (not in) throw std::runtime_error("Error opening file: " + filename); + std::ifstream in(modelname); + if (not in) throw std::runtime_error("Error opening file: " + modelname); std::string line; while (std::getline(in, line)) { @@ -156,13 +163,28 @@ namespace BasisClasses // Generate the orthogonal function instance // - ortho = std::make_shared(nmax, densfunc, rmin, rmax, scale, dof); + ortho = std::make_shared(nmax, densfunc, rmin, rmax, rmapping, dof); // Initialize fieldlabels // fieldLabels.clear(); fieldLabels.push_back("weight"); fieldLabels.push_back("density"); + + // Debug + // + if (true) { + auto tst = ortho->testOrtho(); + double worst = 0.0; + for (int i=0; i(worst, fabs(1.0 - tst(i, j))); + else worst = std::max(worst, fabs(tst(i, j))); + } + } + if (myid==0) + std::cout << "FieldBasis: ortho test worst <" << worst << ">" << std::endl; + } } void FieldBasis::allocateStore() @@ -187,17 +209,17 @@ namespace BasisClasses // Assign values from YAML // try { - if (conf["filename"]) filename = conf["filename"].as(); - if (conf["nfld" ]) nfld = conf["nfld" ].as(); - if (conf["lmax" ]) lmax = conf["lmax" ].as(); - if (conf["nmax" ]) nmax = conf["nmax" ].as(); - if (conf["dof" ]) dof = conf["dof" ].as(); - if (conf["rmin" ]) rmin = conf["rmin" ].as(); - if (conf["rmax" ]) rmax = conf["rmax" ].as(); - if (conf["ascl" ]) ascl = conf["ascl" ].as(); - if (conf["delta" ]) delta = conf["delta" ].as(); - if (conf["scale" ]) scale = conf["scale" ].as(); - if (conf["model" ]) model = conf["model" ].as(); + if (conf["modelname"]) modelname = conf["modelname"].as(); + if (conf["model" ]) model = conf["model" ].as(); + if (conf["nfld" ]) nfld = conf["nfld" ].as(); + if (conf["lmax" ]) lmax = conf["lmax" ].as(); + if (conf["nmax" ]) nmax = conf["nmax" ].as(); + if (conf["dof" ]) dof = conf["dof" ].as(); + if (conf["rmin" ]) rmin = conf["rmin" ].as(); + if (conf["rmax" ]) rmax = conf["rmax" ].as(); + if (conf["ascl" ]) ascl = conf["ascl" ].as(); + if (conf["delta" ]) delta = conf["delta" ].as(); + if (conf["rmapping" ]) rmapping = conf["rmapping" ].as(); } catch (YAML::Exception & error) { if (myid==0) std::cout << "Error parsing parameters in FieldBasis: " @@ -291,7 +313,7 @@ namespace BasisClasses for (int m=0; m<=lmax; m++) { - std::complex P = std::exp(I*(phi*m)); + std::complex P = std::exp(-I*(phi*m)); for (int n=0; n P = - std::exp(I*(phi*m))*Ylm01(l, m)*plgndr(l, m, cth); - + std::exp(-I*(phi*m))*Ylm(l, m)*plgndr(l, m, cth) * s; + + s *= -1.0; // Flip sign for next evaluation + for (int n=0; n0 ? 2.0 : 1.0; for (int n=0; n0 ? 2.0 : 1.0; for (int n=0; n::max(); Component *tcomp; CoefClasses::CoefsPtr coefs; diff --git a/src/OutVel.cc b/src/OutVel.cc index a10a732ce..cd877e4d3 100644 --- a/src/OutVel.cc +++ b/src/OutVel.cc @@ -9,12 +9,12 @@ const std::set OutVel::valid_keys = { - "filename", + "modelname", "nint", "nintsub", "name", "dof", - "scale", + "rmapping", "rmin", "rmax", "ascl", @@ -97,7 +97,7 @@ void OutVel::initialize() model = conf["model"].as(); else { std::string message = "OutVel: no model specified. Please specify " - "either 'file' with the model 'filename' or 'expon' for the\n" + "either 'file' with the model 'modelname' or 'expon' for the\n" "exponential disk model (i.e. Laguerre polynomials)"; throw std::runtime_error(message); } @@ -119,7 +119,7 @@ void OutVel::initialize() throw std::runtime_error(message); } - if (conf["filename"]) filename = conf["filename"].as(); + if (conf["modelname"]) modelname = conf["modelname"].as(); } catch (YAML::Exception & error) { From eb998ddac5ff7727f306b6e0dc56959cfeb606bd Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Fri, 5 Jul 2024 15:25:55 -0700 Subject: [PATCH 138/167] Removed some debugging cruft --- expui/BasisFactory.cc | 2 -- expui/FieldBasis.cc | 4 ---- 2 files changed, 6 deletions(-) diff --git a/expui/BasisFactory.cc b/expui/BasisFactory.cc index ee6d53d4d..5b574a8e2 100644 --- a/expui/BasisFactory.cc +++ b/expui/BasisFactory.cc @@ -84,8 +84,6 @@ namespace BasisClasses // try { conf = node["parameters"]; - YAML::Emitter y; y << conf; - std::cout << "YAML in Basis: " << y.c_str() << std::endl; } catch (YAML::Exception & error) { if (myid==0) std::cout << "Error parsing Basis parameters for <" diff --git a/expui/FieldBasis.cc b/expui/FieldBasis.cc index 75c6d0a23..29adb26d9 100644 --- a/expui/FieldBasis.cc +++ b/expui/FieldBasis.cc @@ -755,10 +755,6 @@ namespace BasisClasses VelocityBasis::VelocityBasis(const YAML::Node& conf) : FieldBasis(conf, "VelocityBasis") { - YAML::Emitter y; y << conf; - std::cout << "YAML configuration in VelocityBasis: " - << y.c_str() << std::endl; - name = "velocity"; assignFunc(); } From 971f33d764a1be110e9fa460c51d3580978973a0 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 6 Jul 2024 11:19:33 -0700 Subject: [PATCH 139/167] Pre-release version bump --- CMakeLists.txt | 2 +- doc/exp.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 598bf4ad9..cd65e7a1a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21) # Needed for CUDA, MPI, and CTest features project( EXP - VERSION "7.7.30" + VERSION "7.7.99" HOMEPAGE_URL https://github.com/EXP-code/EXP LANGUAGES C CXX Fortran) diff --git a/doc/exp.cfg b/doc/exp.cfg index b7ccb4716..32f7444ff 100644 --- a/doc/exp.cfg +++ b/doc/exp.cfg @@ -48,7 +48,7 @@ PROJECT_NAME = EXP # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 7.7.30 +PROJECT_NUMBER = 7.7.99 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a From 1ccff70d7f980afd5f5a1daa3b666a9f8575a963 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 6 Jul 2024 15:33:29 -0700 Subject: [PATCH 140/167] Added a routine to dump the basis [no ci] --- exputil/OrthoFunction.cc | 27 +++++++++++++++++++++++++++ include/OrthoFunction.H | 3 +++ 2 files changed, 30 insertions(+) diff --git a/exputil/OrthoFunction.cc b/exputil/OrthoFunction.cc index 7a1badc20..e0bf4f1f9 100644 --- a/exputil/OrthoFunction.cc +++ b/exputil/OrthoFunction.cc @@ -1,3 +1,4 @@ +#include #include OrthoFunction::OrthoFunction @@ -93,6 +94,32 @@ Eigen::MatrixXd OrthoFunction::testOrtho() return ret; } +void OrthoFunction::dumpOrtho(const std::string& filename) +{ + std::ofstream fout(filename); + + if (fout) { + fout << "# OrthoFunction dump" << std::endl; + + const int number = 1000; + + for (int i=0; i Date: Sat, 6 Jul 2024 15:33:55 -0700 Subject: [PATCH 141/167] Added a factor to make the weight function normalized [no ci] --- expui/FieldBasis.cc | 48 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/expui/FieldBasis.cc b/expui/FieldBasis.cc index 29adb26d9..32533400d 100644 --- a/expui/FieldBasis.cc +++ b/expui/FieldBasis.cc @@ -182,8 +182,10 @@ namespace BasisClasses else worst = std::max(worst, fabs(tst(i, j))); } } - if (myid==0) - std::cout << "FieldBasis: ortho test worst <" << worst << ">" << std::endl; + if (myid==0) { + std::cout << "FieldBasis::orthoTest: worst=" << worst << std::endl; + ortho->dumpOrtho("fieldbasis_ortho.dump"); + } } } @@ -276,9 +278,33 @@ namespace BasisClasses if (dof==2) { auto p = dynamic_cast(c.get()); coefs[0] = p->coefs; + store[0] = p->store; + + // Sanity test dimensions + if (nfld!=p->nfld || lmax!=p->mmax || nmax!=p->nmax) { + std::ostringstream serr; + serr << "FieldBasis::set_coefs: dimension error! " + << " nfld [" << nfld << "!= " << p->nfld << "]" + << " mmax [" << lmax << "!= " << p->mmax << "]" + << " nmax [" << nmax << "!= " << p->nmax << "]"; + throw std::runtime_error(serr.str()); + } + } else { auto p = dynamic_cast(c.get()); coefs[0] = p->coefs; + store[0] = p->store; + + // Sanity test dimensions + if (nfld!=p->nfld || lmax!=p->lmax || nmax!=p->nmax) { + std::ostringstream serr; + serr << "FieldBasis::set_coefs: dimension error! " + << " nfld [" << nfld << "!= " << p->nfld << "]" + << " lmax [" << lmax << "!= " << p->lmax << "]" + << " nmax [" << nmax << "!= " << p->nmax << "]"; + throw std::runtime_error(serr.str()); + } + } } @@ -287,6 +313,8 @@ namespace BasisClasses double u, double v, double w) { constexpr std::complex I(0, 1); + constexpr double fac0 = 1.0/sqrt(4*M_PI); + int tid = omp_get_thread_num(); PS3 pos{x, y, z}, vel{u, v, w}; @@ -309,7 +337,7 @@ namespace BasisClasses auto p = (*ortho)(R); - (*coefs[tid])(0, 0, 0) += mass*p(0); + (*coefs[tid])(0, 0, 0) += mass*p(0)*fac0; for (int m=0; m<=lmax; m++) { @@ -431,6 +459,20 @@ namespace BasisClasses } return ret; } else { + static bool firstime = true; + if (firstime) { + int tid = omp_get_thread_num(); + std::ostringstream file; + file << "field.bin." << tid; + std::ofstream fout(file.str()); + const auto& d = coefs[0]->dimensions(); + fout << "Dim size: " << d.size(); + for (int i=0; i ret(nfld, 0); auto p = (*ortho)(r); for (int i=0; i Date: Sun, 7 Jul 2024 10:57:28 -0700 Subject: [PATCH 142/167] Removed experimental extensions from previous mixed bug-fix and feature merge --- expui/CMakeLists.txt | 2 +- expui/SGSmooth.H | 13 - expui/SGSmooth.cc | 549 ------------------------------------------ expui/expMSSA.cc | 259 -------------------- pyEXP/MSSAWrappers.cc | 43 ---- 5 files changed, 1 insertion(+), 865 deletions(-) delete mode 100644 expui/SGSmooth.H delete mode 100644 expui/SGSmooth.cc diff --git a/expui/CMakeLists.txt b/expui/CMakeLists.txt index d506bea8d..8690817f7 100644 --- a/expui/CMakeLists.txt +++ b/expui/CMakeLists.txt @@ -34,7 +34,7 @@ endif() set(expui_SOURCES BasisFactory.cc BiorthBasis.cc FieldBasis.cc CoefContainer.cc CoefStruct.cc FieldGenerator.cc expMSSA.cc Coefficients.cc KMeans.cc Centering.cc ParticleIterator.cc - Koopman.cc BiorthBess.cc SGSmooth.cc) + Koopman.cc BiorthBess.cc) add_library(expui ${expui_SOURCES}) set_target_properties(expui PROPERTIES OUTPUT_NAME expui) target_include_directories(expui PUBLIC ${common_INCLUDE}) diff --git a/expui/SGSmooth.H b/expui/SGSmooth.H deleted file mode 100644 index 6615bffa2..000000000 --- a/expui/SGSmooth.H +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef __SGSMOOTH_HPP__ -#define __SGSMOOTH_HPP__ - -#include - -//! savitzky golay smoothing. -std::vector sg_smooth(const std::vector &v, const int w, const int deg); - -//! numerical derivative based on savitzky golay smoothing. -std::vector sg_derivative(const std::vector &v, const int w, - const int deg, const double h=1.0); - -#endif // __SGSMOOTH_HPP__ diff --git a/expui/SGSmooth.cc b/expui/SGSmooth.cc deleted file mode 100644 index 1b5150b53..000000000 --- a/expui/SGSmooth.cc +++ /dev/null @@ -1,549 +0,0 @@ -//! -// Sliding window signal processing (and linear algebra toolkit). -// -// supported operations: -//
    -//
  • Savitzky-Golay smoothing. -//
  • computing a numerical derivative based of Savitzky-Golay smoothing. -//
  • required linear algebra support for SG smoothing using STL based -// vector/matrix classes -//
-// -// \brief Linear Algebra "Toolkit". -// -// modified by Rob Patro, 2016 -// modified by MDW, 2024 - -// system headers -#include -#include -#include // for size_t -#include // for fabs -#include - -//! default convergence -static const double TINY_FLOAT = 1.0e-300; - -//! comfortable array of doubles -using float_vect = std::vector; -//! comfortable array of ints; -using int_vect = std::vector; - -/*! matrix class. - * - * This is a matrix class derived from a vector of float_vects. Note that - * the matrix elements indexed [row][column] with indices starting at 0 (c - * style). Also note that because of its design looping through rows should - * be faster than looping through columns. - * - * \brief two dimensional floating point array - */ -class float_mat : public std::vector { -private: - //! disable the default constructor - explicit float_mat() {}; - //! disable assignment operator until it is implemented. - float_mat &operator =(const float_mat &) { return *this; }; -public: - //! constructor with sizes - float_mat(const size_t rows, const size_t cols, const double def=0.0); - //! copy constructor for matrix - float_mat(const float_mat &m); - //! copy constructor for vector - float_mat(const float_vect &v); - - //! use default destructor - // ~float_mat() {}; - - //! get size - size_t nr_rows(void) const { return size(); }; - //! get size - size_t nr_cols(void) const { return front().size(); }; -}; - - - -// constructor with sizes -float_mat::float_mat(const size_t rows,const size_t cols,const double defval) - : std::vector(rows) { - int i; - for (i = 0; i < rows; ++i) { - (*this)[i].resize(cols, defval); - } - if ((rows < 1) || (cols < 1)) { - std::ostringstream msg; - msg << "cannot build matrix with " << rows << " rows and " << cols - << "columns"; - throw std::runtime_error(msg.str()); - } -} - -// copy constructor for matrix -float_mat::float_mat(const float_mat &m) : std::vector(m.size()) { - - float_mat::iterator inew = begin(); - float_mat::const_iterator iold = m.begin(); - for (/* empty */; iold < m.end(); ++inew, ++iold) { - const size_t oldsz = iold->size(); - inew->resize(oldsz); - const float_vect oldvec(*iold); - *inew = oldvec; - } -} - -// copy constructor for vector -float_mat::float_mat(const float_vect &v) - : std::vector(1) { - - const size_t oldsz = v.size(); - front().resize(oldsz); - front() = v; -} - -////////////////////// -// Helper functions // -////////////////////// - -//! permute() orders the rows of A to match the integers in the index array. -void permute(float_mat &A, int_vect &idx) -{ - int_vect i(idx.size()); - int j,k; - - for (j = 0; j < A.nr_rows(); ++j) { - i[j] = j; - } - - // loop over permuted indices - for (j = 0; j < A.nr_rows(); ++j) { - if (i[j] != idx[j]) { - - // search only the remaining indices - for (k = j+1; k < A.nr_rows(); ++k) { - if (i[k] ==idx[j]) { - std::swap(A[j],A[k]); // swap the rows and - i[k] = i[j]; // the elements of - i[j] = idx[j]; // the ordered index. - break; // next j - } - } - } - } -} - -/*! \brief Implicit partial pivoting. - * - * The function looks for pivot element only in rows below the current - * element, A[idx[row]][column], then swaps that row with the current one in - * the index map. The algorithm is for implicit pivoting (i.e., the pivot is - * chosen as if the max coefficient in each row is set to 1) based on the - * scaling information in the vector scale. The map of swapped indices is - * recorded in swp. The return value is +1 or -1 depending on whether the - * number of row swaps was even or odd respectively. */ -static int partial_pivot(float_mat &A, const size_t row, const size_t col, - float_vect &scale, int_vect &idx, double tol) -{ - if (tol <= 0.0) - tol = TINY_FLOAT; - - int swapNum = 1; - - // default pivot is the current position, [row,col] - int pivot = row; - double piv_elem = fabs(A[idx[row]][col]) * scale[idx[row]]; - - // loop over possible pivots below current - int j; - for (j = row + 1; j < A.nr_rows(); ++j) { - - const double tmp = fabs(A[idx[j]][col]) * scale[idx[j]]; - - // if this elem is larger, then it becomes the pivot - if (tmp > piv_elem) { - pivot = j; - piv_elem = tmp; - } - } - -#if 0 - if (piv_elem < tol) { - throw std::runtime_error("partial_pivot(): Zero pivot encountered."); -#endif - - if(pivot > row) { // bring the pivot to the diagonal - j = idx[row]; // reorder swap array - idx[row] = idx[pivot]; - idx[pivot] = j; - swapNum = -swapNum; // keeping track of odd or even swap - } - return swapNum; -} - -/*! \brief Perform backward substitution. - * - * Solves the system of equations A*b=a, ASSUMING that A is upper - * triangular. If diag==1, then the diagonal elements are additionally - * assumed to be 1. Note that the lower triangular elements are never - * checked, so this function is valid to use after a LU-decomposition in - * place. A is not modified, and the solution, b, is returned in a. */ -static void lu_backsubst(float_mat &A, float_mat &a, bool diag=false) -{ - int r,c,k; - - for (r = (A.nr_rows() - 1); r >= 0; --r) { - for (c = (A.nr_cols() - 1); c > r; --c) { - for (k = 0; k < A.nr_cols(); ++k) { - a[r][k] -= A[r][c] * a[c][k]; - } - } - if(!diag) { - for (k = 0; k < A.nr_cols(); ++k) { - a[r][k] /= A[r][r]; - } - } - } -} - -/*! \brief Perform forward substitution. - * - * Solves the system of equations A*b=a, ASSUMING that A is lower - * triangular. If diag==1, then the diagonal elements are additionally - * assumed to be 1. Note that the upper triangular elements are never - * checked, so this function is valid to use after a LU-decomposition in - * place. A is not modified, and the solution, b, is returned in a. */ -static void lu_forwsubst(float_mat &A, float_mat &a, bool diag=true) -{ - int r,k,c; - for (r = 0;r < A.nr_rows(); ++r) { - for(c = 0; c < r; ++c) { - for (k = 0; k < A.nr_cols(); ++k) { - a[r][k] -= A[r][c] * a[c][k]; - } - } - if(!diag) { - for (k = 0; k < A.nr_cols(); ++k) { - a[r][k] /= A[r][r]; - } - } - } -} - -/*! \brief Performs LU factorization in place. - * - * This is Crout's algorithm (cf., Num. Rec. in C, Section 2.3). The map of - * swapped indeces is recorded in idx. The return value is +1 or -1 - * depending on whether the number of row swaps was even or odd - * respectively. idx must be preinitialized to a valid set of indices - * (e.g., {1,2, ... ,A.nr_rows()}). */ -static int lu_factorize(float_mat &A, int_vect &idx, double tol=TINY_FLOAT) -{ - if ( tol <= 0.0) - tol = TINY_FLOAT; - - if ((A.nr_rows() == 0) || (A.nr_rows() != A.nr_cols())) { - throw std::runtime_error("lu_factorize(): cannot handle empty " - "or nonsquare matrices."); - } - - float_vect scale(A.nr_rows()); // implicit pivot scaling - int i,j; - for (i = 0; i < A.nr_rows(); ++i) { - double maxval = 0.0; - for (j = 0; j < A.nr_cols(); ++j) { - if (fabs(A[i][j]) > maxval) - maxval = fabs(A[i][j]); - } - if (maxval == 0.0) { - throw std::runtime_error("lu_factorize(): zero pivot found."); - } - scale[i] = 1.0 / maxval; - } - - int swapNum = 1; - int c,r; - for (c = 0; c < A.nr_cols() ; ++c) { // loop over columns - swapNum *= partial_pivot(A, c, c, scale, idx, tol); // bring pivot to diagonal - for(r = 0; r < A.nr_rows(); ++r) { // loop over rows - int lim = (r < c) ? r : c; - for (j = 0; j < lim; ++j) { - A[idx[r]][c] -= A[idx[r]][j] * A[idx[j]][c]; - } - if (r > c) - A[idx[r]][c] /= A[idx[c]][c]; - } - } - permute(A,idx); - return swapNum; -} - -/*! \brief Solve a system of linear equations. - * Solves the inhomogeneous matrix problem with lu-decomposition. Note that - * inversion may be accomplished by setting a to the identity_matrix. */ -static float_mat lin_solve(const float_mat &A, const float_mat &a, - double tol=TINY_FLOAT) -{ - float_mat B(A); - float_mat b(a); - int_vect idx(B.nr_rows()); - int j; - - for (j = 0; j < B.nr_rows(); ++j) { - idx[j] = j; // init row swap label array - } - lu_factorize(B,idx,tol); // get the lu-decomp. - permute(b,idx); // sort the inhomogeneity to match the lu-decomp - lu_forwsubst(B,b); // solve the forward problem - lu_backsubst(B,b); // solve the backward problem - return b; -} - -/////////////////////// -// related functions // -/////////////////////// - -//! Returns the inverse of a matrix using LU-decomposition. -static float_mat invert(const float_mat &A) -{ - const int n = A.size(); - float_mat E(n, n, 0.0); - float_mat B(A); - int i; - - for (i = 0; i < n; ++i) { - E[i][i] = 1.0; - } - - return lin_solve(B, E); -} - -//! returns the transposed matrix. -static float_mat transpose(const float_mat &a) -{ - float_mat res(a.nr_cols(), a.nr_rows()); - int i,j; - - for (i = 0; i < a.nr_rows(); ++i) { - for (j = 0; j < a.nr_cols(); ++j) { - res[j][i] = a[i][j]; - } - } - return res; -} - -//! matrix multiplication. -float_mat operator *(const float_mat &a, const float_mat &b) -{ - float_mat res(a.nr_rows(), b.nr_cols()); - if (a.nr_cols() != b.nr_rows()) { - throw std::runtime_error("incompatible matrices in multiplication"); - } - - int i,j,k; - - for (i = 0; i < a.nr_rows(); ++i) { - for (j = 0; j < b.nr_cols(); ++j) { - double sum(0.0); - for (k = 0; k < a.nr_cols(); ++k) { - sum += a[i][k] * b[k][j]; - } - res[i][j] = sum; - } - } - return res; -} - - -//! calculate savitzky golay coefficients. -static float_vect sg_coeff(const float_vect &b, const size_t deg) -{ - const size_t rows(b.size()); - const size_t cols(deg + 1); - float_mat A(rows, cols); - float_vect res(rows); - - // generate input matrix for least squares fit - int i,j; - for (i = 0; i < rows; ++i) { - for (j = 0; j < cols; ++j) { - A[i][j] = pow(double(i), double(j)); - } - } - - float_mat c(invert(transpose(A) * A) * (transpose(A) * transpose(b))); - - for (i = 0; i < b.size(); ++i) { - res[i] = c[0][0]; - for (j = 1; j <= deg; ++j) { - res[i] += c[j][0] * pow(double(i), double(j)); - } - } - return res; -} - -/*! \brief savitzky golay smoothing. - * - * This method means fitting a polynome of degree 'deg' to a sliding window - * of width 2w+1 throughout the data. The needed coefficients are - * generated dynamically by doing a least squares fit on a "symmetric" unit - * vector of size 2w+1, e.g. for w=2 b=(0,0,1,0,0). evaluating the polynome - * yields the sg-coefficients. at the border non symmectric vectors b are - * used. */ -float_vect sg_smooth(const float_vect &v, const int width, const int deg) -{ - float_vect res(v.size(), 0.0); - if ((width < 1) || (deg < 0) || (v.size() < (2 * width + 2))) { - throw std::runtime_error("sgsmooth: parameter error."); - } - - const int window = 2 * width + 1; - const int endidx = v.size() - 1; - - // do a regular sliding window average - int i,j; - if (deg == 0) { - // handle border cases first because we need different coefficients -#if defined(_OPENMP) -#pragma omp parallel for private(i,j) schedule(static) -#endif - for (i = 0; i < width; ++i) { - const double scale = 1.0/double(i+1); - const float_vect c1(width, scale); - for (j = 0; j <= i; ++j) { - res[i] += c1[j] * v[j]; - res[endidx - i] += c1[j] * v[endidx - j]; - } - } - - // now loop over rest of data. reusing the "symmetric" coefficients. - const double scale = 1.0/double(window); - const float_vect c2(window, scale); -#if defined(_OPENMP) -#pragma omp parallel for private(i,j) schedule(static) -#endif - for (i = 0; i <= (v.size() - window); ++i) { - for (j = 0; j < window; ++j) { - res[i + width] += c2[j] * v[i + j]; - } - } - return res; - } - - // handle border cases first because we need different coefficients -#if defined(_OPENMP) -#pragma omp parallel for private(i,j) schedule(static) -#endif - for (i = 0; i < width; ++i) { - float_vect b1(window, 0.0); - b1[i] = 1.0; - - const float_vect c1(sg_coeff(b1, deg)); - for (j = 0; j < window; ++j) { - res[i] += c1[j] * v[j]; - res[endidx - i] += c1[j] * v[endidx - j]; - } - } - - // now loop over rest of data. reusing the "symmetric" coefficients. - float_vect b2(window, 0.0); - b2[width] = 1.0; - const float_vect c2(sg_coeff(b2, deg)); - -#if defined(_OPENMP) -#pragma omp parallel for private(i,j) schedule(static) -#endif - for (i = 0; i <= (v.size() - window); ++i) { - for (j = 0; j < window; ++j) { - res[i + width] += c2[j] * v[i + j]; - } - } - return res; -} - -/*! least squares fit a polynome of degree 'deg' to data in 'b'. - * then calculate the first derivative and return it. */ -static float_vect lsqr_fprime(const float_vect &b, const int deg) -{ - const int rows(b.size()); - const int cols(deg + 1); - float_mat A(rows, cols); - float_vect res(rows); - - // generate input matrix for least squares fit - int i,j; - for (i = 0; i < rows; ++i) { - for (j = 0; j < cols; ++j) { - A[i][j] = pow(double(i), double(j)); - } - } - - float_mat c(invert(transpose(A) * A) * (transpose(A) * transpose(b))); - - for (i = 0; i < b.size(); ++i) { - res[i] = c[1][0]; - for (j = 1; j < deg; ++j) { - res[i] += c[j + 1][0] * double(j+1) - * pow(double(i), double(j)); - } - } - return res; -} - -/*! \brief savitzky golay smoothed numerical derivative. - * - * This method means fitting a polynome of degree 'deg' to a sliding window - * of width 2w+1 throughout the data. - * - * In contrast to the sg_smooth function we do a brute force attempt by - * always fitting the data to a polynome of degree 'deg' and using the - * result. */ -float_vect sg_derivative(const float_vect &v, const int width, - const int deg, const double h) -{ - float_vect res(v.size(), 0.0); - if ((width < 1) || (deg < 1) || (v.size() < (2 * width + 2))) { - throw std::runtime_error("sgsderiv: parameter error"); - } - - const int window = 2 * width + 1; - - // handle border cases first because we do not repeat the fit - // lower part - float_vect b(window, 0.0); - int i,j; - - for (i = 0; i < window; ++i) { - b[i] = v[i] / h; - } - const float_vect c(lsqr_fprime(b, deg)); - for (j = 0; j <= width; ++j) { - res[j] = c[j]; - } - // upper part. direction of fit is reversed - for (i = 0; i < window; ++i) { - b[i] = v[v.size() - 1 - i] / h; - } - const float_vect d(lsqr_fprime(b, deg)); - for (i = 0; i <= width; ++i) { - res[v.size() - 1 - i] = -d[i]; - } - - // now loop over rest of data. wasting a lot of least squares calcs - // since we only use the middle value. -#if defined(_OPENMP) -#pragma omp parallel for private(i,j) schedule(static) -#endif - for (i = 1; i < (v.size() - window); ++i) { - for (j = 0; j < window; ++j) { - b[j] = v[i + j] / h; - } - res[i + width] = lsqr_fprime(b, deg)[width]; - } - return res; -} - -// Local Variables: -// mode: c++ -// c-basic-offset: 4 -// fill-column: 76 -// indent-tabs-mode: nil -// End: diff --git a/expui/expMSSA.cc b/expui/expMSSA.cc index 7a19c6f6d..b136d2e7d 100644 --- a/expui/expMSSA.cc +++ b/expui/expMSSA.cc @@ -1554,265 +1554,6 @@ namespace MSSA { } - std::tuple - expMSSA::getKoopmanModes(double tol, int D, bool debug) - { - bool use_fullKh = true; // Use the non-reduced computation of - // Koopman/eDMD - // Number of channels - // - nkeys = mean.size(); - - // Make sure parameters are sane - // - if (numW<=0) numW = numT/2; - if (numW > numT/2) numW = numT/2; - - numK = numT - numW + 1; - - Eigen::VectorXd S1; - Eigen::MatrixXd Y1; - Eigen::MatrixXd V1; - Eigen::MatrixXd VT1; - Eigen::MatrixXd VT2; - - // Make a new trajetory matrix with smoothing - // - Y1.resize(numK, numW*nkeys + D*(nkeys-1)); - - size_t n=0, offset=0; - for (auto k : mean) { - for (int i=0; i 0) { - // Back blending - for (int j=0; j(D-j)/D; - } - } - // Main series - for (int j=0; j(D-j)/D; - } - } - } - offset += numW + D; - n++; - } - - double Scale = Y1.norm(); - - // auto YY = Y1/Scale; - auto YY = Y1; - - // Use one of the built-in Eigen3 algorithms - // - /* - if (params["Jacobi"]) { - // -->Using Jacobi - Eigen::JacobiSVD - svd(YY, Eigen::ComputeThinU | Eigen::ComputeThinV); - S1 = svd.singularValues(); - V1 = svd.matrixV(); - } else if (params["BDCSVD"]) { - */ - // -->Using BDC - Eigen::BDCSVD - svd(YY, Eigen::ComputeFullU | Eigen::ComputeFullV); - // svd(YY, Eigen::ComputeThinU | Eigen::ComputeThinV); - S1 = svd.singularValues(); - V1 = svd.matrixV(); - /* - } else { - // -->Use Random approximation algorithm from Halko, Martinsson, - // and Tropp - int srank = std::min(YY.cols(), YY.rows()); - RedSVD::RedSVD svd(YY, srank); - S1 = svd.singularValues(); - V1 = svd.matrixV(); - } - */ - - std::cout << "shape V1 = " << V1.rows() << " x " - << V1.cols() << std::endl; - - std::cout << "shape Y1 = " << Y1.rows() << " x " - << Y1.cols() << std::endl; - - int lags = V1.rows(); - int rank = V1.cols(); - - std::ofstream out; - if (debug) out.open("debug.txt"); - - if (out) out << "rank=" << rank << " lags=" << lags << std::endl; - - VT1.resize(rank, lags-1); - VT2.resize(rank, lags-1); - - for (int j=0; j uu; - for (int i=0; iUsing Jacobi - Eigen::JacobiSVD - // svd(VT1, Eigen::ComputeThinU | Eigen::ComputeThinV); - svd(VT1, Eigen::ComputeFullU | Eigen::ComputeFullV); - SS = svd.singularValues(); - UU = svd.matrixU(); - VV = svd.matrixV(); - } else if (params["BDCSVD"]) { - */ - { - // -->Using BDC - Eigen::BDCSVD - // svd(VT1, Eigen::ComputeThinU | Eigen::ComputeThinV); - svd(VT1, Eigen::ComputeFullU | Eigen::ComputeFullV); - SS = svd.singularValues(); - UU = svd.matrixU(); - VV = svd.matrixV(); - } - /* - } else { - // -->Use Random approximation algorithm from Halko, Martinsson, - // and Tropp - // RedSVD::RedSVD svd(VT1, std::min(rank, numK-1)); - RedSVD::RedSVD svd(VT1, std::max(VT1.rows(), VT2.cols())); - SS = svd.singularValues(); - UU = svd.matrixU(); - VV = svd.matrixV(); - } - */ - - if (out) out << "Singular values" << std::endl << SS << std::endl; - - // Compute inverse - for (int i=0; i tol) SS(i) = 1.0/SS(i); - else SS(i) = 0.0; - } - - // Compute full Koopman operator - if (use_fullKh) { - - Eigen::MatrixXd DD(VV.cols(), UU.cols()); - DD.setZero(); - for (int i=0; i es(AT); - - L = es.eigenvalues(); - Phi = es.eigenvectors(); - - if (out) { - out << std::endl << "Eigenvalues" << std::endl << L << std::endl - << std::endl << "Eigenvectors" << std::endl << Phi << std::endl; - } - - } - // Compute the reduced Koopman operator - else { - - Eigen::MatrixXd AT = UU.transpose() * (VT2 * VV) * SS.asDiagonal(); - - // Compute spectrum - Eigen::EigenSolver es(AT, true); - - L = es.eigenvalues(); - auto W = es.eigenvectors(); - - // Compute the EDMD modes - // - Eigen::VectorXcd Linv(L); - for (int i=0; i tol) Linv(i) = 1.0/Linv(i); - else Linv(i) = 0.0; - } - - Phi = VT2 * VV * SS.asDiagonal() * W * Linv.asDiagonal(); - - if (out) { - out << std::endl << "Eigenvalues" << std::endl << L << std::endl - << std::endl << "Eigenvectors" << std::endl << Phi << std::endl; - } - } - - // Cache window size - // - window = D; - - return {L, Phi}; - } - - std::map - expMSSA::getReconstructedKoopman(int mode) - { - // Copy the original map for return - // - auto newdata = data; - - size_t n=0, offset=0; - - for (auto u : mean) { - - double disp = totVar; - if (type == TrendType::totPow) disp = totPow; - if (disp==0.0) disp = var[u.first]; - - std::complex phase = 1.0; - for (int i=0; i Date: Sun, 7 Jul 2024 11:20:51 -0700 Subject: [PATCH 143/167] Missing Koopman removal from the header file [no ci] --- expui/expMSSA.H | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/expui/expMSSA.H b/expui/expMSSA.H index a5cdb1606..1200f0d97 100644 --- a/expui/expMSSA.H +++ b/expui/expMSSA.H @@ -50,13 +50,6 @@ namespace MSSA //! Right singular vectors Eigen::MatrixXd U; - //@{ - //! Koopman modes - Eigen::VectorXcd L; - Eigen::MatrixXcd Phi; - int window; - //@} - //! Parameters //@{ bool flip, verbose, powerf; @@ -287,13 +280,6 @@ namespace MSSA return ret; } - //! Estimate Koopman modes from the trajectory eigenvectors - std::tuple - getKoopmanModes(const double tol, int window, bool debug); - - //! Return the reconstructed Koopman modes - std::map getReconstructedKoopman(int mode); - }; From 1dfb00c1c8b0ba0f62294cb990e27b6556581bb0 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Mon, 8 Jul 2024 13:37:51 -0700 Subject: [PATCH 144/167] sqrt not a constexpr in clang --- expui/FieldBasis.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/FieldBasis.cc b/expui/FieldBasis.cc index 32533400d..158180dfa 100644 --- a/expui/FieldBasis.cc +++ b/expui/FieldBasis.cc @@ -313,7 +313,7 @@ namespace BasisClasses double u, double v, double w) { constexpr std::complex I(0, 1); - constexpr double fac0 = 1.0/sqrt(4*M_PI); + double fac0 = 1.0/sqrt(4*M_PI); int tid = omp_get_thread_num(); PS3 pos{x, y, z}, vel{u, v, w}; From 76c68cfc28a38eb5a62116f7fe5c1e6fb3e7349f Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Mon, 8 Jul 2024 17:53:55 -0700 Subject: [PATCH 145/167] Add C++17 workaround for sqrt constexpr --- expui/FieldBasis.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/FieldBasis.cc b/expui/FieldBasis.cc index 158180dfa..7c85e0aee 100644 --- a/expui/FieldBasis.cc +++ b/expui/FieldBasis.cc @@ -313,7 +313,7 @@ namespace BasisClasses double u, double v, double w) { constexpr std::complex I(0, 1); - double fac0 = 1.0/sqrt(4*M_PI); + constexpr double fac0 = 0.25*M_2_SQRTPI; int tid = omp_get_thread_num(); PS3 pos{x, y, z}, vel{u, v, w}; From 23fbb21334233f203f5616acd21eb26d846a5916 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Mon, 8 Jul 2024 23:25:21 -0700 Subject: [PATCH 146/167] Updates to 'make tests' scripts and config [no ci] --- tests/CMakeLists.txt | 6 +++--- tests/Halo/check.py | 7 +++++-- tests/Halo/config.yml | 5 +++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 803800330..5928b4395 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,14 +30,14 @@ if(ENABLE_PYEXP) WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Halo) add_test(NAME removeCylCache - COMMAND ${CMAKE_COMMAND} -E remove .eof.cache.run0t test_adddisk_sl.0 + COMMAND ${CMAKE_COMMAND} -E remove .eof.cache.run0t WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Disk) set_tests_properties(removeSphCache PROPERTIES DEPENDS pyexpSphBasisTest REQUIRED_FILES ".slgrid_sph_cache") set_tests_properties(removeCylCache PROPERTIES DEPENDS pyexpCylBasisTest - REQUIRED_FILES ".eof.cache.run0t;test_adddisk_sl.0") + REQUIRED_FILES ".eof.cache.run0t") # Other tests for pyEXP go here ... @@ -56,7 +56,7 @@ if(ENABLE_NBODY) # Makes some spherical ICs using utils/ICs/gensph add_test(NAME makeICTest - COMMAND ${EXP_MPI_LAUNCH} ${CMAKE_BINARY_DIR}/utils/ICs/gensph -N 1000 -i SLGridSph.model + COMMAND ${EXP_MPI_LAUNCH} ${CMAKE_BINARY_DIR}/utils/ICs/gensph -N 10000 -i SLGridSph.model WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Halo) # Runs those ICs using exp diff --git a/tests/Halo/check.py b/tests/Halo/check.py index 4299f403b..639b02b27 100644 --- a/tests/Halo/check.py +++ b/tests/Halo/check.py @@ -3,12 +3,15 @@ # Read the output log file data = np.loadtxt("OUTLOG.run0", skiprows=6, delimiter="|") -# Column 16 is -2T/VC. The mean should be 1 with a std dev < 0.03 +# Column 16 is -2T/VC. The mean should be 1 with some small number of std dev mean = np.mean(data[:,16]) stdv = np.std (data[:,16]) +# print("Mean = " + str(mean) + ", std dev = " + str(stdv)) +# print("Check = {}".format(np.abs(mean-1.0) - 5.0*stdv)) + # If the values are within 3 sigma, assume that the simulation worked -if np.abs(mean - 1.0) > 3.0*stdv: +if np.abs(mean - 1.0) > 5.0*stdv: exit(1) else: exit(0) diff --git a/tests/Halo/config.yml b/tests/Halo/config.yml index 3743a65e6..bc83d3dc0 100644 --- a/tests/Halo/config.yml +++ b/tests/Halo/config.yml @@ -8,9 +8,9 @@ # ------------------------------------------------------------------------ Global: nthrds : 1 - dtime : 0.005 + dtime : 0.002 runtag : run0 - nsteps : 200 + nsteps : 500 multistep : 4 dynfracV : 0.01 dynfracA : 0.03 @@ -40,6 +40,7 @@ Components: rmapping : 0.0667 self_consistent: true modelname: SLGridSph.model + cachename: SLGridSph.cache.run0 # ------------------------------------------------------------------------ # This is a sequence of outputs From 2e89c5dd856bcbf0eb321c32bcb1657772850637 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Tue, 9 Jul 2024 07:38:50 -0700 Subject: [PATCH 147/167] Testing testing 1 2 3 --- .github/workflows/build.yml | 50 +++++-------------------------------- 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a37377ef8..acdf4f4d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: branches: - main jobs: - pyexp: + exp: strategy: matrix: os: [ubuntu-latest] @@ -31,14 +31,14 @@ jobs: git submodule update --init --recursive mkdir -p build/install - - name: Compile pyEXP + - name: Compile EXP if: runner.os == 'Linux' env: CC: ${{ matrix.cc }} working-directory: ./build run: >- cmake - -DENABLE_NBODY=NO + -DENABLE_NBODY=YES -DENABLE_PYEXP=YES -DCMAKE_BUILD_TYPE=Release -DEigen3_DIR=/usr/include/eigen3/share/eigen3/cmake @@ -50,46 +50,8 @@ jobs: working-directory: ./build run: make -j 2 - # ----------------------------------------------------------------------------------- - - exp: - strategy: - matrix: - os: [ubuntu-latest] - cc: [gcc, mpicc] - - name: "Test Full EXP Build" - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install core dependencies - ubuntu - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y build-essential libeigen3-dev libfftw3-dev libhdf5-dev libopenmpi-dev - - - name: Setup submodule and build - run: | - git submodule update --init --recursive - mkdir -p build/install - - - name: Compile Full EXP - Linux - if: runner.os == 'Linux' - env: - CC: ${{ matrix.cc }} + - name: CTest working-directory: ./build - run: >- - cmake - -DENABLE_NBODY=YES - -DENABLE_PYEXP=NO - -DCMAKE_BUILD_TYPE=Release - -DEigen3_DIR=/usr/include/eigen3/share/eigen3/cmake - -DCMAKE_INSTALL_PREFIX=./install - -Wno-dev - .. + run: ctest -j 2 -L quick + - - name: Make - working-directory: ./build - run: make -j 2 From 93c48f285934b4fd5b633adb6b17a28318e4d877 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Tue, 9 Jul 2024 07:47:29 -0700 Subject: [PATCH 148/167] rm old build action [no ci] --- .github/workflows/buildfull.yml | 146 -------------------------------- 1 file changed, 146 deletions(-) delete mode 100644 .github/workflows/buildfull.yml diff --git a/.github/workflows/buildfull.yml b/.github/workflows/buildfull.yml deleted file mode 100644 index eb9c86552..000000000 --- a/.github/workflows/buildfull.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: "Test Builds" - -on: - -jobs: - pyexp: - strategy: - matrix: - os: [macos-latest, ubuntu-latest] - cc: [gcc, mpicc] - - name: "Test pyEXP Build" - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install core dependencies - ubuntu - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y build-essential libeigen3-dev libfftw3-dev libhdf5-dev libopenmpi-dev - - - name: Install core dependencies - mac - if: startsWith(matrix.os, 'mac') - run: | - brew update - brew reinstall gcc - brew install eigen fftw hdf5 open-mpi libomp - - - name: Setup submodule and build - run: | - git submodule update --init --recursive - mkdir -p build/install - - - name: Compile pyEXP - Linux - if: runner.os == 'Linux' - env: - CC: ${{ matrix.cc }} - working-directory: ./build - run: >- - cmake - -DENABLE_NBODY=NO - -DENABLE_PYEXP=YES - -DCMAKE_BUILD_TYPE=Release - -DEigen3_DIR=/usr/include/eigen3/share/eigen3/cmake - -DCMAKE_INSTALL_PREFIX=./install - -Wno-dev - .. - - # Note for future: The homebrew paths are for intel only. Once ARM macs are - # supported in here, we'll need to update to /opt/homebrew/... instead - - name: Compile pyEXP - Mac - if: startsWith(matrix.os, 'mac') - env: - CC: ${{ matrix.cc }} - LDFLAGS: -L/usr/local/opt/libomp/lib - CPPFLAGS: -I/usr/local/opt/libomp/include - working-directory: ./build - run: >- - cmake - -DENABLE_NBODY=NO - -DENABLE_PYEXP=YES - -DCMAKE_BUILD_TYPE=Release - -DEigen3_DIR=/usr/local/share/eigen3/cmake - -DCMAKE_INSTALL_PREFIX=./install - -DOpenMP_CXX_INCLUDE_DIR=/usr/local/opt/libomp/include - -DOpenMP_C_INCLUDE_DIR=/usr/local/opt/libomp/include - -Wno-dev - .. - - - name: Make - working-directory: ./build - run: make -j 2 - - # ----------------------------------------------------------------------------------- - - exp: - strategy: - matrix: - os: [macos-latest, ubuntu-latest] - cc: [gcc, mpicc] - - name: "Test Full EXP Build" - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install core dependencies - ubuntu - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y build-essential libeigen3-dev libfftw3-dev libhdf5-dev libopenmpi-dev - - - name: Install core dependencies - mac - if: startsWith(matrix.os, 'mac') - run: | - brew update - brew reinstall gcc - brew install eigen fftw hdf5 open-mpi libomp - - - name: Setup submodule and build - run: | - git submodule update --init --recursive - mkdir -p build/install - - - name: Compile Full EXP - Linux - if: runner.os == 'Linux' - env: - CC: ${{ matrix.cc }} - working-directory: ./build - run: >- - cmake - -DENABLE_NBODY=YES - -DENABLE_PYEXP=NO - -DCMAKE_BUILD_TYPE=Release - -DEigen3_DIR=/usr/include/eigen3/share/eigen3/cmake - -DCMAKE_INSTALL_PREFIX=./install - -Wno-dev - .. - - # Note for future: The homebrew paths are for intel only. Once ARM macs are - # supported in here, we'll need to update to /opt/homebrew/... instead - - name: Compile Full EXP - Mac - if: startsWith(matrix.os, 'mac') - env: - CC: ${{ matrix.cc }} - LDFLAGS: -L/usr/local/opt/libomp/lib - CPPFLAGS: -I/usr/local/opt/libomp/include - working-directory: ./build - run: >- - cmake - -DENABLE_NBODY=YES - -DENABLE_PYEXP=NO - -DCMAKE_BUILD_TYPE=Release - -DEigen3_DIR=/usr/local/share/eigen3/cmake - -DCMAKE_INSTALL_PREFIX=./install - -DOpenMP_CXX_INCLUDE_DIR=/usr/local/opt/libomp/include - -DOpenMP_C_INCLUDE_DIR=/usr/local/opt/libomp/include - -Wno-dev - .. - - - name: Make - working-directory: ./build - run: make -j 2 From b02f556c0c0f636e608a10c5ae1d257dd457529e Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 9 Jul 2024 08:52:50 -0700 Subject: [PATCH 149/167] Remove temporary files automatically --- utils/ICs/gensph.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/ICs/gensph.cc b/utils/ICs/gensph.cc index f685133c1..d921c883d 100644 --- a/utils/ICs/gensph.cc +++ b/utils/ICs/gensph.cc @@ -809,6 +809,7 @@ main(int argc, char **argv) if (myid==0) { std::ostringstream sout; sout << "cat " << OUTPS << ".* > " << OUTPS; + sout << "; rm " << OUTPS << ".*"; int ret = system(sout.str().c_str()); } From b2041ad709e3ef4f694b31ad64d8e556a38bc7f6 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 9 Jul 2024 08:53:19 -0700 Subject: [PATCH 150/167] A few minor updates and clean ups [no ci] --- tests/CMakeLists.txt | 6 +++--- tests/Halo/check.py | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5928b4395..f528ca5b0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -92,14 +92,14 @@ if(ENABLE_NBODY) # perhaps there is a better way? add_test(NAME removeTempFiles COMMAND ${CMAKE_COMMAND} -E remove - config.run0.yml current.processor.rates.run0 new.bods new.bods.0 + config.run0.yml current.processor.rates.run0 new.bods OUTLOG.run0 run0.levels SLGridSph.cache.run0 test.grid 'outcoef.dark halo.run0' WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Halo) # Remove the temporary files set_tests_properties(removeTempFiles PROPERTIES DEPENDS expNbodyCheck2TW - REQUIRED_FILES "config.run0.yml;current.processor.rates.run0;new.bods;new.bods.0;OUTLOG.run0;run0.levels;SLGridSph.cache.run0;test.grid;" + REQUIRED_FILES "config.run0.yml;current.processor.rates.run0;new.bods;run0.levels;SLGridSph.cache.run0;test.grid;" ) # Makes some cube ICs using utils/ICs/cubeics @@ -137,7 +137,7 @@ if(ENABLE_NBODY) # Set labels for pyEXP tests set_tests_properties(expExecuteTest PROPERTIES LABELS "quick") set_tests_properties(makeICTest expNbodyTest expNbodyCheck2TW - removeTempFiles expCubeTest removeCubeFiles + removeTempFiles makeCubeICTest expCubeTest removeCubeFiles PROPERTIES LABELS "long") endif() diff --git a/tests/Halo/check.py b/tests/Halo/check.py index 639b02b27..54a499349 100644 --- a/tests/Halo/check.py +++ b/tests/Halo/check.py @@ -3,15 +3,12 @@ # Read the output log file data = np.loadtxt("OUTLOG.run0", skiprows=6, delimiter="|") -# Column 16 is -2T/VC. The mean should be 1 with some small number of std dev +# Column 16 is -2T/VC. The mean should be 1 mean = np.mean(data[:,16]) stdv = np.std (data[:,16]) -# print("Mean = " + str(mean) + ", std dev = " + str(stdv)) -# print("Check = {}".format(np.abs(mean-1.0) - 5.0*stdv)) - -# If the values are within 3 sigma, assume that the simulation worked -if np.abs(mean - 1.0) > 5.0*stdv: +# If the values are within 6 sigma of 1, assume that the simulation worked +if np.abs(mean - 1.0) > 6.0*stdv: exit(1) else: exit(0) From 78b41f0e362049d3e3c1f569a5eae38a9fe221c0 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 9 Jul 2024 09:02:11 -0700 Subject: [PATCH 151/167] Change the component name to prevent a coefficient file name that contains a space [no ci] --- tests/CMakeLists.txt | 2 +- tests/Halo/config.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f528ca5b0..42d84db61 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -94,7 +94,7 @@ if(ENABLE_NBODY) COMMAND ${CMAKE_COMMAND} -E remove config.run0.yml current.processor.rates.run0 new.bods OUTLOG.run0 run0.levels SLGridSph.cache.run0 test.grid - 'outcoef.dark halo.run0' + outcoef.halo.run0 WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Halo) # Remove the temporary files diff --git a/tests/Halo/config.yml b/tests/Halo/config.yml index bc83d3dc0..596d51f36 100644 --- a/tests/Halo/config.yml +++ b/tests/Halo/config.yml @@ -26,7 +26,7 @@ Global: # Each indented stanza beginning with '-' is a component # ------------------------------------------------------------------------ Components: - - name : dark halo + - name : halo parameters : {nlevel: 1, indexing: true} bodyfile : new.bods force : @@ -49,7 +49,7 @@ Output: - id : outlog parameters : {nint: 10} - id : outcoef - parameters : {nint: 1, name: dark halo} + parameters : {nint: 1, name: halo} # ------------------------------------------------------------------------ # This is a sequence of external forces From 60d061e9e8741ad436cc398c68bd67b1ed959707 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 9 Jul 2024 09:32:50 -0700 Subject: [PATCH 152/167] Change component name to remove spaces from the file name; tested all label categories successfully [no ci] --- tests/CMakeLists.txt | 4 ++-- tests/Halo/readCoefs.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 42d84db61..81efdca36 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -94,12 +94,12 @@ if(ENABLE_NBODY) COMMAND ${CMAKE_COMMAND} -E remove config.run0.yml current.processor.rates.run0 new.bods OUTLOG.run0 run0.levels SLGridSph.cache.run0 test.grid - outcoef.halo.run0 + outcoef.halo.run0 halo.cache WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Halo) # Remove the temporary files set_tests_properties(removeTempFiles PROPERTIES DEPENDS expNbodyCheck2TW - REQUIRED_FILES "config.run0.yml;current.processor.rates.run0;new.bods;run0.levels;SLGridSph.cache.run0;test.grid;" + REQUIRED_FILES "config.run0.yml;current.processor.rates.run0;new.bods;run0.levels;halo.cache;test.grid;" ) # Makes some cube ICs using utils/ICs/cubeics diff --git a/tests/Halo/readCoefs.py b/tests/Halo/readCoefs.py index ad7ab60e2..0004001f7 100644 --- a/tests/Halo/readCoefs.py +++ b/tests/Halo/readCoefs.py @@ -3,7 +3,7 @@ import pyEXP -coefs = pyEXP.coefs.Coefs.factory('outcoef.dark halo.run0') +coefs = pyEXP.coefs.Coefs.factory('outcoef.halo.run0') data = coefs.getAllCoefs() print(data.shape) print(coefs.getName()) From a78ded51f7c41bd8f2a8a07cb0668dd95d392ead Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 9 Jul 2024 10:13:17 -0700 Subject: [PATCH 153/167] Typo in temp file removal list. CTest runs should be good to go at this point. [no ci] --- tests/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 81efdca36..9aee83328 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -94,12 +94,12 @@ if(ENABLE_NBODY) COMMAND ${CMAKE_COMMAND} -E remove config.run0.yml current.processor.rates.run0 new.bods OUTLOG.run0 run0.levels SLGridSph.cache.run0 test.grid - outcoef.halo.run0 halo.cache + outcoef.halo.run0 SLGridSph.cache.run0 WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/Halo) # Remove the temporary files set_tests_properties(removeTempFiles PROPERTIES DEPENDS expNbodyCheck2TW - REQUIRED_FILES "config.run0.yml;current.processor.rates.run0;new.bods;run0.levels;halo.cache;test.grid;" + REQUIRED_FILES "config.run0.yml;current.processor.rates.run0;new.bods;run0.levels;SLGridSph.cache.run0;test.grid;" ) # Makes some cube ICs using utils/ICs/cubeics From 824a13e1f15be8d37257d60b453ccd904f9c8eca Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Tue, 9 Jul 2024 10:35:02 -0700 Subject: [PATCH 154/167] updates to ctest suite --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acdf4f4d2..a6514ca58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - cc: [gcc, mpicc] + cc: [gcc] name: "Test pyEXP Build" runs-on: ${{ matrix.os }} From 68ed00e81a5a183ac9b48090b17f7ea145b2754d Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Tue, 9 Jul 2024 16:49:30 -0700 Subject: [PATCH 155/167] numpy dependence tweaks --- .github/workflows/build.yml | 1 + tests/Disk/cyl_basis.py | 1 - tests/Halo/sph_basis.py | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6514ca58..b966d0d4a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y build-essential libeigen3-dev libfftw3-dev libhdf5-dev libopenmpi-dev + sudo pip install numpy - name: Setup submodule and build run: | diff --git a/tests/Disk/cyl_basis.py b/tests/Disk/cyl_basis.py index 227b22f4a..c3c55b4e4 100644 --- a/tests/Disk/cyl_basis.py +++ b/tests/Disk/cyl_basis.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # coding: utf-8 -import numpy as np import pyEXP # Make the disk basis config diff --git a/tests/Halo/sph_basis.py b/tests/Halo/sph_basis.py index 1cee5d8ac..f7b6a18b9 100644 --- a/tests/Halo/sph_basis.py +++ b/tests/Halo/sph_basis.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # coding: utf-8 -import numpy as np import pyEXP # Make the halo basis config From 7ffd4b8554a0d8ef204bfaa208cdf2423c49469a Mon Sep 17 00:00:00 2001 From: mdw Date: Tue, 9 Jul 2024 20:34:01 -0400 Subject: [PATCH 156/167] Fixes to remove numpy --- tests/Disk/cyl_basis.py | 2 +- tests/Halo/check.py | 22 ++++++++++++++-------- tests/Halo/sph_basis.py | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/Disk/cyl_basis.py b/tests/Disk/cyl_basis.py index 227b22f4a..2d2b6e0d3 100644 --- a/tests/Disk/cyl_basis.py +++ b/tests/Disk/cyl_basis.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # coding: utf-8 -import numpy as np +# import numpy as np import pyEXP # Make the disk basis config diff --git a/tests/Halo/check.py b/tests/Halo/check.py index 54a499349..f181081d9 100644 --- a/tests/Halo/check.py +++ b/tests/Halo/check.py @@ -1,14 +1,20 @@ -import numpy as np +# Open the output log file +file = open("OUTLOG.run0") -# Read the output log file -data = np.loadtxt("OUTLOG.run0", skiprows=6, delimiter="|") +n = 0 # Count lines +mean = 0.0 # Accumulate 2T/VC valkues -# Column 16 is -2T/VC. The mean should be 1 -mean = np.mean(data[:,16]) -stdv = np.std (data[:,16]) +# Open the output log file +while (line := file.readline()) != "": + if n >= 6: # Skip the header stuff + v = [float(x) for x in line.split('|')] + mean += v[16] # This is the 2T/VC column + n = n + 1 # Count lines -# If the values are within 6 sigma of 1, assume that the simulation worked -if np.abs(mean - 1.0) > 6.0*stdv: +if n>6: mean /= n-6 # Sanity check + +# Check closeness to 1.0 +if (mean-1.0)*(mean-1.0) > 0.003: exit(1) else: exit(0) diff --git a/tests/Halo/sph_basis.py b/tests/Halo/sph_basis.py index 1cee5d8ac..e72ca54e5 100644 --- a/tests/Halo/sph_basis.py +++ b/tests/Halo/sph_basis.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # coding: utf-8 -import numpy as np +# import numpy as np import pyEXP # Make the halo basis config From 8c100f02ce3ea6906deeacadabe173f757cc53a1 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 9 Jul 2024 17:59:51 -0700 Subject: [PATCH 157/167] Toggle off HIGH_UNIT_TESTS manually --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cd65e7a1a..dbede3fe3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,7 +228,8 @@ add_subdirectory(extern/pybind11) # Set options for the HighFive git submodule in extern set(HIGHFIVE_EXAMPLES OFF CACHE BOOL "Do not build the examples") set(HIGHFIVE_BUILD_DOCS OFF CACHE BOOL "Do not build the documentation") -set(HIGHFIVE_USE_BOOST OFF CACHE BOOL "Do not use Boost in HighFIve") +set(HIGHFIVE_USE_BOOST OFF CACHE BOOL "Do not use Boost in HighFive") +set(HIGHFIVE_UNIT_TESTS OFF CACHE BOOL "Turn off internal testing for HighFIve") set(H5_USE_EIGEN TRUE CACHE BOOL "Eigen3 support in HighFive") add_subdirectory(extern/HighFive EXCLUDE_FROM_ALL) From 0de5e0d3a27031cfdb48697eaeb32d5c33a3e67b Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 9 Jul 2024 19:10:19 -0700 Subject: [PATCH 158/167] Fix comment spelling typo [no ci] --- tests/Halo/check.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Halo/check.py b/tests/Halo/check.py index f181081d9..d05381c3d 100644 --- a/tests/Halo/check.py +++ b/tests/Halo/check.py @@ -2,9 +2,10 @@ file = open("OUTLOG.run0") n = 0 # Count lines -mean = 0.0 # Accumulate 2T/VC valkues +mean = 0.0 # Accumulate 2T/VC values # Open the output log file +# while (line := file.readline()) != "": if n >= 6: # Skip the header stuff v = [float(x) for x in line.split('|')] @@ -14,6 +15,7 @@ if n>6: mean /= n-6 # Sanity check # Check closeness to 1.0 +# if (mean-1.0)*(mean-1.0) > 0.003: exit(1) else: From af674567bd8705123bbc01a1620a46610b368de8 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Tue, 9 Jul 2024 20:22:27 -0700 Subject: [PATCH 159/167] adjustments to build action --- .github/workflows/build.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b966d0d4a..82c76f90e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - main + - devel jobs: exp: strategy: @@ -49,10 +50,12 @@ jobs: - name: Make working-directory: ./build - run: make -j 2 + run: make -j 4 - - name: CTest + - name: CTest Quick working-directory: ./build - run: ctest -j 2 -L quick + run: ctest -j 4 -L quick - + - name: CTest Long + working-directory: ./build + run: ctest -j 4 -L long From 65dd03aabe4e38a1b2f6cc152cf9fa9f488c185f Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Tue, 9 Jul 2024 21:05:25 -0700 Subject: [PATCH 160/167] Remove numpy dependence from Cube OUTLOG check [no ci] --- tests/Cube/check.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/Cube/check.py b/tests/Cube/check.py index 1956e0ee6..b6b0a3eef 100644 --- a/tests/Cube/check.py +++ b/tests/Cube/check.py @@ -1,15 +1,26 @@ -import numpy as np +# open the output log file +file = open("OUTLOG.runS") -# Read the output log file -data = np.loadtxt("OUTLOG.runS", skiprows=6, delimiter="|") +n = 0 # Count lines +mean = [0.0, 0.0, 0.0] # Mean positions -# Columns 4, 5, 6 is mean position -x = np.mean(data[:,3]) -y = np.mean(data[:,4]) -z = np.mean(data[:,5]) +# Open the output log file +# +while (line := file.readline()) != "": + if n >= 6: # Skip the header stuff + v = [float(x) for x in line.split('|')] + mean[0] += v[3] # x pos + mean[1] += v[4] # y pos + mean[2] += v[5] # z pos + n = n + 1 # Count lines -# If the values are close to 0.5, assume it worked -if np.abs(x - 0.5) > 0.15 or np.abs(y - 0.5) > 0.15 or np.abs(z - 0.5) > 0.15: +if n>6: # Sanity check + for i in range(3): + mean[i] = mean[i]/(n-6) - 0.5 + +# If the squared values are close to 0.0, assume it worked +# +if mean[0]*mean[0] > 0.03 or mean[1]*mean[1] > 0.03 or mean[2]*mean[2] > 0.03: exit(1) else: exit(0) From 2a42b53dace10c0532fbf9ca9d6453f0d63e91e5 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Wed, 10 Jul 2024 07:13:40 -0700 Subject: [PATCH 161/167] Disable long tests for actions --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82c76f90e..71dfb58ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,6 +56,6 @@ jobs: working-directory: ./build run: ctest -j 4 -L quick - - name: CTest Long - working-directory: ./build - run: ctest -j 4 -L long + #- name: CTest Long + #working-directory: ./build + #run: ctest -j 4 -L long From 4572df803227c43af29b46460a7d2ae3a5b80166 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Wed, 10 Jul 2024 11:36:06 -0700 Subject: [PATCH 162/167] Rename runner --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71dfb58ba..bfad99340 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: "Test Builds" +name: "Build and Test" on: push: From 7758e867268bcd5fbad5ea5cc0db9ad60c29d6c0 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Wed, 10 Jul 2024 11:55:53 -0700 Subject: [PATCH 163/167] run tests in serial [no ci] --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfad99340..b9ddde223 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,8 +54,8 @@ jobs: - name: CTest Quick working-directory: ./build - run: ctest -j 4 -L quick + run: ctest -L quick #- name: CTest Long #working-directory: ./build - #run: ctest -j 4 -L long + #run: ctest -L long From f9af374209f6198999ecf5cd5e4991dabac44e6e Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 13 Jul 2024 10:16:51 -0700 Subject: [PATCH 164/167] Number of field elements for *FldStruct creation needs to be Nfld; fix typo in output message string --- expui/Coefficients.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/expui/Coefficients.cc b/expui/Coefficients.cc index 818623760..bccd2675f 100644 --- a/expui/Coefficients.cc +++ b/expui/Coefficients.cc @@ -2452,7 +2452,7 @@ namespace CoefClasses arr = it->second->store; int ldim = (Lmax+1)*(Lmax+2)/2; mat = std::make_shared - (arr.data(), 4, ldim, Nmax); + (arr.data(), Nfld, ldim, Nmax); } return *mat; @@ -2891,7 +2891,7 @@ namespace CoefClasses } else { arr = it->second->store; int mdim = Mmax + 1; - mat = std::make_shared(arr.data(), 3, mdim, Nmax); + mat = std::make_shared(arr.data(), Nfld, mdim, Nmax); } return *mat; @@ -2918,7 +2918,7 @@ namespace CoefClasses if (it == coefs.end()) { std::ostringstream str; - str << "CylNfldCoefs::setMatrix: requested time=" << time << " not found"; + str << "CylFldCoefs::setMatrix: requested time=" << time << " not found"; throw std::runtime_error(str.str()); } else { it->second->allocate(); From b71934f4ac6cba8659bb6b6088fb4c997ad727a6 Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 13 Jul 2024 10:18:30 -0700 Subject: [PATCH 165/167] Add routines to create Eigen::tensor objects from numpy.ndarray objects; needed to set coefficient arrays from pyEXP --- pyEXP/CoefWrappers.cc | 36 +++++++++++++++++------ pyEXP/TensorToArray.H | 66 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/pyEXP/CoefWrappers.cc b/pyEXP/CoefWrappers.cc index b1ef1d91e..cce752970 100644 --- a/pyEXP/CoefWrappers.cc +++ b/pyEXP/CoefWrappers.cc @@ -1321,7 +1321,12 @@ void CoefficientClasses(py::module &m) { SphFldCoefs instance )") .def("__call__", - &CoefClasses::SphFldCoefs::getMatrix, + [](CoefClasses::SphFldCoefs& A, double t) + { + Eigen::Tensor, 3> M = A.getMatrix(t); + py::array_t> ret = make_ndarray>(M); + return ret; + }, R"( Return the coefficient tensor for the desired time. @@ -1342,7 +1347,12 @@ void CoefficientClasses(py::module &m) { )", py::arg("time")) .def("setMatrix", - &CoefClasses::SphFldCoefs::setMatrix, + [](CoefClasses::SphFldCoefs& A, double t, + py::array_t> array) + { + auto M = make_tensor3>(array); + A.setMatrix(t, M); + }, R"( Enter and/or rewrite the coefficient tensor at the provided time @@ -1350,8 +1360,8 @@ void CoefficientClasses(py::module &m) { ---------- time : float snapshot time corresponding to the the coefficient matrix - mat : numpy.ndarray - the new coefficient array. + mat : numpy.ndarray + the new coefficient array. Returns ------- @@ -1396,7 +1406,12 @@ void CoefficientClasses(py::module &m) { CylFldCoefs instance )") .def("__call__", - &CoefClasses::CylFldCoefs::getMatrix, + [](CoefClasses::CylFldCoefs& A, double t) + { + Eigen::Tensor, 3> M = A.getMatrix(t); + py::array_t> ret = make_ndarray>(M); + return ret; + }, R"( Return the coefficient tensor for the desired time. @@ -1417,7 +1432,12 @@ void CoefficientClasses(py::module &m) { )", py::arg("time")) .def("setMatrix", - &CoefClasses::CylFldCoefs::setMatrix, + [](CoefClasses::CylFldCoefs& A, double t, + py::array_t> array) + { + auto M = make_tensor3>(array); + A.setMatrix(t, M); + }, R"( Enter and/or rewrite the coefficient tensor at the provided time @@ -1425,8 +1445,8 @@ void CoefficientClasses(py::module &m) { ---------- time : float snapshot time corresponding to the the coefficient matrix - mat : numpy.ndarray - the new coefficient array. + mat : numpy.ndarray + the new coefficient array. Returns ------- diff --git a/pyEXP/TensorToArray.H b/pyEXP/TensorToArray.H index 086d0922f..f55b1bc34 100644 --- a/pyEXP/TensorToArray.H +++ b/pyEXP/TensorToArray.H @@ -27,6 +27,38 @@ py::array_t make_ndarray(Eigen::Tensor& mat) ); } +template +Eigen::Tensor make_tensor3(py::array_t array) +{ + // Request a buffer descriptor from Python + py::buffer_info buffer_info = array.request(); + + // Get the array dimenions + T *data = static_cast(buffer_info.ptr); + std::vector shape = buffer_info.shape; + + // Check rank + if (shape.size() != 3) { + std::ostringstream sout; + sout << "make_tensor3: tensor rank must be 3, found " + << shape.size(); + throw std::runtime_error(sout.str()); + } + + // Build result tensor with col-major ordering + Eigen::Tensor tensor(shape[0], shape[1], shape[2]); + for (int i=0, l=0; i < shape[0]; i++) { + for (int j=0; j < shape[1]; j++) { + for (int k=0; k < shape[2]; k++) { + tensor(i, j, k) = data[l++]; + } + } + } + + return tensor; +} + + //! Helper function that maps the Eigen::Tensor into an numpy.ndarray template py::array_t make_ndarray4(Eigen::Tensor& mat) @@ -53,4 +85,38 @@ py::array_t make_ndarray4(Eigen::Tensor& mat) ); } +template +Eigen::Tensor make_tensor4(py::array_t array) +{ + // Request a buffer descriptor from Python + py::buffer_info buffer_info = array.request(); + + // Get the array dimenions + T *data = static_cast(buffer_info.ptr); + std::vector shape = buffer_info.shape; + + // Check rank + if (shape.size() != 4) { + std::ostringstream sout; + sout << "make_tensor4: tensor rank must be 4, found " + << shape.size(); + throw std::runtime_error(sout.str()); + } + + // Build result tensor with col-major ordering + Eigen::Tensor tensor(shape[0], shape[1], shape[2], shape[3]); + for (int i=0, l=0; i < shape[0]; i++) { + for (int j=0; j < shape[1]; j++) { + for (int k=0; k < shape[2]; k++) { + for (int l=0; l < shape[3]; k++) { + tensor(i, j, k, l) = data[l++]; + } + } + } + } + + return tensor; +} + + #endif From 7bb8a0783df8118d535e854ba7a0aad8a2f9444e Mon Sep 17 00:00:00 2001 From: "Martin D. Weinberg" Date: Sat, 13 Jul 2024 12:06:48 -0700 Subject: [PATCH 166/167] Write velocity coefficients to 'outdir' for consistency with basis coefficients [no ci] --- src/OutVel.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OutVel.cc b/src/OutVel.cc index cd877e4d3..545d99b0d 100644 --- a/src/OutVel.cc +++ b/src/OutVel.cc @@ -39,7 +39,7 @@ OutVel::OutVel(const YAML::Node& conf) : Output(conf) // Target output file // - outfile = "velcoef." + tcomp->name + "." + runtag; + outfile = outdir + "velcoef." + tcomp->name + "." + runtag; // Check for valid model type // From bc4c6fbaeb2df0bd45788356c15d2870d098c312 Mon Sep 17 00:00:00 2001 From: michael-petersen Date: Thu, 8 Aug 2024 15:05:52 +0100 Subject: [PATCH 167/167] Add missing cassert --- expui/BiorthBess.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expui/BiorthBess.cc b/expui/BiorthBess.cc index 82a0c7593..c73a2b7ec 100644 --- a/expui/BiorthBess.cc +++ b/expui/BiorthBess.cc @@ -3,7 +3,7 @@ #include #include #include - +#include BiorthBess::BiorthBess(double rmax, int lmax, int nmax, int RNUM) : rmax(rmax), lmax(lmax), nmax(nmax), RNUM(RNUM)