From 909b9ca120c2e5f79a37127788c604e8584692cd Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 23 Jul 2020 14:40:46 +0100 Subject: [PATCH 01/41] Consider underscores as word boundaries when updating macros --- bin/bout-v5-macro-upgrader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/bout-v5-macro-upgrader.py b/bin/bout-v5-macro-upgrader.py index 7f79d49ab6..5cd2f83c4f 100755 --- a/bin/bout-v5-macro-upgrader.py +++ b/bin/bout-v5-macro-upgrader.py @@ -274,14 +274,14 @@ def fix_ifdefs(old, source): def fix_always_defined_macros(old, new, source): """Fix '#ifdef's that should become plain '#if' """ - new_source = re.sub(r"#ifdef\s+{}".format(old), r"#if {}".format(new), source) - return re.sub(r"#ifndef\s+{}".format(old), r"#if !{}".format(new), new_source) + new_source = re.sub(r"#ifdef\s+{}\b".format(old), r"#if {}".format(new), source) + return re.sub(r"#ifndef\s+{}\b".format(old), r"#if !{}".format(new), new_source) def fix_replacement(old, new, source): """Straight replacements """ - return re.sub(r'([^"])\b{}\b([^"])'.format(old), r"\1{}\2".format(new), source) + return re.sub(r'([^"_])\b{}\b([^"_])'.format(old), r"\1{}\2".format(new), source) def apply_fixes(replacements, source): From d3b008e34ae58608f658e683cf801fe1e92f6862 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 23 Jul 2020 14:45:48 +0100 Subject: [PATCH 02/41] Fix bug with default file format when using legacy netcdf --- autoconf_build_defines.hxx.in | 3 +++ bin/bout-v5-macro-upgrader.py | 7 +++++++ cmake_build_defines.hxx.in | 3 +++ configure | 24 ++++++++++++++++++++---- configure.ac | 10 +++++++--- include/bout/build_config.hxx | 1 + src/bout++.cxx | 20 ++++++++------------ src/fileio/formatfactory.cxx | 8 ++++---- src/fileio/impls/netcdf/nc_format.cxx | 6 +++--- src/fileio/impls/netcdf/nc_format.hxx | 5 +++-- src/fileio/impls/netcdf4/ncxx4.cxx | 2 +- tests/unit/src/test_bout++.cxx | 2 +- 12 files changed, 61 insertions(+), 30 deletions(-) diff --git a/autoconf_build_defines.hxx.in b/autoconf_build_defines.hxx.in index 9116b37c7f..925fe967ec 100644 --- a/autoconf_build_defines.hxx.in +++ b/autoconf_build_defines.hxx.in @@ -24,6 +24,9 @@ /* LAPACK support */ #undef BOUT_HAS_LAPACK +/* NETCDF support */ +#undef BOUT_HAS_LEGACY_NETCDF + /* NETCDF support */ #undef BOUT_HAS_NETCDF diff --git a/bin/bout-v5-macro-upgrader.py b/bin/bout-v5-macro-upgrader.py index 5cd2f83c4f..16d3b3ed5b 100755 --- a/bin/bout-v5-macro-upgrader.py +++ b/bin/bout-v5-macro-upgrader.py @@ -131,6 +131,13 @@ "macro": True, "always_defined": True, }, + { + "old": "NCDF", + "new": "BOUT_HAS_LEGACY_NETCDF", + "headers": "bout/build_config.hxx", + "macro": True, + "always_defined": True, + }, { "old": "HDF5", "new": "BOUT_HAS_HDF5", diff --git a/cmake_build_defines.hxx.in b/cmake_build_defines.hxx.in index 4339b24761..90892a5b38 100644 --- a/cmake_build_defines.hxx.in +++ b/cmake_build_defines.hxx.in @@ -27,4 +27,7 @@ #cmakedefine01 BOUT_USE_SIGNAL #cmakedefine01 BOUT_USE_TRACK +// CMake build does not support legacy interface +#define BOUT_HAS_LEGACY_NETCDF 0 + #endif // BOUT_BUILD_CONFIG_HXX diff --git a/configure b/configure index 9d65639c7a..4b23e54ddd 100755 --- a/configure +++ b/configure @@ -643,6 +643,7 @@ BOUT_HAS_PVODE BOUT_HAS_PRETTY_FUNCTION BOUT_HAS_PNETCDF BOUT_HAS_PETSC +BOUT_HAS_LEGACY_NETCDF BOUT_HAS_NETCDF BOUT_HAS_LAPACK BOUT_HAS_IDA @@ -6610,6 +6611,8 @@ fi NCCONF="" # Configuration script +BOUT_HAS_NETCDF=no +BOUT_HAS_LEGACY_NETCDF=no if test "x$with_netcdf" != "xno"; then : ########################################## @@ -6763,7 +6766,8 @@ ac_compiler_gnu=$ac_cv_cxx_compiler_gnu LIBS=$save_LIBS LDFLAGS=$save_LDFLAGS - CXXFLAGS="$save_CXXFLAGS -DNCDF" + CXXFLAGS="$save_CXXFLAGS" + BOUT_HAS_LEGACY_NETCDF=yes fi EXTRA_LIBS="$EXTRA_LIBS $NCLIB" @@ -7176,7 +7180,7 @@ fi { $as_echo "$as_me:${as_lineno-$LINENO}: -> Legacy NetCDF support enabled" >&5 $as_echo "$as_me: -> Legacy NetCDF support enabled" >&6;} NCPATH="found" - CXXFLAGS="$CXXFLAGS -DNCDF" + BOUT_HAS_LEGACY_NETCDF=yes fi @@ -16163,6 +16167,18 @@ fi +if test "x$BOUT_HAS_LEGACY_NETCDF" = "xyes"; then : + +$as_echo "#define BOUT_HAS_LEGACY_NETCDF 1" >>confdefs.h + +else + +$as_echo "#define BOUT_HAS_LEGACY_NETCDF 0" >>confdefs.h + +fi + + + if test "x$BOUT_HAS_PETSC" = "xyes"; then : $as_echo "#define BOUT_HAS_PETSC 1" >>confdefs.h @@ -17839,8 +17855,8 @@ $as_echo "$as_me: CVODE support : $BOUT_HAS_CVODE" >&6;} $as_echo "$as_me: ARKODE support : $BOUT_HAS_ARKODE" >&6;} { $as_echo "$as_me:${as_lineno-$LINENO}: FFTW support : $BOUT_HAS_FFTW" >&5 $as_echo "$as_me: FFTW support : $BOUT_HAS_FFTW" >&6;} -{ $as_echo "$as_me:${as_lineno-$LINENO}: NetCDF support : $BOUT_HAS_NETCDF" >&5 -$as_echo "$as_me: NetCDF support : $BOUT_HAS_NETCDF" >&6;} +{ $as_echo "$as_me:${as_lineno-$LINENO}: NetCDF support : $BOUT_HAS_NETCDF (legacy: $BOUT_HAS_LEGACY_NETCDF)" >&5 +$as_echo "$as_me: NetCDF support : $BOUT_HAS_NETCDF (legacy: $BOUT_HAS_LEGACY_NETCDF)" >&6;} { $as_echo "$as_me:${as_lineno-$LINENO}: Parallel-NetCDF support : $BOUT_HAS_PNETCDF" >&5 $as_echo "$as_me: Parallel-NetCDF support : $BOUT_HAS_PNETCDF" >&6;} { $as_echo "$as_me:${as_lineno-$LINENO}: HDF5 support : $BOUT_HAS_HDF5 (parallel: $BOUT_HAS_PHDF5)" >&5 diff --git a/configure.ac b/configure.ac index ef4c25ec12..697debda93 100644 --- a/configure.ac +++ b/configure.ac @@ -478,6 +478,8 @@ BOUT_HAS_FFTW="no" NCCONF="" # Configuration script +BOUT_HAS_NETCDF=no +BOUT_HAS_LEGACY_NETCDF=no AS_IF([test "x$with_netcdf" != "xno"], [ ########################################## @@ -543,7 +545,8 @@ AS_IF([test "x$with_netcdf" != "xno"], AC_LANG_POP([C++]) LIBS=$save_LIBS LDFLAGS=$save_LDFLAGS - CXXFLAGS="$save_CXXFLAGS -DNCDF" + CXXFLAGS="$save_CXXFLAGS" + BOUT_HAS_LEGACY_NETCDF=yes ]) EXTRA_LIBS="$EXTRA_LIBS $NCLIB" @@ -564,7 +567,7 @@ AS_IF([test "x$with_netcdf" != "xno"], file_formats="$file_formats netCDF" AC_MSG_NOTICE([ -> Legacy NetCDF support enabled]) NCPATH="found" - CXXFLAGS="$CXXFLAGS -DNCDF" + BOUT_HAS_LEGACY_NETCDF=yes ], []) ]) @@ -1292,6 +1295,7 @@ BOUT_DEFINE_SUBST(BOUT_HAS_HDF5, [HDF5 support]) BOUT_DEFINE_SUBST(BOUT_HAS_IDA, [IDA support]) BOUT_DEFINE_SUBST(BOUT_HAS_LAPACK, [LAPACK support]) BOUT_DEFINE_SUBST(BOUT_HAS_NETCDF, [NETCDF support]) +BOUT_DEFINE_SUBST(BOUT_HAS_LEGACY_NETCDF, [NETCDF support]) BOUT_DEFINE_SUBST(BOUT_HAS_PETSC, [PETSc support]) BOUT_DEFINE_SUBST(BOUT_HAS_PNETCDF, [PNETCDF support]) BOUT_DEFINE_SUBST(BOUT_HAS_PRETTY_FUNCTION, [Compiler PRETTYFUNCTION support]) @@ -1338,7 +1342,7 @@ AC_MSG_NOTICE([ IDA support : $BOUT_HAS_IDA]) AC_MSG_NOTICE([ CVODE support : $BOUT_HAS_CVODE]) AC_MSG_NOTICE([ ARKODE support : $BOUT_HAS_ARKODE]) AC_MSG_NOTICE([ FFTW support : $BOUT_HAS_FFTW]) -AC_MSG_NOTICE([ NetCDF support : $BOUT_HAS_NETCDF]) +AC_MSG_NOTICE([ NetCDF support : $BOUT_HAS_NETCDF (legacy: $BOUT_HAS_LEGACY_NETCDF)]) AC_MSG_NOTICE([ Parallel-NetCDF support : $BOUT_HAS_PNETCDF]) AC_MSG_NOTICE([ HDF5 support : $BOUT_HAS_HDF5 (parallel: $BOUT_HAS_PHDF5)]) AC_MSG_NOTICE([ Lapack support : $BOUT_HAS_LAPACK]) diff --git a/include/bout/build_config.hxx b/include/bout/build_config.hxx index 138fbdd8e2..dee095a015 100644 --- a/include/bout/build_config.hxx +++ b/include/bout/build_config.hxx @@ -17,6 +17,7 @@ constexpr auto has_gettext = static_cast(BOUT_HAS_GETTEXT); constexpr auto has_hdf5 = static_cast(BOUT_HAS_HDF5); constexpr auto has_lapack = static_cast(BOUT_HAS_LAPACK); constexpr auto has_netcdf = static_cast(BOUT_HAS_NETCDF); +constexpr auto has_legacy_netcdf = static_cast(BOUT_HAS_LEGACY_NETCDF); constexpr auto has_petsc = static_cast(BOUT_HAS_PETSC); constexpr auto has_pretty_function = static_cast(BOUT_HAS_PRETTY_FUNCTION); constexpr auto has_pvode = static_cast(BOUT_HAS_PVODE); diff --git a/src/bout++.cxx b/src/bout++.cxx index 6af4c6d922..dac6b13996 100644 --- a/src/bout++.cxx +++ b/src/bout++.cxx @@ -478,16 +478,6 @@ void printCompileTimeOptions() { } output_info.write("\n"); -#ifdef NCDF - output_info.write(_("\tnetCDF support enabled\n")); -#else -#if BOUT_HAS_NETCDF - output_info.write(_("\tnetCDF4 support enabled\n")); -#else - output_info.write(_("\tnetCDF support disabled\n")); -#endif -#endif - #ifdef PNCDF output_info.write(_("\tParallel NetCDF support enabled\n")); #else @@ -502,7 +492,10 @@ void printCompileTimeOptions() { output_info.write(_("\tNatural language support {}\n"), is_enabled(has_gettext)); output_info.write(_("\tHDF5 support {}\n"), is_enabled(has_hdf5)); output_info.write(_("\tLAPACK support {}\n"), is_enabled(has_lapack)); - output_info.write(_("\tNetCDF support {}\n"), is_enabled(has_netcdf)); + // Horrible nested ternary to set this at compile time + constexpr auto netcdf_flavour = + (has_netcdf or has_legacy_netcdf) ? (has_netcdf ? " (NetCDF4)" : " (Legacy)") : ""; + output_info.write(_("\tNetCDF support {}{}\n"), is_enabled(has_netcdf), netcdf_flavour); output_info.write(_("\tPETSc support {}\n"), is_enabled(has_petsc)); output_info.write(_("\tPretty function name support {}\n"), is_enabled(has_pretty_function)); @@ -627,7 +620,8 @@ Datafile setupDumpFile(Options& options, Mesh& mesh, const std::string& data_dir .withDefault(false); // Get file extensions - const auto default_dump_format = bout::build::has_netcdf ? "nc" : "h5"; + const auto default_dump_format = + bout::build::has_netcdf or bout::build::has_legacy_netcdf ? "nc" : "h5"; const auto dump_ext = options["dump_format"].withDefault(default_dump_format); output_progress << "Setting up output (dump) file\n"; @@ -654,6 +648,8 @@ Datafile setupDumpFile(Options& options, Mesh& mesh, const std::string& data_dir dump_file.addOnce(const_cast(bout::build::has_hdf5), "has_hdf5"); dump_file.addOnce(const_cast(bout::build::has_lapack), "has_lapack"); dump_file.addOnce(const_cast(bout::build::has_netcdf), "has_netcdf"); + dump_file.addOnce(const_cast(bout::build::has_legacy_netcdf), + "has_legacy_netcdf"); dump_file.addOnce(const_cast(bout::build::has_petsc), "has_petsc"); dump_file.addOnce(const_cast(bout::build::has_pretty_function), "has_pretty_function"); diff --git a/src/fileio/formatfactory.cxx b/src/fileio/formatfactory.cxx index 8e6c840254..d8dc4aad37 100644 --- a/src/fileio/formatfactory.cxx +++ b/src/fileio/formatfactory.cxx @@ -44,7 +44,7 @@ std::unique_ptr FormatFactory::createDataFormat(const char *filename return bout::utils::make_unique(mesh_in); #else -#ifdef NCDF +#if BOUT_HAS_LEGACY_NETCDF return bout::utils::make_unique(mesh_in); #else @@ -54,8 +54,8 @@ std::unique_ptr FormatFactory::createDataFormat(const char *filename #error No file format available; aborting. -#endif // HDF5 -#endif // NCDF +#endif // BOUT_HAS_HDF5 +#endif // BOUT_HAS_LEGACY_NETCDF #endif // BOUT_HAS_NETCDF #endif // PNCDF throw BoutException("Parallel I/O disabled, no serial library found"); @@ -92,7 +92,7 @@ std::unique_ptr FormatFactory::createDataFormat(const char *filename } #endif -#ifdef NCDF +#if BOUT_HAS_LEGACY_NETCDF const char *ncdf_match[] = {"cdl", "nc", "ncdf"}; if(matchString(s, 3, ncdf_match) != -1) { output.write("\tUsing NetCDF format for file '{:s}'\n", filename); diff --git a/src/fileio/impls/netcdf/nc_format.cxx b/src/fileio/impls/netcdf/nc_format.cxx index c9610a9453..6b7b04c881 100644 --- a/src/fileio/impls/netcdf/nc_format.cxx +++ b/src/fileio/impls/netcdf/nc_format.cxx @@ -23,12 +23,13 @@ #include #include "nc_format.hxx" -#ifdef NCDF +#if BOUT_HAS_LEGACY_NETCDF #include #include #include +#include "bout/build_config.hxx" #include #include @@ -1362,5 +1363,4 @@ void NcFormat::checkName(const char* name) { } } -#endif // NCDF - +#endif // BOUT_HAS_LEGACY_NETCDF diff --git a/src/fileio/impls/netcdf/nc_format.hxx b/src/fileio/impls/netcdf/nc_format.hxx index f8592eb599..5c24a4a041 100644 --- a/src/fileio/impls/netcdf/nc_format.hxx +++ b/src/fileio/impls/netcdf/nc_format.hxx @@ -1,3 +1,4 @@ +#include "bout/build_config.hxx" /*! * \file nc_format.hxx * @@ -33,7 +34,7 @@ * */ -#ifndef NCDF +#if !BOUT_HAS_LEGACY_NETCDF #include "../emptyformat.hxx" using NcFormat = EmptyFormat; @@ -159,4 +160,4 @@ class NcFormat : public DataFormat { #endif // __NCFORMAT_H__ -#endif // NCDF +#endif // BOUT_HAS_LEGACY_NETCDF diff --git a/src/fileio/impls/netcdf4/ncxx4.cxx b/src/fileio/impls/netcdf4/ncxx4.cxx index 5cd84f0bb9..f5e2ea27f9 100644 --- a/src/fileio/impls/netcdf4/ncxx4.cxx +++ b/src/fileio/impls/netcdf4/ncxx4.cxx @@ -1174,5 +1174,5 @@ std::vector Ncxx4::getRecDimVec(int nd) { return vec; } -#endif // NCDF +#endif // BOUT_HAS_NETCDF diff --git a/tests/unit/src/test_bout++.cxx b/tests/unit/src/test_bout++.cxx index 83be339061..a33f20be88 100644 --- a/tests/unit/src/test_bout++.cxx +++ b/tests/unit/src/test_bout++.cxx @@ -302,7 +302,7 @@ TEST_F(PrintStartupTest, CompileTimeOptions) { EXPECT_TRUE(IsSubString(buffer.str(), _("Compile-time options:\n"))); EXPECT_TRUE(IsSubString(buffer.str(), _("Signal"))); - EXPECT_TRUE(IsSubString(buffer.str(), "netCDF")); + EXPECT_TRUE(IsSubString(buffer.str(), "NetCDF")); EXPECT_TRUE(IsSubString(buffer.str(), "OpenMP")); EXPECT_TRUE(IsSubString(buffer.str(), _("Compiled with flags"))); } From 9a22c56821374f2b9569b008390801fd181e8696 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 20 Aug 2020 13:57:05 +0100 Subject: [PATCH 03/41] Change meaning of bout::build::has_netcdf to mean either interface --- include/bout/build_config.hxx | 2 +- src/bout++.cxx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/include/bout/build_config.hxx b/include/bout/build_config.hxx index dee095a015..ff96f6927c 100644 --- a/include/bout/build_config.hxx +++ b/include/bout/build_config.hxx @@ -16,8 +16,8 @@ constexpr auto has_fftw = static_cast(BOUT_HAS_FFTW); constexpr auto has_gettext = static_cast(BOUT_HAS_GETTEXT); constexpr auto has_hdf5 = static_cast(BOUT_HAS_HDF5); constexpr auto has_lapack = static_cast(BOUT_HAS_LAPACK); -constexpr auto has_netcdf = static_cast(BOUT_HAS_NETCDF); constexpr auto has_legacy_netcdf = static_cast(BOUT_HAS_LEGACY_NETCDF); +constexpr auto has_netcdf = static_cast(BOUT_HAS_NETCDF) or has_legacy_netcdf; constexpr auto has_petsc = static_cast(BOUT_HAS_PETSC); constexpr auto has_pretty_function = static_cast(BOUT_HAS_PRETTY_FUNCTION); constexpr auto has_pvode = static_cast(BOUT_HAS_PVODE); diff --git a/src/bout++.cxx b/src/bout++.cxx index dac6b13996..9ce961321f 100644 --- a/src/bout++.cxx +++ b/src/bout++.cxx @@ -494,7 +494,7 @@ void printCompileTimeOptions() { output_info.write(_("\tLAPACK support {}\n"), is_enabled(has_lapack)); // Horrible nested ternary to set this at compile time constexpr auto netcdf_flavour = - (has_netcdf or has_legacy_netcdf) ? (has_netcdf ? " (NetCDF4)" : " (Legacy)") : ""; + has_netcdf ? (has_legacy_netcdf ? " (Legacy)" : " (NetCDF4)") : ""; output_info.write(_("\tNetCDF support {}{}\n"), is_enabled(has_netcdf), netcdf_flavour); output_info.write(_("\tPETSc support {}\n"), is_enabled(has_petsc)); output_info.write(_("\tPretty function name support {}\n"), @@ -620,8 +620,7 @@ Datafile setupDumpFile(Options& options, Mesh& mesh, const std::string& data_dir .withDefault(false); // Get file extensions - const auto default_dump_format = - bout::build::has_netcdf or bout::build::has_legacy_netcdf ? "nc" : "h5"; + constexpr auto default_dump_format = bout::build::has_netcdf ? "nc" : "h5"; const auto dump_ext = options["dump_format"].withDefault(default_dump_format); output_progress << "Setting up output (dump) file\n"; From 2e40ad2470129c5bf87dde3a1e7470bb6d594c80 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 20 Aug 2020 14:05:07 +0100 Subject: [PATCH 04/41] Don't repeat default values for previously set options `datadir` and `dump_format` are set in `BoutInitialise`, so must already have values. Setting a default value is either redundant or inconsistent --- src/physics/physicsmodel.cxx | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/physics/physicsmodel.cxx b/src/physics/physicsmodel.cxx index 473310898f..0886fd1aa9 100644 --- a/src/physics/physicsmodel.cxx +++ b/src/physics/physicsmodel.cxx @@ -97,23 +97,17 @@ int PhysicsModel::postInit(bool restarting) { // Second argument specifies no time history solver->outputVars(restart, false); - std::string restart_dir; ///< Directory for restart files - std::string dump_ext, restart_ext; ///< Dump, Restart file extension - - Options *options = Options::getRoot(); - if (options->isSet("restartdir")) { - // Solver-specific restart directory - options->get("restartdir", restart_dir, "data"); - } else { - // Use the root data directory - options->get("datadir", restart_dir, "data"); - } - /// Get restart file extension - const auto default_dump_format = bout::build::has_netcdf ? "nc" : "h5"; - options->get("dump_format", dump_ext, default_dump_format); - options->get("restart_format", restart_ext, dump_ext); + auto& options = Options::root(); + + const std::string restart_dir = options["restartdir"] + .doc("Directory for restart files") + .withDefault(options["datadir"]); + + const std::string restart_ext = options["restart_format"] + .doc("Restart file extension") + .withDefault(options["dump_format"]); - std::string filename = restart_dir + "/BOUT.restart."+restart_ext; + const std::string filename = restart_dir + "/BOUT.restart." + restart_ext; if (restarting) { output.write("Loading restart file: {:s}\n", filename); From 796c08af5992afeb1141f3916091c5030229f87c Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Thu, 20 Aug 2020 14:10:15 +0100 Subject: [PATCH 05/41] Add docstring for dump_format --- src/bout++.cxx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bout++.cxx b/src/bout++.cxx index 9ce961321f..8d652624e9 100644 --- a/src/bout++.cxx +++ b/src/bout++.cxx @@ -621,7 +621,9 @@ Datafile setupDumpFile(Options& options, Mesh& mesh, const std::string& data_dir // Get file extensions constexpr auto default_dump_format = bout::build::has_netcdf ? "nc" : "h5"; - const auto dump_ext = options["dump_format"].withDefault(default_dump_format); + const auto dump_ext = options["dump_format"] + .doc("File extension for output files") + .withDefault(default_dump_format); output_progress << "Setting up output (dump) file\n"; auto dump_file = Datafile(&(options["output"]), &mesh); From a0efee4adb1cbf8e5e691188c3a23c292d2e0ad1 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 22 Sep 2020 16:08:01 +0100 Subject: [PATCH 06/41] Fix test-solver not setting some expected options Removing the redundant default options in physicsmodel affects things that don't set the default options... My comeuppance for not using BoutInitialise! --- tests/integrated/test-solver/test_solver.cxx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integrated/test-solver/test_solver.cxx b/tests/integrated/test-solver/test_solver.cxx index 1f56354fce..8ad79add17 100644 --- a/tests/integrated/test-solver/test_solver.cxx +++ b/tests/integrated/test-solver/test_solver.cxx @@ -62,6 +62,8 @@ int main(int argc, char** argv) { root["output"]["enabled"] = false; root["restart"]["enabled"] = false; + root["datadir"] = "data"; + root["dump_format"] = "nc"; // Set the command-line arguments SlepcLib::setArgs(argc, argv); From c9b55f2d70d93594b8abe8e63d8b590b0f424c6b Mon Sep 17 00:00:00 2001 From: John Omotani Date: Sun, 3 Jan 2021 17:38:09 +0100 Subject: [PATCH 07/41] Use bout_type="string" for strings in H5Format ... to be consistent with type definitions in the Python tools introduced in https://github.com/boutproject/boututils/pull/15 --- src/fileio/impls/hdf5/h5_format.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fileio/impls/hdf5/h5_format.cxx b/src/fileio/impls/hdf5/h5_format.cxx index 940c773263..ef7e9ab7f4 100644 --- a/src/fileio/impls/hdf5/h5_format.cxx +++ b/src/fileio/impls/hdf5/h5_format.cxx @@ -262,6 +262,7 @@ bool H5Format::addVar(const std::string &name, bool repeat, hid_t write_hdf5_typ int nd = 0; if (datatype == "scalar") nd = 0; else if (datatype == "vector") nd = 1; + else if (datatype == "string") nd = 1; else if (datatype == "FieldX") nd = 1; else if (datatype == "Field2D") nd = 2; else if (datatype == "FieldPerp") nd = 2; @@ -400,7 +401,7 @@ bool H5Format::addVarIntVec(const std::string &name, bool repeat, size_t size) { } bool H5Format::addVarString(const std::string &name, bool repeat, size_t size) { - return addVar(name, repeat, H5T_C_S1, "vector", size); + return addVar(name, repeat, H5T_C_S1, "string", size); } bool H5Format::addVarBoutReal(const std::string &name, bool repeat) { From ed2453faa4a5f27d5f21d645e4c0ab46739e5502 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Sun, 3 Jan 2021 17:51:20 +0100 Subject: [PATCH 08/41] clang-tidy fixes --- src/fileio/impls/hdf5/h5_format.cxx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/fileio/impls/hdf5/h5_format.cxx b/src/fileio/impls/hdf5/h5_format.cxx index ef7e9ab7f4..97c895db31 100644 --- a/src/fileio/impls/hdf5/h5_format.cxx +++ b/src/fileio/impls/hdf5/h5_format.cxx @@ -260,14 +260,19 @@ bool H5Format::addVar(const std::string &name, bool repeat, hid_t write_hdf5_typ } int nd = 0; - if (datatype == "scalar") nd = 0; - else if (datatype == "vector") nd = 1; - else if (datatype == "string") nd = 1; - else if (datatype == "FieldX") nd = 1; - else if (datatype == "Field2D") nd = 2; - else if (datatype == "FieldPerp") nd = 2; - else if (datatype == "Field3D") nd = 3; - else throw BoutException("Unrecognized datatype '"+datatype+"'"); + if (datatype == "scalar") { + nd = 0; + } else if (datatype == "vector" or datatype == "string" or datatype == "FieldX") { + nd = 1; + } else if (datatype == "Field2D") { + nd = 2; + } else if (datatype == "FieldPerp") { + nd = 2; + } else if (datatype == "Field3D") { + nd = 3; + } else throw { + BoutException("Unrecognized datatype '"+datatype+"'"); + } if (repeat) { // add time dimension From 566be3aa79207f9594399f60467012f72da9a83e Mon Sep 17 00:00:00 2001 From: John Omotani Date: Sun, 3 Jan 2021 18:07:42 +0100 Subject: [PATCH 09/41] Fix typo --- src/fileio/impls/hdf5/h5_format.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fileio/impls/hdf5/h5_format.cxx b/src/fileio/impls/hdf5/h5_format.cxx index 97c895db31..1d113565af 100644 --- a/src/fileio/impls/hdf5/h5_format.cxx +++ b/src/fileio/impls/hdf5/h5_format.cxx @@ -270,8 +270,8 @@ bool H5Format::addVar(const std::string &name, bool repeat, hid_t write_hdf5_typ nd = 2; } else if (datatype == "Field3D") { nd = 3; - } else throw { - BoutException("Unrecognized datatype '"+datatype+"'"); + } else { + throw BoutException("Unrecognized datatype '"+datatype+"'"); } if (repeat) { From 44e7a2370368a1588b38cbf0ad373002dd014a14 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Sun, 3 Jan 2021 18:25:17 +0100 Subject: [PATCH 10/41] clang-tidy suggestion --- src/fileio/impls/hdf5/h5_format.cxx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fileio/impls/hdf5/h5_format.cxx b/src/fileio/impls/hdf5/h5_format.cxx index 1d113565af..002b83bf62 100644 --- a/src/fileio/impls/hdf5/h5_format.cxx +++ b/src/fileio/impls/hdf5/h5_format.cxx @@ -264,9 +264,7 @@ bool H5Format::addVar(const std::string &name, bool repeat, hid_t write_hdf5_typ nd = 0; } else if (datatype == "vector" or datatype == "string" or datatype == "FieldX") { nd = 1; - } else if (datatype == "Field2D") { - nd = 2; - } else if (datatype == "FieldPerp") { + } else if (datatype == "Field2D" or datatype == "FieldPerp") { nd = 2; } else if (datatype == "Field3D") { nd = 3; From d54f45b7c6e6fd0f894e11ee316e0eb9625b3134 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Sun, 3 Jan 2021 20:12:09 +0100 Subject: [PATCH 11/41] Update expected results of test-io_hdf5 'bout_type' of svar and svar_evol in the benchmark data need to be 'string' and 'string_t' now. --- .../test-io_hdf5/data/benchmark.out.0.hdf5 | Bin 727950 -> 727950 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/integrated/test-io_hdf5/data/benchmark.out.0.hdf5 b/tests/integrated/test-io_hdf5/data/benchmark.out.0.hdf5 index 80c74f7ad0e3a0984b82062c4c618a2215bece43..2bbc88cbd6ad46ef4c92a828ee7a16d8f5a4bf16 100644 GIT binary patch delta 132 zcmeC%uG6<&r=f+hg=q`(8B^vTVxOj;H)Zx^WNDW*V+LXn%?iY9K+F!r96-#uUDk|i ze=PG0pyK Date: Sun, 3 Jan 2021 13:41:29 +0100 Subject: [PATCH 12/41] Replace boutdata and boututils directories with submodules Get boutdata and boututils from the separate git repos (github.com/boutproject/boutdata and github.com/boutproject/boututils), including them as submodules --- .gitmodules | 6 + externalpackages/boutdata | 1 + externalpackages/boututils | 1 + tools/pylib/boutdata | 1 + tools/pylib/boutdata/__init__.py | 14 - tools/pylib/boutdata/cbdtoeqdsk.py | 26 - tools/pylib/boutdata/collect.py | 817 ------------ tools/pylib/boutdata/data.py | 1276 ------------------- tools/pylib/boutdata/gen_surface.py | 154 --- tools/pylib/boutdata/griddata.py | 493 ------- tools/pylib/boutdata/input.py | 58 - tools/pylib/boutdata/mayavi2.py | 118 -- tools/pylib/boutdata/mms.py | 591 --------- tools/pylib/boutdata/pol_slice.py | 110 -- tools/pylib/boutdata/processor_rearrange.py | 161 --- tools/pylib/boutdata/restart.py | 829 ------------ tools/pylib/boutdata/settings.py | 92 -- tools/pylib/boutdata/shiftz.py | 91 -- tools/pylib/boutdata/squashoutput.py | 160 --- tools/pylib/boututils | 1 + tools/pylib/boututils/View3D.py | 390 ------ tools/pylib/boututils/__init__.py | 42 - tools/pylib/boututils/analyse_equil_2.py | 270 ---- tools/pylib/boututils/anim.py | 115 -- tools/pylib/boututils/ask.py | 53 - tools/pylib/boututils/boutarray.py | 73 -- tools/pylib/boututils/boutgrid.py | 139 -- tools/pylib/boututils/boutwarnings.py | 19 - tools/pylib/boututils/bunch.py | 6 - tools/pylib/boututils/calculus.py | 252 ---- tools/pylib/boututils/check_scaling.py | 90 -- tools/pylib/boututils/closest_line.py | 14 - tools/pylib/boututils/contour.py | 80 -- tools/pylib/boututils/crosslines.py | 1 - tools/pylib/boututils/datafile.py | 955 -------------- tools/pylib/boututils/efit_analyzer.py | 420 ------ tools/pylib/boututils/fft_deriv.py | 46 - tools/pylib/boututils/fft_integrate.py | 64 - tools/pylib/boututils/file_import.py | 27 - tools/pylib/boututils/geqdsk.py | 1 - tools/pylib/boututils/idl_tabulate.py | 15 - tools/pylib/boututils/int_func.py | 53 - tools/pylib/boututils/linear_regression.py | 27 - tools/pylib/boututils/local_min_max.py | 1 - tools/pylib/boututils/mode_structure.py | 417 ------ tools/pylib/boututils/moment_xyzt.py | 74 -- tools/pylib/boututils/options.py | 165 --- tools/pylib/boututils/plotdata.py | 90 -- tools/pylib/boututils/plotpolslice.py | 143 --- tools/pylib/boututils/radial_grid.py | 68 - tools/pylib/boututils/read_geqdsk.py | 90 -- tools/pylib/boututils/run_wrapper.py | 332 ----- tools/pylib/boututils/showdata.py | 702 ---------- tools/pylib/boututils/spectrogram.py | 163 --- tools/pylib/boututils/surface_average.py | 93 -- tools/pylib/boututils/volume_integral.py | 102 -- tools/pylib/boututils/watch.py | 84 -- 57 files changed, 10 insertions(+), 10666 deletions(-) create mode 160000 externalpackages/boutdata create mode 160000 externalpackages/boututils create mode 120000 tools/pylib/boutdata delete mode 100644 tools/pylib/boutdata/__init__.py delete mode 100644 tools/pylib/boutdata/cbdtoeqdsk.py delete mode 100644 tools/pylib/boutdata/collect.py delete mode 100644 tools/pylib/boutdata/data.py delete mode 100644 tools/pylib/boutdata/gen_surface.py delete mode 100644 tools/pylib/boutdata/griddata.py delete mode 100644 tools/pylib/boutdata/input.py delete mode 100644 tools/pylib/boutdata/mayavi2.py delete mode 100644 tools/pylib/boutdata/mms.py delete mode 100644 tools/pylib/boutdata/pol_slice.py delete mode 100644 tools/pylib/boutdata/processor_rearrange.py delete mode 100644 tools/pylib/boutdata/restart.py delete mode 100644 tools/pylib/boutdata/settings.py delete mode 100644 tools/pylib/boutdata/shiftz.py delete mode 100644 tools/pylib/boutdata/squashoutput.py create mode 120000 tools/pylib/boututils delete mode 100644 tools/pylib/boututils/View3D.py delete mode 100644 tools/pylib/boututils/__init__.py delete mode 100644 tools/pylib/boututils/analyse_equil_2.py delete mode 100755 tools/pylib/boututils/anim.py delete mode 100644 tools/pylib/boututils/ask.py delete mode 100644 tools/pylib/boututils/boutarray.py delete mode 100755 tools/pylib/boututils/boutgrid.py delete mode 100644 tools/pylib/boututils/boutwarnings.py delete mode 100644 tools/pylib/boututils/bunch.py delete mode 100644 tools/pylib/boututils/calculus.py delete mode 100644 tools/pylib/boututils/check_scaling.py delete mode 100644 tools/pylib/boututils/closest_line.py delete mode 100644 tools/pylib/boututils/contour.py delete mode 120000 tools/pylib/boututils/crosslines.py delete mode 100644 tools/pylib/boututils/datafile.py delete mode 100644 tools/pylib/boututils/efit_analyzer.py delete mode 100644 tools/pylib/boututils/fft_deriv.py delete mode 100644 tools/pylib/boututils/fft_integrate.py delete mode 100644 tools/pylib/boututils/file_import.py delete mode 120000 tools/pylib/boututils/geqdsk.py delete mode 100644 tools/pylib/boututils/idl_tabulate.py delete mode 100644 tools/pylib/boututils/int_func.py delete mode 100644 tools/pylib/boututils/linear_regression.py delete mode 120000 tools/pylib/boututils/local_min_max.py delete mode 100644 tools/pylib/boututils/mode_structure.py delete mode 100644 tools/pylib/boututils/moment_xyzt.py delete mode 100644 tools/pylib/boututils/options.py delete mode 100644 tools/pylib/boututils/plotdata.py delete mode 100644 tools/pylib/boututils/plotpolslice.py delete mode 100644 tools/pylib/boututils/radial_grid.py delete mode 100644 tools/pylib/boututils/read_geqdsk.py delete mode 100644 tools/pylib/boututils/run_wrapper.py delete mode 100644 tools/pylib/boututils/showdata.py delete mode 100644 tools/pylib/boututils/spectrogram.py delete mode 100644 tools/pylib/boututils/surface_average.py delete mode 100644 tools/pylib/boututils/volume_integral.py delete mode 100644 tools/pylib/boututils/watch.py diff --git a/.gitmodules b/.gitmodules index 897ed08f82..436e03dd4d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,9 @@ [submodule "externalpackages/fmt"] path = externalpackages/fmt url = https://github.com/fmtlib/fmt.git +[submodule "externalpackages/boutdata"] + path = externalpackages/boutdata + url = https://github.com/boutproject/boutdata.git +[submodule "externalpackages/boututils"] + path = externalpackages/boututils + url = https://github.com/boutproject/boututils.git diff --git a/externalpackages/boutdata b/externalpackages/boutdata new file mode 160000 index 0000000000..46fe888a08 --- /dev/null +++ b/externalpackages/boutdata @@ -0,0 +1 @@ +Subproject commit 46fe888a080406c8364a53974f5b629a55241dce diff --git a/externalpackages/boututils b/externalpackages/boututils new file mode 160000 index 0000000000..9b17a3cfe7 --- /dev/null +++ b/externalpackages/boututils @@ -0,0 +1 @@ +Subproject commit 9b17a3cfe7d53839873418b41902e186ac5fef45 diff --git a/tools/pylib/boutdata b/tools/pylib/boutdata new file mode 120000 index 0000000000..1aa0a53311 --- /dev/null +++ b/tools/pylib/boutdata @@ -0,0 +1 @@ +../../externalpackages/boutdata/boutdata/ \ No newline at end of file diff --git a/tools/pylib/boutdata/__init__.py b/tools/pylib/boutdata/__init__.py deleted file mode 100644 index 20b7d0101b..0000000000 --- a/tools/pylib/boutdata/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" Routines for exchanging data to/from BOUT++ """ - -try: - from builtins import str -except ImportError: - raise ImportError("Please install the future module to use Python 2") - -# Import this, as this almost always used when calling this package -from boutdata.collect import collect, attributes - -__all__ = ["attributes", "collect", "gen_surface", "pol_slice"] - -__version__ = '0.1.2' -__name__ = 'boutdata' diff --git a/tools/pylib/boutdata/cbdtoeqdsk.py b/tools/pylib/boutdata/cbdtoeqdsk.py deleted file mode 100644 index ea4c1c4689..0000000000 --- a/tools/pylib/boutdata/cbdtoeqdsk.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import print_function -from boututils.file_import import file_import -import numpy as np - -class Bunch: - pass - -def cbmtogeqdsk(g): - gg=Bunch() - gg.r=g['Rxy'] - gg.z=g['Zxy'] - gg.psi=g['psi'] - gg.pres=g['mu0p'] - gg.qpsi=g['qsafe'] - gg.fpol=g['f'] - gg.nx=g['nx'] - gg.ny=g['ny'] - i=np.argwhere(g['mu0p']==0) - - gg.simagx=gg.psi.min() - gg.sibdry=gg.psi[i[0]] - gg.xlim=0 - gg.ylim=0 - gg.nlim=0 - - return gg diff --git a/tools/pylib/boutdata/collect.py b/tools/pylib/boutdata/collect.py deleted file mode 100644 index 42a7591329..0000000000 --- a/tools/pylib/boutdata/collect.py +++ /dev/null @@ -1,817 +0,0 @@ -from __future__ import print_function -from __future__ import division - -from builtins import str, range - -import os -import sys -import glob - -import numpy as np - -from boututils.datafile import DataFile -from boututils.boutarray import BoutArray - - -def findVar(varname, varlist): - """Find variable name in a list - - First does case insensitive comparison, then - checks for abbreviations. - - Returns the matched string, or raises a ValueError - - Parameters - ---------- - varname : str - Variable name to look for - varlist : list of str - List of possible variable names - - Returns - ------- - str - The closest match to varname in varlist - - """ - # Try a variation on the case - v = [name for name in varlist if name.lower() == varname.lower()] - if len(v) == 1: - # Found case match - print("Variable '%s' not found. Using '%s' instead" % (varname, v[0])) - return v[0] - elif len(v) > 1: - print("Variable '"+varname + - "' not found, and is ambiguous. Could be one of: "+str(v)) - raise ValueError("Variable '"+varname+"' not found") - - # None found. Check if it's an abbreviation - v = [name for name in varlist - if name[:len(varname)].lower() == varname.lower()] - if len(v) == 1: - print("Variable '%s' not found. Using '%s' instead" % (varname, v[0])) - return v[0] - - if len(v) > 1: - print("Variable '"+varname + - "' not found, and is ambiguous. Could be one of: "+str(v)) - raise ValueError("Variable '"+varname+"' not found") - - -def _convert_to_nice_slice(r, N, name="range"): - """Convert r to a "sensible" slice in range [0, N] - - If r is None, the slice corresponds to the full range. - - Lists or tuples of one or two ints are converted to slices. - - Slices with None for one or more arguments have them replaced with - sensible values. - - Private helper function for collect - - Parameters - ---------- - r : None, int, slice or list of int - Range-like to check/convert to slice - N : int - Size of range - name : str, optional - Name of range for error message - - Returns - ------- - slice - "Sensible" slice with no Nones for start, stop or step - """ - - if N == 0: - raise ValueError("No data available in %s"%name) - if r is None: - temp_slice = slice(N) - elif isinstance(r, slice): - temp_slice = r - elif isinstance(r, (int, np.integer)): - if r >= N or r <-N: - # raise out of bounds error as if we'd tried to index the array with r - # without this, would return an empty array instead - raise IndexError(name+" index out of range, value was "+str(r)) - elif r == -1: - temp_slice = slice(r, None) - else: - temp_slice = slice(r, r + 1) - elif len(r) == 0: - return _convert_to_nice_slice(None, N, name) - elif len(r) == 1: - return _convert_to_nice_slice(r[0], N, name) - elif len(r) == 2: - r2 = list(r) - if r2[0] < 0: - r2[0] = r2[0] + N - if r2[1] < 0: - r2[1] = r2[1] + N - if r2[0] > r2[1]: - raise ValueError("{} start ({}) is larger than end ({})" - .format(name, *r2)) - # Lists uses inclusive end, we need exclusive end - temp_slice = slice(r2[0], r2[1] + 1) - elif len(r) == 3: - # Convert 3 element list to slice object - temp_slice = slice(r[0],r[1],r[2]) - else: - raise ValueError("Couldn't convert {} ('{}') to slice".format(name, r)) - - # slice.indices converts None to actual values - return slice(*temp_slice.indices(N)) - - -def collect(varname, xind=None, yind=None, zind=None, tind=None, path=".", - yguards=False, xguards=True, info=True, prefix="BOUT.dmp", - strict=False, tind_auto=False, datafile_cache=None): - """Collect a variable from a set of BOUT++ outputs. - - Parameters - ---------- - varname : str - Name of the variable - xind, yind, zind, tind : int, slice or list of int, optional - Range of X, Y, Z or time indices to collect. Either a single - index to collect, a list containing [start, end] (inclusive - end), or a slice object (usual python indexing). Default is to - fetch all indices - path : str, optional - Path to data files (default: ".") - prefix : str, optional - File prefix (default: "BOUT.dmp") - yguards : bool or "include_upper", optional - Collect Y boundary guard cells? (default: False) - If yguards=="include_upper" the y-boundary cells from the upper (second) target - are also included. - xguards : bool, optional - Collect X boundary guard cells? (default: True) - (Set to True to be consistent with the definition of nx) - info : bool, optional - Print information about collect? (default: True) - strict : bool, optional - Fail if the exact variable name is not found? (default: False) - tind_auto : bool, optional - Read all files, to get the shortest length of time_indices. - Useful if writing got interrupted (default: False) - datafile_cache : datafile_cache_tuple, optional - Optional cache of open DataFile instances: namedtuple as returned - by create_cache. Used by BoutOutputs to pass in a cache so that we - do not have to re-open the dump files to read another variable - (default: None) - - Examples - -------- - - >>> collect(name) - BoutArray([[[[...]]]]) - - """ - - if datafile_cache is None: - # Search for BOUT++ dump files - file_list, parallel, _ = findFiles(path, prefix) - else: - parallel = datafile_cache.parallel - file_list = datafile_cache.file_list - - def getDataFile(i): - """Get the DataFile from the cache, if present, otherwise open the - DataFile - - """ - if datafile_cache is not None: - return datafile_cache.datafile_list[i] - else: - return DataFile(file_list[i]) - - if parallel: - if info: - print("Single (parallel) data file") - - f = getDataFile(0) - - if varname not in f.keys(): - if strict: - raise ValueError("Variable '{}' not found".format(varname)) - else: - varname = findVar(varname, f.list()) - - dimensions = f.dimensions(varname) - - try: - mxg = f["MXG"] - except KeyError: - mxg = 0 - print("MXG not found, setting to {}".format(mxg)) - try: - myg = f["MYG"] - except KeyError: - myg = 0 - print("MYG not found, setting to {}".format(myg)) - - if xguards: - nx = f["nx"] - else: - nx = f["nx"] - 2*mxg - if yguards: - ny = f["ny"] + 2*myg - if yguards == "include_upper" and f["jyseps2_1"] != f["jyseps1_2"]: - # Simulation has a second (upper) target, with a second set of y-boundary - # points - ny = ny + 2*myg - else: - ny = f["ny"] - nz = f["MZ"] - t_array = f.read("t_array") - if t_array is None: - nt = 1 - t_array = np.zeros(1) - else: - try: - nt = len(t_array) - except TypeError: - # t_array is not an array here, which probably means it was a - # one-element array and has been read as a scalar. - nt = 1 - - xind = _convert_to_nice_slice(xind, nx, "xind") - yind = _convert_to_nice_slice(yind, ny, "yind") - zind = _convert_to_nice_slice(zind, nz, "zind") - tind = _convert_to_nice_slice(tind, nt, "tind") - - if not xguards: - xind = slice(xind.start+mxg, xind.stop+mxg, xind.step) - if not yguards: - yind = slice(yind.start+myg, yind.stop+myg, yind.step) - - if dimensions == (): - ranges = [] - elif dimensions == ('t',): - ranges = [tind] - elif dimensions == ('x', 'y'): - # Field2D - ranges = [xind, yind] - elif dimensions == ('x', 'z'): - # FieldPerp - ranges = [xind, zind] - elif dimensions == ('t', 'x', 'y'): - # evolving Field2D - ranges = [tind, xind, yind] - elif dimensions == ('t', 'x', 'z'): - # evolving FieldPerp - ranges = [tind, xind, zind] - elif dimensions == ('x', 'y', 'z'): - # Field3D - ranges = [xind, yind, zind] - elif dimensions == ('t', 'x', 'y', 'z'): - # evolving Field3D - ranges = [tind, xind, yind, zind] - else: - raise ValueError("Variable has incorrect dimensions ({})" - .format(dimensions)) - - data = f.read(varname, ranges) - var_attributes = f.attributes(varname) - return BoutArray(data, attributes=var_attributes) - - nfiles = len(file_list) - - # Read data from the first file - f = getDataFile(0) - - if varname not in f.keys(): - if strict: - raise ValueError("Variable '{}' not found".format(varname)) - else: - varname = findVar(varname, f.list()) - - dimensions = f.dimensions(varname) - - var_attributes = f.attributes(varname) - ndims = len(dimensions) - - # ndims is 0 for reals, and 1 for f.ex. t_array - if ndims == 0: - # Just read from file - data = f.read(varname) - if datafile_cache is None: - # close the DataFile if we are not keeping it in a cache - f.close() - return BoutArray(data, attributes=var_attributes) - - if ndims > 4: - raise ValueError("ERROR: Too many dimensions") - - def load_and_check(varname): - var = f.read(varname) - if var is None: - raise ValueError("Missing " + varname + " variable") - return var - - mxsub = load_and_check("MXSUB") - mysub = load_and_check("MYSUB") - mz = load_and_check("MZ") - mxg = load_and_check("MXG") - myg = load_and_check("MYG") - t_array = f.read("t_array") - if t_array is None: - nt = 1 - t_array = np.zeros(1) - else: - try: - nt = len(t_array) - except TypeError: - # t_array is not an array here, which probably means it was a - # one-element array and has been read as a scalar. - nt = 1 - if tind_auto: - for i in range(nfiles): - t_array_ = getDataFile(i).read("t_array") - nt = min(len(t_array_), nt) - - if info: - print("mxsub = %d mysub = %d mz = %d\n" % (mxsub, mysub, mz)) - - # Get the version of BOUT++ (should be > 0.6 for NetCDF anyway) - try: - version = f["BOUT_VERSION"] - except KeyError: - print("BOUT++ version : Pre-0.2") - version = 0 - if version < 3.5: - # Remove extra point - nz = mz-1 - else: - nz = mz - - # Fallback to sensible (?) defaults - try: - nxpe = f["NXPE"] - except KeyError: - nxpe = 1 - print("NXPE not found, setting to {}".format(nxpe)) - try: - nype = f["NYPE"] - except KeyError: - nype = nfiles - print("NYPE not found, setting to {}".format(nype)) - - npe = nxpe * nype - if info: - print("nxpe = %d, nype = %d, npe = %d\n" % (nxpe, nype, npe)) - if npe < nfiles: - print("WARNING: More files than expected (" + str(npe) + ")") - elif npe > nfiles: - print("WARNING: Some files missing. Expected " + str(npe)) - - if xguards: - nx = nxpe * mxsub + 2*mxg - else: - nx = nxpe * mxsub - - if yguards: - ny = mysub * nype + 2*myg - if yguards == "include_upper" and f["jyseps2_1"] != f["jyseps1_2"]: - # Simulation has a second (upper) target, with a second set of y-boundary - # points - ny = ny + 2*myg - ny_inner = f["ny_inner"] - yproc_upper_target = ny_inner // mysub - 1 - if f["ny_inner"] % mysub != 0: - raise ValueError("Trying to keep upper boundary cells but " - "mysub={} does not divide ny_inner={}" - .format(mysub, ny_inner)) - else: - yproc_upper_target = None - else: - ny = mysub * nype - - xind = _convert_to_nice_slice(xind, nx, "xind") - yind = _convert_to_nice_slice(yind, ny, "yind") - zind = _convert_to_nice_slice(zind, nz, "zind") - tind = _convert_to_nice_slice(tind, nt, "tind") - - xsize = xind.stop - xind.start - ysize = yind.stop - yind.start - zsize = int(np.ceil(float(zind.stop - zind.start)/zind.step)) - tsize = int(np.ceil(float(tind.stop - tind.start)/tind.step)) - - if ndims == 1: - if tind is None: - data = f.read(varname) - else: - data = f.read(varname, ranges=[tind]) - if datafile_cache is None: - # close the DataFile if we are not keeping it in a cache - f.close() - return BoutArray(data, attributes=var_attributes) - - if datafile_cache is None: - # close the DataFile if we are not keeping it in a cache - f.close() - - # Map between dimension names and output size - sizes = {'x': xsize, 'y': ysize, 'z': zsize, 't': tsize} - - # Create a list with size of each dimension - ddims = [sizes[d] for d in dimensions] - - # Create the data array - data = np.zeros(ddims) - - if dimensions == ('t', 'x', 'z') or dimensions == ('x', 'z'): - yindex_global = None - # The pe_yind that this FieldPerp is going to be read from - fieldperp_yproc = None - - for i in range(npe): - # Get X and Y processor indices - pe_yind = int(i/nxpe) - pe_xind = i % nxpe - - inrange = True - - if yguards: - # Get local ranges - ystart = yind.start - pe_yind*mysub - ystop = yind.stop - pe_yind*mysub - - # Check lower y boundary - if pe_yind == 0: - # Keeping inner boundary - if ystop <= 0: - inrange = False - if ystart < 0: - ystart = 0 - else: - if ystop < myg-1: - inrange = False - if ystart < myg: - ystart = myg - # and lower y boundary at upper target - if yproc_upper_target is not None and pe_yind - 1 == yproc_upper_target: - ystart = ystart - myg - - # Upper y boundary - if pe_yind == (nype - 1): - # Keeping outer boundary - if ystart >= (mysub + 2*myg): - inrange = False - if ystop > (mysub + 2*myg): - ystop = (mysub + 2*myg) - else: - if ystart >= (mysub + myg): - inrange = False - if ystop > (mysub + myg): - ystop = (mysub + myg) - # upper y boundary at upper target - if yproc_upper_target is not None and pe_yind == yproc_upper_target: - ystop = ystop + myg - - # Calculate global indices - ygstart = ystart + pe_yind * mysub - ygstop = ystop + pe_yind * mysub - - if yproc_upper_target is not None and pe_yind > yproc_upper_target: - ygstart = ygstart + 2*myg - ygstop = ygstop + 2*myg - - else: - # Get local ranges - ystart = yind.start - pe_yind*mysub + myg - ystop = yind.stop - pe_yind*mysub + myg - - if (ystart >= (mysub + myg)) or (ystop <= myg): - inrange = False # Y out of range - - if ystart < myg: - ystart = myg - if ystop > mysub + myg: - ystop = myg + mysub - - # Calculate global indices - ygstart = ystart + pe_yind * mysub - myg - ygstop = ystop + pe_yind * mysub - myg - - if xguards: - # Get local ranges - xstart = xind.start - pe_xind*mxsub - xstop = xind.stop - pe_xind*mxsub - - # Check lower x boundary - if pe_xind == 0: - # Keeping inner boundary - if xstop <= 0: - inrange = False - if xstart < 0: - xstart = 0 - else: - if xstop <= mxg: - inrange = False - if xstart < mxg: - xstart = mxg - - # Upper x boundary - if pe_xind == (nxpe - 1): - # Keeping outer boundary - if xstart >= (mxsub + 2*mxg): - inrange = False - if xstop > (mxsub + 2*mxg): - xstop = (mxsub + 2*mxg) - else: - if xstart >= (mxsub + mxg): - inrange = False - if xstop > (mxsub + mxg): - xstop = (mxsub+mxg) - - # Calculate global indices - xgstart = xstart + pe_xind * mxsub - xgstop = xstop + pe_xind * mxsub - - else: - # Get local ranges - xstart = xind.start - pe_xind*mxsub + mxg - xstop = xind.stop - pe_xind*mxsub + mxg - - if (xstart >= (mxsub + mxg)) or (xstop <= mxg): - inrange = False # X out of range - - if xstart < mxg: - xstart = mxg - if xstop > mxsub + mxg: - xstop = mxg + mxsub - - # Calculate global indices - xgstart = xstart + pe_xind * mxsub - mxg - xgstop = xstop + pe_xind * mxsub - mxg - - # Number of local values - nx_loc = xstop - xstart - ny_loc = ystop - ystart - - if not inrange: - continue # Don't need this file - - if info: - sys.stdout.write("\rReading from " + file_list[i] + ": [" + - str(xstart) + "-" + str(xstop-1) + "][" + - str(ystart) + "-" + str(ystop-1) + "] -> [" + - str(xgstart) + "-" + str(xgstop-1) + "][" + - str(ygstart) + "-" + str(ygstop-1) + "]") - - f = getDataFile(i) - - if dimensions == ('t', 'x', 'y', 'z'): - d = f.read(varname, ranges=[tind, - slice(xstart, xstop), - slice(ystart, ystop), - zind]) - data[:, (xgstart-xind.start):(xgstart-xind.start+nx_loc), - (ygstart-yind.start):(ygstart-yind.start+ny_loc), :] = d - elif dimensions == ('x', 'y', 'z'): - d = f.read(varname, ranges=[slice(xstart, xstop), - slice(ystart, ystop), - zind]) - data[(xgstart-xind.start):(xgstart-xind.start+nx_loc), - (ygstart-yind.start):(ygstart-yind.start+ny_loc), :] = d - elif dimensions == ('t', 'x', 'y'): - d = f.read(varname, ranges=[tind, - slice(xstart, xstop), - slice(ystart, ystop)]) - data[:, (xgstart-xind.start):(xgstart-xind.start+nx_loc), - (ygstart-yind.start):(ygstart-yind.start+ny_loc)] = d - elif dimensions == ('t', 'x', 'z'): - # FieldPerp should only be defined on processors which contain its yindex_global - f_attributes = f.attributes(varname) - temp_yindex = f_attributes["yindex_global"] - - if temp_yindex >= 0: - if yindex_global is None: - yindex_global = temp_yindex - - # we have found a file with containing the FieldPerp, get the attributes from here - var_attributes = f_attributes - assert temp_yindex == yindex_global - - if temp_yindex >= 0: - # Check we only read from one pe_yind - assert fieldperp_yproc is None or fieldperp_yproc == pe_yind - - fieldperp_yproc = pe_yind - - d = f.read(varname, ranges=[tind, - slice(xstart, xstop), - zind]) - data[:, (xgstart-xind.start):(xgstart-xind.start+nx_loc), :] = d - elif dimensions == ('x', 'y'): - d = f.read(varname, ranges=[slice(xstart, xstop), - slice(ystart, ystop)]) - data[(xgstart-xind.start):(xgstart-xind.start+nx_loc), - (ygstart-yind.start):(ygstart-yind.start+ny_loc)] = d - elif dimensions == ('x', 'z'): - # FieldPerp should only be defined on processors which contain its yindex_global - f_attributes = f.attributes(varname) - temp_yindex = f_attributes["yindex_global"] - - if temp_yindex >= 0: - if yindex_global is None: - yindex_global = temp_yindex - - # we have found a file with containing the FieldPerp, get the attributes from here - var_attributes = f_attributes - assert temp_yindex == yindex_global - - if temp_yindex >= 0: - # Check we only read from one pe_yind - assert fieldperp_yproc is None or fieldperp_yproc == pe_yind - - fieldperp_yproc = pe_yind - - d = f.read(varname, ranges=[slice(xstart, xstop), zind]) - data[(xgstart-xind.start):(xgstart-xind.start+nx_loc), :] = d - else: - raise ValueError('Incorrect dimensions '+str(dimensions)+' in collect') - - if datafile_cache is None: - # close the DataFile if we are not keeping it in a cache - f.close() - - # if a step was requested in x or y, need to apply it here - if xind.step is not None or yind.step is not None: - if dimensions == ('t', 'x', 'y', 'z'): - data = data[:, ::xind.step, ::yind.step] - elif dimensions == ('x', 'y', 'z'): - data = data[::xind.step, ::yind.step, :] - elif dimensions == ('t', 'x', 'y'): - data = data[:, ::xind.step, ::yind.step] - elif dimensions == ('t', 'x', 'z'): - data = data[:, ::xind.step, :] - elif dimensions == ('x', 'y'): - data = data[::xind.step, ::yind.step] - elif dimensions == ('x', 'z'): - data = data[::xind.step, :] - else: - raise ValueError('Incorrect dimensions '+str(dimensions)+' applying steps in collect') - - # Force the precision of arrays of dimension>1 - if ndims > 1: - try: - data = data.astype(t_array.dtype, copy=False) - except TypeError: - data = data.astype(t_array.dtype) - - # Finished looping over all files - if info: - sys.stdout.write("\n") - return BoutArray(data, attributes=var_attributes) - - -def attributes(varname, path=".", prefix="BOUT.dmp"): - """Return a dictionary of variable attributes in an output file - - Parameters - ---------- - varname : str - Name of the variable - path : str, optional - Path to data files (default: ".") - prefix : str, optional - File prefix (default: "BOUT.dmp") - - Returns - ------- - dict - A dictionary of attributes of varname - """ - # Search for BOUT++ dump files in NetCDF format - file_list, _, _ = findFiles(path, prefix) - - # Read data from the first file - f = DataFile(file_list[0]) - - return f.attributes(varname) - - -def dimensions(varname, path=".", prefix="BOUT.dmp"): - """Return the names of dimensions of a variable in an output file - - Parameters - ---------- - varname : str - Name of the variable - path : str, optional - Path to data files (default: ".") - prefix : str, optional - File prefix (default: "BOUT.dmp") - - Returns - ------- - tuple of strs - The elements of the tuple give the names of corresponding variable - dimensions - - """ - file_list, _, _ = findFiles(path, prefix) - return DataFile(file_list[0]).dimensions(varname) - - -def findFiles(path, prefix): - """Find files matching prefix in path. - - Netcdf (".nc", ".ncdf", ".cdl") and HDF5 (".h5", ".hdf5", ".hdf") - files are searched. - - Parameters - ---------- - path : str - Path to data files - prefix : str - File prefix - - Returns - ------- - tuple : (list of str, bool, str) - The first element of the tuple is the list of files, the second is - whether the files are a parallel dump file and the last element is - the file suffix. - - """ - - # Make sure prefix does not have a trailing . - if prefix[-1] == ".": - prefix = prefix[:-1] - - # Look for parallel dump files - suffixes = [".nc", ".ncdf", ".cdl", ".h5", ".hdf5", ".hdf"] - file_list_parallel = None - suffix_parallel = "" - for test_suffix in suffixes: - files = glob.glob(os.path.join(path, prefix+test_suffix)) - if files: - if file_list_parallel: # Already had a list of files - raise IOError("Parallel dump files with both {0} and {1} extensions are present. Do not know which to read.".format( - suffix, test_suffix)) - suffix_parallel = test_suffix - file_list_parallel = files - - file_list = None - suffix = "" - for test_suffix in suffixes: - files = glob.glob(os.path.join(path, prefix+".*"+test_suffix)) - if files: - if file_list: # Already had a list of files - raise IOError("Dump files with both {0} and {1} extensions are present. Do not know which to read.".format( - suffix, test_suffix)) - suffix = test_suffix - file_list = files - - if file_list_parallel and file_list: - raise IOError("Both regular (with suffix {0}) and parallel (with suffix {1}) dump files are present. Do not know which to read.".format( - suffix_parallel, suffix)) - elif file_list_parallel: - return file_list_parallel, True, suffix_parallel - elif file_list: - # make sure files are in the right order - nfiles = len(file_list) - file_list = [os.path.join(path, prefix+"."+str(i)+suffix) - for i in range(nfiles)] - return file_list, False, suffix - else: - raise IOError("ERROR: No data files found in path {0}".format(path)) - - -def create_cache(path, prefix): - """Create a list of DataFile objects to be passed repeatedly to - collect. - - Parameters - ---------- - path : str - Path to data files - prefix : str - File prefix - - Returns - ------- - namedtuple : (list of str, bool, str, list of :py:obj:`~boututils.datafile.DataFile`) - The cache of DataFiles in a namedtuple along with the file_list, - and parallel and suffix attributes - - """ - - # define namedtuple to return as the result - from collections import namedtuple - datafile_cache_tuple = namedtuple( - "datafile_cache", ["file_list", "parallel", "suffix", "datafile_list"]) - - file_list, parallel, suffix = findFiles(path, prefix) - - cache = [] - for f in file_list: - cache.append(DataFile(f)) - - return datafile_cache_tuple(file_list=file_list, parallel=parallel, suffix=suffix, datafile_list=cache) diff --git a/tools/pylib/boutdata/data.py b/tools/pylib/boutdata/data.py deleted file mode 100644 index ddbc543199..0000000000 --- a/tools/pylib/boutdata/data.py +++ /dev/null @@ -1,1276 +0,0 @@ -"""Provides a class BoutData which makes access to code inputs and -outputs easier. Creates a tree of maps, inspired by approach used in -OMFIT - -""" - -import copy -import glob -import io -import numpy -import os -import re - -from boutdata.collect import collect, create_cache -from boututils.boutwarnings import alwayswarn -from boututils.datafile import DataFile - -# These are imported to be used by 'eval' in -# BoutOptions.evaluate_scalar() and BoutOptionsFile.evaluate(). -# Change the names to match those used by C++/BOUT++ -from numpy import ( - pi, - sin, - cos, - tan, - arccos as acos, - arcsin as asin, - arctan as atan, - arctan2 as atan2, - sinh, - cosh, - tanh, - arcsinh as asinh, - arccosh as acosh, - arctanh as atanh, - exp, - log, - log10, - power as pow, - sqrt, - ceil, - floor, - round, - abs, -) - - -from collections import UserDict - - -class CaseInsensitiveDict(UserDict): - def __missing__(self, key): - return CaseInsensitiveDict({key: CaseInsensitiveDict()}) - - def __getitem__(self, key): - return self.data[key.lower()][1] - - def __setitem__(self, key, value): - self.data[key.lower()] = (key, value) - - def __delitem__(self, key): - del self.data[key.lower()] - - def __iter__(self): - return (key for key, _ in self.data.values()) - - def __contains__(self, key): - return key.lower() in self.data - - def __repr__(self): - return repr({key: value for key, value in self.data.values()}) - - -class BoutOptions(object): - """This class represents a tree structure. Each node (BoutOptions - object) can have several sub-nodes (sections), and several - key-value pairs. - - Parameters - ---------- - name : str, optional - Name of the root section (default: "root") - parent : BoutOptions, optional - A parent BoutOptions object (default: None) - - Examples - -------- - - >>> optRoot = BoutOptions() # Create a root - - Specify value of a key in a section "test" - If the section does not exist then it is created - - >>> optRoot.getSection("test")["key"] = 4 - - Get the value of a key in a section "test" - If the section does not exist then a KeyError is raised - - >>> print(optRoot["test"]["key"]) - 4 - - To pretty print the options - - >>> print(optRoot) - [test] - key = 4 - - """ - - def __init__(self, name="root", parent=None): - self._sections = CaseInsensitiveDict() - self._keys = CaseInsensitiveDict() - self._name = name - self._parent = parent - self.comments = CaseInsensitiveDict() - self.inline_comments = CaseInsensitiveDict() - self._comment_whitespace = CaseInsensitiveDict() - - def getSection(self, name): - """Return a section object. If the section does not exist then it is - created - - Parameters - ---------- - name : str - Name of the section to get/create - - Returns - ------- - BoutOptions - A new section with the original object as the parent - - """ - - if name in self._sections: - return self._sections[name] - else: - newsection = BoutOptions(name=name, parent=self) - self._sections[name] = newsection - return newsection - - def __getitem__(self, key): - """ - First check if it's a section, then a value - """ - - key_parts = key.split(":", maxsplit=1) - - if len(key_parts) > 1: - section = self[key_parts[0]] - return section[key_parts[1]] - - if key in self._sections: - return self._sections[key] - - if key not in self._keys: - raise KeyError("Key '%s' not in section '%s'" % (key, self.path())) - return self._keys[key] - - def __setitem__(self, key, value): - """ - Set a key - """ - if len(key) == 0: - return - - key_parts = key.split(":", maxsplit=1) - - if len(key_parts) > 1: - try: - section = self[key_parts[0]] - except KeyError: - section = self.getSection(key_parts[0]) - section[key_parts[1]] = value - else: - self._keys[key] = value - - def __delitem__(self, key): - key_parts = key.split(":", maxsplit=1) - - if len(key_parts) > 1: - section = self[key_parts[0]] - del section[key_parts[1]] - return - - if key in self._sections: - del self._sections[key] - elif key in self._keys: - del self._keys[key] - else: - raise KeyError(key) - - def __contains__(self, key): - key_parts = key.split(":", maxsplit=1) - - if len(key_parts) > 1: - if key_parts[0] in self: - return key_parts[1] in self[key_parts[0]] - return False - - return key in self._keys or key in self._sections - - __marker = object() - - def pop(self, key, default=__marker): - """options.pop(k[,d]) -> v, remove specified key and return the - corresponding value. If key is not found, d is returned if - given, otherwise KeyError is raised. - - """ - return self._pop_impl(key, default)[0] - - def _pop_impl(self, key, default=__marker): - """Private implementation of pop; also pops metadata - - """ - key_parts = key.split(":", maxsplit=1) - - if len(key_parts) > 1: - return self[key_parts[0]]._pop_impl(key_parts[1], default) - - if key in self._sections: - value = self._sections.pop(key) - name = self._name - parent = self._parent - elif key in self._keys: - value = self._keys.pop(key) - name = None - parent = None - elif default is self.__marker: - raise KeyError(key) - else: - return default - - comment = self.comments.pop(key, None) - inline_comment = self.inline_comments.pop(key, None) - comment_whitespace = self._comment_whitespace.pop(key, None) - - return (value, name, parent, comment, inline_comment, comment_whitespace) - - def rename(self, old_name, new_name): - """Rename old_name to new_name - """ - - def setattr_nested(parent, key, attr, value): - """Set one of the comment types on some nested section. Slightly - complicated because the comment attributes are dicts, but - we need to get the (possibly) nested parent section - - """ - # Don't set comment if it's None - if value is None: - return - - key_parts = key.split(":", maxsplit=1) - if len(key_parts) > 1: - setattr_nested(parent[key_parts[0]], key_parts[1], attr, value) - else: - getattr(parent, attr)[key] = value - - def ensure_sections(parent, path): - """Make sure all the components of path in parent are sections - """ - path_parts = path.split(":", maxsplit=1) - - def check_is_section(parent, path): - if path in parent and not isinstance(parent[path], BoutOptions): - raise TypeError( - "'{}:{}' already exists and is not a section!".format( - parent._name, path - ) - ) - - if len(path_parts) > 1: - new_parent_name, child_name = path_parts - check_is_section(parent, new_parent_name) - parent.getSection(new_parent_name) - ensure_sections(parent[new_parent_name], child_name) - else: - check_is_section(parent, path) - parent.getSection(path) - - value = self[old_name] - - if isinstance(value, BoutOptions): - # We're moving a section: make sure we don't clobber existing values - ensure_sections(self, new_name) - # Now we're definitely moving into an existing section, so - # update values and comments - for key in value: - self[new_name][key] = value[key] - setattr_nested(self[new_name], key, "comments", value.comments.get(key)) - setattr_nested( - self[new_name], - key, - "inline_comments", - value.inline_comments.get(key), - ) - setattr_nested( - self[new_name], - key, - "_comment_whitespace", - value._comment_whitespace.get(key), - ) - _, _, _, comment, inline_comment, comment_whitespace = self._pop_impl( - old_name - ) - else: - _, _, _, comment, inline_comment, comment_whitespace = self._pop_impl( - old_name - ) - self[new_name] = value - - # Update comments on new parent section - setattr_nested(self, new_name, "comments", comment) - setattr_nested(self, new_name, "inline_comments", inline_comment) - setattr_nested(self, new_name, "_comment_whitespace", comment_whitespace) - - def path(self): - """Returns the path of this section, joining together names of - parents - - """ - - if self._parent: - return self._parent.path() + ":" + self._name - return self._name - - def keys(self): - """Returns all keys, including sections and values - - """ - return list(self._sections) + list(self._keys) - - def sections(self): - """Return a list of sub-sections - - """ - return self._sections.keys() - - def values(self): - """Return a list of values - - """ - return self._keys.keys() - - def as_dict(self): - """Return a nested dictionary of all the options. - - """ - dicttree = {name: self[name] for name in self.values()} - dicttree.update({name: self[name].as_dict() for name in self.sections()}) - return dicttree - - def __len__(self): - return len(self._sections) + len(self._keys) - - def __eq__(self, other): - """Test if this BoutOptions is the same as another one.""" - if not isinstance(other, BoutOptions): - return False - if self is other: - # other is a reference to the same object - return True - if len(self._sections) != len(other._sections): - return False - if len(self._keys) != len(other._keys): - return False - for secname, section in self._sections.items(): - if secname not in other or section != other[secname]: - return False - for key, value in self._keys.items(): - if key not in other or value != other[key]: - return False - return True - - def __iter__(self): - """Iterates over all keys. First values, then sections - - """ - for k in self._keys: - yield k - for s in self._sections: - yield s - - def as_tree(self, indent=""): - """Return a string formatted as a pretty version of the options tree - - """ - text = self._name + "\n" - - for k in self._keys: - text += indent + " |- " + k + " = " + str(self._keys[k]) + "\n" - - for s in self._sections: - text += indent + " |- " + self._sections[s].as_tree(indent + " | ") - return text - - def __str__(self, basename=None, opts=None, f=None): - if f is None: - f = io.StringIO() - if opts is None: - opts = self - - def format_inline_comment(name, options): - if name in options.inline_comments: - f.write( - "{}{}".format( - options._comment_whitespace[name], options.inline_comments[name] - ) - ) - - for key, value in opts._keys.items(): - if key in opts.comments: - f.write("\n".join(opts.comments[key]) + "\n") - f.write("{} = {}".format(key, value)) - format_inline_comment(key, opts) - f.write("\n") - - for section in opts._sections.keys(): - section_name = basename + ":" + section if basename else section - if section in opts.comments: - f.write("\n".join(opts.comments[section])) - if opts[section]._keys: - f.write("\n[{}]".format(section_name)) - format_inline_comment(section, opts) - f.write("\n") - self.__str__(section_name, opts[section], f) - - return f.getvalue() - - def evaluate_scalar(self, name): - """ - Evaluate (recursively) scalar expressions - """ - expression = self._substitute_expressions(name) - - # replace ^ with ** so that Python evaluates exponentiation - expression = expression.replace("^", "**") - - return eval(expression) - - def _substitute_expressions(self, name): - expression = str(self[name]).lower() - expression = self._evaluate_section(expression, "") - parent = self._parent - while parent is not None: - sectionname = parent._name - if sectionname == "root": - sectionname = "" - expression = parent._evaluate_section(expression, sectionname) - parent = parent._parent - - return expression - - def _evaluate_section(self, expression, nested_sectionname): - # pass a nested section name so that we can traverse the options tree - # rooted at our own level and each level above us so that we can use - # relatively qualified variable names, e.g. if we are in section - # 'foo:bar:baz' then a variable 'x' from section 'bar' could be called - # 'bar:x' (found traversing the tree starting from 'bar') or - # 'foo:bar:x' (found when traversing tree starting from 'foo'). - for var in self.values(): - if nested_sectionname != "": - nested_name = nested_sectionname + ":" + var - else: - nested_name = var - if re.search( - r"(?>> opts = BoutOptionsFile("BOUT.inp") - >>> print(opts) # Print all options in a tree - root - |- nout = 100 - |- timestep = 2 - ... - - >>> opts["All"]["scale"] # Value "scale" in section "All" - 1.0 - - """ - - # Characters that start a comment - VALID_COMMENTS = ("#", ";") - # Get not just the comment, but also the preceeding whitespace - COMMENT_REGEX = re.compile(r"(.*?)(\s*)([{}].*)".format("".join(VALID_COMMENTS))) - - def __init__( - self, - filename="BOUT.inp", - name="root", - gridfilename=None, - nx=None, - ny=None, - nz=None, - ): - BoutOptions.__init__(self, name) - self.filename = filename - self.gridfilename = gridfilename - # Open the file - with open(filename, "r") as f: - # Go through each line in the file - section = self # Start with root section - comments = [] - for linenr, line in enumerate(f.readlines()): - # First remove comments, either # or ; - if line.lstrip().startswith(self.VALID_COMMENTS): - comments.append('#' + line.strip()[1:]) - continue - if line.strip() == "": - comments.append(line.strip()) - continue - - comment_match = self.COMMENT_REGEX.search(line) - if comment_match is not None: - line, comment_whitespace, inline_comment = comment_match.groups() - inline_comment = '#' + inline_comment.strip()[1:] - else: - inline_comment = None - comment_whitespace = None - - # Check section headers - startpos = line.find("[") - endpos = line.find("]") - if startpos != -1: - # A section heading - if endpos == -1: - raise SyntaxError("Missing ']' on line %d" % (linenr,)) - line = line[(startpos + 1) : endpos].strip() - - parent_section = self - while True: - scorepos = line.find(":") - if scorepos == -1: - sectionname = line - break - sectionname = line[0:scorepos] - line = line[(scorepos + 1) :] - parent_section = parent_section.getSection(sectionname) - section = parent_section.getSection(line) - if comments: - parent_section.comments[sectionname] = copy.deepcopy(comments) - comments = [] - if inline_comment is not None: - parent_section.inline_comments[sectionname] = inline_comment - parent_section._comment_whitespace[ - sectionname - ] = comment_whitespace - else: - # A key=value pair - - eqpos = line.find("=") - if eqpos == -1: - # No '=', so just set to true - section[line.strip()] = True - value_name = line.strip() - else: - value = line[(eqpos + 1) :].strip() - try: - # Try to convert to an integer - value = int(value) - except ValueError: - try: - # Try to convert to float - value = float(value) - except ValueError: - # Leave as a string - pass - - value_name = line[:eqpos].strip() - section[value_name] = value - if comments: - section.comments[value_name] = copy.deepcopy(comments) - comments = [] - if inline_comment is not None: - section.inline_comments[value_name] = inline_comment - section._comment_whitespace[value_name] = comment_whitespace - - try: - self.recalculate_xyz(nx=nx, ny=ny, nz=nz) - except Exception as e: - alwayswarn( - "While building x, y, z coordinate arrays, an " - "exception occured: " - + str(e) - + "\nEvaluating non-scalar options not available" - ) - - def recalculate_xyz(self, *, nx=None, ny=None, nz=None): - """ - Recalculate the x, y avd z arrays used to evaluate expressions - """ - # define arrays of x, y, z to be used for substitutions - gridfile = None - nzfromfile = None - if self.gridfilename: - if nx is not None or ny is not None: - raise ValueError( - "nx or ny given as inputs even though " - "gridfilename was given explicitly, " - "don't know which parameters to choose" - ) - with DataFile(self.gridfilename) as gridfile: - self.nx = float(gridfile["nx"]) - self.ny = float(gridfile["ny"]) - try: - nzfromfile = gridfile["MZ"] - except KeyError: - pass - elif nx or ny: - if nx is None: - raise ValueError( - "nx not specified. If either nx or ny are given, then both must be." - ) - if ny is None: - raise ValueError( - "ny not specified. If either nx or ny are given, then both must be." - ) - self.nx = nx - self.ny = ny - else: - try: - self.nx = self["mesh"].evaluate_scalar("nx") - self.ny = self["mesh"].evaluate_scalar("ny") - except KeyError: - try: - # get nx, ny, nz from output files - from boutdata.collect import findFiles - - file_list = findFiles( - path=os.path.dirname("."), prefix="BOUT.dmp" - ) - with DataFile(file_list[0]) as f: - self.nx = f["nx"] - self.ny = f["ny"] - nzfromfile = f["MZ"] - except (IOError, KeyError): - try: - gridfilename = self["mesh"]["file"] - except KeyError: - gridfilename = self["grid"] - with DataFile(gridfilename) as gridfile: - self.nx = float(gridfile["nx"]) - self.ny = float(gridfile["ny"]) - try: - nzfromfile = float(gridfile["MZ"]) - except KeyError: - pass - if nz is not None: - self.nz = nz - else: - try: - self.nz = self["mesh"].evaluate_scalar("nz") - except KeyError: - try: - self.nz = self.evaluate_scalar("mz") - except KeyError: - if nzfromfile is not None: - self.nz = nzfromfile - mxg = self._keys.get("MXG", 2) - myg = self._keys.get("MYG", 2) - - # make self.x, self.y, self.z three dimensional now so - # that expressions broadcast together properly. - self.x = numpy.linspace( - (0.5 - mxg) / (self.nx - 2 * mxg), - 1.0 - (0.5 - mxg) / (self.nx - 2 * mxg), - self.nx, - )[:, numpy.newaxis, numpy.newaxis] - self.y = ( - 2.0 - * numpy.pi - * numpy.linspace( - (0.5 - myg) / self.ny, - 1.0 - (0.5 - myg) / self.ny, - self.ny + 2 * myg, - )[numpy.newaxis, :, numpy.newaxis] - ) - self.z = ( - 2.0 - * numpy.pi - * numpy.linspace(0.5 / self.nz, 1.0 - 0.5 / self.nz, self.nz)[ - numpy.newaxis, numpy.newaxis, : - ] - ) - - def evaluate(self, name): - """Evaluate (recursively) expressions - - Sections and subsections must be given as part of 'name', - separated by colons - - Parameters - ---------- - name : str - Name of variable to evaluate, including sections and - subsections - - """ - section = self - split_name = name.split(":") - for subsection in split_name[:-1]: - section = section.getSection(subsection) - expression = section._substitute_expressions(split_name[-1]) - - # replace ^ with ** so that Python evaluates exponentiation - expression = expression.replace("^", "**") - - # substitute for x, y and z coordinates - for coord in ["x", "y", "z"]: - expression = re.sub( - r"\b" + coord.lower() + r"\b", "self." + coord, expression - ) - - return eval(expression) - - def write(self, filename=None, overwrite=False): - """ Write to BOUT++ options file - - This method will throw an error rather than overwriting an existing - file unless the overwrite argument is set to true. - Note, no comments from the original input file are transferred to the - new one. - - Parameters - ---------- - filename : str - Path of the file to write - (defaults to path of the file that was read in) - overwrite : bool - If False then throw an exception if 'filename' already exists. - Otherwise, just overwrite without asking. - (default False) - """ - if filename is None: - filename = self.filename - - if not overwrite and os.path.exists(filename): - raise ValueError( - "Not overwriting existing file, cannot write output to " + filename - ) - - with open(filename, "w") as f: - f.write(str(self)) - - -class BoutOutputs(object): - """Emulates a map class, represents the contents of a BOUT++ dmp - files. Does not allow writing, only reading of data. By default - there is no cache, so each time a variable is read it is - collected; if caching is set to True variables are stored once - they are read. Extra keyword arguments are passed through to - collect. - - Parameters - ---------- - path : str, optional - Path to data files (default: ".") - prefix : str, optional - File prefix (default: "BOUT.dmp") - suffix : str, optional - File suffix (default: None, searches all file extensions) - caching : bool, float, optional - Switches on caching of data, so it is only read into memory - when first accessed (default False) If caching is set to a - number, it gives the maximum size of the cache in GB, after - which entries will be discarded in first-in-first-out order to - prevent the cache getting too big. If the variable being - returned is bigger than the maximum cache size, then the - variable will be returned without being added to the cache, - and the rest of the cache will be left (default: False) - DataFileCaching : bool, optional - Switch for creation of a cache of DataFile objects to be - passed to collect so that DataFiles do not need to be - re-opened to read each variable (default: True) - - **kwargs - keyword arguments that are passed through to _caching_collect() - - Examples - -------- - - >>> d = BoutOutputs(".") # Current directory - >> d.keys() # List all valid keys - ['iteration', - 'zperiod', - 'MYSUB', - ... - ] - - >>> d.dimensions["ne"] # Get the dimensions of the field ne - ('t', 'x', 'y', 'z') - - >>> d["ne"] # Read "ne" from data files - BoutArray([[[[...]]]]) - - >>> d = BoutOutputs(".", prefix="BOUT.dmp", caching=True) # Turn on caching - - """ - - def __init__( - self, - path=".", - prefix="BOUT.dmp", - suffix=None, - caching=False, - DataFileCaching=True, - **kwargs - ): - """ - Initialise BoutOutputs object - """ - self._path = path - # normalize prefix by removing trailing '.' if present - self._prefix = prefix.rstrip(".") - if suffix is None: - temp_file_list = glob.glob(os.path.join(self._path, self._prefix + "*")) - latest_file = max(temp_file_list, key=os.path.getctime) - self._suffix = latest_file.split(".")[-1] - else: - # normalize suffix by removing leading '.' if present - self._suffix = suffix.lstrip(".") - self._caching = caching - self._DataFileCaching = DataFileCaching - self._kwargs = kwargs - - # Label for this data - self.label = path - - self._file_list = glob.glob( - os.path.join(path, self._prefix + "*" + self._suffix) - ) - if suffix is not None: - latest_file = max(self._file_list, key=os.path.getctime) - # if suffix==None we already found latest_file - - # Check that the path contains some data - if len(self._file_list) == 0: - raise ValueError("ERROR: No data files found") - - # Available variables - self.varNames = [] - self.dimensions = {} - self.evolvingVariableNames = [] - - with DataFile(latest_file) as f: - npes = f.read("NXPE") * f.read("NYPE") - if len(self._file_list) != npes: - alwayswarn("Too many data files, reading most recent ones") - if npes == 1: - # single output file - # do like this to catch, e.g. either 'BOUT.dmp.nc' or 'BOUT.dmp.0.nc' - self._file_list = [latest_file] - else: - self._file_list = [ - os.path.join( - path, self._prefix + "." + str(i) + "." + self._suffix - ) - for i in range(npes) - ] - - # Get variable names - self.varNames = f.keys() - for name in f.keys(): - dimensions = f.dimensions(name) - self.dimensions[name] = dimensions - if name != "t_array" and "t" in dimensions: - self.evolvingVariableNames.append(name) - - # Private variables - if self._caching: - from collections import OrderedDict - - self._datacache = OrderedDict() - if self._caching is not True: - # Track the size of _datacache and limit it to a maximum of _caching - try: - # Check that _caching is a number of some sort - float(self._caching) - except ValueError: - raise ValueError( - "BoutOutputs: Invalid value for caching argument. Caching should be either a number (giving the maximum size of the cache in GB), True for unlimited size or False for no caching." - ) - self._datacachesize = 0 - self._datacachemaxsize = self._caching * 1.0e9 - - self._DataFileCache = None - - def keys(self): - """Return a list of available variable names - - """ - return self.varNames - - def evolvingVariables(self): - """Return a list of names of time-evolving variables - - """ - return self.evolvingVariableNames - - def redistribute(self, npes, nxpe=None, mxg=2, myg=2, include_restarts=True): - """Create a new set of dump files for npes processors. - - Useful for restarting simulations using more or fewer processors. - - Existing data and restart files are kept in the directory - "redistribution_backups". redistribute() will fail if this - directory already exists, to avoid overwriting anything - - Parameters - ---------- - npes : int - Number of new files to create - nxpe : int, optional - If nxpe is None (the default), then an 'optimal' number will be - selected automatically - mxg, myg : int, optional - Number of guard cells in x, y (default: 2) - include_restarts : bool, optional - If True, then restart.redistribute will be used to - redistribute the restart files also (default: True) - - """ - from boutdata.processor_rearrange import ( - get_processor_layout, - create_processor_layout, - ) - from os import rename, path, mkdir - - # use get_processor_layout to get nx, ny - old_processor_layout = get_processor_layout( - DataFile(self._file_list[0]), has_t_dimension=True, mxg=mxg, myg=myg - ) - old_nxpe = old_processor_layout.nxpe - old_nype = old_processor_layout.nype - old_npes = old_processor_layout.npes - old_mxsub = old_processor_layout.mxsub - old_mysub = old_processor_layout.mysub - nx = old_processor_layout.nx - ny = old_processor_layout.ny - mz = old_processor_layout.mz - mxg = old_processor_layout.mxg - myg = old_processor_layout.myg - - # calculate new processor layout - new_processor_layout = create_processor_layout( - old_processor_layout, npes, nxpe=nxpe - ) - nxpe = new_processor_layout.nxpe - nype = new_processor_layout.nype - mxsub = new_processor_layout.mxsub - mysub = new_processor_layout.mysub - - # move existing files to backup directory - # don't overwrite backup: os.mkdir will raise exception if directory already exists - backupdir = path.join(self._path, "redistribution_backups") - mkdir(backupdir) - for f in self._file_list: - rename(f, path.join(backupdir, path.basename(f))) - - # create new output files - outfile_list = [] - this_prefix = self._prefix - if not this_prefix[-1] == ".": - # ensure prefix ends with a '.' - this_prefix = this_prefix + "." - for i in range(npes): - outpath = os.path.join( - self._path, this_prefix + str(i) + "." + self._suffix - ) - if self._suffix.split(".")[-1] in ["nc", "ncdf", "cdl"]: - # set format option to DataFile explicitly to avoid creating netCDF3 files, which can only contain up to 2GB of data - outfile_list.append( - DataFile(outpath, write=True, create=True, format="NETCDF4") - ) - else: - outfile_list.append(DataFile(outpath, write=True, create=True)) - - # Create a DataFileCache, if needed - if self._DataFileCaching: - DataFileCache = create_cache(backupdir, self._prefix) - else: - DataFileCache = None - # read and write the data - for v in self.varNames: - print("processing " + v) - data = collect( - v, - path=backupdir, - prefix=self._prefix, - xguards=True, - yguards=True, - info=False, - datafile_cache=DataFileCache, - ) - ndims = len(data.shape) - - # write data - for i in range(npes): - ix = i % nxpe - iy = int(i / nxpe) - outfile = outfile_list[i] - if v == "NPES": - outfile.write(v, npes) - elif v == "NXPE": - outfile.write(v, nxpe) - elif v == "NYPE": - outfile.write(v, nype) - elif v == "MXSUB": - outfile.write(v, mxsub) - elif v == "MYSUB": - outfile.write(v, mysub) - elif ndims == 0: - # scalar - outfile.write(v, data) - elif ndims == 1: - # time evolving scalar - outfile.write(v, data) - elif ndims == 2: - # Field2D - if data.shape != (nx + 2 * mxg, ny + 2 * myg): - # FieldPerp? - # check is not perfect, fails if ny=nz - raise ValueError( - "Error: Found FieldPerp '" - + v - + "'. This case is not currently handled by BoutOutputs.redistribute()." - ) - outfile.write( - v, - data[ - ix * mxsub : (ix + 1) * mxsub + 2 * mxg, - iy * mysub : (iy + 1) * mysub + 2 * myg, - ], - ) - elif ndims == 3: - # Field3D - if data.shape[:2] != (nx + 2 * mxg, ny + 2 * myg): - # evolving Field2D, but this case is not handled - # check is not perfect, fails if ny=nx and nx=nt - raise ValueError( - "Error: Found evolving Field2D '" - + v - + "'. This case is not currently handled by BoutOutputs.redistribute()." - ) - outfile.write( - v, - data[ - ix * mxsub : (ix + 1) * mxsub + 2 * mxg, - iy * mysub : (iy + 1) * mysub + 2 * myg, - :, - ], - ) - elif ndims == 4: - outfile.write( - v, - data[ - :, - ix * mxsub : (ix + 1) * mxsub + 2 * mxg, - iy * mysub : (iy + 1) * mysub + 2 * myg, - :, - ], - ) - else: - print( - "ERROR: variable found with unexpected number of dimensions,", - ndims, - ) - - for outfile in outfile_list: - outfile.close() - - if include_restarts: - print("processing restarts") - from boutdata import restart - from glob import glob - - restart_prefix = "BOUT.restart" - restarts_list = glob(path.join(self._path, restart_prefix + "*")) - - # Move existing restart files to backup directory - for f in restarts_list: - rename(f, path.join(backupdir, path.basename(f))) - - # Redistribute restarts - restart.redistribute( - npes, path=backupdir, nxpe=nxpe, output=self._path, mxg=mxg, myg=myg - ) - - def _collect(self, *args, **kwargs): - """Wrapper for collect to pass self._DataFileCache if necessary. - - """ - if self._DataFileCaching and self._DataFileCache is None: - # Need to create the cache - self._DataFileCache = create_cache(self._path, self._prefix) - return collect(*args, datafile_cache=self._DataFileCache, **kwargs) - - def __len__(self): - return len(self.varNames) - - def __getitem__(self, name): - """Reads a variable - - Caches result and returns later if called again, if caching is - turned on for this instance - - """ - - if self._caching: - if name not in self._datacache.keys(): - item = self._collect( - name, path=self._path, prefix=self._prefix, **self._kwargs - ) - if self._caching is not True: - itemsize = item.nbytes - if itemsize > self._datacachemaxsize: - return item - self._datacache[name] = item - self._datacachesize += itemsize - while self._datacachesize > self._datacachemaxsize: - self._removeFirstFromCache() - else: - self._datacache[name] = item - return item - else: - return self._datacache[name] - else: - # Collect the data from the repository - data = self._collect( - name, path=self._path, prefix=self._prefix, **self._kwargs - ) - return data - - def _removeFirstFromCache(self): - """Pop the first item from the OrderedDict _datacache - - """ - item = self._datacache.popitem(last=False) - self._datacachesize -= item[1].nbytes - - def __iter__(self): - """Iterate through all keys, starting with "options" then going - through all variables for _caching_collect - - """ - for k in self.varNames: - yield k - - def __str__(self, indent=""): - """Print a pretty version of the tree - - """ - text = "" - for k in self.varNames: - text += indent + k + "\n" - - return text - - -def BoutData(path=".", prefix="BOUT.dmp", caching=False, **kwargs): - """Returns a dictionary, containing the contents of a BOUT++ output - directory. - - Does not allow writing, only reading of data. By default there is - no cache, so each time a variable is read it is collected; if - caching is set to True variables are stored once they are read. - - Parameters - ---------- - path : str, optional - Path to data files (default: ".") - prefix : str, optional - File prefix (default: "BOUT.dmp") - caching : bool, float, optional - Switches on caching of data, so it is only read into memory - when first accessed (default False) If caching is set to a - number, it gives the maximum size of the cache in GB, after - which entries will be discarded in first-in-first-out order to - prevent the cache getting too big. If the variable being - returned is bigger than the maximum cache size, then the - variable will be returned without being added to the cache, - and the rest of the cache will be left (default: False) - DataFileCaching : bool, optional - Switch for creation of a cache of DataFile objects to be - passed to collect so that DataFiles do not need to be - re-opened to read each variable (default: True) - **kwargs - Keyword arguments that are passed through to collect() - - Returns - ------- - dict - Contents of a BOUT++ output directory, including options and - output files - - Examples - -------- - - >>> d = BoutData(".") # Current directory - - >>> d.keys() # List all valid keys - - >>> print(d["options"]) # Prints tree of options - - >>> d["options"]["nout"] # Value of nout in BOUT.inp file - - >>> print(d["outputs"]) # Print available outputs - - >>> d["outputs"]["ne"] # Read "ne" from data files - - >>> d = BoutData(".", prefix="BOUT.dmp", caching=True) # Turn on caching - - """ - - data = {} # Map for the result - - data["path"] = path - - # Options from BOUT.inp file - data["options"] = BoutOptionsFile(os.path.join(path, "BOUT.inp"), name="options") - - # Output from .dmp.* files - data["outputs"] = BoutOutputs(path, prefix=prefix, caching=caching, **kwargs) - - return data diff --git a/tools/pylib/boutdata/gen_surface.py b/tools/pylib/boutdata/gen_surface.py deleted file mode 100644 index 9b1caf81a0..0000000000 --- a/tools/pylib/boutdata/gen_surface.py +++ /dev/null @@ -1,154 +0,0 @@ -"""Flux surface generator for tokamak grid files - -""" -from __future__ import print_function - -import numpy as np - - -def gen_surface(grid): - """Generator for iterating over flux surfaces - - Parameters - ---------- - grid : DataFile - An input grid file to read to find flux surfaces - - Yields - ------ - tuple : (int, list of int, bool) - A tuple containing the x index, list of y indices and whether - this flux surface is periodic - - """ - # Read the grid data - nx = grid.read("nx") - ny = grid.read("ny") - - npol = grid.read("npol") - if npol is None: - # Domains not stored in file (BOUT style input) - ixseps1 = grid.read("ixseps1") - ixseps2 = grid.read("ixseps2") - jyseps1_1 = grid.read("jyseps1_1") - jyseps1_2 = grid.read("jyseps1_2") - jyseps2_1 = grid.read("jyseps2_1") - jyseps2_2 = grid.read("jyseps2_2") - - if ixseps1 == ixseps2: - # Single null - ndomains = 3 - else: - # Double null - ndomains = 6 - - yup_xsplit = np.zeros(ndomains) - ydown_xsplit = np.zeros(ndomains) - yup_xin = np.zeros(ndomains) - yup_xout = np.zeros(ndomains) - ydown_xin = np.zeros(ndomains) - ydown_xout = np.zeros(ndomains) - - ystart = np.zeros(ndomains+1) - ystart[ndomains] = ny - - # Inner lower leg - ydown_xsplit[0] = -1 - ydown_xout[0] = -1 - yup_xsplit[0] = ixseps1 - yup_xin[0] = ndomains-1 # Outer lower leg - yup_xout[0] = 1 - - # Outer lower leg - ydown_xsplit[ndomains-1] = ixseps1 - ydown_xin[ndomains-1] = 0 - ydown_xout[ndomains-1] = ndomains-2 - yup_xsplit[ndomains-1] = -1 - yup_xout[ndomains-1] = -1 - ystart[ndomains-1] = jyseps2_2+1 - - if ixseps1 == ixseps2: - # Single null - - ydown_xsplit[1] = ixseps1 - ydown_xin[1] = 1 - ydown_xout[1] = 0 - yup_xsplit[1] = ixseps1 - yup_xin[1] = 1 - yup_xout[1] = 2 - ystart[1] = jyseps1_1+1 - else: - # Double null - raise RuntimeError("SORRY - NO DOUBLE NULL YET") - else: - # Use domains stored in the file - ndomains = npol.size # Number of domains - yup_xsplit = grid.read("yup_xsplit") - ydown_xsplit = grid.read("ydown_xsplit") - yup_xin = grid.read("yup_xin") - yup_xout = grid.read("yup_xout") - ydown_xin = grid.read("ydown_xin") - ydown_xout = grid.read("ydown_xout") - - # Calculate starting positions - ystart = np.zeros(ndomains+1) - for i in np.arange(1,ndomains): - ystart[i] = ystart[i-1] + npol[i-1] - ystart[ndomains] = ny - - # Record whether a domain has been visited - visited = np.zeros(ndomains) - - x = 0 # X index - while True: - yinds = None # Y indices result - - # Find a domain which hasn't been visited - domain = None - for i in np.arange(ndomains): - if visited[i] == 0: - domain = i - break - - if domain is None: - # All domains visited - x = x + 1 # Go to next x surface - visited = np.zeros(ndomains) # Clear the visited array - if x == nx: - break # Finished - continue - - # Follow surface back until it hits a boundary - while True: - if x < ydown_xsplit[domain]: - d = ydown_xin[domain] - else: - d = ydown_xout[domain] - if d < 0: - break # Hit boundary - domain = d # Keep going - - # Starting from domain, follow surface - - periodic = False - while domain >= 0: - if visited[domain] == 1: - # Already visited domain -> periodic - periodic = True - break; - # Get range of y indices in this domain - yi = np.arange(ystart[domain], ystart[domain+1]) - if yinds is None: - yinds = yi - else: - yinds = np.concatenate((yinds, yi)) - # mark this domain as visited - visited[domain] = 1 - # Get next domain - if x < yup_xsplit[domain]: - domain = yup_xin[domain] - else: - domain = yup_xout[domain] - - # Finished this surface - yield x, yinds, periodic diff --git a/tools/pylib/boutdata/griddata.py b/tools/pylib/boutdata/griddata.py deleted file mode 100644 index 8246ddd8ab..0000000000 --- a/tools/pylib/boutdata/griddata.py +++ /dev/null @@ -1,493 +0,0 @@ -"""Routines for manipulating grid files - -""" -from __future__ import print_function - -from numpy import ndarray, zeros, concatenate, linspace, amin, amax -import matplotlib.pyplot as plt - -from boututils.datafile import DataFile - - -def slice(infile, outfile, region=None, xind=None, yind=None): - """Copy an X-Y slice from one DataFile to another - - Parameters - ---------- - infile : str - Name of DataFile to read slice from - outfile : str - Name of DataFile to write slice to. File will be created, and - will be overwritten if it already exists - region : {0, 1, 2, 3, 4, 5, None}, optional - Copy a whole region. The available regions are: - - 0: Lower inner leg - - 1: Inner core - - 2: Upper inner leg - - 3: Upper outer leg - - 4: Outer core - - 5: Lower outer leg - xind, yind : (int, int), optional - Index ranges for x and y. Range includes first point, but not - last point - - TODO - ---- - - Rename to not clobber builtin `slice` - - Better regions? - - """ - - # Open input and output files - indf = DataFile(infile) - outdf = DataFile(outfile, create=True) - - nx = indf["nx"][0] - ny = indf["ny"][0] - - if region: - # Select a region of the mesh - - xind = [0, nx] - if region == 0: - # Lower inner leg - yind = [0, indf["jyseps1_1"][0]+1] - elif region == 1: - # Inner core - yind = [indf["jyseps1_1"][0]+1, indf["jyseps2_1"][0]+1] - elif region == 2: - # Upper inner leg - yind = [indf["jyseps2_1"][0]+1, indf["ny_inner"][0]] - elif region == 3: - # Upper outer leg - yind = [indf["ny_inner"][0], indf["jyseps1_2"][0]+1] - elif region == 4: - # Outer core - yind = [indf["jyseps1_2"][0]+1, indf["jyseps2_2"][0]+1] - else: - # Lower outer leg - yind = [indf["jyseps2_2"][0]+1, ny] - else: - # Use indices - if not xind: - xind = [0, nx] - if not yind: - yind = [0, ny] - - print("Indices: [%d:%d, %d:%d]" % (xind[0], xind[1], yind[0], yind[1])) - # List of variables requiring special handling - special = ["nx", "ny", "ny_inner", - "ixseps1", "ixseps2", - "jyseps1_1", "jyseps1_2", "jyseps2_1", "jyseps2_2", - "ShiftAngle"] - - outdf["nx"] = xind[1] - xind[0] - outdf["ny"] = yind[1] - yind[0] - outdf["ny_inner"] = indf["ny_inner"][0] - yind[0] - - outdf["ixseps1"] = indf["ixseps1"][0] - outdf["ixseps2"] = indf["ixseps2"][0] - - outdf["jyseps1_1"] = indf["jyseps1_1"][0] - yind[0] - outdf["jyseps2_1"] = indf["jyseps2_1"][0] - yind[0] - outdf["jyseps1_2"] = indf["jyseps1_2"][0] - yind[0] - outdf["jyseps2_2"] = indf["jyseps2_2"][0] - yind[0] - - outdf["ShiftAngle"] = indf["ShiftAngle"][xind[0]:xind[1]] - - # Loop over all variables - for v in list(indf.keys()): - if v in special: - continue # Skip these variables - - ndims = indf.ndims(v) - if ndims == 0: - # Copy scalars - print("Copying variable: " + v) - outdf[v] = indf[v][0] - elif ndims == 2: - # Assume [x,y] - print("Slicing variable: " + v); - outdf[v] = indf[v][xind[0]:xind[1], yind[0]:yind[1]] - else: - # Skip - print("Skipping variable: " + v) - - indf.close() - outdf.close() - - -def rotate(gridfile, yshift, output=None): - """Shifts a grid file by the specified number of points in y - - This moves the branch cut around, and can be used to change the - limiter location - - Parameters - ---------- - gridfile : str - Name of DataFile to rotate - yshift : int - Number of points in y to shift by - output : str, optional - Name of DataFile to write to. If None, will write to a new - file with the same name as `gridfile` + '_rot' - - """ - - if output is None: - output = gridfile + "_rot" - - print("Rotating grid file '%s' -> '%s'" % (gridfile, output)) - - # Open input grid file - with DataFile(gridfile) as d: - # Open output file - with DataFile(output, write=True, create=True) as out: - # Loop over variables - for varname in d.list(): - # Number of dimensions - ndims = d.ndims(varname) - - if ndims == 2: - print("Shifting '%s' (x,y)" % (varname,)) - # 2D, assume X-Y - - var = d[varname] # Read - ny = var.shape[1] - - # Make sure yshift is positive and in range - yshift = ((yshift % ny) + ny) % ny - - newvar = ndarray(var.shape) - - # Rotate - newvar[:,0:(ny-yshift)] = var[:,yshift:ny] - newvar[:,(ny-yshift):] = var[:,:yshift] - - # Write to output - #out[varname] = newvar # Write - out.write(varname, newvar) - elif ndims == 3: - print("Shifting '%s' (x,y,z)" % (varname,)) - # 3D, assume X-Y-Z - - var = d[varname] # Read - ny = var.shape[1] - - # Make sure yshift is positive and in range - yshift = ((yshift % ny) + ny) % ny - - newvar = ndarray(var.shape) - - newvar[:,0:(ny-yshift),:] = var[:,yshift:ny,:] - newvar[:,(ny-yshift):,:] = var[:,:yshift,:] - - # Write to output - out.write(varname, newvar) - else: - # Just copy - print("Copying '%s' (%d dimensions)" % (varname, ndims)) - out.write(varname, d[varname]) - - - -def gridcontourf(grid, data2d, nlevel=31, show=True, - mind=None, maxd=None, symmetric=False, - cmap=None, ax=None, - xlabel="Major radius [m]", ylabel="Height [m]", - separatrix=False): - """Plots a 2D contour plot, taking into account branch cuts - (X-points). - - Parameters - ---------- - grid : DataFile - A DataFile object - data2d : array_like - A 2D (x,y) NumPy array of data to plot - nlevel : int, optional - Number of levels in the contour plot - show : bool, optional - If True, will immediately show the plot - mind : float, optional - Minimum data level - maxd : float, optional - Maximum data level - symmetric : bool, optional - Make mind, maxd symmetric about zero - cmap : Colormap, optional - A matplotlib colormap to use. If None, use the current default - ax : Axes, optional - A matplotlib axes instance to plot to. If None, create a new - figure and axes, and plot to that - xlabel, ylabel : str, optional - Labels for the x/y axes - separatrix : bool, optional - Add separatrix - - Returns - ------- - con - The contourf instance - - Examples - -------- - - To put a plot into an axis with a color bar: - - >>> fig, axis = plt.subplots() - >>> c = gridcontourf(grid, data, show=False, ax=axis) - >>> fig.colorbar(c, ax=axis) - >>> plt.show() - - TODO - ---- - - Move into a plotting module - - """ - - if cmap is None: - cmap = plt.cm.get_cmap("YlOrRd") - - if len(data2d.shape) != 2: - raise ValueError("data2d must be 2D (x,y)") - - j11 = grid["jyseps1_1"] - j12 = grid["jyseps1_2"] - j21 = grid["jyseps2_1"] - j22 = grid["jyseps2_2"] - ix1 = grid["ixseps1"] - ix2 = grid["ixseps2"] - try: - nin = grid["ny_inner"] - except: - nin = j12 - - nx = grid["nx"] - ny = grid["ny"] - - if (data2d.shape[0] != nx) or (data2d.shape[1] != ny): - raise ValueError("data2d has wrong size: (%d,%d), expected (%d,%d)" % (data2d.shape[0], data2d.shape[1], nx, ny)) - - if hasattr(j11, "__len__"): - # Arrays rather than scalars - try: - j11 = j11[0] - j12 = j12[0] - j21 = j21[0] - j22 = j22[0] - ix1 = ix1[0] - ix2 = ix2[0] - nin = nin[0] - nx = nx[0] - ny = ny[0] - except: - pass - - R = grid["Rxy"] - Z = grid["Zxy"] - - if data2d.shape != (nx, ny): - raise ValueError("Dimensions do not match") - - add_colorbar = False - if ax is None: - fig = plt.figure() - ax = fig.add_subplot(111) - add_colorbar = True - - if mind is None: - mind = amin(data2d) - if maxd is None: - maxd = amax(data2d) - - if symmetric: - # Make mind, maxd symmetric about zero - maxd = max([maxd, abs(mind)]) - mind = -maxd - - levels = linspace(mind, maxd, nlevel, endpoint=True) - - ystart = 0 # Y index to start the next section - if j11 >= 0: - # plot lower inner leg - ax.contourf(R[:,ystart:(j11+1)], Z[:,ystart:(j11+1)], data2d[:,ystart:(j11+1)], levels,cmap=cmap) - - yind = [j11, j22+1] - ax.contourf(R[:ix1, yind].transpose(), Z[:ix1, yind].transpose(), data2d[:ix1, yind].transpose(), levels,cmap=cmap) - - ax.contourf(R[ix1:,j11:(j11+2)], Z[ix1:,j11:(j11+2)], data2d[ix1:,j11:(j11+2)], levels,cmap=cmap) - ystart = j11+1 - - yind = [j22, j11+1] - ax.contourf(R[:ix1, yind].transpose(), Z[:ix1, yind].transpose(), data2d[:ix1, yind].transpose(), levels, cmap=cmap) - - # Inner SOL - con = ax.contourf(R[:,ystart:(j21+1)], Z[:,ystart:(j21+1)], data2d[:,ystart:(j21+1)], levels, cmap=cmap) - ystart = j21+1 - - if j12 > j21: - # Contains upper PF region - - # Inner leg - ax.contourf(R[ix1:,j21:(j21+2)], Z[ix1:,j21:(j21+2)], data2d[ix1:,j21:(j21+2)], levels, cmap=cmap) - ax.contourf(R[:,ystart:nin], Z[:,ystart:nin], data2d[:,ystart:nin], levels, cmap=cmap) - - # Outer leg - ax.contourf(R[:,nin:(j12+1)], Z[:,nin:(j12+1)], data2d[:,nin:(j12+1)], levels, cmap=cmap) - ax.contourf(R[ix1:,j12:(j12+2)], Z[ix1:,j12:(j12+2)], data2d[ix1:,j12:(j12+2)], levels, cmap=cmap) - ystart = j12+1 - - yind = [j21, j12+1] - ax.contourf(R[:ix1, yind].transpose(), Z[:ix1, yind].transpose(), data2d[:ix1, yind].transpose(), levels, cmap=cmap) - - yind = [j21+1, j12] - ax.contourf(R[:ix1, yind].transpose(), Z[:ix1, yind].transpose(), data2d[:ix1, yind].transpose(), levels, cmap=cmap) - else: - ystart -= 1 - # Outer SOL - ax.contourf(R[:,ystart:(j22+1)], Z[:,ystart:(j22+1)], data2d[:,ystart:(j22+1)], levels, cmap=cmap) - - ystart = j22+1 - - if j22+1 < ny: - # Outer leg - ax.contourf(R[ix1:,j22:(j22+2)], Z[ix1:,j22:(j22+2)], data2d[ix1:,j22:(j22+2)], levels, cmap=cmap) - ax.contourf(R[:,ystart:ny], Z[:,ystart:ny], data2d[:,ystart:ny], levels, cmap=cmap) - - # X-point - Rx = [ [R[ix1-1,j11], R[ix1,j11], R[ix1,j11+1], R[ix1-1,j11+1]], - [R[ix1-1,j22+1], R[ix1,j22+1], R[ix1,j22], R[ix1-1,j22]] ] - - - Zx = [ [Z[ix1-1,j11], Z[ix1,j11], Z[ix1,j11+1], Z[ix1-1,j11+1]], - [Z[ix1-1,j22+1], Z[ix1,j22+1], Z[ix1,j22], Z[ix1-1,j22]] ] - Dx = [ [data2d[ix1-1,j11], data2d[ix1,j11], data2d[ix1,j11+1], data2d[ix1-1,j11+1]], - [data2d[ix1-1,j22+1], data2d[ix1,j22+1], data2d[ix1,j22], data2d[ix1-1,j22]] ] - ax.contourf(Rx, Zx, Dx, levels, cmap=cmap) - - if add_colorbar: - fig.colorbar(con) - - ax.set_aspect("equal") - if xlabel is not None: - ax.set_xlabel(xlabel) - if ylabel is not None: - ax.set_ylabel(ylabel) - - if separatrix: - # Plot separatrix - - # Lower X-point location - Rx = 0.125*(R[ix1-1,j11] + R[ix1,j11] + R[ix1,j11+1] + R[ix1-1,j11+1] - + R[ix1-1,j22+1] + R[ix1,j22+1] + R[ix1,j22] + R[ix1-1,j22]) - Zx = 0.125*(Z[ix1-1,j11] + Z[ix1,j11] + Z[ix1,j11+1] + Z[ix1-1,j11+1] - + Z[ix1-1,j22+1] + Z[ix1,j22+1] + Z[ix1,j22] + Z[ix1-1,j22]) - # Lower inner leg - ax.plot( concatenate( (0.5*(R[ix1-1,0:(j11+1)] + R[ix1,0:(j11+1)]), [Rx]) ), concatenate( (0.5*(Z[ix1-1,0:(j11+1)] + Z[ix1,0:(j11+1)]), [Zx]) ), 'k-') - # Lower outer leg - ax.plot( concatenate( ([Rx],0.5*(R[ix1-1,(j22+1):] + R[ix1,(j22+1):])) ), concatenate( ([Zx], 0.5*(Z[ix1-1,(j22+1):] + Z[ix1,(j22+1):])) ), 'k-') - # Core - - ax.plot( concatenate( ([Rx], 0.5*(R[ix1-1,(j11+1):(j21+1)] + R[ix1,(j11+1):(j21+1)]), 0.5*(R[ix1-1,(j12+1):(j22+1)] + R[ix1,(j12+1):(j22+1)]), [Rx]) ), - concatenate( ([Zx], 0.5*(Z[ix1-1,(j11+1):(j21+1)] + Z[ix1,(j11+1):(j21+1)]), 0.5*(Z[ix1-1,(j12+1):(j22+1)] + Z[ix1,(j12+1):(j22+1)]), [Zx]) ), 'k-') - if show: - plt.show() - - return con - - -def bout2sonnet(grdname, outf): - """Creates a Sonnet format grid from a BOUT++ grid. - - NOTE: Branch cuts are not yet supported - - Parameters - ---------- - grdname : str - Filename of BOUT++ grid file - outf : File - The file-like object to write to - - Examples - -------- - - >>> with open("output.sonnet", "w") as f: - ... bout2sonnet("BOUT.grd.nc", f) - - """ - - with DataFile(grdname) as g: - Rxy = g["Rxy"] - Zxy = g["Zxy"] - Bpxy = g["Bpxy"] - Btxy = g["Btxy"] - Bxy = g["Bxy"] - - # Now iterate over cells in the order Eirene expects - - nx, ny = Rxy.shape - - # Extrapolate values in Y - R = zeros([nx,ny+2]) - Z = zeros([nx,ny+2]) - - R[:,1:-1] = Rxy - Z[:,1:-1] = Zxy - - R[:,0] = 2.*R[:,1] - R[:,2] - Z[:,0] = 2.*Z[:,1] - Z[:,2] - - R[:,-1] = 2.*R[:,-2] - R[:,-3] - Z[:,-1] = 2.*Z[:,-2] - Z[:,-3] - - element = 1 # Element number - - outf.write("BOUT++: "+grdname+"\n\n") - - outf.write("=====================================\n") - - for i in range(2, nx-2): - # Loop in X, excluding guard cells - for j in range(1,ny+1): - # Loop in Y. Guard cells not in grid file - - # Lower left (low Y, low X) - ll = ( 0.25*(R[i-1,j-1] + R[i-1,j] + R[i,j-1] + R[i,j]), - 0.25*(Z[i-1,j-1] + Z[i-1,j] + Z[i,j-1] + Z[i,j]) ) - - # Lower right (low Y, upper X) - lr = ( 0.25*(R[i+1,j-1] + R[i+1,j] + R[i,j-1] + R[i,j]), - 0.25*(Z[i+1,j-1] + Z[i+1,j] + Z[i,j-1] + Z[i,j]) ) - - # Upper left (upper Y, lower X) - ul = ( 0.25*(R[i-1,j+1] + R[i-1,j] + R[i,j+1] + R[i,j]), - 0.25*(Z[i-1,j+1] + Z[i-1,j] + Z[i,j+1] + Z[i,j]) ) - - # Upper right (upper Y, upper X) - ur = ( 0.25*(R[i+1,j+1] + R[i+1,j] + R[i,j+1] + R[i,j]), - 0.25*(Z[i+1,j+1] + Z[i+1,j] + Z[i,j+1] + Z[i,j]) ) - - # Element number - outf.write(" ELEMENT %d = ( %d, %d): (%e, %e) (%e, %e)\n" % ( - element, - j-1, i-2, - ll[0], ll[1], - ul[0], ul[1])) - - # Ratio Bt / Bp at cell centre. Note j-1 because - # Bpxy and Btxy have not had extra points added - outf.write(" FIELD RATIO = %e (%e, %e)\n" % (Bpxy[i,j-1] / Btxy[i,j-1], R[i,j], Z[i,j]) ) - - outf.write(" (%e, %e) (%e, %e)\n" % ( - lr[0], lr[1], - ur[0], ur[1])) - - if (i == nx-3) and (j == ny+1): - # Last element - outf.write("=====================================\n") - else: - outf.write("-------------------------------------\n") - - element += 1 diff --git a/tools/pylib/boutdata/input.py b/tools/pylib/boutdata/input.py deleted file mode 100644 index 46759cfaa5..0000000000 --- a/tools/pylib/boutdata/input.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Fourier transform data for input to BOUT++ - -""" -from builtins import range - -from numpy.fft import rfft -from numpy import ndarray - - -def transform3D(arr): - """Fourier transforms a 3D array in the Z dimension - - BOUT++ can take 3D inputs to be Fourier transformed in the Z - direction. - - Parameters - ---------- - arr : array_like - Input 3-D array - - Returns - ------- - array_like - A 3D array [x,y,kz] where kz is organised in the standard FFT - order, with constant (DC, kz=0) component first, followed by - real/imaginary pairs. - - kz = [0, (real, imag), (real, imag), ...] - - """ - - if len(arr.shape) != 3: - raise ValueError("Input array must be 3D") - - # Take FFT over z (last index), returning a complex array - fa = rfft(arr, axis=-1) - - nmodes = fa.shape[-1] - - # scipy fft normalises to N, but fftw doesn't - fa /= arr.shape[-1] - # Unpack complex array into a real array - - shape = list(arr.shape) - shape[-1] = 1 + (nmodes-1)*2 # One for DC + 2 for other modes - - result = ndarray(shape) - - # kz = 0 (DC) component only has real part - result[:,:,0] = fa[:,:,0].real - - # All other components have both real and imaginary parts - for k in range(1,nmodes): - result[:,:,2*k-1] = fa[:,:,k].real - result[:,:,2*k] = fa[:,:,k].imag - - return result - diff --git a/tools/pylib/boutdata/mayavi2.py b/tools/pylib/boutdata/mayavi2.py deleted file mode 100644 index a32b7433a1..0000000000 --- a/tools/pylib/boutdata/mayavi2.py +++ /dev/null @@ -1,118 +0,0 @@ -from __future__ import print_function -from builtins import range - -import numpy as np -from numpy import cos, sin, pi - -from enthought.tvtk.api import tvtk -from enthought.mayavi.scripts import mayavi2 - -def aligned_points(grid, nz=1, period=1.0, maxshift=0.4): - try: - nx = grid["nx"] - ny = grid["ny"] - zshift = grid["zShift"] - Rxy = grid["Rxy"] - Zxy = grid["Zxy"] - except: - print("Missing required data") - return None - - dz = 2.*pi / (period * (nz-1)) - phi0 = np.linspace(0,2.*pi / period, nz) - - # Need to insert additional points in Y so mesh looks smooth - #for y in range(1,ny): - # ms = np.max(np.abs(zshift[:,y] - zshift[:,y-1])) - # if( - - # Create array of points, structured - points = np.zeros([nx*ny*nz, 3]) - - start = 0 - for y in range(ny): - end = start + nx*nz - - phi = zshift[:,y] + phi0[:,None] - r = Rxy[:,y] + (np.zeros([nz]))[:,None] - - xz_points = points[start:end] - xz_points[:,0] = (r*cos(phi)).ravel() # X - xz_points[:,1] = (r*sin(phi)).ravel() # Y - xz_points[:,2] = (Zxy[:,y]+(np.zeros([nz]))[:,None]).ravel() # Z - - start = end - - return points - -def create_grid(grid, data, period=1): - - s = np.shape(data) - - nx = grid["nx"] - ny = grid["ny"] - nz = s[2] - - print("data: %d,%d,%d grid: %d,%d\n" % (s[0],s[1],s[2], nx,ny)) - - dims = (nx, nz, ny) - sgrid = tvtk.StructuredGrid(dimensions=dims) - pts = aligned_points(grid, nz, period) - print(np.shape(pts)) - sgrid.points = pts - - scalar = np.zeros([nx*ny*nz]) - start = 0 - for y in range(ny): - end = start + nx*nz - scalar[start:end] = (data[:,y,:]).transpose().ravel() - print(y, " = " , np.max(scalar[start:end])) - start = end - - sgrid.point_data.scalars = np.ravel(scalar.copy()) - sgrid.point_data.scalars.name = "data" - - return sgrid - -@mayavi2.standalone -def view3d(sgrid): - from enthought.mayavi.sources.vtk_data_source import VTKDataSource - from enthought.mayavi.modules.api import Outline, GridPlane - - mayavi.new_scene() - src = VTKDataSource(data=sgrid) - mayavi.add_source(src) - mayavi.add_module(Outline()) - g = GridPlane() - g.grid_plane.axis = 'x' - mayavi.add_module(g) - -if __name__ == '__main__': - from boutdata.collect import collect - from boututils.file_import import file_import - - path = "/media/449db594-b2fe-4171-9e79-2d9b76ac69b6/runs/data_33/" - #path="/home/ben/run4" - - #g = file_import("../cbm18_dens8.grid_nx68ny64.nc") - g = file_import("data/cbm18_8_y064_x516_090309.nc") - #g = file_import("/home/ben/run4/reduced_y064_x256.nc") - - data = collect("P", tind=50, path=path) - data = data[0,:,:,:] - s = np.shape(data) - nz = s[2] - - bkgd = collect("P0", path=path) - for z in range(nz): - data[:,:,z] += bkgd - - # Create a structured grid - sgrid = create_grid(g, data, 10) - - - w = tvtk.XMLStructuredGridWriter(input=sgrid, file_name='sgrid.vts') - w.write() - - # View the structured grid - view3d(sgrid) diff --git a/tools/pylib/boutdata/mms.py b/tools/pylib/boutdata/mms.py deleted file mode 100644 index b95ff175e1..0000000000 --- a/tools/pylib/boutdata/mms.py +++ /dev/null @@ -1,591 +0,0 @@ -""" Functions for calculating sources for the - Method of Manufactured Solutions (MMS) - -""" -from __future__ import print_function -from __future__ import division - -from sympy import symbols, cos, sin, diff, sqrt, pi, simplify, trigsimp, Wild - -from numpy import arange, zeros - -# Constants -qe = 1.602e-19 -Mp = 1.67262158e-27 -mu0 = 4.e-7*3.141592653589793 - -# Define symbols - -x = symbols('x') -y = symbols('y') -z = symbols('z') -t = symbols('t') - -class Metric(object): - def __init__(self): - # Create an identity metric - self.x = symbols('x\'') - self.y = symbols('y\'') - self.z = symbols('z\'') - - self.g11 = self.g22 = self.g33 = 1.0 - self.g12 = self.g23 = self.g13 = 0.0 - - self.g_11 = self.g_22 = self.g_33 = 1.0 - self.g_12 = self.g_23 = self.g_13 = 0.0 - - self.J = 1.0 - self.B = 1.0 - -identity = Metric() - -# Basic differencing -def ddt(f): - """Time derivative""" - return diff(f, t) - - -def DDX(f, metric = identity): - return diff(f, metric.x) - -def DDY(f, metric = identity): - return diff(f, metric.y) - -def DDZ(f, metric = identity): - return diff(f, metric.z) - - -def D2DX2(f, metric = identity): - return diff(f, metric.x, 2) - -def D2DY2(f, metric = identity): - return diff(f, metric.y, 2) - -def D2DZ2(f, metric = identity): - return diff(f, metric.z, 2) - - -def D2DXDY(f, metric = identity): - message = "* WARNING: D2DXDY is currently not set in BOUT++."+\ - " Check src/sys/derivs.cxx if situation has changed. *" - print("\n"*3) - print("*"*len(message)) - print(message) - print("*"*len(message)) - print("\n"*3) - return DDX(DDY(f, metric), metric) - -def D2DXDZ(f, metric = identity): - return DDX(DDZ(f, metric), metric) - -def D2DYDZ(f, metric = identity): - return DDY(DDZ(f, metric), metric) - -# Operators - -def bracket(f, g, metric = identity): - """ - Calculates [f,g] symbolically - """ - - dfdx = diff(f, metric.x) - dfdz = diff(f, metric.z) - - dgdx = diff(g, metric.x) - dgdz = diff(g, metric.z) - - return dfdz * dgdx - dfdx * dgdz - -def b0xGrad_dot_Grad(phi, A, metric = identity): - """ - Perpendicular advection operator, including - derivatives in y - - Note: If y derivatives are neglected, then this reduces - to bracket(f, g, metric) * metric.B - (in a Clebsch coordinate system) - - """ - dpdx = DDX(phi, metric) - dpdy = DDY(phi, metric) - dpdz = DDZ(phi, metric) - - vx = metric.g_22*dpdz - metric.g_23*dpdy; - vy = metric.g_23*dpdx - metric.g_12*dpdz; - vz = metric.g_12*dpdy - metric.g_22*dpdx; - - return (+ vx*DDX(A, metric) - + vy*DDY(A, metric) - + vz*DDZ(A, metric) ) / (metric.J*sqrt(metric.g_22)) - -def Delp2(f, metric = identity, all_terms=True): - """ Laplacian in X-Z - - If all_terms is false then first derivative terms are neglected. - By default all_terms is true, but can be disabled - in the BOUT.inp file (laplace section) - - """ - d2fdx2 = diff(f, metric.x, 2) - d2fdz2 = diff(f, metric.z, 2) - d2fdxdz = diff(f, metric.x, metric.z) - - result = metric.g11*d2fdx2 + metric.g33*d2fdz2 + 2.*metric.g13*d2fdxdz - - if all_terms: - G1 = (DDX(metric.J*metric.g11, metric) + DDY(metric.J*metric.g12, metric) + DDZ(metric.J*metric.g13, metric)) / metric.J - G3 = (DDX(metric.J*metric.g13, metric) + DDY(metric.J*metric.g23, metric) + DDZ(metric.J*metric.g33, metric)) / metric.J - result += G1 * diff(f, metric.x) + G3 * diff(f, metric.z) - - return result - -def Delp4(f, metric = identity): - d4fdx4 = diff(f, metric.x, 4) - d4fdz4 = diff(f, metric.z, 4) - - return d4fdx4 + d4fdz4 - -def Grad_par(f, metric = identity): - """The parallel gradient""" - return diff(f, metric.y) / sqrt(metric.g_22) - -def Vpar_Grad_par(v, f, metric = identity): - """Parallel advection operator $$v_\parallel \cdot \nabla_\parallel (f)$$""" - return v * Grad_par(f, metric=metric) - -def Div_par(f, metric=identity): - ''' - Divergence of magnetic field aligned vector $$v = \hat{b} f - \nabla \cdot (\hat{b} f) = 1/J \partial_y (f/B) - = B Grad_par(f/B)$$ - ''' - return metric.B*Grad_par(f/metric.B, metric) - -def Laplace(f, metric=identity): - """The full Laplace operator""" - G1 = (DDX(metric.J*metric.g11, metric) + DDY(metric.J*metric.g12, metric) + DDZ(metric.J*metric.g13, metric)) / metric.J - G2 = (DDX(metric.J*metric.g12, metric) + DDY(metric.J*metric.g22, metric) + DDZ(metric.J*metric.g23, metric)) / metric.J - G3 = (DDX(metric.J*metric.g13, metric) + DDY(metric.J*metric.g23, metric) + DDZ(metric.J*metric.g33, metric)) / metric.J - - result = G1*DDX(f, metric) + G2*DDY(f, metric) + G3*DDZ(f, metric)\ - + metric.g11*D2DX2(f, metric) + metric.g22*D2DY2(f, metric) + metric.g33*D2DZ2(f, metric)\ - + 2.0*(metric.g12*D2DXDY(f, metric) + metric.g13*D2DXDZ(f, metric) + metric.g23*D2DYDZ(f, metric)) - - return result - -def Laplace_par(f, metric=identity): - """ - Div( b (b.Grad(f) ) ) = (1/J) d/dy ( J/g_22 * df/dy ) - """ - return diff( (metric.J/metric.g_22)*diff(f, metric.y), metric.y)/ metric.J - -def Laplace_perp(f, metric=identity): - """ - The perpendicular Laplace operator - - Laplace_perp = Laplace - Laplace_par - """ - return Laplace(f, metric) - Laplace_par(f, metric) - -# Convert expression to string - -def trySimplify(expr): - """ - Tries to simplify an expression - """ - try: - return simplify(expr) - except ValueError: - return expr - -def exprToStr(expr): - """ Convert a sympy expression to a string for BOUT++ input - """ - - s = str(expr).replace("**", "^") # Replace exponent operator - - # Try to remove lots of 1.0*... - s = s.replace("(1.0*", "(") - s = s.replace(" 1.0*", " ") - - return s - -def exprMag(expr): - """ - Estimate the magnitude of an expression - - """ - - # Replace all sin, cos with 1 - any = Wild('a') # Wildcard - expr = expr.replace(sin(any), 1.0) - expr = expr.replace(cos(any), 1.0) - - # Pick maximum values of x,y,z - expr = expr.subs(x, 1.0) - expr = expr.subs(y, 2.*pi) - expr = expr.subs(z, 2.*pi) - - return expr.evalf() - -################################## - -class SimpleTokamak(object): - """ - Simple tokamak - - NOTE: This is NOT an equilibrium calculation. The input - is intended solely for testing with MMS - """ - def __init__(self, R = 2, Bt = 1.0, eps = 0.1, dr=0.02, q = lambda x:2+x**2): - """ - R - Major radius [m] - - Bt - Toroidal field [T] - - eps - Inverse aspect ratio - - dr - Width of the radial region [m] - - q(x) - A function which returns the safety factor - as a function of x in range [0,1] - - - Coordinates: - x - Radial, [0,1] - y - Poloidal, [0,2pi]. Origin is at inboard midplane. - - - """ - # X has a range [0,1], and y [0,2pi] - #x, y = symbols("x y") - - self.x = x - self.y = y - - self.R = R - - self.dr = dr - - # Minor radius - self.r = R * eps - - # Get safety factor - self.q = q(x) - - # Toroidal angle of a field-line as function - # of poloidal angle y - self.zShift = self.q*(y + eps * sin(y)) - - # Field-line pitch - self.nu = self.q*(1 + eps*cos(y)) #diff(self.zShift, y) - - # Coordinates of grid points - self.Rxy = R - self.r * cos(y) - self.Zxy = self.r * sin(y) - - # Poloidal arc length - self.hthe = self.r + 0.*x - - # Toroidal magnetic field - self.Btxy = Bt * R / self.Rxy - - # Poloidal magnetic field - self.Bpxy = self.Btxy * self.hthe / (self.nu * self.Rxy) - - # Total magnetic field - self.Bxy = sqrt(self.Btxy**2 + self.Bpxy**2) - - # Approximate poloidal field for radial width calculation - Bp0 = Bt * self.r / (q(0.5) * R) - print("Bp0 = %e" % Bp0) - - # dx = Bp * R * dr -- width of the box in psi space - self.psiwidth = Bp0 * R * dr - print("psi width = %e" % self.psiwidth) - - # Integrated shear - self.sinty = diff(self.zShift, x) / self.psiwidth - - # Extra expressions to add to grid file - self._extra = {} - - def add(self, expr, name): - """ - Add an additional expression to be written to the grid files - - """ - self._extra[name] = expr - - - def write(self, nx, ny, output, MXG=2): - """ - Outputs a tokamak shape to a grid file - - nx - Number of radial grid points, not including guard cells - ny - Number of poloidal (parallel) grid points - output - boututils.datafile object, e.g., an open netCDF file - MXG, Number of guard cells in the x-direction - """ - - ngx = nx + 2*MXG - ngy = ny - - # Create an x and y grid to evaluate expressions on - xarr = (arange(nx + 2*MXG) - MXG + 0.5) / nx - yarr = 2.*pi*arange(ny)/ny - - output.write("nx", ngx) - output.write("ny", ngy) - - dx = self.psiwidth / nx + 0.*self.x - dy = 2.*pi / ny + 0.*self.x - - for name, var in [ ("dx", dx), - ("dy", dy), - ("Rxy", self.Rxy), - ("Zxy", self.Zxy), - ("Btxy", self.Btxy), - ("Bpxy", self.Bpxy), - ("Bxy", self.Bxy), - ("hthe", self.hthe), - ("sinty", self.sinty), - ("zShift", self.zShift)]: - - # Note: This is slow, and could be improved using something like lambdify - values = zeros([ngx, ngy]) - for i, x in enumerate(xarr): - for j, y in enumerate(yarr): - values[i,j] = var.evalf(subs={self.x:x, self.y:y}) - - output.write(name, values) - - for name, var in list(self._extra.items()): - values = zeros([ngx, ngy]) - for i, x in enumerate(xarr): - for j, y in enumerate(yarr): - values[i,j] = var.evalf(subs={self.x:x, self.y:y}) - - output.write(name, values) - - shiftAngle = zeros(ngx) - for i, x in enumerate(xarr): - shiftAngle[i] = 2.*pi*self.q.evalf(subs={self.x:x}) - - output.write("ShiftAngle", shiftAngle) - - def metric(self): - """ - Returns an analytic metric tensor - """ - m = Metric() - - # Set symbols for x and y directions - m.x = self.x - m.y = self.y - - # Calculate metric tensor - - m.g11 = (self.Rxy * self.Bpxy)**2 - m.g22 = 1./self.hthe**2 - m.g33 = self.sinty**2*m.g11 + self.Bxy**2/m.g11 - m.g12 = 0.0*x - m.g13 = -self.sinty*m.g11 - m.g23 = -self.Btxy / (self.hthe * self.Bpxy * self.R) - - m.g_11 = 1./m.g11 + (self.sinty*self.Rxy)**2 - m.g_22 = (self.Bxy * self.hthe / self.Bpxy)**2 - m.g_33 = self.Rxy**2 - m.g_12 = self.Btxy*self.hthe*self.sinty*self.Rxy / self.Bpxy - m.g_13 = self.sinty*self.Rxy**2 - m.g_23 = self.Btxy*self.hthe*self.Rxy / self.Bpxy - - m.J = self.hthe / self.Bpxy - m.B = self.Bxy - - # Convert all "x" symbols from [0,1] into flux - m.Lx = self.psiwidth - xsub = m.x / self.psiwidth - - m.g11 = m.g11.subs(x, xsub) - m.g22 = m.g22.subs(x, xsub) - m.g33 = m.g33.subs(x, xsub) - m.g12 = m.g12.subs(x, xsub) - m.g13 = m.g13.subs(x, xsub) - m.g23 = m.g23.subs(x, xsub) - - m.g_11 = m.g_11.subs(x, xsub) - m.g_22 = m.g_22.subs(x, xsub) - m.g_33 = m.g_33.subs(x, xsub) - m.g_12 = m.g_12.subs(x, xsub) - m.g_13 = m.g_13.subs(x, xsub) - m.g_23 = m.g_23.subs(x, xsub) - - m.J = m.J.subs(x, xsub) - m.B = m.B.subs(x, xsub) - - return m - -########################## -# Shaped tokamak - -class ShapedTokamak(object): - def __init__(self, Rmaj=6.0, rmin=2.0, dr=0.1, kappa=1.0, delta=0.0, b=0.0, ss=0.0, Bt0=1.0, Bp0 = 0.2): - """ - Rmaj - Major radius [m] - rmin - Minor radius [m] - dr - Radial width of region [m] - - kappa - Ellipticity, 1 for a circle - delta - Triangularity, 0 for circle - b - Indentation ("bean" shape), 0 for circle - - ss - Shafranov shift [m] - - Bt0 - Toroidal magnetic field on axis [T]. Varies as 1/R - Bp0 - Poloidal field at outboard midplane [T] - - Outputs - ------- - - Assigns member variables - - x, y - Symbols for x and y coordinates - - R (x,y) - Z (x,y) - - """ - - # X has a range [0,1], and y [0,2pi] - x, y = symbols("x y") - - # Minor radius as function of x - rminx = rmin + (x-0.5)*dr - - # Analytical expression for R and Z coordinates as function of x and y - Rxy = Rmaj - b + (rminx + b*cos(y))*cos(y + delta*sin(y)) + ss*(0.5-x)*(dr/rmin) - Zxy = kappa * rminx * sin(y) - - # Toroidal magnetic field - Btxy = Bt0 * Rmaj / Rxy - - # Poloidal field. dx constant, so set poloidal field - # at outboard midplane (y = 0) - # NOTE: Approximate calculation - - # Distance between flux surface relative to outboard midplane. - expansion = (1 - (old_div(ss,rmin))*cos(y))/(1 - (ss/rmin)) - - Bpxy = Bp0 * ((Rmaj + rmin) / Rxy) / expansion - - # Calculate hthe - hthe = sqrt(diff(Rxy, y)**2 + diff(Zxy, y)**2) - try: - hthe = trigsimp(hthe) - except ValueError: - pass - - # Field-line pitch - nu = Btxy * hthe / (Bpxy * Rxy) - - # Shift angle - # NOTE: Since x has a range [0,1] this could be done better - # than ignoring convergence conditions - self.zShift = integrate(nu, y, conds='none') - - # Safety factor - self.shiftAngle = self.zShift.subs(y, 2*pi) - self.zShift.subs(y, 0) - - # Integrated shear - self.I = diff(self.zShift, x) - - self.x = x - self.y = y - - self.R = Rxy - self.Z = Zxy - - self.Bt = Btxy - self.Bp = Bpxy - self.B = sqrt(Btxy**2 + Bpxy**2) - - self.hthe = hthe - - def write(self, nx, ny, filename, MXG=2): - """ - Outputs a tokamak shape to a grid file - - nx - Number of radial grid points, not including guard cells - ny - Number of poloidal (parallel) grid points - output - boututils.datafile object, e.g., an open netCDF file - MXG, Number of guard cells in the x-direction - """ - - ngx = nx + 2*MXG - ngy = ny - - # Create an x and y grid to evaluate expressions on - xarr = (arange(nx + 2*MXG) - MXG + 0.5) / nx - yarr = 2.*pi*arange(ny)/ny - - Rxy = zeros([ngx, ngy]) - Zxy = zeros([ngx, ngy]) - - Btxy = zeros([ngx, ngy]) - Bpxy = zeros([ngx, ngy]) - - hthe = zeros([ngx, ngy]) - - - I = zeros([ngx, ngy]) - - # Note: This is slow, and could be improved using something like lambdify - for i, x in enumerate(xarr): - for j, y in enumerate(yarr): - Rxy[i,j] = self.R.evalf(subs={self.x:x, self.y:y}) - Zxy[i,j] = self.Z.evalf(subs={self.x:x, self.y:y}) - - Btxy[i,j] = self.Bt.evalf(subs={self.x:x, self.y:y}) - Bpxy[i,j] = self.Bp.evalf(subs={self.x:x, self.y:y}) - - hthe[i,j] = self.hthe.evalf(subs={self.x:x, self.y:y}) - - - plt.plot(Rxy[i,:], Zxy[i,:]) - plt.show() - - Bxy = sqrt(Btxy**2 + Bpxy**2) - - def metric(self): - """ - Returns an analytic metric tensor - """ - m = Metric() - - # Set symbols for x and y directions - m.x = self.x - m.y = self.y - - # Calculate metric tensor - - m.g11 = (self.R * self.Bp)**2 - m.g22 = 1./self.hthe**2 - m.g33 = self.I**2*m.g11 + self.B**2 / m.g11 - m.g12 = 0.0 - m.g13 = -self.I*m.g11 - m.g23 = -self.Bt / (self.hthe * self.Bp * self.R) - - m.g_11 = 1./m.g11 + (self.I*self.R)**2 - m.g_22 = (self.B * self.hthe / self.Bpxy)**2 - m.g_33 = self.R**2 - m.g_12 = self.Bt*self.hthe*self.I*self.R / self.Bp - m.g_13 = self.I*self.R**2 - m.g_23 = self.Bt*self.hthe*self.R / self.Bp - - m.J = self.hthe / self.Bp - m.B = self.B - - return m - - diff --git a/tools/pylib/boutdata/pol_slice.py b/tools/pylib/boutdata/pol_slice.py deleted file mode 100644 index 7adea90c50..0000000000 --- a/tools/pylib/boutdata/pol_slice.py +++ /dev/null @@ -1,110 +0,0 @@ -from __future__ import print_function -from __future__ import division - -from boututils.datafile import DataFile -import numpy as np -from scipy.ndimage import map_coordinates - - -def pol_slice(var3d, gridfile, n=1, zangle=0.0, nyInterp=None): - """Takes a 3D variable, and returns a 2D slice at fixed toroidal angle - - Parameters - ---------- - var3d : array_like - The input array. Should be 3D - gridfile : str - The gridfile containing the coordinate system to used - n : int, optional - The number of times the data must be repeated for a full torus, - e.g. n=2 is half a torus - zangle : float, optional - The (real) toroidal angle of the result - nyInterp : int, optional - The number of y (theta) points to use in the final result. - - Returns - ------- - array - A 2D-slice of var3d interpolated at a fixed toroidal angle - """ - n = int(n) - zangle = float(zangle) - - s = np.shape(var3d) - if len(s) != 3: - raise ValueError("pol_slice expects a 3D variable (got {} dimensions)" - .format(len(s))) - - nx, ny, nz = s - - # Open the grid file - with DataFile(gridfile) as gf: - # Check the grid size is correct - grid_nx = gf.read("nx") - if grid_nx != nx: - raise ValueError("Grid X size ({}) is different to the variable ({})" - .format(grid_nx, nx)) - grid_ny = gf.read("ny") - if grid_ny != ny: - raise ValueError("Grid Y size ({}) is different to the variable ({})" - .format(grid_ny, ny)) - - # Get the toroidal shift - zShift = gf.read("qinty") - - if zShift is not None: - print("Using qinty as toroidal shift angle") - else: - zShift = gf.read("zShift") - if zShift is not None: - print("Using zShift as toroidal shift angle") - else: - raise ValueError("Neither qinty nor zShift found") - - # Decide if we've asked to do interpolation - if nyInterp is not None and nyInterp != ny: - varTmp = var3d - - # Interpolate to output positions and make the correct shape - # np.mgrid gives us an array of indices - # 0:ny-1:nyInterp*1j means use nyInterp points between 0 and ny-1 inclusive - var3d = map_coordinates(varTmp, np.mgrid[0:nx, 0:ny-1:nyInterp*1j, 0:nz], - cval=-999) - zShift = map_coordinates(zShift, np.mgrid[0:nx, 0:ny-1:nyInterp*1j], - cval=-999) - - # Update shape - ny = nyInterp - - var2d = np.zeros([nx, ny]) - - ###################################### - # Perform 2D slice - dz = 2.*np.pi / float(n * nz) - zind = (zangle - zShift) / dz - z0f = np.floor(zind) - z0 = z0f.astype(int) - p = zind - z0f - - # Make z0 between 0 and (nz-2) - z0 = ((z0 % (nz-1)) + (nz-1)) % (nz-1) - - # Get z+ and z- - zp = (z0 + 1) % (nz-1) - zm = (z0 - 1 + (nz-1)) % (nz-1) - - # For some reason numpy imposes a limit of 32 entries to choose - # so if nz>32 we have to use a different approach. This limit may change with numpy version - if nz >= 32: - for x in np.arange(nx): - for y in np.arange(ny): - var2d[x, y] = (0.5*p[x, y]*(p[x, y]-1.0) * var3d[x, y, zm[x, y]] + - (1.0 - p[x, y]*p[x, y]) * var3d[x, y, z0[x, y]] + - 0.5*p[x, y]*(p[x, y]+1.0) * var3d[x, y, zp[x, y]]) - else: - var2d = (0.5*p*(p-1.0) * np.choose(zm.T, var3d.T).T + - (1.0 - p*p) * np.choose(z0.T, var3d.T).T + - 0.5*p*(p+1.0) * np.choose(zp.T, var3d.T).T) - - return var2d diff --git a/tools/pylib/boutdata/processor_rearrange.py b/tools/pylib/boutdata/processor_rearrange.py deleted file mode 100644 index fb91af3763..0000000000 --- a/tools/pylib/boutdata/processor_rearrange.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Routines for redistributing files over different numbers of -processors - -""" - -from math import sqrt -from collections import namedtuple - -processor_layout_ = namedtuple("BOUT_processor_layout", - ["nxpe", "nype", "npes", "mxsub", "mysub", - "nx", "ny", "mz", "mxg", "myg"]) - - -# Subclass the namedtuple above so we can add a docstring -class processor_layout(processor_layout_): - """A namedtuple describing the processor layout, including grid sizes - and guard cells - - Parameters - ---------- - - nxpe, nype : int - The number of processors in x and y - npes : int - The total number of procesors - mxsub, mysub : int - The size of the grid in x and y on a single processor - nx, ny, mz : int - The total size of the grid in x, y and z - mxg : int - The number of guard cells in x and y - - """ - pass - - -def get_processor_layout(boutfile, has_t_dimension=True, mxg=2, myg=2): - """Given a BOUT.restart.* or BOUT.dmp.* file (as a DataFile object), - return the processor layout for its data - - Parameters - ---------- - boutfile : DataFile - Restart or dump file to read - has_t_dimension : bool, optional - Does this file have a time dimension? - mxg, myg : int, optional - Number of x, y guard cells - - Returns - ------- - processor_layout - A description of the processor layout and grid sizes - - """ - - nxpe = boutfile.read('NXPE') - nype = boutfile.read("NYPE") - npes = nxpe * nype - - # Get list of variables - var_list = boutfile.list() - if len(var_list) == 0: - raise ValueError("ERROR: No data found") - - mxsub = 0 - mysub = 0 - mz = 0 - - if has_t_dimension: - maxdims = 4 - else: - maxdims = 3 - for v in var_list: - if boutfile.ndims(v) == maxdims: - s = boutfile.size(v) - mxsub = s[maxdims - 3] - 2 * mxg - if mxsub < 0: - if s[maxdims - 3] == 1: - mxsub = 1 - mxg = 0 - elif s[maxdims - 3] == 3: - mxsub = 1 - mxg = 1 - else: - print("Number of x points is wrong?") - return False - - mysub = s[maxdims - 2] - 2 * myg - if mysub < 0: - if s[maxdims - 2] == 1: - mysub = 1 - myg = 0 - elif s[maxdims - 2] == 3: - mysub = 1 - myg = 1 - else: - print("Number of y points is wrong?") - return False - - mz = s[maxdims - 1] - break - - # Calculate total size of the grid - nx = mxsub * nxpe - ny = mysub * nype - - result = processor_layout(nxpe=nxpe, nype=nype, npes=npes, mxsub=mxsub, mysub=mysub, nx=nx, ny=ny, mz=mz, mxg=mxg, myg=myg) - - return result - - -def create_processor_layout(old_processor_layout, npes, nxpe=None): - """Convert one processor layout into another one with a different - total number of processors - - If nxpe is None, use algorithm from BoutMesh to select optimal nxpe. - Otherwise, check nxpe is valid (divides npes) - - Parameters - ---------- - old_processor_layout : processor_layout - The processor layout to convert - npes : int - The new total number of procesors - nxpe : int, optional - The number of procesors in x to use - - Returns - ------- - processor_layout - A description of the processor layout and grid sizes - - """ - - if nxpe is None: # Copy algorithm from BoutMesh for selecting nxpe - ideal = sqrt(float(old_processor_layout.nx) * float(npes) / float(old_processor_layout.ny)) - # Results in square domain - - for i in range(1, npes + 1): - if npes % i == 0 and old_processor_layout.nx % i == 0 and int(old_processor_layout.nx / i) >= old_processor_layout.mxg and old_processor_layout.ny % (npes / i) == 0: - # Found an acceptable value - # Warning: does not check branch cuts! - - if nxpe is None or abs(ideal - i) < abs(ideal - nxpe): - nxpe = i # Keep value nearest to the ideal - - if nxpe is None: - raise ValueError("ERROR: could not find a valid value for nxpe") - elif npes % nxpe != 0: - raise ValueError( - "ERROR: requested nxpe is invalid, it does not divide npes") - - nype = int(npes / nxpe) - - mxsub = int(old_processor_layout.nx / nxpe) - mysub = int(old_processor_layout.ny / nype) - - result = processor_layout(nxpe=nxpe, nype=nype, npes=npes, mxsub=mxsub, mysub=mysub, nx=old_processor_layout.nx, ny=old_processor_layout.ny, mz=old_processor_layout.mz, mxg=old_processor_layout.mxg, myg=old_processor_layout.myg) - - return result diff --git a/tools/pylib/boutdata/restart.py b/tools/pylib/boutdata/restart.py deleted file mode 100644 index 092c7a98cf..0000000000 --- a/tools/pylib/boutdata/restart.py +++ /dev/null @@ -1,829 +0,0 @@ -"""Routines for manipulating restart files - -TODO ----- - -- Don't import ``numpy.random.normal`` directly, just the ``random`` - submodule, or sphinx includes the documentation for ``normal`` - -""" - -from __future__ import print_function -from __future__ import division -from builtins import str, range - -import os -import glob - -from boutdata.collect import collect, create_cache -from boututils.datafile import DataFile -from boututils.boutarray import BoutArray -from boutdata.processor_rearrange import get_processor_layout, create_processor_layout - -import multiprocessing -import numpy as np -from numpy import mean, zeros, arange -from numpy.random import normal - -from scipy.interpolate import interp1d -try: - from scipy.interpolate import RegularGridInterpolator -except ImportError: - pass - -def resize3DField(var, data, coordsAndSizesTuple, method, mute): - """Resize 3D fields - - To be called by resize. - - Written as a function in order to call it using multiprocess. Must - be defined as a top level function in order to be pickable by the - multiprocess. - - See the function resize for details - - """ - - # Unpack the tuple for better readability - xCoordOld, yCoordOld, zCoordOld,\ - xCoordNew, yCoordNew, zCoordNew,\ - newNx, newNy, newNz = coordsAndSizesTuple - - if not(mute): - print(" Resizing "+var + - ' to (nx,ny,nz) = ({},{},{})'.format(newNx, newNy, newNz)) - - # Make the regular grid function (see examples in - # https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.RegularGridInterpolator.html - # for details) - gridInterpolator = RegularGridInterpolator( - (xCoordOld, yCoordOld, zCoordOld), data, method) - - # Need to fill with one exrta z plane (will only contain zeros) - newData = np.zeros((newNx, newNy, newNz)) - - # Interpolate to the new values - for xInd, x in enumerate(xCoordNew): - for yInd, y in enumerate(yCoordNew): - for zInd, z in enumerate(zCoordNew): - newData[xInd, yInd, zInd] = gridInterpolator([x, y, z]) - - return var, newData - - -def resize(newNx, newNy, newNz, mxg=2, myg=2, - path="data", output="./", informat="nc", outformat=None, - method='linear', maxProc=None, mute=False): - """Increase/decrease the number of points in restart files. - - NOTE: Can't overwrite - WARNING: Currently only implemented with uniform BOUT++ grid - - Parameters - ---------- - newNx, newNy, newNz : int - nx, ny, nz for the new file (including ghost points) - mxg, myg : int, optional - Number of ghost points in x, y (default: 2) - path : str, optional - Input path to data files - output : str, optional - Path to write new files - informat : str, optional - File extension of input - outformat : {None, str}, optional - File extension of output (default: use the same as `informat`) - method : {'linear', 'nearest'}, optional - What interpolation method to be used - maxProc : {None, int}, optional - Limits maximum processors to use when interpolating if set - mute : bool, optional - Whether or not output should be printed from this function - - Returns - ------- - return : bool - True on success, else False - - TODO - ---- - - Add 2D field interpolation - - Replace printing errors with raising `ValueError` - - Make informat work like `redistribute` - - """ - - if method is None: - # Make sure the method is set - method = 'linear' - - if outformat is None: - outformat = informat - - if path == output: - print("ERROR: Can't overwrite restart files when expanding") - return False - - def is_pow2(x): - """Returns true if x is a power of 2""" - return (x > 0) and ((x & (x-1)) == 0) - - if not is_pow2(newNz): - print("ERROR: New Z size {} must be a power of 2".format(newNz)) - return False - - file_list = glob.glob(os.path.join(path, "BOUT.restart.*."+informat)) - file_list.sort() - nfiles = len(file_list) - - if nfiles == 0: - print("ERROR: No data found in {}".format(path)) - return False - - if not(mute): - print("Number of files found: " + str(nfiles)) - - for f in file_list: - new_f = os.path.join(output, f.split('/')[-1]) - if not(mute): - print("Changing {} => {}".format(f, new_f)) - - # Open the restart file in read mode and create the new file - with DataFile(f) as old, DataFile(new_f, write=True, create=True) as new: - - # Find the dimension - for var in old.list(): - # Read the data - data = old.read(var) - # Find 3D variables - if old.ndims(var) == 3: - break - - nx, ny, nz = data.shape - # Make coordinates - # NOTE: The max min of the coordinates are irrelevant when - # interpolating (as long as old and new coordinates - # are consistent), so we just choose all variable to - # be between 0 and 1 Calculate the old coordinates - xCoordOld = np.linspace(0, 1, nx) - yCoordOld = np.linspace(0, 1, ny) - zCoordOld = np.linspace(0, 1, nz) - - # Calculate the new coordinates - xCoordNew = np.linspace(xCoordOld[0], xCoordOld[-1], newNx) - yCoordNew = np.linspace(yCoordOld[0], yCoordOld[-1], newNy) - zCoordNew = np.linspace(zCoordOld[0], zCoordOld[-1], newNz) - - # Make a pool of workers - pool = multiprocessing.Pool(maxProc) - # List of jobs and results - jobs = [] - # Pack input to resize3DField together - coordsAndSizesTuple = (xCoordOld, yCoordOld, zCoordOld, - xCoordNew, yCoordNew, zCoordNew, - newNx, newNy, newNz) - - # Loop over the variables in the old file - for var in old.list(): - # Read the data - data = old.read(var) - attributes = old.attributes(var) - - # Find 3D variables - if old.ndims(var) == 3: - - # Asynchronous call (locks first at .get()) - jobs.append(pool.apply_async(resize3DField, - args=(var, data, coordsAndSizesTuple, method, mute, ))) - - else: - if not(mute): - print(" Copying "+var) - newData = data.copy() - if not(mute): - print("Writing "+var) - new.write(var, newData) - - for job in jobs: - var, newData = job.get() - newData = BoutArray(newData, attributes=attributes) - if not(mute): - print("Writing "+var) - new.write(var, newData) - - # Close the pool of workers - pool.close() - # Wait for all processes to finish - pool.join() - - return True - - -def resizeZ(newNz, path="data", output="./", informat="nc", outformat=None): - """Increase the number of Z points in restart files - - NOTE: - * Can't overwrite - * Will not yield a result close to the original if there are - asymmetries in the z-direction - - Parameters - ---------- - newNz : int - nz for the new file - path : str, optional - Path to original restart files (default: "data") - output : str, optional - Path to write new restart files (default: current directory) - informat : str, optional - File extension of original files (default: "nc") - outformat : str, optional - File extension of new files (default: use the same as `informat`) - - Returns - ------- - True on success, else False - - TODO - ---- - - Replace printing errors with raising `ValueError` - - Make informat work like `redistribute` - - """ - - if outformat is None: - outformat = informat - - if path == output: - print("ERROR: Can't overwrite restart files when expanding") - return False - - def is_pow2(x): - """Returns true if x is a power of 2""" - return (x > 0) and ((x & (x-1)) == 0) - - if not is_pow2(newNz): - print("ERROR: New Z size must be a power of 2") - return False - - file_list = glob.glob(os.path.join(path, "BOUT.restart.*."+informat)) - file_list.sort() - nfiles = len(file_list) - - if nfiles == 0: - print("ERROR: No data found") - return False - - print("Number of files found: " + str(nfiles)) - - for f in file_list: - new_f = os.path.join(output, f.split('/')[-1]) - print("Changing {} => {}".format(f, new_f)) - - # Open the restart file in read mode and create the new file - with DataFile(f) as old,\ - DataFile(new_f, write=True, create=True) as new: - # Loop over the variables in the old file - for var in old.list(): - # Read the data - data = old.read(var) - attributes = old.attributes(var) - - # Find 3D variables - if old.ndims(var) == 3: - print(" Resizing "+var) - - nx, ny, nz = data.shape - - newdata = np.zeros((nx, ny, newNz)) - for x in range(nx): - for y in range(ny): - f_old = np.fft.fft(data[x, y, :]) - - # Number of points in f is power of 2 - f_new = np.zeros(newNz) - - # Copy coefficients across (ignoring Nyquist) - f_new[0] = f_old[0] # DC - for m in range(1, int(nz/2)): - # + ve frequencies - f_new[m] = f_old[m] - # - ve frequencies - f_new[newNz-m] = f_old[nz-m] - - # Invert fft - newdata[x, y, :] = np.fft.ifft(f_new).real - newdata[x, y, :] = newdata[x, y, 0] - - # Multiply with the ratio of newNz/nz - # This is not needed in the IDL routine as the - # forward transfrom has the scaling factor 1/N in - # the forward transform, whereas the scaling factor - # 1/N is the inverse transform in np.fft - # Note that ifft(fft(a)) = a for the same number of - # points in both IDL and np.ftt - newdata *= (newNz/nz) - else: - print(" Copying "+var) - newdata = data.copy() - - newdata = BoutArray(newdata, attributes=attributes) - - new.write(var, newdata) - - return True - - -def addnoise(path=".", var=None, scale=1e-5): - """Add random noise to restart files - - .. warning:: Modifies restart files in place! This is in contrast - to most of the functions in this module! - - Parameters - ---------- - path : str, optional - Path to restart files (default: current directory) - var : str, optional - The variable to modify. By default all 3D variables are modified - scale : float - Amplitude of the noise. Gaussian noise is used, with zero mean - and this parameter as the standard deviation - - """ - file_list = glob.glob(os.path.join(path, "BOUT.restart.*")) - nfiles = len(file_list) - - print("Number of restart files: %d" % (nfiles,)) - - for file in file_list: - print(file) - with DataFile(file, write=True) as d: - if var is None: - for v in d.list(): - if d.ndims(v) == 3: - print(" -> "+v) - data = d.read(v, asBoutArray=True) - data += normal(scale=scale, size=data.shape) - d.write(v, data) - else: - # Modify a single variable - print(" -> "+var) - data = d.read(var) - data += normal(scale=scale, size=data.shape) - d.write(var, data) - - -def scalevar(var, factor, path="."): - """Scales a variable by a given factor, modifying restart files in - place - - .. warning:: Modifies restart files in place! This is in contrast - to most of the functions in this module! - - Parameters - ---------- - var : str - Name of the variable - factor : float - Factor to multiply - path : str, optional - Path to the restart files (default: current directory) - - """ - - file_list = glob.glob(os.path.join(path, "BOUT.restart.*")) - nfiles = len(file_list) - - print("Number of restart files: %d" % (nfiles,)) - for file in file_list: - print(file) - with DataFile(file, write=True) as d: - d[var] = d[var] * factor - - -def create(averagelast=1, final=-1, path="data", output="./", informat="nc", outformat=None): - """Create restart files from data (dmp) files. - - Parameters - ---------- - averagelast : int, optional - Number of time points (counting from `final`, inclusive) to - average over (default is 1 i.e. just take last time-point) - final : int, optional - The last time point to use (default is last, -1) - path : str, optional - Path to original restart files (default: "data") - output : str, optional - Path to write new restart files (default: current directory) - informat : str, optional - File extension of original files (default: "nc") - outformat : str, optional - File extension of new files (default: use the same as `informat`) - - """ - - if outformat is None: - outformat = informat - - file_list = glob.glob(os.path.join(path, "BOUT.dmp.*."+informat)) - nfiles = len(file_list) - - print(("Number of data files: ", nfiles)) - - for i in range(nfiles): - # Open each data file - infname = os.path.join(path, "BOUT.dmp."+str(i)+"."+informat) - outfname = os.path.join(output, "BOUT.restart."+str(i)+"."+outformat) - - print((infname, " -> ", outfname)) - - infile = DataFile(infname) - outfile = DataFile(outfname, create=True) - - # Get the data always needed in restart files - hist_hi = infile.read("iteration") - print(("hist_hi = ", hist_hi)) - outfile.write("hist_hi", hist_hi) - - t_array = infile.read("t_array") - tt = t_array[final] - print(("tt = ", tt)) - outfile.write("tt", tt) - - tind = final - if tind < 0.0: - tind = len(t_array) + final - - NXPE = infile.read("NXPE") - NYPE = infile.read("NYPE") - print(("NXPE = ", NXPE, " NYPE = ", NYPE)) - outfile.write("NXPE", NXPE) - outfile.write("NYPE", NYPE) - - # Get a list of variables - varnames = infile.list() - - for var in varnames: - if infile.ndims(var) == 4: - # Could be an evolving variable - - print((" -> ", var)) - - data = infile.read(var) - - if averagelast == 1: - slice = data[final, :, :, :] - else: - slice = mean(data[(final - averagelast) - :final, :, :, :], axis=0) - - print(slice.shape) - - outfile.write(var, slice) - - infile.close() - outfile.close() - - -def redistribute(npes, path="data", nxpe=None, output=".", informat=None, outformat=None, mxg=2, myg=2): - """Resize restart files across NPES processors. - - Does not check if new processor arrangement is compatible with the - branch cuts. In this respect :py:func:`restart.split` is - safer. However, BOUT++ checks the topology during initialisation - anyway so this is not too serious. - - Parameters - ---------- - npes : int - Number of processors for the new restart files - path : str, optional - Path to original restart files (default: "data") - nxpe : int, optional - Number of processors to use in the x-direction (determines - split: npes = nxpe * nype). Default is None which uses the - same algorithm as BoutMesh (but without topology information) - to determine a suitable value for nxpe. - output : str, optional - Location to save new restart files (default: current directory) - informat : str, optional - Specify file format of old restart files (must be a suffix - understood by DataFile, e.g. 'nc'). Default uses the format of - the first 'BOUT.restart.*' file listed by glob.glob. - outformat : str, optional - Specify file format of new restart files (must be a suffix - understood by DataFile, e.g. 'nc'). Default is to use the same - as informat. - - Returns - ------- - True on success - - TODO - ---- - - Replace printing errors with raising `ValueError` - - """ - - if npes <= 0: - print("ERROR: Negative or zero number of processors") - return False - - if path == output: - print("ERROR: Can't overwrite restart files") - return False - - if informat is None: - file_list = glob.glob(os.path.join(path, "BOUT.restart.*")) - else: - file_list = glob.glob(os.path.join(path, "BOUT.restart.*."+informat)) - - nfiles = len(file_list) - - # Read old processor layout - f = DataFile(file_list[0]) - - # Get list of variables - var_list = f.list() - if len(var_list) == 0: - print("ERROR: No data found") - return False - - old_processor_layout = get_processor_layout(f, has_t_dimension=False) - print("Grid sizes: ", old_processor_layout.nx, - old_processor_layout.ny, old_processor_layout.mz) - - if nfiles != old_processor_layout.npes: - print("WARNING: Number of restart files inconsistent with NPES") - print("Setting nfiles = " + str(old_processor_layout.npes)) - nfiles = old_processor_layout.npes - - if nfiles == 0: - print("ERROR: No restart files found") - return False - - informat = file_list[0].split(".")[-1] - if outformat is None: - outformat = informat - - try: - new_processor_layout = create_processor_layout( - old_processor_layout, npes, nxpe=nxpe) - except ValueError as e: - print("Could not find valid processor split. " + e.what()) - - nx = old_processor_layout.nx - ny = old_processor_layout.ny - mz = old_processor_layout.mz - mxg = old_processor_layout.mxg - myg = old_processor_layout.myg - old_npes = old_processor_layout.npes - old_nxpe = old_processor_layout.nxpe - old_nype = old_processor_layout.nype - old_mxsub = old_processor_layout.mxsub - old_mysub = old_processor_layout.mysub - - nxpe = new_processor_layout.nxpe - nype = new_processor_layout.nype - mxsub = new_processor_layout.mxsub - mysub = new_processor_layout.mysub - mzsub = new_processor_layout.mz - - outfile_list = [] - for i in range(npes): - outpath = os.path.join(output, "BOUT.restart."+str(i)+"."+outformat) - outfile_list.append(DataFile(outpath, write=True, create=True)) - - DataFileCache = create_cache(path, "BOUT.restart") - - for v in var_list: - dimensions = f.dimensions(v) - ndims = len(dimensions) - - # collect data - data = collect(v, xguards=True, yguards=True, info=False, - datafile_cache=DataFileCache) - - # write data - for i in range(npes): - ix = i % nxpe - iy = int(i/nxpe) - outfile = outfile_list[i] - if v == "NPES": - outfile.write(v, npes) - elif v == "NXPE": - outfile.write(v, nxpe) - elif v == "NYPE": - outfile.write(v, nype) - elif v == "MXSUB": - outfile.write(v, mxsub) - elif v == "MYSUB": - outfile.write(v, mysub) - elif v == "MZSUB": - outfile.write(v, mzsub) - elif dimensions == (): - # scalar - outfile.write(v, data) - elif dimensions == ('x', 'y'): - # Field2D - outfile.write( - v, data[ix*mxsub:(ix+1)*mxsub+2*mxg, iy*mysub:(iy+1)*mysub+2*myg]) - elif dimensions == ('x', 'z'): - # FieldPerp - yindex_global = data.attributes['yindex_global'] - if yindex_global + myg >= iy*mysub and yindex_global + myg < (iy+1)*mysub+2*myg: - outfile.write(v, data[ix*mxsub:(ix+1)*mxsub+2*mxg, :]) - else: - nullarray = BoutArray(np.zeros([mxsub+2*mxg, mysub+2*myg]), attributes={"bout_type":"FieldPerp", "yindex_global":-myg-1}) - outfile.write(v, nullarray) - elif dimensions == ('x', 'y', 'z'): - # Field3D - outfile.write( - v, data[ix*mxsub:(ix+1)*mxsub+2*mxg, iy*mysub:(iy+1)*mysub+2*myg, :]) - else: - print( - "ERROR: variable found with unexpected dimensions,", dimensions, v) - - f.close() - for outfile in outfile_list: - outfile.close() - - return True - - -def resizeY(newy, path="data", output=".", informat="nc", outformat=None, myg=2): - """Increase the number of Y points in restart files - - NOTE: - * Can't overwrite - - Parameters - ---------- - newy : int - ny for the new file - path : str, optional - Path to original restart files (default: "data") - output : str, optional - Path to write new restart files (default: current directory) - informat : str, optional - File extension of original files (default: "nc") - outformat : str, optional - File extension of new files (default: use the same as `informat`) - myg : int, optional - Number of ghost points in y (default: 2) - - Returns - ------- - True on success, else False - - TODO - ---- - - Replace printing errors with raising `ValueError` - - Make informat work like `redistribute` - - """ - - if outformat is None: - outformat = informat - - file_list = glob.glob(os.path.join(path, "BOUT.restart.*."+informat)) - - nfiles = len(file_list) - - if nfiles == 0: - print("ERROR: No restart files found") - return False - - for i in range(nfiles): - # Open each data file - infname = os.path.join(path, "BOUT.restart."+str(i)+"."+informat) - outfname = os.path.join(output, "BOUT.restart."+str(i)+"."+outformat) - - print("Processing %s -> %s" % (infname, outfname)) - - infile = DataFile(infname) - outfile = DataFile(outfname, create=True) - - # Copy basic information - for var in ["hist_hi", "NXPE", "NYPE", "tt"]: - data = infile.read(var) - try: - # Convert to scalar if necessary - data = data[0] - except: - pass - outfile.write(var, data) - - # Get a list of variables - varnames = infile.list() - - for var in varnames: - dimensions = infile.dimensions(var) - if dimensions == ('x', 'y', 'z'): - # Could be an evolving variable [x,y,z] - - print(" -> Resizing " + var) - - # Read variable from input - indata = infile.read(var) - - nx, ny, nz = indata.shape - - # y coordinate in input and output data - iny = (arange(ny) - myg + 0.5) / (ny - 2*myg) - outy = (arange(newy) - myg + 0.5) / (newy - 2*myg) - - outdata = zeros([nx, newy, nz]) - - for x in range(nx): - for z in range(nz): - f = interp1d( - iny, indata[x, :, z], bounds_error=False, fill_value=0.0) - outdata[x, :, z] = f(outy) - - outfile.write(var, outdata) - elif dimensions == ('x', 'y'): - # Assume evolving variable [x,y] - print(" -> Resizing " + var) - - # Read variable from input - indata = infile.read(var) - - nx, ny = indata.shape - - # y coordinate in input and output data - iny = (arange(ny) - myg + 0.5) / (ny - 2*myg) - outy = (arange(newy) - myg + 0.5) / (newy - 2*myg) - - outdata = zeros([nx, newy]) - - for x in range(nx): - f = interp1d(iny, indata[x, :], - bounds_error=False, fill_value=0.0) - outdata[x, :] = f(outy) - - outfile.write(var, outdata) - else: - # Copy variable - print(" -> Copying " + var) - - # Read variable from input - data = infile.read(var) - try: - # Convert to scalar if necessary - data = data[0] - except: - pass - outfile.write(var, data) - - infile.close() - outfile.close() - - -def addvar(var, value, path="."): - """Adds a variable with constant value to all restart files. - - .. warning:: Modifies restart files in place! This is in contrast - to most of the functions in this module! - - This is useful for restarting simulations whilst turning on new - equations. By default BOUT++ throws an error if an evolving - variable is not in the restart file. By setting an option the - variable can be set to zero. This allows it to start with a - non-zero value. - - Parameters - ---------- - var : str - The name of the variable to add - value : float - Constant value for the variable - path : str, optional - Input path to data files (default: current directory) - - """ - - file_list = glob.glob(os.path.join(path, "BOUT.restart.*")) - nfiles = len(file_list) - - print("Number of restart files: %d" % (nfiles,)) - # Loop through all the restart files - for filename in file_list: - print(filename) - # Open the restart file for writing (modification) - with DataFile(filename, write=True) as df: - size = None - # Find a 3D variable and get its size - for varname in df.list(): - size = df.size(varname) - if len(size) == 3: - break - if size is None: - raise Exception("no 3D variables found") - - # Create a new 3D array with input value - data = np.zeros(size) + value - - # Set the variable in the NetCDF file - df.write(var, data) diff --git a/tools/pylib/boutdata/settings.py b/tools/pylib/boutdata/settings.py deleted file mode 100644 index c09b5cbd7f..0000000000 --- a/tools/pylib/boutdata/settings.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Parse BOUT.inp settings file - -""" - - -def get(filename, name, section=None): - """Find and return a single value from a BOUT.inp settings file - - .. deprecated::3.0 - `settings.get` has been replaced with - `boututils.options.BoutOptions` - - Parameters - ---------- - filename : str - Name of the settings file - name : str - The name of the setting - section : str, optional - The section to look in (default: the global section) - - Note that names and sections are case insensitive - - Returns - ------- - str - Value of the setting. If not found, raises a ValueError - - Examples - -------- - - >>> settings.get("BOUT.inp", "nout") - '100' - - >>> settings.get("BOUT.inp", "compress", section="highbeta") - 'true' - - """ - with open(filename, "rt") as f: - if section is not None: - # First find the section - found = False - for line in f: - # Strip spaces from left - line = line.lstrip(' \t\n\r') - if len(line) < 1: - continue # Empty line - - # if line starts with '[' then this is a section - if line[0] == '[': - # Split on ']' - head, _ = line[1:].split(']', 1) - # head is now the section name - if head.lower() == section.lower(): - found = True - break - if not found: - raise ValueError("Section '%s' not found" % (section)) - - # Now in the correct section - - for line in f: - # Strip spaces from left - line = line.lstrip(' \t\n\r') - if len(line) < 1: - continue # Empty line - - # if line starts with '[' then this is a section - if line[0] == '[': - raise ValueError("Name '%s' not found in section '%s'" % (name,section)) - # Check if this line contains an '=' - if '=' in line: - # Check if contains comment - comment = '' - if '#' in line: - line, comment = line.split('#', 1) - # Split on '=' - key, value = line.split('=',1) - # Strip whitespace - key = key.strip(' \t\n\r') - value = value.strip(' \t\n\r') - - # Strip out quotes if present - if value[0] == '"' or value[0] == "'": - value = value[1:] - if value[-1] == '"' or value[-1] == "'": - value = value[:-1] - - #print("'%s' = '%s'" % (key, value)) - if key.lower() == name.lower(): # Case insensitive - return value - diff --git a/tools/pylib/boutdata/shiftz.py b/tools/pylib/boutdata/shiftz.py deleted file mode 100644 index 606b94f176..0000000000 --- a/tools/pylib/boutdata/shiftz.py +++ /dev/null @@ -1,91 +0,0 @@ -from numpy import ndarray, pi, cos, sin -from numpy import fft - - -def shiftz(var, zangle, zperiod=1.0): - """Shift a variable in Z, changing between field-aligned and - orthogonal X-Z coordinates. This mainly used for tokamak - simulations in field-aligned coordinates. - - Parameters - ---------- - var : array_like - Data to be shifted - 4D [t,x,y,z] - 3D [x,y,z] or [t,x,z] - 2D [x,z] - zangle : array_like - The shift angle - 2D [x,y] (if var is 4D or 3D [x,y,z]) - 1D [x] (if var is 3D [t,x,z] or 2D) - zperiod : float, optional - The fraction of 2pi covered by the variable in Z. This - corresponds to the ZPERIOD variable in BOUT.inp and multiplies - the kz wavenumbers by this factor. - - Returns - ------- - ndarray - A numpy array of the same size and shape as var - - Examples - -------- - - >>> from boutdata import collect - >>> from boututils.datafile import DataFile - >>> from boutdata.shiftz import shiftz - >>> n = collect("Ne") # Read 4D variable [t,x,y,z] - >>> d = DataFile("grid.nc") # Read the grid file - >>> nxz = shiftz(n, d["zShift"], zperiod=4) - - nxz is now in orthogonal X-Z coordinates (X is psi). - - Note that in older grid files "qinty" is used rather - than "zShift". - - """ - - if len(var.shape) == 4: - # 4D variable [t,x,y,z] - result = ndarray(var.shape) - for t in range(var.shape[0]): - # Shift each time slice separately - result[t,:,:,:] = shiftz(var[t,:,:,:], zangle, zperiod=zperiod) - return result - elif len(var.shape) == 3: - if len(zangle.shape) == 2: - # 3D variable [x,y,z], array [x,y] - result = ndarray(var.shape) - for y in range(var.shape[1]): - result[:,y,:] = shiftz(var[:,y,:], zangle[:,y], zperiod=zperiod) - return result - elif len(zangle.shape) == 1: - # 3D variable [t,x,z], array [x] - result = ndarray(var.shape) - for t in range(var.shape[0]): - result[t,:,:] = shiftz(var[t,:,:], zangle, zperiod=zperiod) - return result - else: - raise ValueError("Expecting zangle to be 1 or 2D") - elif len(var.shape) == 2: - if len(zangle.shape) != 1: - raise ValueError("Expecting zangle to be 1D") - - ################################ - # Main algorithm here - # var is [x,z] - # zangle is [x] - - # Take FFT in Z direction - f = fft.rfft(var, axis=1) - - zlength = 2.*pi/zperiod - - for z in range(1, f.shape[1]): - kwave=z*2.0*pi/zlength - f[:,z] *= cos(kwave * zangle) - 1j*sin(kwave*zangle) - return fft.irfft(f, var.shape[1], axis=1) - - else: - raise ValueError("Don't know how to handle 1D variable") - diff --git a/tools/pylib/boutdata/squashoutput.py b/tools/pylib/boutdata/squashoutput.py deleted file mode 100644 index 983393980c..0000000000 --- a/tools/pylib/boutdata/squashoutput.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -Collect all data from BOUT.dmp.* files and create a single output file. - -Output file named BOUT.dmp.nc by default - -Useful because this discards ghost cell data (that is only useful for debugging) -and because single files are quicker to download. - -""" - -from boutdata.data import BoutOutputs -from boututils.datafile import DataFile -from boututils.boutarray import BoutArray -import numpy -import os -import gc -import tempfile -import shutil -import glob - - -def squashoutput(datadir=".", outputname="BOUT.dmp.nc", format="NETCDF4", tind=None, - xind=None, yind=None, zind=None, xguards=True, yguards="include_upper", - singleprecision=False, compress=False, least_significant_digit=None, - quiet=False, complevel=None, append=False, delete=False): - """ - Collect all data from BOUT.dmp.* files and create a single output file. - - Parameters - ---------- - datadir : str - Directory where dump files are and where output file will be created. - default "." - outputname : str - Name of the output file. File suffix specifies whether to use NetCDF or - HDF5 (see boututils.datafile.DataFile for suffixes). - default "BOUT.dmp.nc" - format : str - format argument passed to DataFile - default "NETCDF4" - tind : slice, int, or [int, int, int] - tind argument passed to collect - default None - xind : slice, int, or [int, int, int] - xind argument passed to collect - default None - yind : slice, int, or [int, int, int] - yind argument passed to collect - default None - zind : slice, int, or [int, int, int] - zind argument passed to collect - default None - xguards : bool - xguards argument passed to collect - default True - yguards : bool or "include_upper" - yguards argument passed to collect (note different default to collect's) - default "include_upper" - singleprecision : bool - If true convert data to single-precision floats - default False - compress : bool - If true enable compression in the output file - least_significant_digit : int or None - How many digits should be retained? Enables lossy - compression. Default is lossless compression. Needs - compression to be enabled. - complevel : int or None - Compression level, 1 should be fastest, and 9 should yield - highest compression. - quiet : bool - Be less verbose. default False - append : bool - Append to existing squashed file - delete : bool - Delete the original files after squashing. - """ - - fullpath = os.path.join(datadir, outputname) - - if append: - datadirnew = tempfile.mkdtemp(dir=datadir) - for f in glob.glob(datadir + "/BOUT.dmp.*.??"): - if not quiet: - print("moving", f, flush=True) - shutil.move(f, datadirnew) - oldfile = datadirnew + "/" + outputname - datadir = datadirnew - - if os.path.isfile(fullpath) and not append: - raise ValueError( - fullpath + " already exists. Collect may try to read from this file, which is presumably not desired behaviour.") - - # useful object from BOUT pylib to access output data - outputs = BoutOutputs(datadir, info=False, xguards=xguards, - yguards=yguards, tind=tind, xind=xind, yind=yind, zind=zind) - outputvars = outputs.keys() - # Read a value to cache the files - outputs[outputvars[0]] - - if append: - # move only after the file list is cached - shutil.move(fullpath, oldfile) - - t_array_index = outputvars.index("t_array") - outputvars.append(outputvars.pop(t_array_index)) - - kwargs = {} - if compress: - kwargs['zlib'] = True - if least_significant_digit is not None: - kwargs['least_significant_digit'] = least_significant_digit - if complevel is not None: - kwargs['complevel'] = complevel - if append: - old = DataFile(oldfile) - # Check if dump on restart was enabled - # If so, we want to drop the duplicated entry - cropnew = 0 - if old['t_array'][-1] == outputs['t_array'][0]: - cropnew = 1 - # Make sure we don't end up with duplicated data: - for ot in old['t_array']: - if ot in outputs['t_array'][cropnew:]: - raise RuntimeError( - "For some reason t_array has some duplicated entries in the new and old file.") - # Create single file for output and write data - with DataFile(fullpath, create=True, write=True, format=format, **kwargs) as f: - for varname in outputvars: - if not quiet: - print(varname, flush=True) - - var = outputs[varname] - if append: - dims = outputs.dimensions[varname] - if 't' in dims: - var = var[cropnew:, ...] - varold = old[varname] - var = BoutArray(numpy.append( - varold, var, axis=0), var.attributes) - - if singleprecision: - if not isinstance(var, int): - var = BoutArray(numpy.float32(var), var.attributes) - - f.write(varname, var) - # Write changes, free memory - f.sync() - var = None - gc.collect() - - if delete: - if append: - os.remove(oldfile) - for f in glob.glob(datadir + "/BOUT.dmp.*.??"): - if not quiet: - print("Deleting", f, flush=True) - os.remove(f) - if append: - os.rmdir(datadir) diff --git a/tools/pylib/boututils b/tools/pylib/boututils new file mode 120000 index 0000000000..5eaca68d8c --- /dev/null +++ b/tools/pylib/boututils @@ -0,0 +1 @@ +../../externalpackages/boututils/boututils/ \ No newline at end of file diff --git a/tools/pylib/boututils/View3D.py b/tools/pylib/boututils/View3D.py deleted file mode 100644 index f3a771dfc7..0000000000 --- a/tools/pylib/boututils/View3D.py +++ /dev/null @@ -1,390 +0,0 @@ -""" -View a 3D rendering of the magnetic field lines and the streamlines of the rational surfaces. -The quality of the later can be used as an indicator of the quality of the grid. The magnetic field -is computed from efit_analyzed.py. The script can be used as a template to show additional properties of the field - -based on enthought's example by Gael Varoquaux -https://docs.enthought.com/mayavi/mayavi/auto/example_magnetic_field.html#example-magnetic-field - -""" -from __future__ import absolute_import -from __future__ import division -from builtins import range -from past.utils import old_div - - -from boutdata.collect import collect -import numpy as np - -import sys - -if sys.version_info[0]>=3: - message = "View3D uses the VTK library through mayavi, which"+\ - " is currently only available in python 2" - raise ImportError(message) -else: - from mayavi import mlab - -from .read_geqdsk import read_geqdsk -from boututils.View2D import View2D -from scipy import interpolate -from .boutgrid import * - - -def View3D(g,path=None, gb=None): - ############################################################################## - # Resolution - - n=51 - - #compute Bxy - [Br,Bz,x,y,q]=View2D(g,option=1) - - - rd=g.r.max()+.5 - zd=g.z.max()+.5 - ############################################################################## - # The grid of points on which we want to evaluate the field - X, Y, Z = np.mgrid[-rd:rd:n*1j, -rd:rd:n*1j, -zd:zd:n*1j] - ## Avoid rounding issues : - #f = 1e4 # this gives the precision we are interested by : - #X = np.round(X * f) / f - #Y = np.round(Y * f) / f - #Z = np.round(Z * f) / f - - r = np.c_[X.ravel(), Y.ravel(), Z.ravel()] - - ############################################################################## - # Calculate field - # First initialize a container matrix for the field vector : - B = np.empty_like(r) - - - #Compute Toroidal field - # fpol is given between simagx (psi on the axis) and sibdry ( - # psi on limiter or separatrix). So the toroidal field (fpol/R) and the q profile are within these boundaries - # For each r,z we have psi thus we get fpol if (r,z) is within the boundary (limiter or separatrix) and fpol=fpol(outer_boundary) for outside - - #The range of psi is g.psi.max(), g.psi.min() but we have f(psi) up to the limit. Thus we use a new extended variable padded up to max psi - # set points between psi_limit and psi_max - - add_psi=np.linspace(g.sibdry,g.psi.max(),10) - - # define the x (psi) array - xf=np.arange(np.float(g.qpsi.size))*(g.sibdry-g.simagx)/np.float(g.qpsi.size-1) + g.simagx - - # pad the extra values excluding the 1st value - - xf=np.concatenate((xf, add_psi[1::]), axis=0) - - # pad fpol with corresponding points - - fp=np.lib.pad(g.fpol, (0,9), 'edge') - - # create interpolating function - - f = interpolate.interp1d(xf, fp) - - #calculate Toroidal field - - Btrz = old_div(f(g.psi), g.r) - - - rmin=g.r[:,0].min() - rmax=g.r[:,0].max() - zmin=g.z[0,:].min() - zmax=g.z[0,:].max() - - - B1p,B2p,B3p,B1t,B2t,B3t = magnetic_field(g,X,Y,Z,rmin,rmax,zmin,zmax, Br,Bz,Btrz) - - bpnorm = np.sqrt(B1p**2 + B2p**2 + B3p**2) - btnorm = np.sqrt(B1t**2 + B2t**2 + B3t**2) - - BBx=B1p+B1t - BBy=B2p+B2t - BBz=B3p+B3t - btotal = np.sqrt(BBx**2 + BBy**2 + BBz**2) - - Psi = psi_field(g,X,Y,Z,rmin,rmax,zmin,zmax) - - ############################################################################## - # Visualization - - # We threshold the data ourselves, as the threshold filter produce a - # data structure inefficient with IsoSurface - #bmax = bnorm.max() - # - #B1[B > bmax] = 0 - #B2[B > bmax] = 0 - #B3[B > bmax] = 0 - #bnorm[bnorm > bmax] = bmax - - mlab.figure(1, size=(1080,1080))#, bgcolor=(1, 1, 1), fgcolor=(0.5, 0.5, 0.5)) - - mlab.clf() - - fieldp = mlab.pipeline.vector_field(X, Y, Z, B1p, B2p, B3p, - scalars=bpnorm, name='Bp field') - - fieldt = mlab.pipeline.vector_field(X, Y, Z, B1t, B2t, B3t, - scalars=btnorm, name='Bt field') - - field = mlab.pipeline.vector_field(X, Y, Z, BBx, BBy, BBz, - scalars=btotal, name='B field') - - - - field2 = mlab.pipeline.scalar_field(X, Y, Z, Psi, name='Psi field') - - #vectors = mlab.pipeline.vectors(field, - # scale_factor=1,#(X[1, 0, 0] - X[0, 0, 0]), - # ) - - #vcp1 = mlab.pipeline.vector_cut_plane(fieldp, - # scale_factor=1, - # colormap='jet', - # plane_orientation='y_axes') - ## - #vcp2 = mlab.pipeline.vector_cut_plane(fieldt, - # scale_factor=1, - # colormap='jet', - # plane_orientation='x_axes') - - - # Mask random points, to have a lighter visualization. - #vectors.glyph.mask_input_points = True - #vectors.glyph.mask_points.on_ratio = 6 - - #vcp = mlab.pipeline.vector_cut_plane(field1) - #vcp.glyph.glyph.scale_factor=5*(X[1, 0, 0] - X[0, 0, 0]) - # For prettier picture: - #vcp1.implicit_plane.widget.enabled = False - #vcp2.implicit_plane.widget.enabled = False - - iso = mlab.pipeline.iso_surface(field2, - contours=[Psi.min()+.01], - opacity=0.4, - colormap='bone') - - for i in range(q.size): - iso.contour.contours[i+1:i+2]=[q[i]] - - iso.compute_normals = True - # - - #mlab.pipeline.image_plane_widget(field2, - # plane_orientation='x_axes', - # #slice_index=10, - # extent=[-rd, rd, -rd, rd, -zd,zd] - # ) - #mlab.pipeline.image_plane_widget(field2, - # plane_orientation='y_axes', - # # slice_index=10, - # extent=[-rd, rd, -rd,rd, -zd,zd] - # ) - - - - #scp = mlab.pipeline.scalar_cut_plane(field2, - # colormap='jet', - # plane_orientation='x_axes') - # For prettier picture and with 2D streamlines: - #scp.implicit_plane.widget.enabled = False - #scp.enable_contours = True - #scp.contour.number_of_contours = 20 - - # - - # Magnetic Axis - - s=mlab.pipeline.streamline(field) - s.streamline_type = 'line' - s.seed.widget = s.seed.widget_list[3] - s.seed.widget.position=[g.rmagx,0.,g.zmagx] - s.seed.widget.enabled = False - - - # q=i surfaces - - for i in range(np.shape(x)[0]): - - s=mlab.pipeline.streamline(field) - s.streamline_type = 'line' - ##s.seed.widget = s.seed.widget_list[0] - ##s.seed.widget.center = 0.0, 0.0, 0.0 - ##s.seed.widget.radius = 1.725 - ##s.seed.widget.phi_resolution = 16 - ##s.seed.widget.handle_direction =[ 1., 0., 0.] - ##s.seed.widget.enabled = False - ##s.seed.widget.enabled = True - ##s.seed.widget.enabled = False - # - if x[i].size>1 : - s.seed.widget = s.seed.widget_list[3] - s.seed.widget.position=[x[i][0],0.,y[i][0]] - s.seed.widget.enabled = False - - - # A trick to make transparency look better: cull the front face - iso.actor.property.frontface_culling = True - - #mlab.view(39, 74, 0.59, [.008, .0007, -.005]) - out=mlab.outline(extent=[-rd, rd, -rd, rd, -zd, zd], line_width=.5 ) - out.outline_mode = 'cornered' - out.outline_filter.corner_factor = 0.0897222 - - - w = mlab.gcf() - w.scene.camera.position = [13.296429046581462, 13.296429046581462, 12.979811259697154] - w.scene.camera.focal_point = [0.0, 0.0, -0.31661778688430786] - w.scene.camera.view_angle = 30.0 - w.scene.camera.view_up = [0.0, 0.0, 1.0] - w.scene.camera.clipping_range = [13.220595435695394, 35.020427055647517] - w.scene.camera.compute_view_plane_normal() - w.scene.render() - w.scene.show_axes = True - - mlab.show() - - if(path is not None): - #BOUT data - #path='../Aiba/' - # - #gb = file_import(path+'aiba.bout.grd.nc') - #gb = file_import("../cbm18_8_y064_x516_090309.nc") - #gb = file_import("cbm18_dens8.grid_nx68ny64.nc") - #gb = file_import("/home/ben/run4/reduced_y064_x256.nc") - - data = collect('P', path=path) - data = data[50,:,:,:] - #data0=collect("P0", path=path) - #data=data+data0[:,:,None] - - s = np.shape(data) - nz = s[2] - - - sgrid = create_grid(gb, data, 1) - - # OVERPLOT the GRID - #mlab.pipeline.add_dataset(sgrid) - #gr=mlab.pipeline.grid_plane(sgrid) - #gr.grid_plane.axis='x' - - - ## pressure scalar cut plane from bout - scpb = mlab.pipeline.scalar_cut_plane(sgrid, - colormap='jet', - plane_orientation='x_axes') - - scpb.implicit_plane.widget.enabled = False - scpb.enable_contours = True - scpb.contour.filled_contours=True - # - scpb.contour.number_of_contours = 20 - # - # - #loc=sgrid.points - #p=sgrid.point_data.scalars - - # compute pressure from scatter points interpolation - #pint=interpolate.griddata(loc, p, (X, Y, Z), method='linear') - #dpint=np.ma.masked_array(pint,np.isnan(pint)).filled(0.) - # - #p2 = mlab.pipeline.scalar_field(X, Y, Z, dpint, name='P field') - # - #scp2 = mlab.pipeline.scalar_cut_plane(p2, - # colormap='jet', - # plane_orientation='y_axes') - # - #scp2.implicit_plane.widget.enabled = False - #scp2.enable_contours = True - #scp2.contour.filled_contours=True - #scp2.contour.number_of_contours = 20 - #scp2.contour.minimum_contour=.001 - - - - # CHECK grid orientation - #fieldr = mlab.pipeline.vector_field(X, Y, Z, -BBx, BBy, BBz, - # scalars=btotal, name='B field') - # - #sg=mlab.pipeline.streamline(fieldr) - #sg.streamline_type = 'tube' - #sg.seed.widget = sg.seed.widget_list[3] - #sg.seed.widget.position=loc[0] - #sg.seed.widget.enabled = False - - - - #OUTPUT grid - - #ww = tvtk.XMLStructuredGridWriter(input=sgrid, file_name='sgrid.vts') - #ww.write() - - return - -def magnetic_field(g,X,Y,Z,rmin,rmax,zmin,zmax,Br,Bz,Btrz): - - rho = np.sqrt(X**2 + Y**2) - phi=np.arctan2(Y,X) - - br=np.zeros(np.shape(X)) - bz=np.zeros(np.shape(X)) - bt=np.zeros(np.shape(X)) - - nx,ny,nz=np.shape(X) - - mask = (rho >= rmin) & (rho <= rmax) & (Z >= zmin) & (Z <= zmax) - k=np.argwhere(mask==True) - - fr=interpolate.interp2d(g.r[:,0], g.z[0,:], Br.T) - fz=interpolate.interp2d(g.r[:,0], g.z[0,:], Bz.T) - ft=interpolate.interp2d(g.r[:,0], g.z[0,:], Btrz.T) - - for i in range(len(k)): - br[k[i,0],k[i,1],k[i,2]]=fr(rho[k[i,0],k[i,1],k[i,2]],Z[k[i,0],k[i,1],k[i,2]]) - bz[k[i,0],k[i,1],k[i,2]]=fz(rho[k[i,0],k[i,1],k[i,2]],Z[k[i,0],k[i,1],k[i,2]]) - bt[k[i,0],k[i,1],k[i,2]]=ft(rho[k[i,0],k[i,1],k[i,2]],Z[k[i,0],k[i,1],k[i,2]]) - - # Toroidal component - B1t=-bt*np.sin(phi) - B2t=bt*np.cos(phi) - B3t=0*bz - - # Poloidal component - B1p=br*np.cos(phi) - B2p=br*np.sin(phi) - B3p=bz - - - # Rotate the field back in the lab's frame - return B1p,B2p,B3p,B1t,B2t,B3t - - -def psi_field(g,X,Y,Z,rmin,rmax,zmin,zmax): - - rho = np.sqrt(X**2 + Y**2) - - psi=np.zeros(np.shape(X)) - - nx,ny,nz=np.shape(X) - - mask = (rho >= rmin) & (rho <= rmax) & (Z >= zmin) & (Z <= zmax) - k=np.argwhere(mask==True) - - f=interpolate.interp2d(g.r[:,0], g.z[0,:], g.psi.T) - - for i in range(len(k)): - psi[k[i,0],k[i,1],k[i,2]]=f(rho[k[i,0],k[i,1],k[i,2]],Z[k[i,0],k[i,1],k[i,2]]) - - # Rotate the field back in the lab's frame - return psi - - -if __name__ == '__main__': - path='../../tokamak_grids/pyGridGen/' - g=read_geqdsk(path+"g118898.03400") - View3D(g) - mlab.show() diff --git a/tools/pylib/boututils/__init__.py b/tools/pylib/boututils/__init__.py deleted file mode 100644 index 2b3a54d3ce..0000000000 --- a/tools/pylib/boututils/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -""" Generic routines, useful for all data """ - -import sys - -try: - from builtins import str -except ImportError: - raise ImportError("Please install the future module to use Python 2") - -# Modules to be imported independent of version -for_all_versions = [\ - 'calculus',\ - 'closest_line',\ - 'datafile',\ - # 'efit_analyzer',\ # bunch pkg required - 'fft_deriv',\ - 'fft_integrate',\ - 'file_import',\ - 'int_func',\ - 'linear_regression',\ - 'mode_structure',\ - # 'moment_xyzt',\ # bunch pkg requried - 'run_wrapper',\ - 'shell',\ - 'showdata',\ - # 'surface_average',\ - # 'volume_integral',\ #bunch pkg required - ] - -# Check the current python version -if sys.version_info[0]>=3: - do_import = for_all_versions - __all__ = do_import -else: - do_import = for_all_versions - do_import.append('anim') - do_import.append('plotpolslice') - do_import.append('View3D') - __all__ = do_import - -__version__ = '0.1.4' -__name__ = 'boututils' diff --git a/tools/pylib/boututils/analyse_equil_2.py b/tools/pylib/boututils/analyse_equil_2.py deleted file mode 100644 index d315c6abd6..0000000000 --- a/tools/pylib/boututils/analyse_equil_2.py +++ /dev/null @@ -1,270 +0,0 @@ -"""Equilibrium analysis routine - -Takes a RZ psi grid, and finds x-points and o-points -""" - -from __future__ import print_function -from __future__ import division - -from builtins import zip -from builtins import str -from builtins import range -from past.utils import old_div - -import numpy -from . import local_min_max -from scipy.interpolate import RectBivariateSpline -from matplotlib.pyplot import contour, gradient, annotate, plot, draw -from crosslines import find_inter - - -def analyse_equil(F, R, Z): - """Takes an RZ psi grid, and finds x-points and o-points - - Parameters - ---------- - F : array_like - 2-D array of psi values - R : array_like - 1-D array of major radii, its length should be the same as the - first dimension of F - Z : array_like - 1-D array of heights, its length should be the same as the - second dimension of F - - Returns - ------- - object - An object of critical points containing: - - n_opoint, n_xpoint - Number of O- and X-points - primary_opt - Index of plasma centre O-point - inner_sep - X-point index of inner separatrix - opt_ri, opt_zi - R and Z indices for each O-point - opt_f - Psi value at each O-point - xpt_ri, xpt_zi - R and Z indices for each X-point - xpt_f - Psi value of each X-point - - """ - s = numpy.shape(F) - nx = s[0] - ny = s[1] - - #;;;;;;;;;;;;;;; Find critical points ;;;;;;;;;;;;; - # - # Need to find starting locations for O-points (minima/maxima) - # and X-points (saddle points) - # - Rr=numpy.tile(R,nx).reshape(nx,ny).T - Zz=numpy.tile(Z,ny).reshape(nx,ny) - - contour1=contour(Rr,Zz,gradient(F)[0], levels=[0.0], colors='r') - contour2=contour(Rr,Zz,gradient(F)[1], levels=[0.0], colors='r') - - draw() - - -### 1st method - line crossings --------------------------- - res=find_inter( contour1, contour2) - - #rex1=numpy.interp(res[0], R, numpy.arange(R.size)).astype(int) - #zex1=numpy.interp(res[1], Z, numpy.arange(Z.size)).astype(int) - - rex1=res[0] - zex1=res[1] - - w=numpy.where((rex1 > R[2]) & (rex1 < R[nx-3]) & (zex1 > Z[2]) & (zex1 < Z[nx-3])) - nextrema = numpy.size(w) - rex1=rex1[w].flatten() - zex1=zex1[w].flatten() - - -### 2nd method - local maxima_minima ----------------------- - res1=local_min_max.detect_local_minima(F) - res2=local_min_max.detect_local_maxima(F) - res=numpy.append(res1,res2,1) - - rex2=res[0,:].flatten() - zex2=res[1,:].flatten() - - - w=numpy.where((rex2 > 2) & (rex2 < nx-3) & (zex2 >2) & (zex2 < nx-3)) - nextrema = numpy.size(w) - rex2=rex2[w].flatten() - zex2=zex2[w].flatten() - - - n_opoint=nextrema - n_xpoint=numpy.size(rex1)-n_opoint - - # Needed for interp below - - Rx=numpy.arange(numpy.size(R)) - Zx=numpy.arange(numpy.size(Z)) - - - - print("Number of O-points: "+numpy.str(n_opoint)) - print("Number of X-points: "+numpy.str(n_xpoint)) - - # Deduce the O & X points - - x=R[rex2] - y=Z[zex2] - - dr=old_div((R[numpy.size(R)-1]-R[0]),numpy.size(R)) - dz=old_div((Z[numpy.size(Z)-1]-Z[0]),numpy.size(Z)) - - - repeated=set() - for i in range(numpy.size(rex1)): - for j in range(numpy.size(x)): - if numpy.abs(rex1[i]-x[j]) < 2*dr and numpy.abs(zex1[i]-y[j]) < 2*dz : repeated.add(i) - - # o-points - - o_ri=numpy.take(rex1,numpy.array(list(repeated))) - opt_ri=numpy.interp(o_ri,R,Rx) - o_zi=numpy.take(zex1,numpy.array(list(repeated))) - opt_zi=numpy.interp(o_zi,Z,Zx) - opt_f=numpy.zeros(numpy.size(opt_ri)) - func = RectBivariateSpline(Rx, Zx, F) - for i in range(numpy.size(opt_ri)): opt_f[i]=func(opt_ri[i], opt_zi[i]) - - n_opoint=numpy.size(opt_ri) - - # x-points - - x_ri=numpy.delete(rex1, numpy.array(list(repeated))) - xpt_ri=numpy.interp(x_ri,R,Rx) - x_zi=numpy.delete(zex1, numpy.array(list(repeated))) - xpt_zi=numpy.interp(x_zi,Z,Zx) - xpt_f=numpy.zeros(numpy.size(xpt_ri)) - func = RectBivariateSpline(Rx, Zx, F) - for i in range(numpy.size(xpt_ri)): xpt_f[i]=func(xpt_ri[i], xpt_zi[i]) - - n_xpoint=numpy.size(xpt_ri) - - # plot o-points - - plot(o_ri,o_zi,'o', markersize=10) - - labels = ['{0}'.format(i) for i in range(o_ri.size)] - for label, xp, yp in zip(labels, o_ri, o_zi): - annotate(label, xy = (xp, yp), xytext = (10, 10), textcoords = 'offset points',size='large', color='b') - - draw() - - # plot x-points - - plot(x_ri,x_zi,'x', markersize=10) - - labels = ['{0}'.format(i) for i in range(x_ri.size)] - for label, xp, yp in zip(labels, x_ri, x_zi): - annotate(label, xy = (xp, yp), xytext = (10, 10), textcoords = 'offset points',size='large', color='r') - - draw() - - print("Number of O-points: "+str(n_opoint)) - - if n_opoint == 0 : - raise RuntimeError("No O-points! Giving up on this equilibrium") - - - #;;;;;;;;;;;;;; Find plasma centre ;;;;;;;;;;;;;;;;;;; - # Find the O-point closest to the middle of the grid - - mind = (opt_ri[0] - (old_div(numpy.float(nx),2.)))**2 + (opt_zi[0] - (old_div(numpy.float(ny),2.)))**2 - ind = 0 - for i in range (1, n_opoint) : - d = (opt_ri[i] - (old_div(numpy.float(nx),2.)))**2 + (opt_zi[i] - (old_div(numpy.float(ny),2.)))**2 - if d < mind : - ind = i - mind = d - - primary_opt = ind - print("Primary O-point is at "+ numpy.str(numpy.interp(opt_ri[ind],numpy.arange(numpy.size(R)),R)) + ", " + numpy.str(numpy.interp(opt_zi[ind],numpy.arange(numpy.size(Z)),Z))) - print("") - - if n_xpoint > 0 : - - # Find the primary separatrix - - # First remove non-monotonic separatrices - nkeep = 0 - for i in range (n_xpoint) : - # Draw a line between the O-point and X-point - - n = 100 # Number of points - farr = numpy.zeros(n) - dr = old_div((xpt_ri[i] - opt_ri[ind]), numpy.float(n)) - dz = old_div((xpt_zi[i] - opt_zi[ind]), numpy.float(n)) - for j in range (n) : - # interpolate f at this location - func = RectBivariateSpline(Rx, Zx, F) - - farr[j] = func(opt_ri[ind] + dr*numpy.float(j), opt_zi[ind] + dz*numpy.float(j)) - - - # farr should be monotonic, and shouldn't cross any other separatrices - - maxind = numpy.argmax(farr) - minind = numpy.argmin(farr) - if (maxind < minind) : maxind, minind = minind, maxind - - # Allow a little leeway to account for errors - # NOTE: This needs a bit of refining - if (maxind > (n-3)) and (minind < 3) : - # Monotonic, so add this to a list of x-points to keep - if nkeep == 0 : - keep = [i] - else: - keep = numpy.append(keep, i) - - - nkeep = nkeep + 1 - - - if nkeep > 0 : - print("Keeping x-points ", keep) - xpt_ri = xpt_ri[keep] - xpt_zi = xpt_zi[keep] - xpt_f = xpt_f[keep] - else: - "No x-points kept" - - n_xpoint = nkeep - - - # Now find x-point closest to primary O-point - s = numpy.argsort(numpy.abs(opt_f[ind] - xpt_f)) - xpt_ri = xpt_ri[s] - xpt_zi = xpt_zi[s] - xpt_f = xpt_f[s] - inner_sep = 0 - - else: - - # No x-points. Pick mid-point in f - - xpt_f = 0.5*(numpy.max(F) + numpy.min(F)) - - print("WARNING: No X-points. Setting separatrix to F = "+str(xpt_f)) - - xpt_ri = 0 - xpt_zi = 0 - inner_sep = 0 - - - - #;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - # Put results into a structure - - result = Bunch(n_opoint=n_opoint, n_xpoint=n_xpoint, # Number of O- and X-points - primary_opt=primary_opt, # Which O-point is the plasma centre - inner_sep=inner_sep, #Innermost X-point separatrix - opt_ri=opt_ri, opt_zi=opt_zi, opt_f=opt_f, # O-point location (indices) and psi values - xpt_ri=xpt_ri, xpt_zi=xpt_zi, xpt_f=xpt_f) # X-point locations and psi values - - return result - diff --git a/tools/pylib/boututils/anim.py b/tools/pylib/boututils/anim.py deleted file mode 100755 index d2f783858c..0000000000 --- a/tools/pylib/boututils/anim.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -"""Animate graph with mayavi - -""" - -from __future__ import print_function -from builtins import range -from boutdata.collect import collect -import numpy as np -import os -try: - from enthought.mayavi import mlab - from enthought.mayavi.mlab import * -except ImportError: - try: - from mayavi import mlab - from mayavi.mlab import * - except ImportError: - print("No mlab available") - -from tvtk.tools import visual - - -@mlab.show -@mlab.animate(delay=250) -def anim(s, d, *args, **kwargs): - """Animate graph with mayavi - - Parameters - ---------- - s : mayavi axis object - Axis to animate data on - d : array_like - 3-D array to animate - s1 : mayavi axis object, optional - Additional bundled graph (first item in *args) - save : bool, optional - Save png files for creating movie (default: False) - - """ - - if len(args) == 1: - s1 = args[0] - else: - s1=None - - try: - save = kwargs['save'] - except: - save = False - - - nt=d.shape[0] - - print('animating for ',nt,'timesteps') - if save == True : - print('Saving pics in folder Movie') - if not os.path.exists('Movie'): - os.makedirs('Movie') - - - for i in range(nt): - s.mlab_source.scalars = d[i,:,:] - if s1 is not None : s1.mlab_source.scalars = d[i,:,:] - title="t="+np.string0(i) - mlab.title(title,height=1.1, size=0.26) - if save == True : mlab.savefig('Movie/anim%d.png'%i) - yield - -if __name__ == '__main__': - - path='../../../examples/elm-pb/data' - - data = collect("P", path=path) - - nt=data.shape[0] - - ns=data.shape[1] - ne=data.shape[2] - nz=data.shape[3] - - - f = mayavi.mlab.figure(size=(600,600)) - # Tell visual to use this as the viewer. - visual.set_viewer(f) - - #First way - - s1 = contour_surf(data[0,:,:,10]+.1, contours=30, line_width=.5, transparent=True) - s = surf(data[0,:,:,10]+.1, colormap='Spectral')#, warp_scale='.1')#, representation='wireframe') - - - # second way - - #x, y= mgrid[0:ns:1, 0:ne:1] - #s = mesh(x,y,data[0,:,:,10], colormap='Spectral')#, warp_scale='auto')#, representation='wireframe') - s.enable_contours=True - s.contour.filled_contours=True -# - - #x, y, z= mgrid[0:ns:1, 0:ne:1, 0:nz:1] - # - #p=plot3d(x,y,z,data[10,:,:,:], tube_radius=0.025, colormap='Spectral') - #p=points3d(x,y,z,data[10,:,:,:], colormap='Spectral') -# - #s=contour3d(x,y,z,data[10,:,:,:], contours=4, transparent=True) - - #mlab.view(0.,0.) - colorbar() - #axes() - #outline() - - - # Run the animation. - anim(s,data[:,:,:,10]+.1,s1, save=True) diff --git a/tools/pylib/boututils/ask.py b/tools/pylib/boututils/ask.py deleted file mode 100644 index 31cbef059c..0000000000 --- a/tools/pylib/boututils/ask.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Ask a yes/no question and return the answer. - -""" - -from builtins import input -import sys - - -def query_yes_no(question, default="yes"): - """Ask a yes/no question via input() and return their answer. - - Answers are case-insensitive. - - Probably originally from https://code.activestate.com/recipes/577058/ - via https://stackoverflow.com/a/3041990/2043465 - - Parameters - ---------- - question : str - Question to be presented to the user - default : {"yes", "no", None} - The presumed answer if the user just hits . - It must be "yes" (the default), "no" or None (meaning - an answer is required of the user). - - Returns - ------- - bool - True if the answer was "yes" or "y", False if "no" or "n" - """ - - valid = {"yes":True, "y":True, "ye":True, - "no":False, "n":False, "No":False, "N":False } - - if default is None: - prompt = " [y/n] " - elif default == "yes": - prompt = " [Y/n] " - elif default == "no": - prompt = " [y/N] " - else: - raise ValueError("invalid default answer: '%s'" % default) - - while True: - sys.stdout.write(question + prompt) - choice = input().lower() - if default is not None and choice == '': - return valid[default] - elif choice in valid: - return valid[choice] - else: - sys.stdout.write("Please respond with 'yes' or 'no' "\ - "(or 'y' or 'n').\n") diff --git a/tools/pylib/boututils/boutarray.py b/tools/pylib/boututils/boutarray.py deleted file mode 100644 index ce38baec2c..0000000000 --- a/tools/pylib/boututils/boutarray.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Wrapper for ndarray with extra attributes for BOUT++ fields. - -""" - -import numpy - - -class BoutArray(numpy.ndarray): - """Wrapper for ndarray with extra attributes for BOUT++ fields. - - Parameters - ---------- - input_array : array_like - Data to convert to BoutArray - attributes : dict - Dictionary of extra attributes for BOUT++ fields - - Notably, these attributes should contain - ``bout_type``. Possible values are: - - - scalar - - Field2D - - Field3D - - If the variable is an evolving variable (i.e. has a time - dimension), then it is appended with a "_t" - - """ - - # See https://docs.scipy.org/doc/numpy-1.13.0/user/basics.subclassing.html - # for explanation of the structure of this numpy.ndarray wrapper - - def __new__(cls, input_array, attributes={}): - # Input array is an already formed ndarray instance - # We first cast to be our class type - obj = numpy.asarray(input_array).view(cls) - # add the dict of attributes to the created instance - obj.attributes = attributes - # Finally, we must return the newly created object: - return obj - - def __array_finalize__(self, obj): - # ``self`` is a new object resulting from - # ndarray.__new__(BoutArray, ...), therefore it only has - # attributes that the ndarray.__new__ constructor gave it - - # i.e. those of a standard ndarray. - # - # We could have got to the ndarray.__new__ call in 3 ways: - # From an explicit constructor - e.g. BoutArray(): - # obj is None - # (we're in the middle of the BoutArray.__new__ - # constructor, and self.attributes will be set when we return to - # BoutArray.__new__) - if obj is None: - return - # From view casting - e.g arr.view(BoutArray): - # obj is arr - # (type(obj) can be BoutArray) - # From new-from-template - e.g boutarray[:3] - # type(obj) is BoutArray - # - # Note that it is here, rather than in the __new__ method, that we set - # the default value for 'attributes', because this method sees all - # creation of default objects - with the BoutArray.__new__ constructor, - # but also with arr.view(BoutArray). - self.attributes = getattr(obj, 'attributes', None) - # We do not need to return anything - - def __format__(self, str): - try: - return super().__format__(str) - except TypeError: - return float(self).__format__(str) diff --git a/tools/pylib/boututils/boutgrid.py b/tools/pylib/boututils/boutgrid.py deleted file mode 100755 index ace67663cd..0000000000 --- a/tools/pylib/boututils/boutgrid.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import print_function -from builtins import range - -import numpy as np -from numpy import cos, sin, pi - -from tvtk.api import tvtk -#from enthought.mayavi.scripts import mayavi2 - -def aligned_points(grid, nz=1, period=1.0, maxshift=0.4): - try: - nx = grid["nx"]#[0] - ny = grid["ny"]#[0] - zshift = grid["zShift"] - Rxy = grid["Rxy"] - Zxy = grid["Zxy"] - except: - print("Missing required data") - return None - - - dz = 2.*pi / (period * (nz-1)) - phi0 = np.linspace(0,2.*pi / period, nz) - - - # Need to insert additional points in Y so mesh looks smooth - #for y in range(1,ny): - # ms = np.max(np.abs(zshift[:,y] - zshift[:,y-1])) - # if( - - # Create array of points, structured - - points = np.zeros([nx*ny*nz, 3]) - - - start = 0 - for y in range(ny): - - - end = start + nx*nz - - phi = zshift[:,y] + phi0[:,None] - r = Rxy[:,y] + (np.zeros([nz]))[:,None] - - xz_points = points[start:end] - - - xz_points[:,0] = (r*cos(phi)).ravel() # X - xz_points[:,1] = (r*sin(phi)).ravel() # Y - xz_points[:,2] = (Zxy[:,y]+(np.zeros([nz]))[:,None]).ravel() # Z - - - start = end - - return points - -def create_grid(grid, data, period=1): - - s = np.shape(data) - - nx = grid["nx"]#[0] - ny = grid["ny"]#[0] - nz = s[2] - - print("data: %d,%d,%d grid: %d,%d\n" % (s[0],s[1],s[2], nx,ny)) - - dims = (nx, nz, ny) - sgrid = tvtk.StructuredGrid(dimensions=dims) - pts = aligned_points(grid, nz, period) - print(np.shape(pts)) - sgrid.points = pts - - scalar = np.zeros([nx*ny*nz]) - start = 0 - for y in range(ny): - end = start + nx*nz - - #scalar[start:end] = (data[:,y,:]).transpose().ravel() - scalar[start:end] = (data[:,y,:]).ravel() - - print(y, " = " , np.max(scalar[start:end])) - start = end - - sgrid.point_data.scalars = np.ravel(scalar.copy()) - sgrid.point_data.scalars.name = "data" - - return sgrid - -#@mayavi2.standalone -def view3d(sgrid): - from mayavi.sources.vtk_data_source import VTKDataSource - from mayavi.modules.api import Outline, GridPlane - from mayavi.api import Engine - from mayavi.core.ui.engine_view import EngineView - e=Engine() - e.start() - s = e.new_scene() - # Do this if you need to see the MayaVi tree view UI. - ev = EngineView(engine=e) - ui = ev.edit_traits() - -# mayavi.new_scene() - src = VTKDataSource(data=sgrid) - e.add_source(src) - e.add_module(Outline()) - g = GridPlane() - g.grid_plane.axis = 'x' - e.add_module(g) - -if __name__ == '__main__': - from boutdata.collect import collect - from boututils.file_import import file_import - - #path = "/media/449db594-b2fe-4171-9e79-2d9b76ac69b6/runs/data_33/" - path="../data" - - g = file_import("../bout.grd.nc") - #g = file_import("../cbm18_8_y064_x516_090309.nc") - #g = file_import("/home/ben/run4/reduced_y064_x256.nc") - - data = collect("P", tind=10, path=path) - data = data[0,:,:,:] - s = np.shape(data) - nz = s[2] - - #bkgd = collect("P0", path=path) - #for z in range(nz): - # data[:,:,z] += bkgd - - # Create a structured grid - sgrid = create_grid(g, data, 1) - - - w = tvtk.XMLStructuredGridWriter(input=sgrid, file_name='sgrid.vts') - w.write() - - # View the structured grid - view3d(sgrid) diff --git a/tools/pylib/boututils/boutwarnings.py b/tools/pylib/boututils/boutwarnings.py deleted file mode 100644 index cdb03b0518..0000000000 --- a/tools/pylib/boututils/boutwarnings.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Wrappers for warnings functions. - -Allows raising warnings that are always printed by default. -""" - -import warnings - -class AlwaysWarning(UserWarning): - def __init__(self, *args, **kwargs): - super(AlwaysWarning, self).__init__(*args, **kwargs) - -warnings.simplefilter("always", AlwaysWarning) - -def alwayswarn(message): - warnings.warn(message, AlwaysWarning, stacklevel=2) - -def defaultwarn(message): - warnings.warn(message, stacklevel=2) diff --git a/tools/pylib/boututils/bunch.py b/tools/pylib/boututils/bunch.py deleted file mode 100644 index 2bc1ca04c0..0000000000 --- a/tools/pylib/boututils/bunch.py +++ /dev/null @@ -1,6 +0,0 @@ -# what we need from bunch - -class Bunch: - def __init__(self, **dict): - for k in dict: - setattr(self, k, dict[k]) diff --git a/tools/pylib/boututils/calculus.py b/tools/pylib/boututils/calculus.py deleted file mode 100644 index 274230906b..0000000000 --- a/tools/pylib/boututils/calculus.py +++ /dev/null @@ -1,252 +0,0 @@ -""" -Derivatives and integrals of periodic and non-periodic functions - - -B.Dudson, University of York, Nov 2009 -""" -from __future__ import print_function -from __future__ import division - -from builtins import range - -try: - from past.utils import old_div -except ImportError: - def old_div(a, b): - return a / b - -from numpy import zeros, pi, array, transpose, sum, where, arange, multiply -from numpy.fft import rfft, irfft - -def deriv(*args, **kwargs): - """Take derivative of 1D array - - result = deriv(y) - result = deriv(x, y) - - keywords - - periodic = False Domain is periodic - """ - - nargs = len(args) - if nargs == 1: - var = args[0] - x = arange(var.size) - elif nargs == 2: - x = args[0] - var = args[1] - else: - raise RuntimeError("deriv must be given 1 or 2 arguments") - - try: - periodic = kwargs['periodic'] - except: - periodic = False - - n = var.size - if periodic: - # Use FFTs to take derivatives - f = rfft(var) - f[0] = 0.0 # Zero constant term - if n % 2 == 0: - # Even n - for i in arange(1,old_div(n,2)): - f[i] *= 2.0j * pi * float(i)/float(n) - f[-1] = 0.0 # Nothing from Nyquist frequency - else: - # Odd n - for i in arange(1,old_div((n-1),2) + 1): - f[i] *= 2.0j * pi * float(i)/float(n) - return irfft(f) - else: - # Non-periodic function - result = zeros(n) # Create empty array - if n > 2: - for i in arange(1, n-1): - # 2nd-order central difference in the middle of the domain - result[i] = old_div((var[i+1] - var[i-1]), (x[i+1] - x[i-1])) - # Use left,right-biased stencils on edges (2nd order) - result[0] = old_div((-1.5*var[0] + 2.*var[1] - 0.5*var[2]), (x[1] - x[0])) - result[n-1] = old_div((1.5*var[n-1] - 2.*var[n-2] + 0.5*var[n-3]), (x[n-1] - x[n-2])) - elif n == 2: - # Just 1st-order difference for both points - result[0] = result[1] = old_div((var[1] - var[0]),(x[1] - x[0])) - elif n == 1: - result[0] = 0.0 - return result - -def deriv2D(data,axis=-1,dx=1.0,noise_suppression=True): - """ Takes 1D or 2D Derivative of 2D array using convolution - - result = deriv2D(data) - result = deriv2D(data, dx) - - output is 2D (if only one axis specified) - output is 3D if no axis specified [nx,ny,2] with the third dimension being [dfdx, dfdy] - - keywords: - axis = 0/1 If no axis specified 2D derivative will be returned - dx = 1.0 axis spacing, must be 2D if 2D deriv is taken - default is [1.0,1.0] - noise_suppression = True noise suppressing coefficients used to take derivative - default = True - """ - - from scipy.signal import convolve - - s = data.shape - if axis > len(s)-1: - raise RuntimeError("ERROR: axis out of bounds for derivative") - - if noise_suppression: - if s[axis] < 11: - raise RuntimeError("Data too small to use 11th order method") - tmp = array([old_div(-1.0,512.0),old_div(-8.0,512.0),old_div(-27.0,512.0),old_div(-48.0,512.0),old_div(-42.0,512.0),0.0,old_div(42.0,512.0),old_div(48.0,512.0),old_div(27.0,512.0),old_div(8.0,512.0),old_div(1.0,512.0)]) - else: - if s[axis] < 9: - raise RuntimeError("Data too small to use 9th order method") - tmp = array([old_div(1.0,280.0),old_div(-4.0,105.0),old_div(1.0,5.0),old_div(-4.0,5.0),0.0,old_div(4.0,5.0),old_div(-1.0,5.0),old_div(4.0,105.0),old_div(-1.0,280.0)]) - - N = int((tmp.size-1)/2) - if axis==1: - W = transpose(tmp[:,None]) - data_deriv = convolve(data,W,mode='same')/dx*-1.0 - for i in range(s[0]): - data_deriv[i,0:N-1] = old_div(deriv(data[i,0:N-1]),dx) - data_deriv[i,s[1]-N:] = old_div(deriv(data[i,s[1]-N:]),dx) - - elif axis==0: - W = tmp[:,None] - data_deriv = convolve(data,W,mode='same')/dx*-1.0 - for i in range(s[1]): - data_deriv[0:N-1,i] = old_div(deriv(data[0:N-1,i]),dx) - data_deriv[s[0]-N:,i] = old_div(deriv(data[s[0]-N:,i]),dx) - else: - data_deriv = zeros((s[0],s[1],2)) - if (not hasattr(dx, '__len__')) or len(dx)==1: - dx = array([dx,dx]) - - W = tmp[:,None]#transpose(multiply(tmp,ones((s[1],tmp.size)))) - data_deriv[:,:,0] = convolve(data,W,mode='same')/dx[0]*-1.0 - for i in range(s[1]): - data_deriv[0:N-1,i,0] = old_div(deriv(data[0:N-1,i]),dx[0]) - data_deriv[s[0]-N:s[0]+1,i,0] = old_div(deriv(data[s[0]-N:s[0]+1,i]),dx[0]) - - W = transpose(tmp[:,None])#multiply(tmp,ones((s[0],tmp.size))) - data_deriv[:,:,1] = convolve(data,W,mode='same')/dx[1]*-1.0 - for i in range(s[0]): - data_deriv[i,0:N-1,1] = old_div(deriv(data[i,0:N-1]),dx[1]) - data_deriv[i,s[1]-N:s[1]+1,1] = old_div(deriv(data[i,s[1]-N:s[1]+1]),dx[1]) - - return data_deriv - -def integrate(var, periodic=False): - """Integrate a 1D array - - Return array is the same size as the input - """ - if periodic: - # Use FFT - f = rfft(var) - n = var.size - # Zero frequency term - result = f[0].real*arange(n, dtype=float) - f[0] = 0. - if n % 2 == 0: - # Even n - for i in arange(1,old_div(n,2)): - f[i] /= 2.0j * pi * float(i)/float(n) - f[-1] = 0.0 # Nothing from Nyquist frequency - else: - # Odd n - for i in arange(1,old_div((n-1),2) + 1): - f[i] /= 2.0j * pi * float(i)/float(n) - return result + irfft(f) - else: - # Non-periodic function - def int_total(f): - """Integrate over a set of points""" - n = f.size - if n > 7: - # Need to split into several segments - # Use one 5-point, leaving at least 4-points - return int_total(f[0:5]) + int_total(f[4:]) - elif (n == 7) or (n == 6): - # Try to keep 4th-order - # Split into 4+4 or 4+3 - return int_total(f[0:4]) + int_total(f[3:]) - elif n == 5: - # 6th-order Bool's rule - return 4.*(7.*f[0] + 32.*f[1] + 12.*f[2] + 32.*f[3] + 7.*f[4])/90. - elif n == 4: - # 4th-order Simpson's 3/8ths rule - return 3.*(f[0] + 3.*f[1] + 3.*f[2] + f[3])/8. - elif n == 3: - # 4th-order Simpson's rule - return (f[0] + 4.*f[1] + f[2])/3. - elif n == 2: - # 2nd-order Trapezium rule - return 0.5*(f[0] + f[1]) - else: - print("WARNING: Integrating a single point") - return 0.0 - # Integrate using maximum number of grid-points - n = var.size - n2 = int(old_div(n,2)) - result = zeros(n) - for i in arange(n2, n): - result[i] = int_total(var[0:(i+1)]) - for i in arange(1, n2): - result[i] = result[-1] - int_total(var[i:]) - return result - -def simpson_integrate(data,dx,dy,kernel=0.0,weight=1.0): - """ Integrates 2D data to one value using the simpson method and matrix convolution - - result = simpson_integrate(data,dx,dy) - - keywords: - - kernel - can be supplied if the simpson matrix is calculated ahead of time - - if not supplied, is calculated within this function - - if you need to integrate the same shape data over and over, calculated - it ahead of time using: - kernel = simpson_matrix(Nx,Ny,dx,dy) - - weight - can be used to scale data if single number - - can be used to mask data if weight is array (same size as data) - """ - s = data.shape - Nx = s[0] - Ny = s[1] - - if len(kernel)==1: - kernel = simpson_matrix(Nx,Ny,dx,dy) - - return sum(multiply(multiply(weight,kernel),data))/sum(multiply(weight,kernel)) - - -def simpson_matrix(Nx,Ny,dx,dy): - """ - Creates a 2D matrix of coefficients for the simpson_integrate function - - Call ahead of time if you need to perform integration of the same size data with the - same dx and dy - - Otherwise, simpson_integrate will automatically call this - - """ - Wx = arange(Nx) + 2 - Wx[where(arange(Nx) % 2 == 1)] = 4 - Wx[0] = 1 - Wx[Nx-1] = 1 - - Wy = arange(Ny) + 2 - Wy[where(arange(Ny) % 2 == 1)] = 4 - Wy[0] = 1 - Wy[Ny-1] = 1 - - W = Wy[None,:] * Wx[:,None] - - A = dx*dy/9.0 - - return W*A diff --git a/tools/pylib/boututils/check_scaling.py b/tools/pylib/boututils/check_scaling.py deleted file mode 100644 index af59b0b786..0000000000 --- a/tools/pylib/boututils/check_scaling.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Functions for checking the error scaling of MMS or MES results - -""" - -from numpy import array, isclose, log, polyfit - - -def get_order(grid_spacing, errors): - """Get the convergence order of errors over the full range of - grid_spacing, and at small spacings - - Parameters - ---------- - grid_spacing : list of float - The grid spacing or inverse of number of grid points - errors : list of float - The error at each grid spacing - - Returns - ------- - tuple of float - The first value is the error scaling over the full range of - grid spacings; the second value is the scaling over the last - two points - - """ - if len(errors) != len(grid_spacing): - raise ValueError("errors (len: {}) and grid_spacing (len: {}) should be the same length" - .format(len(errors), len(grid_spacing))) - - full_range = polyfit(log(grid_spacing), log(errors), 1) - - small_spacing = log(errors[-2] / errors[-1]) / log(grid_spacing[-2] / grid_spacing[-1]) - - return (full_range[0], small_spacing) - - -def check_order(error_list, expected_order, tolerance=2.e-1, spacing=None): - """Check if the actual_order is sufficiently close to the - expected_order within a given tolerance - - """ - - if len(error_list) < 2: - raise RuntimeError("Expected at least 2 data points to calculate error") - - success = True - - for i in range(len(error_list)-1): - grid_spacing = 2 if spacing is None else spacing[i] / spacing[i+1] - actual_order = log(error_list[i] / error_list[i+1]) / log(grid_spacing) - - if not isclose(actual_order, expected_order, atol=tolerance, rtol=0): - success = False - return success - - -def error_rate_table(errors, grid_sizes, label): - """Create a nicely formatted table of the error convergence rate over - the grid_sizes - - The error rate is calculated between adjacent points - - Parameters - ---------- - errors : list of float - The errors at each grid size - grid_sizes : list of int - The number of grid points - label : string - What the error is measuring - - Returns - ------- - string - - """ - if len(errors) != len(grid_sizes): - raise ValueError("errors (len: {}) and grid_sizes (len: {}) should be the same length" - .format(len(errors), len(grid_sizes))) - - dx = 1. / array(grid_sizes) - message = "{}:\nGrid points | Error | Rate\n".format(label) - for i, grid_size in enumerate(grid_sizes): - message += "{:<11} | {:f} | ".format(grid_size, errors[i]) - if i > 0: - message += "{:f} \n".format(log(errors[i] / errors[i-1]) / log(dx[i] / dx[i-1])) - else: - message += "--\n" - return message diff --git a/tools/pylib/boututils/closest_line.py b/tools/pylib/boututils/closest_line.py deleted file mode 100644 index 42dbbe047f..0000000000 --- a/tools/pylib/boututils/closest_line.py +++ /dev/null @@ -1,14 +0,0 @@ -from builtins import range -import numpy -# Find the closest contour line to a given point -def closest_line(n, x, y, ri, zi, mind=None): - - mind = numpy.min( (x[0] - ri)**2 + (y[0] - zi)**2 ) - ind = 0 - - for i in range (1, n) : - d = numpy.min( (x[i] - ri)**2 + (y[i] - zi)**2 ) - if d < mind : - mind = d - ind = i - return ind diff --git a/tools/pylib/boututils/contour.py b/tools/pylib/boututils/contour.py deleted file mode 100644 index 1d3fc97e22..0000000000 --- a/tools/pylib/boututils/contour.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Contour calculation routines - -https://web.archive.org/web/20140901225541/https://members.bellatlantic.net/~vze2vrva/thesis.html""" -from __future__ import print_function -from __future__ import division -from past.utils import old_div - -import numpy as np - - -def contour(f, level): - """Return a list of contours matching the given level""" - - if len(f.shape) != 2: - print("Contour only works on 2D data") - return None - nx,ny = f.shape - - # Go through each cell edge and mark which ones contain - # a level crossing. Approximating function as - # f = axy + bx + cy + d - # Hence linear interpolation along edges. - - edgecross = {} # Dictionary: (cell number, edge number) to crossing location - - for i in np.arange(nx-1): - for j in np.arange(ny-1): - # Lower-left corner of cell is (i,j) - if (np.max(f[i:(i+2),j:(j+2)]) < level) or (np.min(f[i:(i+2),j:(j+2)]) > level): - # not in the correct range - skip - continue - - # Check each edge - ncross = 0 - def location(a, b): - if (a > level) ^ (a > level): - # One of the corners is > level, and the other is <= level - ncross += 1 - # Find location - return old_div((level - a), (b - a)) - else: - return None - - loc = [ - location(f[i,j], f[i+1,j]), - location(f[i+1,j], f[i+1,j+1]), - location(f[i+1,j+1], f[i,j+1]), - location(f[i,j+1], f[i,j])] - - if ncross != 0: # Only put into dictionary if has a crossing - cellnr = (ny-1)*i + j # The cell number - edgecross[cellnr] = [loc,ncross] # Tack ncross onto the end - - # Process crossings into contour lines - - while True: - # Start from an arbitrary location and follow until - # it goes out of the domain or closes - try: - startcell, cross = edgecross.popitem() - except KeyError: - # No keys left so finished - break - - def follow(): - return - - # Follow - - return - -def find_opoints(var2d): - """Find O-points in psi i.e. local minima/maxima""" - return - -def find_xpoints(var2d): - """Find X-points in psi i.e. inflection points""" - return - diff --git a/tools/pylib/boututils/crosslines.py b/tools/pylib/boututils/crosslines.py deleted file mode 120000 index fa0acafd36..0000000000 --- a/tools/pylib/boututils/crosslines.py +++ /dev/null @@ -1 +0,0 @@ -../../tokamak_grids/pyGridGen/crosslines.py \ No newline at end of file diff --git a/tools/pylib/boututils/datafile.py b/tools/pylib/boututils/datafile.py deleted file mode 100644 index 00bb987706..0000000000 --- a/tools/pylib/boututils/datafile.py +++ /dev/null @@ -1,955 +0,0 @@ -"""File I/O class - -A wrapper around various NetCDF libraries and h5py, used by BOUT++ -routines. Creates a consistent interface across machines - -Supported libraries: - -- ``h5py`` (for HDF5 files) -- ``netCDF4`` (preferred NetCDF library) - -NOTE ----- -NetCDF and HDF5 include unlimited dimensions, but this library is just -for very simple I/O operations. Educated guesses are made for the -dimensions. - -TODO ----- -- Don't raise ``ImportError`` if no NetCDF libraries found, use HDF5 - instead? -- Cleaner handling of different NetCDF libraries -- Support for h5netcdf? - -""" - -from __future__ import print_function -from builtins import map, zip, str, object - -import numpy as np -import time -import getpass -from boututils.boutwarnings import alwayswarn -from boututils.boutarray import BoutArray - -try: - from netCDF4 import Dataset - has_netCDF = True -except ImportError: - raise ImportError( - "DataFile: No supported NetCDF modules available -- requires netCDF4") - -try: - import h5py - has_h5py = True -except ImportError: - has_h5py = False - - -class DataFile(object): - """File I/O class - - A wrapper around various NetCDF libraries and h5py, used by BOUT++ - routines. Creates a consistent interface across machines - - Parameters - ---------- - filename : str, optional - Name of file to open. If no filename supplied, you will need - to call :py:obj:`~DataFile.open` and supply `filename` there - write : bool, optional - If True, open the file in read-write mode (existing files will - be appended to). Default is read-only mode - create : bool, optional - If True, open the file in write mode (existing files will be - truncated). Default is read-only mode - format : str, optional - Name of a filetype to use (e.g. ``NETCDF3_CLASSIC``, - ``NETCDF3_64BIT``, ``NETCDF4``, ``HDF5``) - - TODO - ---- - - `filename` should not be optional! - - Take a ``mode`` argument to be more in line with other file types - - `format` should be checked to be a sensible value - - Make sure ``__init__`` methods are first - - Make `impl` and `handle` private - - """ - impl = None - - def __init__(self, filename=None, write=False, create=False, format='NETCDF3_64BIT', **kwargs): - """ - - NetCDF formats are described here: https://unidata.github.io/netcdf4-python/ - - NETCDF3_CLASSIC Limited to 2.1Gb files - - NETCDF3_64BIT_OFFSET or NETCDF3_64BIT is an extension to allow larger file sizes - - NETCDF3_64BIT_DATA adds 64-bit integer data types and 64-bit dimension sizes - - NETCDF4 and NETCDF4_CLASSIC use HDF5 as the disk format - """ - if filename is not None: - if filename.split('.')[-1] in ('hdf5', 'hdf', 'h5'): - self.impl = DataFile_HDF5( - filename=filename, write=write, create=create, format=format) - else: - self.impl = DataFile_netCDF( - filename=filename, write=write, create=create, format=format, **kwargs) - elif format == 'HDF5': - self.impl = DataFile_HDF5( - filename=filename, write=write, create=create, - format=format) - else: - self.impl = DataFile_netCDF( - filename=filename, write=write, create=create, format=format, **kwargs) - - def open(self, filename, write=False, create=False, - format='NETCDF3_CLASSIC'): - """Open the file - - Parameters - ---------- - filename : str, optional - Name of file to open - write : bool, optional - If True, open the file in read-write mode (existing files will - be appended to). Default is read-only mode - create : bool, optional - If True, open the file in write mode (existing files will be - truncated). Default is read-only mode - format : str, optional - Name of a filetype to use (e.g. ``NETCDF3_CLASSIC``, - ``NETCDF4``, ``HDF5``) - - TODO - ---- - - Return the result of calling open to be more like stdlib's - open - - `keys` should be more pythonic (return generator) - - """ - self.impl.open(filename, write=write, create=create, - format=format) - - def close(self): - """Close a file and flush data to disk - - """ - self.impl.close() - - def __del__(self): - if self.impl is not None: - self.impl.__del__() - - def __enter__(self): - self.impl.__enter__() - return self - - def __exit__(self, type, value, traceback): - self.impl.__exit__(type, value, traceback) - - def read(self, name, ranges=None, asBoutArray=True): - """Read a variable from the file - - Parameters - ---------- - name : str - Name of the variable to read - ranges : list of slice objects, optional - Slices of variable to read, can also be converted from lists or - tuples of (start, stop, stride). The number of elements in `ranges` - should be equal to the number of dimensions of the variable you - wish to read. See :py:obj:`~DataFile.size` for how to get the - dimensions - asBoutArray : bool, optional - If True, return the variable as a - :py:obj:`~boututils.boutarray.BoutArray` (the default) - - Returns - ------- - ndarray or :py:obj:`~boututils.boutarray.BoutArray` - The variable from the file - (:py:obj:`~boututils.boutarray.BoutArray` if `asBoutArray` - is True) - - """ - if ranges is not None: - for x in ranges: - if isinstance(x, (list, tuple)): - x = slice(*x) - return self.impl.read(name, ranges=ranges, asBoutArray=asBoutArray) - - def list(self): - """List all variables in the file - - Returns - ------- - list of str - A list containing all the names of the variables - - """ - return self.impl.list() - - def keys(self): - """A synonym for :py:obj:`~DataFile.list` - - TODO - ---- - - Make a generator to be more like python3 dict keys - - """ - return self.list() - - def dimensions(self, varname): - """Return the names of all the dimensions of a variable - - Parameters - ---------- - varname : str - The name of the variable - - Returns - ------- - tuple of str - The names of the variable's dimensions - - """ - return self.impl.dimensions(varname) - - def ndims(self, varname): - """Return the number of dimensions for a variable - - Parameters - ---------- - varname : str - The name of the variable - - Returns - ------- - int - The number of dimensions - - """ - return self.impl.ndims(varname) - - def sync(self): - """Write pending changes to disk. - - """ - self.impl.sync() - - def size(self, varname): - """Return the size of each dimension of a variable - - Parameters - ---------- - varname : str - The name of the variable - - Returns - ------- - tuple of int - The size of each dimension - - """ - return self.impl.size(varname) - - def bout_type(self, varname): - """Return the name of the BOUT++ type of a variable - - Possible values are: - - - scalar - - Field2D - - Field3D - - If the variable is an evolving variable (i.e. has a time - dimension), then it is appended with a "_t" - - Parameters - ---------- - varname : str - The name of the variable - - Returns - ------- - str - The name of the BOUT++ type - - """ - return self.attributes(varname)["bout_type"] - - def write(self, name, data, info=False): - """Write a variable to file - - If the variable is not a :py:obj:`~boututils.boutarray.BoutArray` with - the ``bout_type`` attribute, a guess will be made for the - dimensions - - Parameters - ---------- - name : str - Name of the variable to use in the file - data : :py:obj:`~boututils.boutarray.BoutArray` or ndarray - An array containing the variable data - info : bool, optional - If True, print information about what is being written to - file - - Returns - ------- - None - - """ - return self.impl.write(name, data, info) - - def __getitem__(self, name): - return self.impl.__getitem__(name) - - def __setitem__(self, key, value): - self.impl.__setitem__(key, value) - - def attributes(self, varname): - """Return a dictionary of attributes - - Parameters - ---------- - varname : str - The name of the variable - - Returns - ------- - dict - The attribute names and their values - - """ - return self.impl.attributes(varname) - - -class DataFile_netCDF(DataFile): - handle = None - - def open(self, filename, write=False, create=False, - format='NETCDF3_CLASSIC'): - if (not write) and (not create): - self.handle = Dataset(filename, "r") - elif create: - self.handle = Dataset(filename, "w", format=format) - else: - self.handle = Dataset(filename, "a") - # Record if writing - self.writeable = write or create - - def close(self): - if self.handle is not None: - self.handle.close() - self.handle = None - - def __init__(self, filename=None, write=False, create=False, - format='NETCDF3_CLASSIC', **kwargs): - self._kwargs = kwargs - if not has_netCDF: - message = "DataFile: No supported NetCDF python-modules available" - raise ImportError(message) - if filename is not None: - self.open(filename, write=write, create=create, format=format) - self._attributes_cache = {} - - def __del__(self): - self.close() - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def read(self, name, ranges=None, asBoutArray=True): - """Read a variable from the file.""" - if self.handle is None: - return None - - try: - var = self.handle.variables[name] - n = name - except KeyError: - # Not found. Try to find using case-insensitive search - var = None - for n in list(self.handle.variables.keys()): - if n.lower() == name.lower(): - print( - "WARNING: Reading '" + n + "' instead of '" + name + "'") - var = self.handle.variables[n] - if var is None: - return None - - if asBoutArray: - attributes = self.attributes(n) - - ndims = len(var.dimensions) - if ndims == 0: - data = var.getValue() - if asBoutArray: - data = BoutArray(data, attributes=attributes) - return data # [0] - else: - if ranges: - if len(ranges) == 2 * ndims: - # Reform list of pairs of ints into slices - ranges = [slice(a, b) for a, b in - zip(ranges[::2], ranges[1::2])] - elif len(ranges) != ndims: - raise ValueError("Incorrect number of elements in ranges argument " - "(got {}, expected {} or {})" - .format(len(ranges), ndims, 2 * ndims)) - - data = var[ranges[:ndims]] - if asBoutArray: - data = BoutArray(data, attributes=attributes) - return data - else: - data = var[:] - if asBoutArray: - data = BoutArray(data, attributes=attributes) - return data - - def __getitem__(self, name): - var = self.read(name) - if var is None: - raise KeyError("No variable found: " + name) - return var - - def __setitem__(self, key, value): - self.write(key, value) - - def list(self): - if self.handle is None: - return [] - return list(self.handle.variables.keys()) - - def keys(self): - return self.list() - - def dimensions(self, varname): - if self.handle is None: - return None - try: - var = self.handle.variables[varname] - except KeyError: - raise ValueError("No such variable") - return var.dimensions - - def ndims(self, varname): - if self.handle is None: - raise ValueError("File not open") - try: - var = self.handle.variables[varname] - except KeyError: - raise ValueError("No such variable") - return len(var.dimensions) - - def sync(self): - self.handle.sync() - - def size(self, varname): - if self.handle is None: - return [] - try: - var = self.handle.variables[varname] - except KeyError: - return [] - - def dimlen(d): - dim = self.handle.dimensions[d] - if dim is not None: - t = type(dim).__name__ - if t == 'int': - return dim - return len(dim) - return 0 - return [dimlen(d) for d in var.dimensions] - - def _bout_type_from_dimensions(self, varname): - dims = self.dimensions(varname) - dims_dict = { - ('t', 'x', 'y', 'z'): "Field3D_t", - ('t', 'x', 'y'): "Field2D_t", - ('t', 'x', 'z'): "FieldPerp_t", - ('t',): "scalar_t", - ('x', 'y', 'z'): "Field3D", - ('x', 'y'): "Field2D", - ('x', 'z'): "FieldPerp", - ('x',): "ArrayX", - (): "scalar", - } - - return dims_dict.get(dims, None) - - def _bout_dimensions_from_type(self, bout_type): - dims_dict = { - "Field3D_t": ('t', 'x', 'y', 'z'), - "Field2D_t": ('t', 'x', 'y'), - "FieldPerp_t": ('t', 'x', 'z'), - "scalar_t": ('t',), - "Field3D": ('x', 'y', 'z'), - "Field2D": ('x', 'y'), - "FieldPerp": ('x', 'z'), - "ArrayX": ('x',), - "scalar": (), - } - - return dims_dict.get(bout_type, None) - - def write(self, name, data, info=False): - - if not self.writeable: - raise Exception("File not writeable. Open with write=True keyword") - - s = np.shape(data) - - # Get the variable type - t = type(data).__name__ - - if t == 'NoneType': - print("DataFile: None passed as data to write. Ignoring") - return - - if t == 'ndarray' or t == 'BoutArray': - # Numpy type or BoutArray wrapper for Numpy type. Get the data type - t = data.dtype.str - - if t == 'list': - # List -> convert to numpy array - data = np.array(data) - t = data.dtype.str - - if (t == 'int') or (t == ' -# -# * Modified to allow calls with only one argument -# - -def int_func( xin, fin=None, simple=None): - if fin is None : - f = copy.deepcopy(xin) - x = numpy.arange(numpy.size(f)).astype(float) - else: - f = copy.deepcopy(fin) - x = copy.deepcopy(xin) - - n = numpy.size(f) - - g = numpy.zeros(n) - - if simple is not None : - # Just use trapezium rule - - g[0] = 0.0 - for i in range (1, n) : - g[i] = g[i-1] + 0.5*(x[i] - x[i-1])*(f[i] + f[i-1]) - - else: - - n2 = numpy.int(old_div(n,2)) - - g[0] = 0.0 - for i in range (n2, n) : - g[i] = simps( f[0:i+1], x[0:i+1]) - - - - for i in range (1, n2) : - g[i] = g[n-1] - simps( f[i::], x[i::]) - - return g - - diff --git a/tools/pylib/boututils/linear_regression.py b/tools/pylib/boututils/linear_regression.py deleted file mode 100644 index c2f3f2cc39..0000000000 --- a/tools/pylib/boututils/linear_regression.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import division -# -# Perform a linear regression fit -# - -from numpy import mean - -def linear_regression(x, y): - """ Simple linear regression of two variables - - y = a + bx - - a, b = linear_regression(x, y) - - """ - - if x.size != y.size: - raise ValueError("x and y inputs must be the same size") - - mx = mean(x) - my = mean(y) - - b = (mean(x*y) - mx*my) / (mean(x**2) - mx**2) - a = my - b*mx - - return a, b - diff --git a/tools/pylib/boututils/local_min_max.py b/tools/pylib/boututils/local_min_max.py deleted file mode 120000 index 6ac6b0819e..0000000000 --- a/tools/pylib/boututils/local_min_max.py +++ /dev/null @@ -1 +0,0 @@ -../../tokamak_grids/pyGridGen/local_min_max.py \ No newline at end of file diff --git a/tools/pylib/boututils/mode_structure.py b/tools/pylib/boututils/mode_structure.py deleted file mode 100644 index 1196c823c6..0000000000 --- a/tools/pylib/boututils/mode_structure.py +++ /dev/null @@ -1,417 +0,0 @@ -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division -from builtins import range -from past.utils import old_div -import numpy as numpy -import sys -from pylab import plot,xlabel,ylim,savefig,gca, xlim, show, clf, draw, title -from boututils.fft_integrate import fft_integrate -from .ask import query_yes_no - -#; Calculates mode structure from BOUT++ output -#; for comparison to ELITE -#; -#; April 2009 - Added ERGOS flag. This is intended -#; for producing plots similar to the ERGOS -#; vacuum RMP code - -# interpolates a 1D periodic function -def zinterp( v, zind): - - v = numpy.ravel(v) - - nz = numpy.size(v) - z0 = numpy.round(zind) - - p = zind - float(z0) # between -0.5 and 0.5 - - if p < 0.0 : - z0 = z0 - 1 - p = p + 1.0 - - - z0 = ((z0 % (nz-1)) + (nz-1)) % (nz-1) - - # for now 3-point interpolation - - zp = (z0 + 1) % (nz - 1) - zm = (z0 - 1 + (nz-1)) % (nz - 1) - - - result = 0.5*p*(p-1.0)*v[zm.astype(int)] \ - + (1.0 - p*p)*v[z0.astype(int)] \ - + 0.5*p*(p+1.0)*v[zp.astype(int)] - - return result - - -def mode_structure( var_in, grid_in, period=1, - zangle=0.0, n=None, addq=None, output=None, - xq=None, xpsi=None, slow=None, subset=None, - filter=None, famp=None, quiet=None, - ergos=None, ftitle=None, - xrange=None, yrange=None, rational=None, pmodes=None, - _extra=None): - - - #ON_ERROR, 2 - # - # period = 1 ; default = full torus - - if n is None : - if filter is not None : - n = filter*period - else: n = period - - - # if (grid_in.JYSEPS1_1 GE 0) OR (grid_in.JYSEPS1_2 NE grid_in.JYSEPS2_1) OR (grid_in.JYSEPS2_2 NE grid_in.ny-1) THEN BEGIN - # PRINT, "Mesh contains branch-cuts. Keeping only core" - # - # grid = core_mesh(grid_in) - # var = core_mesh(var_in, grid_in) - #ENDIF ELSE BEGIN - grid = grid_in - vr = var_in - #ENDELSE - - - #IF KEYWORD_SET(filter) THEN BEGIN - # var = zfilter(var, filter) - #ENDIF - - nx = grid.get('nx') - ny = grid.get('ny') - - s = numpy.shape(vr) - if numpy.size(s) != 3 : - print("Error: Variable must be 3 dimensional") - return - - if (s[0] != nx) or (s[1] != ny) : - print("Error: Size of variable doesn't match grid") - - return - - nz = s[2] - - dz = 2.0*numpy.pi / numpy.float(period*(nz-1)) - - # GET THE TOROIDAL SHIFT - tn = list(grid.keys()) - tn = numpy.char.upper(tn) - count = numpy.where(tn == "QINTY") - if numpy.size(count) > 0 : - print("Using qinty as toroidal shift angle") - zShift = grid.get('qinty') - else: - count = numpy.where(tn == "ZSHIFT") - if numpy.size(count) > 0 : - print("Using zShift as toroidal shift angle") - zShift = grid.get('zShift') - else: - print("ERROR: Can't find qinty or zShift variable") - return - - zshift=grid.get('zShift') - - rxy=grid.get('Rxy') - zxy=grid.get('Zxy') - Btxy=grid.get('Btxy') - Bpxy=grid.get('Bpxy') - shiftangle=grid.get('ShiftAngle') - psixy=grid.get('psixy') - psi_axis=grid.get('psi_axis') - psi_bndry=grid.get('psi_bndry') - - np = 4*ny - - nf = old_div((np - 2), 2) - famp = numpy.zeros((nx, nf)) - - for x in range (nx): - #;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - # transform data into fixed poloidal angle - - # get number of poloidal points - nskip = numpy.zeros(ny-1) - for y in range (ny-1): - yp = y + 1 - nskip[y] = old_div(numpy.abs(zshift[x,yp] - zshift[x,y]), dz) - 1 - - - nskip =numpy.int_(numpy.round(nskip)) - nskip=numpy.where(nskip > 0, nskip, 0) - - - ny2 = numpy.int_(ny + numpy.sum(nskip)) # number of poloidal points - - # IF NOT KEYWORD_SET(quiet) THEN PRINT, x, ny2 - - f = numpy.zeros(ny2) # array for values - R = numpy.zeros(ny2) # Rxy - Z = numpy.zeros(ny2) # Zxy - BtBp = numpy.zeros(ny2) # Bt / Bp - - # interpolate values onto points - - ypos = 0 - for y in range(ny-1): - # original points - zind = old_div((zangle - zshift[x,y]),dz) - - - if numpy.size(zind) != 1 : sys.exit() - f[ypos] = zinterp(vr[x,y,:], zind) - R[ypos] = rxy[x,y] - Z[ypos] = zxy[x,y] - BtBp[ypos] = old_div(Btxy[x,y], Bpxy[x,y]) - - ypos = ypos + 1 - - # add the extra points - - zi0 = old_div((zangle - zshift[x,y]),dz) - zip1 = old_div((zangle - zshift[x,y+1]),dz) - - dzi = old_div((zip1 - zi0), (nskip[y] + 1)) - - for i in range (nskip[y]): - zi = zi0 + numpy.float(i+1)*dzi # zindex - w = old_div(numpy.float(i+1),numpy.float(nskip[y]+1)) # weighting - - f[ypos+i] = w*zinterp(vr[x,y+1,:], zi) + (1.0-w)*zinterp(vr[x,y,:], zi) - - R[ypos+i] = w*rxy[x,y+1] + (1.0-w)*rxy[x,y] - Z[ypos+i] = w*zxy[x,y+1] + (1.0-w)*zxy[x,y] - BtBp[ypos+i] = old_div((w*Btxy[x,y+1] + (1.0-w)*Btxy[x,y]), (w*Bpxy[x,y+1] + (1.0-w)*Bpxy[x,y])) - - ypos = ypos + nskip[y] - - # final point - - zind = old_div((zangle - zShift[x,ny-1]),dz) - - f[ypos] = zinterp(vr[x,ny-1,:], zind) - R[ypos] = rxy[x,ny-1] - Z[ypos] = zxy[x,ny-1] - BtBp[ypos] = old_div(Btxy[x,ny-1], Bpxy[x,ny-1]) - - - #STOP - - #;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - #; calculate poloidal angle - - - drxy = numpy.gradient(R) - dzxy = numpy.gradient(Z) - dl = numpy.sqrt(drxy*drxy + dzxy*dzxy) - - nu = dl * BtBp / R # field-line pitch - theta = old_div(numpy.real(fft_integrate(nu)), shiftangle[x]) - - if numpy.max(theta) > 1.0 : - # mis-match between q and nu (integration error?) - if quiet is None : print("Mismatch ", x, numpy.max(theta)) - theta = old_div(theta, (numpy.max(theta) + numpy.abs(theta[1] - theta[0]))) - - - theta = 2.0*numpy.pi * theta - - #;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - #; take Fourier transform in theta angle - - tarr = 2.0*numpy.pi*numpy.arange(np) / numpy.float(np) # regular array in theta - - farr = numpy.interp(tarr, theta, f) - - #STOP - - ff = old_div(numpy.fft.fft(farr),numpy.size(farr)) - - for i in range (nf): - famp[x, i] = 2.0*numpy.abs(ff[i+1]) - - - - - # sort modes by maximum size - - fmax = numpy.zeros(nf) - for i in range(nf): - fmax[i] = numpy.max(famp[:,i]) - - - inds = numpy.argsort(fmax)[::-1] - - - if pmodes is None : pmodes = 10 - - qprof = old_div(numpy.abs(shiftangle), (2.0*numpy.pi)) - - xarr = numpy.arange(nx) - xtitle="Radial index" - if xq is not None : - # show as a function of q*n - xarr = qprof*numpy.float(n) - - xtitle="q * n" - elif xpsi is not None : - # show as a function of psi. Should be normalised psi - xarr = psixy[:,0] - - # Check if the grid includes psi axis and boundary - count1 = numpy.where(tn == "PSI_AXIS") - count2 = numpy.where(tn == "PSI_BNDRY") - - if (numpy.size(count1) > 0) and (numpy.size(count2) > 0) : - xarr = old_div((xarr - psi_axis), (psi_bndry - psi_axis)) - - else: - # Use hard-wired values - print("WARNING: Using hard-wired psi normalisation") - # for circular case - #xarr = (xarr + 0.1937) / (0.25044 + 0.1937) - # for ellipse case - #xarr = xarr / 0.74156 - - # cbm18_dens8 - xarr = old_div((xarr + 0.854856), (0.854856 + 0.0760856)) - - - xtitle="Psi normalised" - - - - if slow is not None : - # plot modes slowly for examination - #safe_colors, /first -# ax = fig.add_subplot(111) - # go through and plot each mode - for i in range(nf): - if numpy.max(famp[:,i]) > 0.05*numpy.max(famp): - print("Mode m = ", i+1, " of ", nf) - plot(xarr, famp[:,i], 'k') - ylim(0,numpy.max(famp)) - xlim(xrange) - xlabel(xtitle) - show(block=False) - - q = old_div(numpy.float(i+1), numpy.float(n)) - - pos = numpy.interp(q, qprof, xarr) - - plot( [pos, pos],[0, 2.*numpy.max(fmax)], 'k--') - draw() - - ans=query_yes_no('next mode') - if ans: - clf() - - - - elif ergos is not None : - # ERGOS - style output - - if output is not None and slow is None : - savefig('output.png') - - -# -# contour2, famp, xarr, indgen(nf)+1, $ -# xlabel=xtitle, xrange=xrange, yrange=yrange, _extra=_extra -# -# ; overplot the q profile -# -# oplot, xarr, qprof * n, color=1, thick=2 -# -# IF KEYWORD_SET(rational) THEN BEGIN -# maxm = FIX(MAX(qprof)) * n -# -# qreson = (FINDGEN(maxm)+1) / FLOAT(n) -# -# ; get x location for each of these resonances -# qloc = INTERPOL(xarr, qprof, qreson) -# -# oplot, qloc, findgen(maxm)+1., psym=4, color=1 -# ENDIF -# -# IF KEYWORD_SET(output) THEN BEGIN -# ; output data to save file -# SAVE, xarr, qprof, famp, file=output+".idl" -# -# DEVICE, /close -# SET_PLOT, 'X' -# ENDIF - - else: - if output is not None and slow is None : - savefig('output.png') - # savefig('output.ps') - - # - # - if subset is not None : - - # get number of modes larger than 5% of the maximum - count = numpy.size(numpy.where(fmax > 0.10*numpy.max(fmax))) - - minind = numpy.min(inds[0:count]) - maxind = numpy.max(inds[0:count]) - - print("Mode number range: ", minind, maxind) - - plot( xarr, famp[:,0], 'k', visible=False) - ylim(0,numpy.max(famp)) - xlabel(xtitle) - xlim(xrange) - title(ftitle) - - gca().set_color_cycle(['red', 'red', 'black', 'black']) - - for i in range(minind, maxind+1, subset): - plot( xarr, famp[:,i]) - - q = old_div(numpy.float(i+1), numpy.float(n)) - pos = numpy.interp(q, qprof, xarr) - - plot( [pos, pos], [0, 2.*numpy.max(fmax)], '--') - - - # - else: - # default - just plot everything - gca().set_color_cycle(['black', 'red']) - - plot(xarr, famp[:,0]) - ylim(0,numpy.max(famp)) #, color=1, - xlabel(xtitle) #, chars=1.5, xrange=xrange,title=title, _extra=_extra - xlim(xrange) - for i in range (nf): - plot( xarr, famp[:,i]) - - - # - # IF KEYWORD_SET(addq) THEN BEGIN - # - # FOR i=0, pmodes-1 DO BEGIN - # PRINT, "m = "+STRTRIM(STRING(inds[i]+1), 2)+" amp = "+STRTRIM(STRING(fmax[inds[i]]),2) - # q = FLOAT(inds[i]+1) / FLOAT(n) - # - # pos = INTERPOL(xarr, qprof, q) - # - # oplot, [pos, pos], [0, 2.*MAX(fmax)], lines=2, color=1 - # ENDFOR - # ENDIF - # - # ENDELSE - # IF KEYWORD_SET(output) THEN BEGIN - # ; output data to save file - # SAVE, xarr, qprof, famp, file=output+".idl" - # - # DEVICE, /close - # SET_PLOT, 'X' - # ENDIF - # ENDELSE - # diff --git a/tools/pylib/boututils/moment_xyzt.py b/tools/pylib/boututils/moment_xyzt.py deleted file mode 100644 index 675d1f135b..0000000000 --- a/tools/pylib/boututils/moment_xyzt.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import print_function -from __future__ import division -from builtins import range -from past.utils import old_div -import numpy as np -from boututils.bunch import Bunch - -def RMSvalue( vec1d): -#; -#; -get rms of a 1D signal -#;------------------------ - - nel=np.size(vec1d) - valav=old_div(np.sum(vec1d),nel) - valrms=np.sqrt(old_div(np.sum((vec1d-valav)**2),nel)) - acvec=vec1d-valav - - return Bunch(valrms=valrms, - valav=valav, - acvec=acvec) - - - -def moment_xyzt( sig_xyzt, *args):#rms=None, dc=None, ac=None): -#; -#; Calculate moments of a 4d signal of (x,y,z,t), i.e, -#; -RMS, i.e., a function of (x,y,t) -#; -DC (average in z), i.e., a function of (x,y,t) -#; -AC (DC subtracted out), i.e., a function of (x,y,z,t) -#;------------------------------------------------------------------- - - d = np.shape(sig_xyzt) - if np.size(d) != 4 : - print("Error: Variable must be 4D (x,y,z,t)") - return - - - siz=np.shape(sig_xyzt) - rms=np.zeros((siz[0],siz[1],siz[2])) - dc=np.zeros((siz[0],siz[1],siz[2])) - if 'AC' in args : ac=np.zeros((siz[0],siz[1],siz[2],siz[3])) - - - data = sig_xyzt - if np.modf(np.log2(siz[3]))[0] != 0.0 : - print("WARNING: Expecting a power of 2 in Z direction") - - if np.modf(np.log2(siz[3]-1))[0] and (siz[3] > 1) : - print(" -> Truncating last point to get power of 2") - data = data[:,:,0:(siz[3]-2),:] - siz[3] = siz[3] - 1 - - - for ix in range (siz[1]): - for iy in range (siz[2]): - for it in range (siz[0]): - val=RMSvalue(sig_xyzt[it,ix,iy,:]) - - rms[it,ix,iy]=val.valrms - dc[it,ix,iy]=val.valav - if 'AC' in args : ac[it,ix,iy,:]=[val.acvec,val.acvec[0]] - - res=Bunch() - - if 'RMS' in args: - res.rms = rms - if 'DC' in args: - res.dc = dc - if 'AC' in args: - res.ac = ac - - if 'RMS' not in args and 'DC' not in args and 'AC' not in args : - raise RuntimeError('Wrong argument') - return res diff --git a/tools/pylib/boututils/options.py b/tools/pylib/boututils/options.py deleted file mode 100644 index 2de3ebc3e0..0000000000 --- a/tools/pylib/boututils/options.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Module to allow BOUT.inp files to be read into python and -manipulated with ease. - - -Nick Walkden, June 2015 -nick.walkden@ccfe.ac.uk - -""" - -from copy import copy -import os - - -class BOUTOptions(object): - """Class to store and interact with options from BOUT++ - - Parameters - ---------- - inp_path : str, optional - Path to BOUT++ options file - - Examples - -------- - - Instantiate with - - >>> myOpts = BOUTOptions() - >>> myOpts.read_inp('path/to/input/file') - - or - - >>> myOpts = BOUTOptions('path/to/input/file') - - To get a list of sections use - - >>> section_list = myOpts.list_sections - >>> # Also print to screen: - >>> section_list = myOpts.list_sections(verbose=True) - - Each section of the input is stored as a dictionary attribute so - that, if you want all the settings in the section [ddx]: - - >> ddx_opt_dict = myOpts.ddx - - and access individual settings by - - >>> ddx_setting = myOpts.ddx['first'] - - Any settings in BOUT.inp without a section are stored in - - >>> root_dict = myOpts.root - - TODO - ---- - - Merge this and BoutOptionsFile or replace with better class - - """ - - def __init__(self, inp_path=None): - - self._sections = ['root'] - - for section in self._sections: - super(BOUTOptions,self).__setattr__(section,{}) - - if inp_path is not None: - self.read_inp(inp_path) - - def read_inp(self, inp_path=''): - """Read a BOUT++ input file - - Parameters - ---------- - inp_path : str, optional - Path to the input file (default: current directory) - - """ - - try: - inpfile = open(os.path.join(inp_path, 'BOUT.inp'),'r') - except: - raise TypeError("ERROR: Could not read file "+\ - os.path.join(inp_path, "BOUT.inp")) - - current_section = 'root' - inplines = inpfile.read().splitlines() - # Close the file after use - inpfile.close() - for line in inplines: - #remove white space - line = line.replace(" ","") - - - if len(line) > 0 and line[0] is not '#': - #Only read lines that are not comments or blank - if '[' in line: - #Section header - section = line.split('[')[1].split(']')[0] - current_section = copy(section) - if current_section not in self._sections: - self.add_section(current_section) - - elif '=' in line: - #option setting - attribute = line.split('=')[0] - value = line.split('=')[1].split('#')[0] - value = value.replace("\n","") - value = value.replace("\t","") - value = value.replace("\r","") - value = value.replace("\"","") - self.__dict__[copy(current_section)][copy(attribute)] = copy(value) - else: - pass - - def add_section(self, section): - """Add a section to the options - - Parameters - ---------- - section : str - The name of a new section - - TODO - ---- - - Guard against wrong type - """ - self._sections.append(section) - super(BOUTOptions,self).__setattr__(section,{}) - - def remove_section(self, section): - """Remove a section from the options - - Parameters - ---------- - section : str - The name of a section to remove - - TODO - ---- - - Fix undefined variable - """ - if section in self._sections: - self._sections.pop(self._sections.index(sections)) - super(BOUTOptions,self).__delattr__(section) - else: - print("WARNING: Section "+section+" not found.\n") - - def list_sections(self, verbose=False): - """Return all the sections in the options - - Parameters - ---------- - verbose : bool, optional - If True, print sections to screen - - TODO - ---- - - Better pretty-print - """ - if verbose: - print("Sections Contained: \n") - for section in self._sections: - print("\t"+section+"\n") - - return self._sections diff --git a/tools/pylib/boututils/plotdata.py b/tools/pylib/boututils/plotdata.py deleted file mode 100644 index 72627f4033..0000000000 --- a/tools/pylib/boututils/plotdata.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import print_function -# Plot a data set - -import numpy as np -import matplotlib -import matplotlib.cm as cm -import matplotlib.mlab as mlab -import matplotlib.pyplot as plt - -matplotlib.rcParams['xtick.direction'] = 'out' -matplotlib.rcParams['ytick.direction'] = 'out' - -def plotdata(data, x=None, y=None, - title=None, xtitle=None, ytitle=None, - output=None, range=None, - fill=True, mono=False, colorbar=True, - xerr=None, yerr=None): - """Plot 1D or 2D data, with a variety of options.""" - - size = data.shape - ndims = len(size) - - if ndims == 1: - if (xerr is not None) or (yerr is not None): - # Points with error bars - if x is None: - x = np.arange(size) - errorbar(x, data, xerr, yerr) - # Line plot - if x is None: - plt.plot(data) - else: - plt.plot(x, data) - - elif ndims == 2: - # A contour plot - - if x is None: - x = np.arange(size[1]) - if y is None: - y = np.arange(size[0]) - - if fill: - #plt.contourf(data, colors=colors) - cmap=None - if mono: cmap = cm.gray - plt.imshow(data, interpolation='bilinear', cmap=cmap) - else: - colors = None - if mono: colors = 'k' - - plt.contour(x, y, data, colors=colors) - - # Add a color bar - if colorbar: - CB = plt.colorbar(shrink=0.8, extend='both') - - else: - print("Sorry, can't handle %d-D variables" % ndims) - return - - if title is not None: - plt.title(title) - if xtitle is not None: - plt.xlabel(xtitle) - if ytitle is not None: - plt.ylabel(ytitle) - - if output is not None: - # Write to a file - plt.savefig(output) - else: - # Plot to screen - plt.show() - -def test(): - """Test the plotdata routine.""" - # Generate and plot test data - - delta = 0.025 - x = np.arange(-3.0, 3.0, delta) - y = np.arange(-2.0, 2.0, delta) - X, Y = np.meshgrid(x, y) - Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0) - Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1) - # difference of Gaussians - Z = 10.0 * (Z2 - Z1) - - plotdata(Z, title="test data", fill=False, mono=False) - plotdata(Z, title="Fill in mono", fill=True, mono=True) diff --git a/tools/pylib/boututils/plotpolslice.py b/tools/pylib/boututils/plotpolslice.py deleted file mode 100644 index 924272fb85..0000000000 --- a/tools/pylib/boututils/plotpolslice.py +++ /dev/null @@ -1,143 +0,0 @@ -from __future__ import print_function -from __future__ import division -from builtins import str -from builtins import range -from past.utils import old_div -import numpy as np -from boututils.file_import import file_import -import sys - -if sys.version_info[0]>=3: - message = "polplotslice uses the VTK library through mayavi, which"+\ - " is currently only available in python 2" - raise ImportError(message) -else: - from mayavi import mlab - from tvtk.tools import visual - - - -def zinterp( v, zind): - #v = REFORM(v) - nz = np.size(v) - z0 = np.round(zind) - - p = zind - float(z0) # between -0.5 and 0.5 - - if p < 0.0 : - z0 = z0 - 1 - p = p + 1.0 - - - z0 = ((z0 % (nz-1)) + (nz-1)) % (nz-1) - - # for now 3-point interpolation - - zp = (z0 + 1) % (nz - 1) - zm = (z0 - 1 + (nz-1)) % (nz - 1) - - result = 0.5*p*(p-1.0)*v[zm] \ - + (1.0 - p*p)*v[z0] \ - + 0.5*p*(p+1.0)*v[zp] - - return result - - -def plotpolslice(var3d,gridfile,period=1,zangle=0.0, rz=1, fig=0): - """ data2d = plotpolslice(data3d, 'gridfile' , period=1, zangle=0.0, rz:return (r,z) grid also=1, fig: to do the graph, set to 1 ) """ - - g=file_import(gridfile) - - nx=var3d.shape[0] - ny=var3d.shape[1] - nz=var3d.shape[2] - - - zShift=g.get('zShift') - rxy=g.get('Rxy') - zxy=g.get('Zxy') - - dz = 2.0*np.pi / float(period*nz) - - ny2=ny - nskip=np.zeros(ny-1) - for i in range(ny-1): - ip=(i+1)%ny - nskip[i]=0 - for x in range(nx): - ns=old_div(np.max(np.abs(zShift[x,ip]-zShift[x,i])),dz)-1 - if ns > nskip[i] : nskip[i] = ns - - nskip = np.int_(np.round(nskip)) - ny2 = np.int_(ny2 + np.sum(nskip)) - - print("Number of poloidal points in output:" + str(ny2)) - - var2d = np.zeros((nx, ny2)) - r = np.zeros((nx, ny2)) - z = np.zeros((nx, ny2)) - - ypos = 0 - for y in range (ny-1) : - # put in the original points - for x in range (nx): - zind = old_div((zangle - zShift[x,y]),dz) - var2d[x,ypos] = zinterp(var3d[x,y,:], zind) - # IF KEYWORD_SET(profile) THEN var2d[x,ypos] = var2d[x,ypos] + profile[x,y] - r[x,ypos] = rxy[x,y] - z[x,ypos] = zxy[x,y] - - ypos = ypos + 1 - - print((y, ypos)) - - # and the extra points - - for x in range (nx): - zi0 = old_div((zangle - zShift[x,y]),dz) - zip1 = old_div((zangle - zShift[x,y+1]),dz) - - dzi = old_div((zip1 - zi0), (nskip[y] + 1)) - - for i in range (nskip[y]): - zi = zi0 + float(i+1)*dzi # zindex - w = old_div(float(i+1),float(nskip[y]+1)) # weighting - - var2d[x,ypos+i] = w*zinterp(var3d[x,y+1,:], zi) + (1.0-w)*zinterp(var3d[x,y,:], zi) - # IF KEYWORD_SET(profile) THEN var2d[x,ypos+i] = var2d[x,ypos+i] + w*profile[x,y+1] + (1.0-w)*profile[x,y] - r[x,ypos+i] = w*rxy[x,y+1] + (1.0-w)*rxy[x,y] - z[x,ypos+i] = w*zxy[x,y+1] + (1.0-w)*zxy[x,y] - - - - ypos = ypos + nskip[y] - - - # FINAL POINT - - for x in range(nx): - zind = old_div((zangle - zShift[x,ny-1]),dz) - var2d[x,ypos] = zinterp(var3d[x,ny-1,:], zind) - # IF KEYWORD_SET(profile) THEN var2d[x,ypos] = var2d[x,ypos] + profile[x,ny-1] - r[x,ypos] = rxy[x,ny-1] - z[x,ypos] = zxy[x,ny-1] - - - if(fig==1): - - f = mlab.figure(size=(600,600)) - # Tell visual to use this as the viewer. - visual.set_viewer(f) - - - s = mlab.mesh(r,z,var2d, colormap='PuOr')#, wrap_scale='true')#, representation='wireframe') - s.enable_contours=True - s.contour.filled_contours=True - mlab.view(0,0) - - else: - # return according to opt - if rz==1 : - return r,z,var2d - else: - return var2d diff --git a/tools/pylib/boututils/radial_grid.py b/tools/pylib/boututils/radial_grid.py deleted file mode 100644 index e0cf9c446c..0000000000 --- a/tools/pylib/boututils/radial_grid.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import division -from past.utils import old_div -import numpy -#;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -# -# radial grid -# -# n - number of grid points -# pin, pout - range of psi -# seps - locations of separatrices -# sep_factor - separatrix peaking -# in_dp=in_dp - Fix the dx on the lower side -# out_dp=out_dp - Fix the dx on the upper side - -def radial_grid( n, pin, pout, include_in, include_out, seps, sep_factor, - in_dp=None, out_dp=None): - - if n == 1 : - return [0.5*(pin+pout)] - - - x = numpy.arange(0.,n) - m = numpy.float(n-1) - if include_in is None : - x = x + 0.5 - m = m + 0.5 - - - if include_out is None: - m = m + 0.5 - - x = old_div(x, m) - - - if in_dp is None and out_dp is None : - # Neither inner or outer gradients set. Just return equal spacing - return pin + (pout - pin)*x - - - norm = (x[1] - x[0])*(pout - pin) - - if in_dp is not None and out_dp is not None : - # Fit to dist = a*i^3 + b*i^2 + c*i - c = old_div(in_dp,norm) - b = 3.*(1. - c) - old_div(out_dp,norm) + c - a = 1. - c - b - elif in_dp is not None : - # Only inner set - c = old_div(in_dp,norm) - a = 0.5*(c-1.) - b = 1. - c - a - - #a = 0 - #c = in_dp/norm - #b = 1. - c - else: - # Only outer set. Used in PF region - # Fit to (1-b)*x^a + bx for fixed b - df = old_div(out_dp, norm) - b = 0.25 < df # Make sure a > 0 - a = old_div((df - b), (1. - b)) - vals = pin + (pout - pin)*( (1.-b)*x^a + b*x ) - return vals - - - vals = pin + (pout - pin)*(c*x + b*x^2 + a*x^3) - #STOP - return vals diff --git a/tools/pylib/boututils/read_geqdsk.py b/tools/pylib/boututils/read_geqdsk.py deleted file mode 100644 index f665f59ac4..0000000000 --- a/tools/pylib/boututils/read_geqdsk.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import print_function -from builtins import range -import numpy -from geqdsk import Geqdsk -from boututils.bunch import Bunch - -def read_geqdsk (file): - - data=Geqdsk() - - data.openFile(file) - - nxefit =data.get('nw') - nyefit =data.get('nh') - xdim =data.get('rdim') - zdim =data.get('zdim') - rcentr =data.get('rcentr') - rgrid1 =data.get('rleft') - zmid =data.get('zmid') - - rmagx =data.get('rmaxis') - zmagx =data.get('zmaxis') - simagx =data.get('simag') - sibdry =data.get('sibry') - bcentr =data.get('bcentr') - - cpasma =data.get('current') - #simagx =data.get('simag') - #xdum =data.get() - #rmagx =data.get('rmaxis') - #xdum =data.get() - - #zmagx =data.get('zmaxis') - #xdum =data.get() - #sibdry =data.get('sibry') - #xdum =data.get() - #xdum =data.get() - -# Read arrays - - fpol=data.get('fpol') - pres=data.get('pres') - - f=data.get('psirz') - qpsi=data.get('qpsi') - - nbdry=data.get('nbbbs') - nlim=data.get('limitr') - - if(nlim != 0) : - xlim=data.get('rlim') - ylim=data.get('zlim') - else: - xlim=[0] - ylim=[0] - - rbdry=data.get('rbbbs') - zbdry=data.get('zbbbs') - - - # Reconstruct the (R,Z) mesh - r=numpy.zeros((nxefit, nyefit), numpy.float64) - z=numpy.zeros((nxefit, nyefit), numpy.float64) - - - for i in range(0,nxefit): - for j in range(0,nyefit): - r[i,j] = rgrid1 + xdim*i/(nxefit-1) - z[i,j] = (zmid-0.5*zdim) + zdim*j/(nyefit-1) - - f=f.T - - print('nxefit = ', nxefit, ' nyefit= ', nyefit) - - return Bunch(nx=nxefit, ny=nyefit, # Number of horizontal and vertical points - r=r, z=z, # Location of the grid-points - xdim=xdim, zdim=zdim, # Size of the domain in meters - rcentr=rcentr, bcentr=bcentr, # Reference vacuum toroidal field (m, T) - rgrid1=rgrid1, # R of left side of domain - zmid=zmid, # Z at the middle of the domain - rmagx=rmagx, zmagx=zmagx, # Location of magnetic axis - simagx=simagx, # Poloidal flux at the axis (Weber / rad) - sibdry=sibdry, # Poloidal flux at plasma boundary (Weber / rad) - cpasma=cpasma, # - psi=f, # Poloidal flux in Weber/rad on grid points - fpol=fpol, # Poloidal current function on uniform flux grid - pres=pres, # Plasma pressure in nt/m^2 on uniform flux grid - qpsi=qpsi, # q values on uniform flux grid - nbdry=nbdry, rbdry=rbdry, zbdry=zbdry, # Plasma boundary - nlim=nlim, xlim=xlim, ylim=ylim) # Wall boundary diff --git a/tools/pylib/boututils/run_wrapper.py b/tools/pylib/boututils/run_wrapper.py deleted file mode 100644 index 8c0aa658fe..0000000000 --- a/tools/pylib/boututils/run_wrapper.py +++ /dev/null @@ -1,332 +0,0 @@ -"""Collection of functions which can be used to make a BOUT++ run""" - -from builtins import str -import os -import pathlib -import re -import subprocess -from subprocess import call, Popen, STDOUT, PIPE - - -if os.name == "nt": - # Default on Windows - DEFAULT_MPIRUN = "mpiexec.exe -n" -else: - DEFAULT_MPIRUN = "mpirun -np" - - -def getmpirun(default=DEFAULT_MPIRUN): - """Return environment variable named MPIRUN, if it exists else return - a default mpirun command - - Parameters - ---------- - default : str, optional - An mpirun command to return if ``MPIRUN`` is not set in the environment - - """ - MPIRUN = os.getenv("MPIRUN") - - if MPIRUN is None or MPIRUN == "": - MPIRUN = default - print("getmpirun: using the default " + str(default)) - - return MPIRUN - - -def shell(command, pipe=False): - """Run a shell command - - Parameters - ---------- - command : list of str - The command to run, split into (shell) words - pipe : bool, optional - Grab the output as text, else just run the command in the - background - - Returns - ------- - tuple : (int, str) - The return code, and either command output if pipe=True else None - """ - output = None - status = 0 - if pipe: - child = Popen(command, stderr=STDOUT, stdout=PIPE, shell=True) - # This returns a b'string' which is casted to string in - # python 2. However, as we want to use f.write() in our - # runtest, we cast this to utf-8 here - output = child.stdout.read().decode("utf-8", "ignore") - # Wait for the process to finish. Note that child.wait() - # would have deadlocked the system as stdout is PIPEd, we - # therefore use communicate, which in the end also waits for - # the process to finish - child.communicate() - status = child.returncode - else: - status = call(command, shell=True) - - return status, output - - -def determineNumberOfCPUs(): - """Number of virtual or physical CPUs on this system - - i.e. user/real as output by time(1) when called with an optimally - scaling userspace-only program - - Taken from a post on stackoverflow: - https://stackoverflow.com/questions/1006289/how-to-find-out-the-number-of-cpus-in-python - - Returns - ------- - int - The number of CPUs - """ - - # cpuset - # cpuset may restrict the number of *available* processors - try: - m = re.search(r'(?m)^Cpus_allowed:\s*(.*)$', - open('/proc/self/status').read()) - if m: - res = bin(int(m.group(1).replace(',', ''), 16)).count('1') - if res > 0: - return res - except IOError: - pass - - # Python 2.6+ - try: - import multiprocessing - return multiprocessing.cpu_count() - except (ImportError,NotImplementedError): - pass - - # POSIX - try: - res = int(os.sysconf('SC_NPROCESSORS_ONLN')) - - if res > 0: - return res - except (AttributeError,ValueError): - pass - - # Windows - try: - res = int(os.environ['NUMBER_OF_PROCESSORS']) - - if res > 0: - return res - except (KeyError, ValueError): - pass - - # jython - try: - from java.lang import Runtime - runtime = Runtime.getRuntime() - res = runtime.availableProcessors() - if res > 0: - return res - except ImportError: - pass - - # BSD - try: - sysctl = subprocess.Popen(['sysctl', '-n', 'hw.ncpu'], - stdout=subprocess.PIPE) - scStdout = sysctl.communicate()[0] - res = int(scStdout) - - if res > 0: - return res - except (OSError, ValueError): - pass - - # Linux - try: - res = open('/proc/cpuinfo').read().count('processor\t:') - - if res > 0: - return res - except IOError: - pass - - # Solaris - try: - pseudoDevices = os.listdir('/devices/pseudo/') - expr = re.compile('^cpuid@[0-9]+$') - - res = 0 - for pd in pseudoDevices: - if expr.match(pd) is not None: - res += 1 - - if res > 0: - return res - except OSError: - pass - - # Other UNIXes (heuristic) - try: - try: - dmesg = open('/var/run/dmesg.boot').read() - except IOError: - dmesgProcess = subprocess.Popen(['dmesg'], stdout=subprocess.PIPE) - dmesg = dmesgProcess.communicate()[0] - - res = 0 - while '\ncpu' + str(res) + ':' in dmesg: - res += 1 - - if res > 0: - return res - except OSError: - pass - - raise Exception('Can not determine number of CPUs on this system') - - -def launch(command, runcmd=None, nproc=None, mthread=None, - output=None, pipe=False, verbose=False): - """Launch parallel MPI jobs - - >>> status = launch(command, nproc, output=None) - - Parameters - ---------- - command : str - The command to run - runcmd : str, optional - Command for running parallel job; defaults to what getmpirun() returns" - nproc : int, optional - Number of processors (default: all available processors) - mthread : int, optional - Number of omp threads (default: the value of the - ``OMP_NUM_THREADS`` environment variable - output : str, optional - Name of file to save output to - pipe : bool, optional - If True, return the output of the command - verbose : bool, optional - Print the full command to be run before running it - - Returns - ------- - tuple : (int, str) - The return code, and either command output if pipe=True else None - - """ - - if runcmd is None: - runcmd = getmpirun() - - if nproc is None: - # Determine number of CPUs on this machine - nproc = determineNumberOfCPUs() - - cmd = runcmd + " " + str(nproc) + " " + command - - if output is not None: - cmd = cmd + " > "+output - - if mthread is not None: - if os.name == "nt": - # We're on windows, so we have to do it a little different - cmd = 'cmd /C "set OMP_NUM_THREADS={} && {}"'.format(mthread, cmd) - else: - cmd = "OMP_NUM_THREADS={} {}".format(mthread, cmd) - - if verbose == True: - print(cmd) - - return shell(cmd, pipe=pipe) - - -def shell_safe(command, *args, **kwargs): - """'Safe' version of shell. - - Raises a `RuntimeError` exception if the command is not - successful - - Parameters - ---------- - command : str - The command to run - *args, **kwargs - Optional arguments passed to `shell` - - """ - s, out = shell(command,*args,**kwargs) - if s: - raise RuntimeError("Run failed with %d.\nCommand was:\n%s\n\n" - "Output was\n\n%s"% - (s,command,out)) - return s, out - - -def launch_safe(command, *args, **kwargs): - """'Safe' version of launch. - - Raises an RuntimeError exception if the command is not successful - - Parameters - ---------- - command : str - The command to run - *args, **kwargs - Optional arguments passed to `shell` - - """ - s, out = launch(command,*args,**kwargs) - if s: - raise RuntimeError("Run failed with %d.\nCommand was:\n%s\n\n" - "Output was\n\n%s"% - (s,command,out)) - return s, out - - -def build_and_log(test): - """Run make and redirect the output to a log file. Prints input - - On Windows, does nothing because executable should have already - been built - - """ - - if os.name == "nt": - return - - print("Making {}".format(test)) - - if os.path.exists("makefile") or os.path.exists("Makefile"): - return shell_safe("make > make.log") - - ctest_filename = "CTestTestfile.cmake" - if not os.path.exists(ctest_filename): - raise RuntimeError("Could not build: no makefile and no CMake files detected") - - # We're using CMake, but we need to know the target name. If - # bout_add_integrated_test was used (which it should have been!), - # then the test name is the same as the target name - with open(ctest_filename, "r") as f: - contents = f.read() - match = re.search("add_test.(.*) ", contents) - if match is None: - raise RuntimeError("Using CMake, but could not determine test name") - test_name = match.group(1).split()[0] - - # Now we need to find the build directory. It'll be the first - # parent containing CMakeCache.txt - here = pathlib.Path(".").absolute() - for parent in here.parents: - if (parent / "CMakeCache.txt").exists(): - return shell_safe( - "cmake --build {} --target {} > make.log".format(parent, test_name) - ) - - # We've just looked up the entire directory structure and not - # found the build directory, this could happen if CMakeCache was - # deleted, in which case we can't build anyway - raise RuntimeError("Using CMake, but could not find build directory") diff --git a/tools/pylib/boututils/showdata.py b/tools/pylib/boututils/showdata.py deleted file mode 100644 index 4c0eb974f6..0000000000 --- a/tools/pylib/boututils/showdata.py +++ /dev/null @@ -1,702 +0,0 @@ -""" -Visualisation and animation routines - -Written by Luke Easy -le590@york.ac.uk -Last Updated 19/3/2015 -Additional functionality by George Breyiannis 26/12/2014 - -""" -from __future__ import print_function -from __future__ import division -from builtins import str, chr, range - -from matplotlib import pyplot as plt -from matplotlib import animation -from numpy import linspace, meshgrid, array, min, max, abs, floor, pi, isclose -from boutdata.collect import collect -from boututils.boutwarnings import alwayswarn - -#################################################################### -# Specify manually ffmpeg path -#plt.rcParams['animation.ffmpeg_path'] = '/usr/bin/ffmpeg' - -FFwriter = animation.FFMpegWriter() -#################################################################### - - -################### -#https://stackoverflow.com/questions/16732379/stop-start-pause-in-python-matplotlib-animation -# -j=-2 -pause = False -################### - - -def showdata(vars, titles=[], legendlabels=[], surf=[], polar=[], tslice=0, t_array=None, - movie=0, fps=28, dpi=200, intv=1, Ncolors=25, x=[], y=[], - global_colors=False, symmetric_colors=False, hold_aspect=False, - cmap=None, clear_between_frames=None, return_animation=False, window_title=""): - """A Function to animate time dependent data from BOUT++ - - To animate multiple variables on different axes: - - >>> showdata([var1, var2, var3]) - - To animate more than one line on a single axes: - - >>> showdata([[var1, var2, var3]]) - - The default graph types are: - 2D (time + 1 spatial dimension) arrays = animated line plot - 3D (time + 2 spatial dimensions) arrays = animated contour plot. - - To use surface or polar plots: - - >>> showdata(var, surf=1) - >>> showdata(var, polar=1) - - Can plot different graph types on different axes. Default graph types will - be used depending on the dimensions of the input arrays. To specify - polar/surface plots on different axes: - - >>> showdata([var1, var2], surf=[1, 0], polar=[0, 1]) - - Movies require FFmpeg (for .mp4) and/or ImageMagick (for .gif) to be - installed. The 'movie' option can be set to 1 (which will produce an mp4 - called 'animation.mp4'), to a name with no extension (which will produce an - mp4 called '.mp4') - - The `tslice` variable is used to control the time value that is printed on - each frame of the animation. If the input data matches the time values - found within BOUT++'s dmp data files, then these time values will be used. - Otherwise, an integer counter is used. - - The `cmap` variable (if specified) will set the colormap used in the plot - cmap must be a matplotlib colormap instance, or the name of a registered - matplotlib colormap - - During animation click once to stop in the current frame. Click again to - continue. - - Parameters - ---------- - vars : array_like or list of array_like - Variable or list of variables to plot - titles : str or list of str, optional - Title or list of titles for each axis - legendlabels : str or list of str, optional - Legend or list of legends for each variable - surf : list of int - Which axes to plot as a surface plot - polar : list of int - Which axes to plot as a polar plot - tslice : list of int - Use these time values from a dump file (see above) - t_array : array - Pass in t_array using this argument to use the simulation time in plot - titles. Otherwise, just use the t-index. - movie : int - If 1, save the animation to file - fps : int - Number of frames per second to use when saving animation - dpi : int - Dots per inch to use when saving animation - intv : int - ??? - Ncolors : int - Number of levels in contour plots - x, y : array_like, list of array_like - X, Y coordinates - global_colors : bool - If "vars" is a list the colorlevels are determined from the - maximum of the maxima and and the minimum of the minima in all - fields in vars - symmetric_colors : bool - Colour levels are symmetric - hold_aspect : bool - Use equal aspect ratio in plots - cmap : colormap - A matplotlib colormap instance to use - clear_between_frames : bool, optional - - Default (None) - all plots except line plots will clear between frames - - True - all plots will clear between frames - - False - no plots will clear between frames - return_animation : bool - Return the matplotlib animation instance - window_title : str - Give a title for the animation window - - TODO - ---- - - Replace empty lists in signature with None - - Use bools in sensible places - - Put massive list of arguments in kwargs - - Speed up animations ???? - - Look at theta in polar plots - periodic?!? - - Log axes, colorbars - - Figureplot - - """ - plt.ioff() - - # Check to see whether vars is a list or not. - if isinstance(vars, list): - Nvar = len(vars) - else: - vars = [vars] - Nvar = len(vars) - - if Nvar < 1: - raise ValueError("No data supplied") - - # Check to see whether each variable is a list - used for line plots only - Nlines = [] - for i in range(0, Nvar): - if isinstance(vars[i], list): - Nlines.append(len(vars[i])) - else: - Nlines.append(1) - vars[i] = [vars[i]] - - # Sort out titles - if len(titles) == 0: - for i in range(0,Nvar): - titles.append(('Var' + str(i+1))) - elif len(titles) != Nvar: - raise ValueError('The length of the titles input list must match the length of the vars list.') - - # Sort out legend labels - if len(legendlabels) == 0: - for i in range(0,Nvar): - legendlabels.append([]) - for j in range(0,Nlines[i]): - legendlabels[i].append(chr(97+j)) - elif (isinstance(legendlabels[0], list) != 1): - if Nvar != 1: - check = 0 - for i in range(0,Nvar): - if len(legendlabels) != Nlines[i]: - check = check+1 - if check == 0: - alwayswarn("The legendlabels list does not contain a sublist for each variable, but its length matches the number of lines on each plot. Will apply labels to each plot") - legendlabelsdummy = [] - for i in range(0, Nvar): - legendlabelsdummy.append([]) - for j in range(0,Nlines[i]): - legendlabelsdummy[i].append(legendlabels[j]) - legendlabels = legendlabelsdummy - else: - alwayswarn("The legendlabels list does not contain a sublist for each variable, and it's length does not match the number of lines on each plot. Will default apply labels to each plot") - legendlabels = [] - for i in range(0,Nvar): - legendlabels.append([]) - for j in range(0,Nlines[i]): - legendlabels[i].append(chr(97+j)) - else: - if (Nlines[0] == len(legendlabels)): - legendlabels = [legendlabels] - elif len(legendlabels) != Nvar: - alwayswarn("The length of the legendlabels list does not match the length of the vars list, will continue with default values") - legendlabels = [] - for i in range(0,Nvar): - legendlabels.append([]) - for j in range(0,Nlines[i]): - legendlabels[i].append(chr(97+j)) - else: - for i in range(0,Nvar): - if isinstance(legendlabels[i], list): - if len(legendlabels[i]) != Nlines[i]: - alwayswarn('The length of the legendlabel (sub)list for each plot does not match the number of datasets for each plot. Will continue with default values') - legendlabels[i] = [] - for j in range(0,Nlines[i]): - legendlabels[i].append(chr(97+j)) - else: - legendlabels[i] = [legendlabels[i]] - if len(legendlabels[i]) != Nlines[i]: - alwayswarn('The length of the legendlabel (sub)list for each plot does not match the number of datasets for each plot. Will continue with default values') - legendlabels[i] = [] - for j in range(0,Nlines[i]): - legendlabels[i].append(chr(97+j)) - - - # Sort out surf list - if isinstance(surf, list): - if (len(surf) == Nvar): - for i in range(0, Nvar): - if surf[i] >= 1: - surf[i] = 1 - else: - surf[i] = 0 - elif (len(surf) == 1): - if surf[0] >= 1: - surf[0] = 1 - else: - surf[0] = 0 - if (Nvar > 1): - for i in range(1,Nvar): - surf.append(surf[0]) - elif (len(surf) == 0): - for i in range(0,Nvar): - surf.append(0) - else: - alwayswarn('Length of surf list does not match number of variables. Will default to no polar plots') - for i in range(0,Nvar): - surf.append(0) - - else: - surf = [surf] - if surf[0] >= 1: - surf[0] = 1 - else: - surf[0] = 0 - if (Nvar > 1): - for i in range(1,Nvar): - surf.append(surf[0]) - - # Sort out polar list - if isinstance(polar, list): - if (len(polar) == Nvar): - for i in range(0, Nvar): - if polar[i] >= 1: - polar[i] = 1 - else: - polar[i] = 0 - elif (len(polar) == 1): - if polar[0] >= 1: - polar[0] = 1 - else: - polar[0] = 0 - if (Nvar > 1): - for i in range(1,Nvar): - polar.append(polar[0]) - elif (len(polar) == 0): - for i in range(0,Nvar): - polar.append(0) - else: - alwayswarn('Length of polar list does not match number of variables. Will default to no polar plots') - for i in range(0,Nvar): - polar.append(0) - else: - polar = [polar] - if polar[0] >= 1: - polar[0] = 1 - else: - polar[0] = 0 - if (Nvar > 1): - for i in range(1,Nvar): - polar.append(polar[0]) - - # Determine shapes of arrays - dims = [] - Ndims = [] - lineplot = [] - contour = [] - for i in range(0,Nvar): - dims.append([]) - Ndims.append([]) - for j in range(0, Nlines[i]): - dims[i].append(array((vars[i][j].shape))) - Ndims[i].append(dims[i][j].shape[0]) - # Perform check to make sure that data is either 2D or 3D - if (Ndims[i][j] < 2): - raise ValueError('data must be either 2 or 3 dimensional. Exiting') - - if (Ndims[i][j] > 3): - raise ValueError('data must be either 2 or 3 dimensional. Exiting') - - if ((Ndims[i][j] == 2) & (polar[i] != 0)): - alwayswarn('Data must be 3 dimensional (time, r, theta) for polar plots. Will plot lineplot instead') - - if ((Ndims[i][j] == 2) & (surf[i] != 0)): - alwayswarn('Data must be 3 dimensional (time, x, y) for surface plots. Will plot lineplot instead') - - if ((Ndims[i][j] == 3) & (Nlines[i] != 1)): - raise ValueError('cannot have multiple sets of 3D (time + 2 spatial dimensions) on each subplot') - - - if ((Ndims[i][j] != Ndims[i][0])): - raise ValueError('Error, Number of dimensions must be the same for all variables on each plot.') - - if (Ndims[i][0] == 2): # Set polar and surf list entries to 0 - polar[i] = 0 - surf[i] = 0 - lineplot.append(1) - contour.append(0) - else: - if ((polar[i] == 1) & (surf[i] == 1)): - alwayswarn('Cannot do polar and surface plots at the same time. Default to contour plot') - contour.append(1) - lineplot.append(0) - polar[i] = 0 - surf[i] = 0 - elif (polar[i] == 1) | (surf[i] == 1): - contour.append(0) - lineplot.append(0) - else: - contour.append(1) - lineplot.append(0) - - # Obtain size of data arrays - Nt = [] - Nx = [] - Ny = [] - for i in range(0, Nvar): - Nt.append([]) - Nx.append([]) - Ny.append([]) - for j in range(0, Nlines[i]): - Nt[i].append(vars[i][j].shape[0]) - Nx[i].append(vars[i][j].shape[1]) - if (Nt[i][j] != Nt[0][0]): - raise ValueError('time dimensions must be the same for all variables.') - - #if (Nx[i][j] != Nx[i][0]): - # raise ValueError('Dimensions must be the same for all variables on each plot.') - - if (Ndims[i][j] == 3): - Ny[i].append(vars[i][j].shape[2]) - #if (Ny[i][j] != Ny[i][0]): - # raise ValueError('Dimensions must be the same for all variables.') - - # Obtain number of frames - Nframes = int(Nt[0][0]/intv) - - # Generate grids for plotting - # Try to use provided grids where possible - # If x and/or y are not lists, apply to all variables - if not isinstance(x, (list,tuple)): - x = [x]*Nvar # Make list of x with length Nvar - if not isinstance(y, (list,tuple)): - y = [y]*Nvar # Make list of x with length Nvar - xnew = [] - ynew = [] - for i in range(0,Nvar): - xnew.append([]) - try: - xnew[i].append(x[i]) - if not (x[i].shape==(Nx[i][0],) or x[i].shape==(Nx[i][0],Ny[i][0]) or x[i].shape==(Nt[i][0],Nx[i][0],Ny[i],[0])): - raise ValueError("For variable number "+str(i)+", "+titles[i]+", the shape of x is not compatible with the shape of the variable. Shape of x should be (Nx), (Nx,Ny) or (Nt,Nx,Ny).") - except: - for j in range(0, Nlines[i]): - xnew[i].append(linspace(0,Nx[i][j]-1, Nx[i][j])) - - #x.append(linspace(0,Nx[i][0]-1, Nx[i][0])) - - if (Ndims[i][0] == 3): - try: - ynew.append(y[i]) - if not (y[i].shape==(Ny[i][0],) or y[i].shape==(Nx[i][0],Ny[i][0]) or y[i].shape==(Nt[i][0],Nx[i][0],Ny[i],[0])): - raise ValueError("For variable number "+str(i)+", "+titles[i]+", the shape of y is not compatible with the shape of the variable. Shape of y should be (Ny), (Nx,Ny) or (Nt,Nx,Ny).") - except: - ynew.append(linspace(0, Ny[i][0]-1, Ny[i][0])) - else: - ynew.append(0) - x = xnew - y = ynew - # Determine range of data. Used to ensure constant colour map and - # to set y scale of line plot. - fmax = [] - fmin = [] - xmax = [] - dummymax = [] - dummymin = [] - clevels = [] - - for i in range(0,Nvar): - - dummymax.append([]) - dummymin.append([]) - for j in range(0,Nlines[i]): - dummymax[i].append(max(vars[i][j])) - dummymin[i].append(min(vars[i][j])) - - fmax.append(max(dummymax[i])) - fmin.append(min(dummymin[i])) - - if(symmetric_colors): - absmax =max(abs(array(fmax[i], fmin[i]))) - fmax[i] = absmax - fmin[i] = -absmax - - for j in range(0,Nlines[i]): - dummymax[i][j] = max(x[i][j]) - xmax.append(max(dummymax[i])) - - - if not (global_colors): - if isclose(fmin[i], fmax[i]): - # add/subtract very small constant in case fmin=fmax=0 - thiscontourmin = fmin[i] - 3.e-15*abs(fmin[i]) - 1.e-36 - thiscontourmax = fmax[i] + 3.e-15*abs(fmax[i]) + 1.e-36 - alwayswarn("Contour levels too close, adding padding to colorbar range") - clevels.append(linspace(thiscontourmin, thiscontourmax, Ncolors)) - else: - clevels.append(linspace(fmin[i], fmax[i], Ncolors)) - - if(global_colors): - fmaxglobal = max(fmax) - fminglobal = min(fmin) - if isclose(fminglobal, fmaxglobal): - fminglobal = fminglobal - 3.e-15*abs(fminglobal) - 1.e-36 - fmaxglobal = fmaxglobal + 3.e-15*abs(fmaxglobal) + 1.e-36 - for i in range(0,Nvar): - clevels.append(linspace(fminglobal, fmaxglobal, Ncolors)) - - # Create figures for animation plotting - if (Nvar < 2): - row = 1 - col = 1 - h = 6.0 - w = 8.0 - elif (Nvar <3): - row = 1 - col = 2 - h = 6.0 - w = 12.0 - elif (Nvar < 5): - row = 2 - col = 2 - h = 8.0 - w = 12.0 - - elif (Nvar < 7): - row = 2 - col = 3 - h = 8.0 - w = 14.0 - - elif (Nvar < 10) : - row = 3 - col = 3 - h = 12.0 - w = 14.0 - else: - raise ValueError('too many variables...') - - - fig = plt.figure(window_title, figsize=(w,h)) - title = fig.suptitle(r' ', fontsize=14 ) - - # Initiate all list variables required for plotting here - ax = [] - lines = [] - plots = [] - cbars = [] - xstride = [] - ystride = [] - r = [] - theta = [] - - - # Initiate figure frame - for i in range(0,Nvar): - lines.append([]) - if (lineplot[i] == 1): - ax.append(fig.add_subplot(row,col,i+1)) - ax[i].set_xlim((0,xmax[i])) - ax[i].set_ylim((fmin[i], fmax[i])) - for j in range(0,Nlines[i]): - lines[i].append(ax[i].plot([],[],lw=2, label = legendlabels[i][j])[0]) - #Need the [0] to 'unpack' the line object from tuple. Alternatively: - #lines[i], = lines[i] - ax[i].set_xlabel(r'x') - ax[i].set_ylabel(titles[i]) - if (Nlines[i] != 1): - legendneeded = 1 - for k in range(0,i): - if (Nlines[i] == Nlines[k]): - legendneeded = 0 - if (legendneeded == 1): - plt.axes(ax[i]) - plt.legend(loc = 0) - # Pad out unused list variables with zeros - plots.append(0) - cbars.append(0) - xstride.append(0) - ystride.append(0) - r.append(0) - theta.append(0) - - elif (contour[i] == 1): - ax.append(fig.add_subplot(row,col,i+1)) - #ax[i].set_xlim((0,Nx[i][0]-1)) - #ax[i].set_ylim((0,Ny[i][0]-1)) - ax[i].set_xlim(min(x[i]),max(x[i])) - ax[i].set_ylim(min(y[i]),max(y[i])) - ax[i].set_xlabel(r'x') - ax[i].set_ylabel(r'y') - ax[i].set_title(titles[i]) - if hold_aspect: - ax[i].set_aspect('equal') - thisx = x[i][0] - if len(thisx.shape) == 3: - thisx = thisx[0] - thisy = y[i] - if len(thisy.shape) == 3: - thisy = thisy[0] - plots.append(ax[i].contourf(thisx.T,thisy.T,vars[i][0][0,:,:].T, Ncolors, cmap=cmap, lw=0, levels=clevels[i] )) - plt.axes(ax[i]) - cbars.append(fig.colorbar(plots[i], format='%1.1e')) - # Pad out unused list variables with zeros - lines[i].append(0) - xstride.append(0) - ystride.append(0) - r.append(0) - theta.append(0) - - elif (surf[i] == 1): - if (len(x[i][0].shape)==1 and len(y[i].shape)==1): - # plot_wireframe() requires 2d arrays for x and y coordinates - x[i][0],y[i] = meshgrid(x[i][0],y[i]) - thisx = x[i][0] - if len(thisx.shape) == 3: - thisx = thisx[0] - thisy = y[i] - if len(thisy.shape) == 3: - thisy = thisy[0] - if (Nx[i][0]<= 20): - xstride.append(1) - else: - xstride.append(int(floor(Nx[i][0]/20))) - if (Ny[i][0]<=20): - ystride.append(1) - else: - ystride.append(int(floor(Ny[i][0]/20))) - ax.append(fig.add_subplot(row,col,i+1, projection='3d')) - plots.append(ax[i].plot_wireframe(thisx, thisy, vars[i][0][0,:,:].T, rstride=ystride[i], cstride=xstride[i])) - title = fig.suptitle(r'', fontsize=14 ) - ax[i].set_xlabel(r'x') - ax[i].set_ylabel(r'y') - ax[i].set_zlabel(titles[i]) - # Pad out unused list variables with zeros - lines[i].append(0) - cbars.append(0) - r.append(0) - theta.append(0) - - elif (polar[i] == 1): - r.append(linspace(1,Nx[i][0], Nx[i][0])) - theta.append(linspace(0,2*pi, Ny[i][0])) - r[i],theta[i] = meshgrid(r[i], theta[i]) - ax.append(fig.add_subplot(row,col,i+1, projection='polar')) - plots.append(ax[i].contourf(theta[i], r[i], vars[i][0][0,:,:].T, cmap=cmap, levels=clevels[i])) - plt.axes(ax[i]) - cbars.append(fig.colorbar(plots[i], format='%1.1e')) - ax[i].set_rmax(Nx[i][0]-1) - ax[i].set_title(titles[i]) - # Pad out unused list variables with zeros - lines[i].append(0) - xstride.append(0) - ystride.append(0) - - - - def onClick(event): - global pause - pause ^= True - - - def control(): - global j, pause - if j == Nframes-1 : j = -1 - if not pause: - j=j+1 - - return j - - - # Animation function - def animate(i): - j=control() - - index = j*intv - - for j in range(0,Nvar): - #Default to clearing axis between frames on all plots except line plots - if (clear_between_frames is None and lineplot[j] != 1 ) or clear_between_frames is True: - ax[j].cla() #Clear axis between frames so that masked arrays can be plotted - if (lineplot[j] == 1): - for k in range(0,Nlines[j]): - lines[j][k].set_data(x[j][k], vars[j][k][index,:]) - elif (contour[j] == 1): - thisx = x[j][0] - if len(thisx.shape) == 3: - thisx = thisx[index] - thisy = y[j] - if len(thisy.shape) == 3: - thisy = thisy[index] - plots[j] = ax[j].contourf(x[j][0].T,y[j].T,vars[j][0][index,:,:].T, Ncolors, cmap=cmap, lw=0, levels=clevels[j]) - ax[j].set_xlabel(r'x') - ax[j].set_ylabel(r'y') - ax[j].set_title(titles[j]) - elif (surf[j] == 1): - thisx = x[j][0] - if len(thisx.shape) == 3: - thisx = thisx[index] - thisy = y[j][0] - if len(thisy.shape) == 3: - thisy = thisy[index] - ax[j] = fig.add_subplot(row,col,j+1, projection='3d') - plots[j] = ax[j].plot_wireframe(thisx, thisy, vars[j][0][index,:,:].T, rstride=ystride[j], cstride=xstride[j]) - ax[j].set_zlim(fmin[j],fmax[j]) - ax[j].set_xlabel(r'x') - ax[j].set_ylabel(r'y') - ax[j].set_title(titles[j]) - elif (polar[j] == 1): - plots[j] = ax[j].contourf(theta[j], r[j], vars[j][0][index,:,:].T,cmap=cmap, levels=clevels[j]) - ax[j].set_rmax(Nx[j][0]-1) - ax[j].set_title(titles[j]) - - if t_array is not None: - title.set_text('t = %1.2e' % t_array[index]) - else: - title.set_text('t = %i' % index) - return plots - - def init(): - global j, pause - j=-2 - pause = False - return animate(0) - - - - - - - # Call Animation function - - fig.canvas.mpl_connect('button_press_event', onClick) - anim = animation.FuncAnimation(fig, animate, init_func=init, frames=Nframes) - - #If movie is not passed as a string assign the default filename - if (movie==1): - movie='animation.mp4' - - # Save movie with given or default name - if ((isinstance(movie,str)==1)): - movietype = movie.split('.')[-1] - if movietype == 'mp4': - try: - anim.save(movie,writer = FFwriter, fps=fps, dpi=dpi, extra_args=['-vcodec', 'libx264']) - except Exception: - #Try specifying writer by string if ffmpeg not found - try: - anim.save(movie,writer = 'ffmpeg', fps=fps, dpi=dpi, extra_args=['-vcodec', 'libx264']) - except Exception: - print('Save failed: Check ffmpeg path') - raise - elif movietype == 'gif': - anim.save(movie,writer = 'imagemagick', fps=fps, dpi=dpi) - else: - raise ValueError("Unrecognized file type for movie. Supported types are .mp4 and .gif") - - # Show animation if not saved or returned, otherwise close the plot - if (movie==0 and return_animation == 0): - plt.show() - else: - plt.close() - # Return animation object - if(return_animation == 1): - return(anim) diff --git a/tools/pylib/boututils/spectrogram.py b/tools/pylib/boututils/spectrogram.py deleted file mode 100644 index d1c2a3617b..0000000000 --- a/tools/pylib/boututils/spectrogram.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Creates spectrograms using the Gabor transform to maintain time and -frequency resolution - -written by: Jarrod Leddy -updated: 23/06/2016 - -""" -from __future__ import print_function -from __future__ import division -from builtins import range - -from numpy import arange, zeros, exp, power, transpose, sin, cos, linspace, min, max -from scipy import fftpack, pi - - -def spectrogram(data, dx, sigma, clip=1.0, optimise_clipping=True, nskip=1.0): - """Creates spectrograms using the Gabor transform to maintain time - and frequency resolution - - .. note:: Very early and very late times will have some issues due - to the method - truncate them after taking the spectrogram - if they are below your required standards - - .. note:: If you are seeing issues at the top or bottom of the - frequency range, you need a longer time series - - written by: Jarrod Leddy - updated: 23/06/2016 - - Parameters - ---------- - data : array_like - The time series you want spectrogrammed - dt : float - Time resolution - sigma : float - Used in the Gabor transform, will balance time and frequency - resolution suggested value is 1.0, but may need to be adjusted - manually until result is as desired: - - - If bands are too tall raise sigma - - If bands are too wide, lower sigma - clip : float, optional - Makes the spectrogram run faster, but decreases frequency - resolution. clip is by what factor the time spectrum should be - clipped by --> N_new = N / clip - optimise_clip : bool - If true (default) will change the data length to be 2^N - (rounded down from your inputed clip value) to make FFT's fast - nskip : float - Scales final time axis, skipping points over which to centre - the gaussian window for the FFTs - - Returns - ------- - tuple : (array_like, array_like, array_like) - A tuple containing the spectrogram, frequency and time - - """ - n = data.size - nnew = int(n/nskip) - xx = arange(n)*dx - xxnew = arange(nnew)*dx*nskip - sigma = sigma * dx - - n_clipped = int(n/clip) - - # check to see if n_clipped is near a 2^n factor for speed - if(optimise_clipping): - nn = n_clipped - two_count = 1 - while(1): - nn = nn/2.0 - if(nn <= 2.0): - n_clipped = 2**two_count - print('clipping window length from ',n,' to ',n_clipped,' points') - break - else: - two_count += 1 - else: - print('using full window length: ',n_clipped,' points') - - halfclip = int(n_clipped/2) - spectra = zeros((nnew,halfclip)) - - omega = fftpack.fftfreq(n_clipped, dx) - omega = omega[0:halfclip] - - for i in range(nnew): - beg = i*nskip-halfclip - end = i*nskip+halfclip-1 - - if beg < 0: - end = end-beg - beg = 0 - elif end >= n: - end = n-1 - beg = end - n_clipped + 1 - - gaussian = 1.0 / (sigma * 2.0 * pi) * exp(-0.5 * power(( xx[beg:end] - xx[i*nskip] ),2.0) / (2.0 * sigma) ) - fftt = abs(fftpack.fft(data[beg:end] * gaussian)) - fftt = fftt[:halfclip] - spectra[i,:] = fftt - - return (transpose(spectra), omega, xxnew) - - -def test_spectrogram(n, d, s): - """Function used to test the performance of spectrogram with various - values of sigma - - Parameters - ---------- - n : int - Number of points - d : float - Grid spacing - s : float - Initial sigma - - """ - - import matplotlib.pyplot as plt - - nskip = 10 - xx = arange(n)/d - test_data = sin(2.0*pi*512.0*xx * ( 1.0 + 0.005*cos(xx*50.0))) + 0.5*exp(xx)*cos(2.0*pi*100.0*power(xx,2)) - test_sigma = s - dx = 1.0/d - - s1 = test_sigma*0.1 - s2 = test_sigma - s3 = test_sigma*10.0 - - (spec2,omega2,xx) = spectrogram(test_data, dx, s2, clip=5.0, nskip=nskip) - (spec3,omega3,xx) = spectrogram(test_data, dx, s3, clip=5.0, nskip=nskip) - (spec1,omega1,xx) = spectrogram(test_data, dx, s1, clip=5.0, nskip=nskip) - - levels = linspace(min(spec1),max(spec1),100) - plt.subplot(311) - plt.contourf(xx,omega1,spec1,levels=levels) - plt.ylabel("frequency") - plt.xlabel(r"$t$") - plt.title(r"Spectrogram of $sin(t + cos(t) )$ with $\sigma=$%3.1f"%s1) - - levels = linspace(min(spec2),max(spec2),100) - plt.subplot(312) - plt.contourf(xx,omega2,spec2,levels=levels) - plt.ylabel("frequency") - plt.xlabel(r"$t$") - plt.title(r"Spectrogram of $sin(t + cos(t) )$ with $\sigma=$%3.1f"%s2) - - levels = linspace(min(spec3),max(spec3),100) - plt.subplot(313) - plt.contourf(xx,omega3,spec3,levels=levels) - plt.ylabel("frequency") - plt.xlabel(r"$t$") - plt.title(r"Spectrogram of $sin(t + cos(t) )$ with $\sigma=$%3.1f"%s3) - plt.tight_layout() - plt.show() - -if __name__ == "__main__": - test_spectrogram(2048, 2048.0, 0.01) # array size, divisions per unit, sigma of gaussian diff --git a/tools/pylib/boututils/surface_average.py b/tools/pylib/boututils/surface_average.py deleted file mode 100644 index 340d252a69..0000000000 --- a/tools/pylib/boututils/surface_average.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Average over a surface - -""" - -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division -from builtins import range -from past.utils import old_div -import numpy as np -from boututils.calculus import deriv -from boututils.int_func import int_func -from .idl_tabulate import idl_tabulate - - -def surface_average(var, grid, area=None): - """Average a variable over a surface - - Parameters - ---------- - var : array_like - 3-D or 4D variable to integrate (either [x,y,z] or [t,x,y,z]) - grid : dict - A dictionary of various grid quantities - area : bool - Average by flux-surface area = (B/Bp)*dl * R*dz - - Returns - ------- - float - Surface average of variable - - """ - - s = np.ndim(var) - - if s == 4 : - nx = np.shape(var)[1] - ny = np.shape(var)[2] - nt = np.shape(var)[0] - - result = np.zeros((nx,nt)) - for t in range (nt): - result[:,t] = surface_average(var[t,:,:,:], grid, area=area) - - return result - elif s != 3 : - raise RuntimeError("ERROR: surface_average var must be 3D or 4D") - - # 3D [x,y,z] - nx = np.shape(var)[0] - ny = np.shape(var)[1] - - - # Calculate poloidal angle from grid - theta = np.zeros((nx,ny)) - - #status = gen_surface(mesh=grid) ; Start generator - xi = -1 - yi = np.arange(0,ny,dtype=int) - last = 0 - while True: - #yi = gen_surface(last=last, xi=xi, period=periodic) - xi = xi + 1 - if xi == nx-1 : - last = 1 - - dtheta = 2.*np.pi / np.float(ny) - r = grid['Rxy'][xi,yi] - z = grid['Zxy'][xi,yi] - n = np.size(r) - - dl = old_div(np.sqrt( deriv(r)**2 + deriv(z)**2 ), dtheta) - if area: - dA = (old_div(grid['Bxy'][xi,yi],grid['Bpxy'][xi,yi]))*r*dl - A = int_func(np.arange(n),dA) - theta[xi,yi] = 2.*np.pi*A/A[n-1] - else: - nu = dl * (grid['Btxy'][xi,yi]) / ((grid['Bpxy'][xi,yi]) * r ) - theta[xi,yi] = int_func(np.arange(n)*dtheta,nu) - theta[xi,yi] = 2.*np.pi*theta[xi,yi] / theta[xi,yi[n-1]] - - if last==1 : break - - vy = np.zeros(ny) - result = np.zeros(nx) - for x in range(nx) : - for y in range(ny) : - vy[y] = np.mean(var[x,y,:]) - - result[x] = old_div(idl_tabulate(theta[x,:], vy), (2.*np.pi)) - - return result diff --git a/tools/pylib/boututils/volume_integral.py b/tools/pylib/boututils/volume_integral.py deleted file mode 100644 index 7e5d4da5cd..0000000000 --- a/tools/pylib/boututils/volume_integral.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Integrate over a volume - -""" - -from __future__ import print_function -from __future__ import division -from builtins import range -from past.utils import old_div -import numpy as np -from boututils.calculus import deriv - -def volume_integral(var, grid, xr=False): - """Integrate a variable over a volume - - Parameters - ---------- - var : array_like - Variable to integrate - grid : dict - A dictionary of various grid quantities - xr : (int, int), optional - Range of x indices (default: all of x) - - Returns - ------- - float - Volumne integral of variable - - """ - - s = np.ndim(var) - - if s == 4 : - # 4D [t,x,y,z] - integrate for each t - nx = np.shape(var)[1] - ny = np.shape(var)[2] - nt = np.shape(var)[0] - - result = np.zeros(nt) - for t in range(nt) : - result[t] = volume_integral(var[t,:,:,:],g,xr=xr) - return result - - elif s == 3 : - # 3D [x,y,z] - average in Z - nx = np.shape(var)[0] - ny = np.shape(var)[1] - # nz = np.shape(var)[2] - - zi = np.zeros((nx, ny)) - for x in range(nx): - for y in range(ny): - zi[x,y] = np.mean(var[x,y,:]) - - return volume_integral(zi, g, xr=xr) - - - elif s != 2 : - print("ERROR: volume_integral var must be 2, 3 or 4D") - - - # 2D [x,y] - nx = np.shape(var)[0] - ny = np.shape(var)[1] - - if xr == False : xr=[0,nx-1] - - result = 0.0 - - #status = gen_surface(mesh=grid) ; Start generator - xi = -1 - yi = np.arange(0,ny,dtype=int) - last = 0 - # iy = np.zeros(nx) - while True: - #yi = gen_surface(last=last, xi=xi, period=periodic) - xi = xi + 1 - if xi == nx-1 : last = 1 - - if (xi >= np.min(xr)) & (xi <= np.max(xr)) : - dtheta = 2.*np.pi / np.float(ny) - r = grid['Rxy'][xi,yi] - z = grid['Zxy'][xi,yi] - n = np.size(r) - dl = old_div(np.sqrt( deriv(r)**2 + deriv(z)**2 ), dtheta) - - # Area of flux-surface - dA = (grid['Bxy'][xi,yi]/grid['Bpxy'][xi,yi]*dl) * (r*2.*np.pi) - # Volume - if xi == nx-1 : - dpsi = (grid['psixy'][xi,yi] - grid['psixy'][xi-1,yi]) - else: - dpsi = (grid['psixy'][xi+1,yi] - grid['psixy'][xi,yi]) - - dV = dA * dpsi / (r*(grid['Bpxy'][xi,yi])) # May need factor of 2pi - dV = np.abs(dV) - - result = result + np.sum(var[xi,yi] * dV) - - if last==1 : break - - return result diff --git a/tools/pylib/boututils/watch.py b/tools/pylib/boututils/watch.py deleted file mode 100644 index e7d038c4e1..0000000000 --- a/tools/pylib/boututils/watch.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Routines for watching files for changes - -""" -from __future__ import print_function -from builtins import zip - -import time -import os - - -def watch(files, timeout=None, poll=2): - """Watch a given file or collection of files until one changes. Uses - polling. - - Parameters - ---------- - files : str or list of str - Name of one or more files to watch - timeout : int, optional - Timeout in seconds (default is no timeout) - poll : int, optional - Polling interval in seconds (default: 2) - - Returns - ------- - str - The name of the first changed file, - or None if timed out before any changes - - Examples - -------- - - To watch one file, timing out after 60 seconds: - - >>> watch('file1', timeout=60) - - To watch 2 files, never timing out: - - >>> watch(['file1', 'file2']) - - Author: Ben Dudson - - """ - - # Get modification time of file(s) - try: - if hasattr(files, '__iter__'): - # Iterable - lastmod = [ os.stat(f).st_mtime for f in files ] - iterable = True - else: - # Not iterable -> just one file - lastmod = os.stat(files).st_mtime - iterable = False - except: - print("Can't test modified time. Wrong file name?") - raise - - start_time = time.time() - running = True - while running: - sleepfor = poll - if timeout: - # Check if timeout will be reached before next poll - if time.time() - start_time + sleepfor > timeout: - # Adjust time so that finish at timeout - sleepfor = timeout - (time.time() - start_time) - running = False # Stop after next test - - time.sleep(sleepfor) - - if iterable: - for last_t, f in zip(lastmod, files): - # Get the new modification time - t = os.stat(f).st_mtime - if t > last_t + 1.0: # +1 to reduce risk of false alarms - # File has been modified - return f - else: - t = os.stat(files).st_mtime - if t > lastmod + 1.0: - return files - return None From e5c6af7d9ae069f889e4448d60fda23912264b04 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Mon, 4 Jan 2021 14:12:41 +0100 Subject: [PATCH 13/41] Update boututils and boutdata versions to latest releases --- externalpackages/boutdata | 2 +- externalpackages/boututils | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/externalpackages/boutdata b/externalpackages/boutdata index 46fe888a08..f0a84525ce 160000 --- a/externalpackages/boutdata +++ b/externalpackages/boutdata @@ -1 +1 @@ -Subproject commit 46fe888a080406c8364a53974f5b629a55241dce +Subproject commit f0a84525cedc372fd063fcc95f4ddc51a8f42cca diff --git a/externalpackages/boututils b/externalpackages/boututils index 9b17a3cfe7..f0ada15891 160000 --- a/externalpackages/boututils +++ b/externalpackages/boututils @@ -1 +1 @@ -Subproject commit 9b17a3cfe7d53839873418b41902e186ac5fef45 +Subproject commit f0ada158913b1c77948390d4d15adbfbf7fdb09c From f24404377d8fb9b9363ddc04a240f030a1305a67 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Mon, 4 Jan 2021 15:33:47 +0000 Subject: [PATCH 14/41] Ignore some variables when comparing squashed outputs in test-squash The CVODE/ARKODE solver internal diagnostics may depend on processor count, etc., so should not be compared across runs with different processor counts --- tests/integrated/test-squash/runtest | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/integrated/test-squash/runtest b/tests/integrated/test-squash/runtest index 35a2f674ad..8b4dcf56c8 100755 --- a/tests/integrated/test-squash/runtest +++ b/tests/integrated/test-squash/runtest @@ -6,11 +6,16 @@ import time import numpy as np from boututils.run_wrapper import launch_safe, shell_safe, build_and_log import argparse +import re + #requires: all_tests #requires: netcdf #cores: 4 +IGNORED_VARS_PATTERN = re.compile("(wtime|ncalls|arkode|cvode).*") + + class timer(object): """Context manager for printing how long a command took @@ -54,7 +59,7 @@ def verify(f1, f2): raise RuntimeError("shape mismatch in ", v, d1[v], d2[v]) if v in ["MXSUB", "MYSUB", "NXPE", "NYPE", "iteration","wall_time"]: continue - if v.startswith("wtime") or v.startswith("ncalls"): + if IGNORED_VARS_PATTERN.match(v): continue if not np.allclose(d1[v], d2[v], equal_nan=True): err = "" From 13bff630115472115b866d1c9004d677e67676a2 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Mon, 4 Jan 2021 15:35:39 +0000 Subject: [PATCH 15/41] Apply black formatting to test-squash --- tests/integrated/test-squash/runtest | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/tests/integrated/test-squash/runtest b/tests/integrated/test-squash/runtest index 8b4dcf56c8..97362117fa 100755 --- a/tests/integrated/test-squash/runtest +++ b/tests/integrated/test-squash/runtest @@ -9,17 +9,16 @@ import argparse import re -#requires: all_tests -#requires: netcdf -#cores: 4 +# requires: all_tests +# requires: netcdf +# cores: 4 IGNORED_VARS_PATTERN = re.compile("(wtime|ncalls|arkode|cvode).*") class timer(object): - """Context manager for printing how long a command took + """Context manager for printing how long a command took""" - """ def __init__(self, msg): self.msg = msg @@ -32,32 +31,26 @@ class timer(object): def timed_shell_safe(cmd, *args, **kwargs): - """Wraps shell_safe in a timer - - """ + """Wraps shell_safe in a timer""" with timer(cmd): shell_safe(cmd, *args, **kwargs) def timed_launch_safe(cmd, *args, **kwargs): - """Wraps launch_safe in a timer - - """ + """Wraps launch_safe in a timer""" with timer(cmd): launch_safe(cmd, *args, **kwargs) def verify(f1, f2): - """Verifies that two BOUT++ files are identical - - """ + """Verifies that two BOUT++ files are identical""" with timer("verify %s %s" % (f1, f2)): d1 = DataFile(f1) d2 = DataFile(f2) for v in d1.keys(): if d1[v].shape != d2[v].shape: raise RuntimeError("shape mismatch in ", v, d1[v], d2[v]) - if v in ["MXSUB", "MYSUB", "NXPE", "NYPE", "iteration","wall_time"]: + if v in ["MXSUB", "MYSUB", "NXPE", "NYPE", "iteration", "wall_time"]: continue if IGNORED_VARS_PATTERN.match(v): continue @@ -71,8 +64,9 @@ def verify(f1, f2): parser = argparse.ArgumentParser(description="Test the bout-squashoutput wrapper") -parser.add_argument("executable", help="Path to bout-squashoutput", - default="../../../bin") +parser.add_argument( + "executable", help="Path to bout-squashoutput", default="../../../bin" +) args = parser.parse_args() build_and_log("Squash test") From 6f06227a1fe9876df6dcae121b4745daed298d0e Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Mon, 4 Jan 2021 17:05:35 +0000 Subject: [PATCH 16/41] Fix missing BOUT_HAS_NETCDF in configure for legacy netCDF --- configure | 1 + configure.ac | 1 + src/fileio/formatfactory.cxx | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/configure b/configure index b5650b35c4..5d838a2463 100755 --- a/configure +++ b/configure @@ -7188,6 +7188,7 @@ fi { $as_echo "$as_me:${as_lineno-$LINENO}: -> Legacy NetCDF support enabled" >&5 $as_echo "$as_me: -> Legacy NetCDF support enabled" >&6;} NCPATH="found" + BOUT_HAS_NETCDF=yes BOUT_HAS_LEGACY_NETCDF=yes fi diff --git a/configure.ac b/configure.ac index 8b1f7e183a..98cf757c28 100644 --- a/configure.ac +++ b/configure.ac @@ -575,6 +575,7 @@ AS_IF([test "x$with_netcdf" != "xno"], file_formats="$file_formats netCDF" AC_MSG_NOTICE([ -> Legacy NetCDF support enabled]) NCPATH="found" + BOUT_HAS_NETCDF=yes BOUT_HAS_LEGACY_NETCDF=yes ], []) ]) diff --git a/src/fileio/formatfactory.cxx b/src/fileio/formatfactory.cxx index d8dc4aad37..dbe8fdd125 100644 --- a/src/fileio/formatfactory.cxx +++ b/src/fileio/formatfactory.cxx @@ -40,7 +40,7 @@ std::unique_ptr FormatFactory::createDataFormat(const char *filename #else } -#if BOUT_HAS_NETCDF +#if BOUT_HAS_NETCDF && !BOUT_HAS_LEGACY_NETCDF return bout::utils::make_unique(mesh_in); #else From 46500eef2a05e6a9411e85fda1d08d7e87987f24 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Mon, 4 Jan 2021 17:06:07 +0000 Subject: [PATCH 17/41] Remove unnecessary logical OR in build_config header --- include/bout/build_config.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/bout/build_config.hxx b/include/bout/build_config.hxx index ff96f6927c..acd8a6b33b 100644 --- a/include/bout/build_config.hxx +++ b/include/bout/build_config.hxx @@ -17,7 +17,7 @@ constexpr auto has_gettext = static_cast(BOUT_HAS_GETTEXT); constexpr auto has_hdf5 = static_cast(BOUT_HAS_HDF5); constexpr auto has_lapack = static_cast(BOUT_HAS_LAPACK); constexpr auto has_legacy_netcdf = static_cast(BOUT_HAS_LEGACY_NETCDF); -constexpr auto has_netcdf = static_cast(BOUT_HAS_NETCDF) or has_legacy_netcdf; +constexpr auto has_netcdf = static_cast(BOUT_HAS_NETCDF); constexpr auto has_petsc = static_cast(BOUT_HAS_PETSC); constexpr auto has_pretty_function = static_cast(BOUT_HAS_PRETTY_FUNCTION); constexpr auto has_pvode = static_cast(BOUT_HAS_PVODE); From 2b3004ab8db64c0532c91a4ab1ef47c6e4300d00 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Mon, 4 Jan 2021 17:33:22 +0000 Subject: [PATCH 18/41] Add expectation of `datadir`, `dump_format` options having been set `BoutInitialise` ensures these are set, so only an issue if that isn't called --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b44ffde11..fb162f99de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ will need to be updated to either use `bout::globals::mesh` or `Field::getMesh()` in free functions. [\#2042](https://github.com/boutproject/BOUT-dev/pull/2042) +- `PhysicsModel` expects the options `datadir` and `dump_format` to + have been set; this is only a problem if you don't call + `BoutInitialise`. [\#2062](https://github.com/boutproject/BOUT-dev/pull/2062) ## [v4.3.1](https://github.com/boutproject/BOUT-dev/tree/v4.3.1) (2020-03-27) From 796e129ec0c2bff4cea117a9a95fda05ab65b361 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Mon, 4 Jan 2021 17:50:15 +0000 Subject: [PATCH 19/41] GHA: Increase test timeout to 6 minutes Some non-deterministic behaviour observed in debug build, see https://github.com/boutproject/BOUT-dev/issues/2185 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index abd3397e92..e3cd8195d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} env: - BOUT_TEST_TIMEOUT: "5m" + BOUT_TEST_TIMEOUT: "6m" PETSC_DIR: /usr/lib/petscdir/3.7.7/x86_64-linux-gnu-real PETSC_ARCH: "" SLEPC_DIR: /usr/lib/slepcdir/3.7.4/x86_64-linux-gnu-real From 1f0649eff83c228536e8e1501dde05896f553a2f Mon Sep 17 00:00:00 2001 From: John Omotani Date: Mon, 4 Jan 2021 19:54:13 +0100 Subject: [PATCH 20/41] Update versions of boututils and boutdata to fix issue with symlinking --- externalpackages/boutdata | 2 +- externalpackages/boututils | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/externalpackages/boutdata b/externalpackages/boutdata index f0a84525ce..0b849bd326 160000 --- a/externalpackages/boutdata +++ b/externalpackages/boutdata @@ -1 +1 @@ -Subproject commit f0a84525cedc372fd063fcc95f4ddc51a8f42cca +Subproject commit 0b849bd3263574afd1d468e56a116d2956896fac diff --git a/externalpackages/boututils b/externalpackages/boututils index f0ada15891..1db58c0701 160000 --- a/externalpackages/boututils +++ b/externalpackages/boututils @@ -1 +1 @@ -Subproject commit f0ada158913b1c77948390d4d15adbfbf7fdb09c +Subproject commit 1db58c0701823ca5ddb67c9b29be1643b3c604b6 From d104bfc9706a2aff887fadaf808e137250867d75 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Mon, 4 Jan 2021 20:50:50 +0100 Subject: [PATCH 21/41] Install setuptools_scm in CI --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index abd3397e92..d5cff04735 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -135,7 +135,7 @@ jobs: - name: Install pip packages run: | - ./.pip_install_for_travis.sh 'cython~=0.29' 'netcdf4~=1.5' 'sympy~=1.5' 'gcovr' 'cmake' 'h5py' + ./.pip_install_for_travis.sh 'cython~=0.29' 'netcdf4~=1.5' 'sympy~=1.5' 'gcovr' 'cmake' 'h5py' 'setuptools_scm' # Add the pip install location to the runner's PATH echo ~/.local/bin >> $GITHUB_PATH From ecc7d42961d4b17d9f6e471704693b1c50ad0d1f Mon Sep 17 00:00:00 2001 From: John Omotani Date: Mon, 4 Jan 2021 21:14:16 +0100 Subject: [PATCH 22/41] Update Python setup instructions --- manual/sphinx/user_docs/installing.rst | 44 ++++++++++++++++++-------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/manual/sphinx/user_docs/installing.rst b/manual/sphinx/user_docs/installing.rst index aa2634e186..4d0974e782 100644 --- a/manual/sphinx/user_docs/installing.rst +++ b/manual/sphinx/user_docs/installing.rst @@ -439,22 +439,34 @@ make a note of what configure printed out. Python configuration ~~~~~~~~~~~~~~~~~~~~ -To use Python, you will need the NumPy and SciPy libraries. On Debian or -Ubuntu these can be installed with:: +To use Python, you will need the dependencies of the `boututils +`__ and `boutdata +`__ libraries. The simplest way to get these is +to install the packages, plus additional developer dependencies, with pip:: - $ sudo apt-get install python-scipy + $ pip install --user boutdata setuptools_scm -which should then add all the other dependencies like NumPy. To test if -everything is installed, run:: +or conda:: - $ python -c "import scipy" + $ conda install boutdata setuptools_scm -If not, see the SciPy website https://www.scipy.org for instructions on -installing. +You can also install all the packages directly (see the documentation in the `boututils +`__ and `boutdata +`__ repos for the most up to date list) +using pip:: -To do this, the path to ``tools/pylib`` should be added to the -``PYTHONPATH`` environment variable. Instructions for doing this are -printed at the end of the configure script, for example:: + $ pip install --user numpy scipy matplotlib sympy netCDF4 h5py future importlib-metadata setuptools_scm + +or conda:: + + $ conda install numpy scipy matplotlib sympy netcdf4 h5py future importlib-metadata setuptools_scm + +They may also be available from your Linux system's package manager. + +To use the versions of ``boututils`` and ``boutdata`` provided by BOUT++, the path to +``tools/pylib`` should be added to the ``PYTHONPATH`` environment variable. This is not +necessary if you have installed the ``boututils`` and ``boutdata`` packages. Instructions +for doing this are printed at the end of the configure script, for example:: Make sure that the tools/pylib directory is in your PYTHONPATH e.g. by adding to your ~/.bashrc file @@ -465,8 +477,14 @@ To test if this command has worked, try running:: $ python -c "import boutdata" -If this doesn’t produce any error messages then Python is configured -correctly. +If this doesn’t produce any error messages then Python is configured correctly. + +Note that ``boututils`` and ``boutdata`` are provided by BOUT++ as submodules, so versions +compatible with the checked out version of BOUT++ are downloaded into the +``externalpackages`` directory. These are the versions used by the tests run by ``make +check`` even if you have installed ``boututils`` and ``boutdata`` on your system, so you +do need the 'developer' dependencies of the packages (e.g. ``setuptools_scm``). + .. _sec-config-idl: From ac139d49f5d8345aedb45958855d082318120fba Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 5 Jan 2021 11:25:40 +0000 Subject: [PATCH 23/41] Set BOUT_HAS_NETCDF for both legacy detection methods --- configure | 1 + configure.ac | 1 + 2 files changed, 2 insertions(+) diff --git a/configure b/configure index ac15ea9b9f..cb5593809b 100755 --- a/configure +++ b/configure @@ -6788,6 +6788,7 @@ ac_compiler_gnu=$ac_cv_cxx_compiler_gnu LIBS=$save_LIBS LDFLAGS=$save_LDFLAGS CXXFLAGS="$save_CXXFLAGS" + BOUT_HAS_NETCDF=yes BOUT_HAS_LEGACY_NETCDF=yes fi diff --git a/configure.ac b/configure.ac index 907203853c..1f6614f9c3 100644 --- a/configure.ac +++ b/configure.ac @@ -556,6 +556,7 @@ AS_IF([test "x$with_netcdf" != "xno"], LIBS=$save_LIBS LDFLAGS=$save_LDFLAGS CXXFLAGS="$save_CXXFLAGS" + BOUT_HAS_NETCDF=yes BOUT_HAS_LEGACY_NETCDF=yes ]) EXTRA_LIBS="$EXTRA_LIBS $NCLIB" From 4ff3fd50e3b0ec8f56e9bd00c1ba00f7f7deeb31 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 5 Jan 2021 11:26:05 +0000 Subject: [PATCH 24/41] Fix duplicated code when using legacy netCDF --- src/fileio/formatfactory.cxx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/fileio/formatfactory.cxx b/src/fileio/formatfactory.cxx index dbe8fdd125..0586bfb1b1 100644 --- a/src/fileio/formatfactory.cxx +++ b/src/fileio/formatfactory.cxx @@ -88,15 +88,11 @@ std::unique_ptr FormatFactory::createDataFormat(const char *filename const char *ncdf_match[] = {"cdl", "nc", "ncdf"}; if(matchString(s, 3, ncdf_match) != -1) { output.write("\tUsing NetCDF4 format for file '{:s}'\n", filename); - return bout::utils::make_unique(); - } -#endif - #if BOUT_HAS_LEGACY_NETCDF - const char *ncdf_match[] = {"cdl", "nc", "ncdf"}; - if(matchString(s, 3, ncdf_match) != -1) { - output.write("\tUsing NetCDF format for file '{:s}'\n", filename); return bout::utils::make_unique(); +#else + return bout::utils::make_unique(); +#endif } #endif From 6af3a7527ad10a950c9ac27e2667c3e3cf3f5a66 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Tue, 5 Jan 2021 22:32:24 +0100 Subject: [PATCH 25/41] Update boututils and boutdata to remove setuptools_scm hard dependency --- externalpackages/boutdata | 2 +- externalpackages/boututils | 2 +- manual/sphinx/user_docs/installing.rst | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/externalpackages/boutdata b/externalpackages/boutdata index 0b849bd326..211434161d 160000 --- a/externalpackages/boutdata +++ b/externalpackages/boutdata @@ -1 +1 @@ -Subproject commit 0b849bd3263574afd1d468e56a116d2956896fac +Subproject commit 211434161df05a85af4d152df44ed9a8225f170a diff --git a/externalpackages/boututils b/externalpackages/boututils index 1db58c0701..08b572d20a 160000 --- a/externalpackages/boututils +++ b/externalpackages/boututils @@ -1 +1 @@ -Subproject commit 1db58c0701823ca5ddb67c9b29be1643b3c604b6 +Subproject commit 08b572d20a6c693b051f6504c599c539f5a68e82 diff --git a/manual/sphinx/user_docs/installing.rst b/manual/sphinx/user_docs/installing.rst index 4d0974e782..8f95d79831 100644 --- a/manual/sphinx/user_docs/installing.rst +++ b/manual/sphinx/user_docs/installing.rst @@ -442,24 +442,24 @@ Python configuration To use Python, you will need the dependencies of the `boututils `__ and `boutdata `__ libraries. The simplest way to get these is -to install the packages, plus additional developer dependencies, with pip:: +to install the packages with pip:: - $ pip install --user boutdata setuptools_scm + $ pip install --user boutdata or conda:: - $ conda install boutdata setuptools_scm + $ conda install boutdata You can also install all the packages directly (see the documentation in the `boututils `__ and `boutdata `__ repos for the most up to date list) using pip:: - $ pip install --user numpy scipy matplotlib sympy netCDF4 h5py future importlib-metadata setuptools_scm + $ pip install --user numpy scipy matplotlib sympy netCDF4 h5py future importlib-metadata or conda:: - $ conda install numpy scipy matplotlib sympy netcdf4 h5py future importlib-metadata setuptools_scm + $ conda install numpy scipy matplotlib sympy netcdf4 h5py future importlib-metadata They may also be available from your Linux system's package manager. @@ -482,8 +482,7 @@ If this doesn’t produce any error messages then Python is configured correctly Note that ``boututils`` and ``boutdata`` are provided by BOUT++ as submodules, so versions compatible with the checked out version of BOUT++ are downloaded into the ``externalpackages`` directory. These are the versions used by the tests run by ``make -check`` even if you have installed ``boututils`` and ``boutdata`` on your system, so you -do need the 'developer' dependencies of the packages (e.g. ``setuptools_scm``). +check`` even if you have installed ``boututils`` and ``boutdata`` on your system. .. _sec-config-idl: From 7e8831b28fb67403ec32f0f974473e369572b043 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Tue, 5 Jan 2021 22:36:43 +0100 Subject: [PATCH 26/41] Don't install setuptools_scm for CI It is not required any more. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d5cff04735..abd3397e92 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -135,7 +135,7 @@ jobs: - name: Install pip packages run: | - ./.pip_install_for_travis.sh 'cython~=0.29' 'netcdf4~=1.5' 'sympy~=1.5' 'gcovr' 'cmake' 'h5py' 'setuptools_scm' + ./.pip_install_for_travis.sh 'cython~=0.29' 'netcdf4~=1.5' 'sympy~=1.5' 'gcovr' 'cmake' 'h5py' # Add the pip install location to the runner's PATH echo ~/.local/bin >> $GITHUB_PATH From 047ffa769cca0f3ce50aebfd827815f9f9ee502e Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 10:16:54 +0000 Subject: [PATCH 27/41] Guard against using legacy netCDF for ncxx4 and OptionsNetcdf --- include/options_netcdf.hxx | 2 +- src/fileio/impls/netcdf4/ncxx4.cxx | 2 +- src/fileio/impls/netcdf4/ncxx4.hxx | 2 +- src/sys/options/options_netcdf.cxx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/options_netcdf.hxx b/include/options_netcdf.hxx index 59a599d3f2..8b0885fc67 100644 --- a/include/options_netcdf.hxx +++ b/include/options_netcdf.hxx @@ -6,7 +6,7 @@ #include "bout/build_config.hxx" -#if !BOUT_HAS_NETCDF +#if !BOUT_HAS_NETCDF || BOUT_HAS_LEGACY_NETCDF #include diff --git a/src/fileio/impls/netcdf4/ncxx4.cxx b/src/fileio/impls/netcdf4/ncxx4.cxx index 5d6c0cd901..dc085c4040 100644 --- a/src/fileio/impls/netcdf4/ncxx4.cxx +++ b/src/fileio/impls/netcdf4/ncxx4.cxx @@ -24,7 +24,7 @@ #include "ncxx4.hxx" -#if BOUT_HAS_NETCDF +#if BOUT_HAS_NETCDF && !BOUT_HAS_LEGACY_NETCDF #include #include diff --git a/src/fileio/impls/netcdf4/ncxx4.hxx b/src/fileio/impls/netcdf4/ncxx4.hxx index 57dc2a52b9..fc78923c0a 100644 --- a/src/fileio/impls/netcdf4/ncxx4.hxx +++ b/src/fileio/impls/netcdf4/ncxx4.hxx @@ -35,7 +35,7 @@ #include "bout/build_config.hxx" -#if !BOUT_HAS_NETCDF +#if !BOUT_HAS_NETCDF || BOUT_HAS_LEGACY_NETCDF #include "../emptyformat.hxx" using Ncxx4 = EmptyFormat; diff --git a/src/sys/options/options_netcdf.cxx b/src/sys/options/options_netcdf.cxx index f4f5f1610c..7a05d8e48f 100644 --- a/src/sys/options/options_netcdf.cxx +++ b/src/sys/options/options_netcdf.cxx @@ -1,6 +1,6 @@ #include "bout/build_config.hxx" -#if BOUT_HAS_NETCDF +#if BOUT_HAS_NETCDF && !BOUT_HAS_LEGACY_NETCDF #include "options_netcdf.hxx" From 67a711b7a6e03ff4dfea5b3e6afbe135f1c7b5d5 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 14:19:37 +0000 Subject: [PATCH 28/41] Remove some unused code from FCI --- src/mesh/parallel/fci.cxx | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index bf37c88256..32e0cc7031 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -65,12 +65,6 @@ FCIMap::FCIMap(Mesh& mesh, Options& options, int offset_, BoundaryRegionPar* bou interp_corner = XZInterpolationFactory::getInstance().create(&interpolation_options, &map_mesh); interp_corner->setYOffset(offset); - // Index arrays contain guard cells in order to get subscripts right - // x-index of bottom-left grid point - auto i_corner = Tensor(map_mesh.LocalNx, map_mesh.LocalNy, map_mesh.LocalNz); - // z-index of bottom-left grid point - auto k_corner = Tensor(map_mesh.LocalNx, map_mesh.LocalNy, map_mesh.LocalNz); - // Index-space coordinates of forward/backward points Field3D xt_prime{&map_mesh}, zt_prime{&map_mesh}; @@ -159,18 +153,12 @@ FCIMap::FCIMap(Mesh& mesh, Options& options, int offset_, BoundaryRegionPar* bou int ncz = map_mesh.LocalNz; - BoutReal t_x, t_z; - Coordinates &coord = *(map_mesh.getCoordinates()); for (int x = map_mesh.xstart; x <= map_mesh.xend; x++) { for (int y = map_mesh.ystart; y <= map_mesh.yend; y++) { for (int z = 0; z < ncz; z++) { - // The integer part of xt_prime, zt_prime are the indices of the cell - // containing the field line end-point - i_corner(x, y, z) = static_cast(floor(xt_prime(x, y, z))); - // z is periodic, so make sure the z-index wraps around if (zperiodic) { zt_prime(x, y, z) = @@ -181,26 +169,6 @@ FCIMap::FCIMap(Mesh& mesh, Options& options, int offset_, BoundaryRegionPar* bou zt_prime(x, y, z) += ncz; } - k_corner(x, y, z) = static_cast(floor(zt_prime(x, y, z))); - - // t_x, t_z are the normalised coordinates \in [0,1) within the cell - // calculated by taking the remainder of the floating point index - t_x = xt_prime(x, y, z) - static_cast(i_corner(x, y, z)); - t_z = zt_prime(x, y, z) - static_cast(k_corner(x, y, z)); - - // Check that t_x and t_z are in range - if ((t_x < 0.0) || (t_x > 1.0)) { - throw BoutException( - "t_x={:e} out of range at ({:d},{:d},{:d}) (xt_prime={:e}, i_corner={:d})", - t_x, x, y, z, xt_prime(x, y, z), i_corner(x, y, z)); - } - - if ((t_z < 0.0) || (t_z > 1.0)) { - throw BoutException( - "t_z={:e} out of range at ({:d},{:d},{:d}) (zt_prime={:e}, k_corner={:d})", - t_z, x, y, z, zt_prime(x, y, z), k_corner(x, y, z)); - } - //---------------------------------------- // Boundary stuff // @@ -253,7 +221,7 @@ FCIMap::FCIMap(Mesh& mesh, Options& options, int offset_, BoundaryRegionPar* bou BoutReal dz = (dR_dx * dZ - dZ_dx * dR) / det; boundary->add_point(x, y, z, x + dx, y + 0.5*offset, z + dz, // Intersection point in local index space - 0.5*coord.dy(x,y), //sqrt( SQ(dR) + SQ(dZ) ), // Distance to intersection + 0.5*coord.dy(x,y), // Distance to intersection PI // Right-angle intersection ); } From e3fa21b5de6f5394d36db7be1abea488c10d6647 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 16:43:22 +0000 Subject: [PATCH 29/41] Pass dy down into FCIMap Because we construct the FCI transform _during_ the construction of Coordinates, we can't call `Mesh::getCoordinates` in the `FCIMap` constructor. An alternative workaround would be to call `Mesh::get(dy)` instead, but the implemented method is more explicit --- src/mesh/coordinates.cxx | 4 ++-- src/mesh/parallel/fci.cxx | 6 ++---- src/mesh/parallel/fci.hxx | 8 ++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/mesh/coordinates.cxx b/src/mesh/coordinates.cxx index 9e95af5f03..0dc9eae62e 100644 --- a/src/mesh/coordinates.cxx +++ b/src/mesh/coordinates.cxx @@ -1224,8 +1224,8 @@ void Coordinates::setParallelTransform(Options* options) { // Flux Coordinate Independent method const bool fci_zperiodic = (*ptoptions)["z_periodic"].withDefault(true); - transform = bout::utils::make_unique(*localmesh, fci_zperiodic, - ptoptions); + transform = + bout::utils::make_unique(*localmesh, dy, fci_zperiodic, ptoptions); } else { throw BoutException(_("Unrecognised paralleltransform option.\n" diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index 32e0cc7031..b9001a6c73 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -47,7 +47,7 @@ #include -FCIMap::FCIMap(Mesh& mesh, Options& options, int offset_, BoundaryRegionPar* boundary, +FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRegionPar* boundary, bool zperiodic) : map_mesh(mesh), offset(offset_), boundary_mask(map_mesh), corner_boundary_mask(map_mesh) { @@ -153,8 +153,6 @@ FCIMap::FCIMap(Mesh& mesh, Options& options, int offset_, BoundaryRegionPar* bou int ncz = map_mesh.LocalNz; - Coordinates &coord = *(map_mesh.getCoordinates()); - for (int x = map_mesh.xstart; x <= map_mesh.xend; x++) { for (int y = map_mesh.ystart; y <= map_mesh.yend; y++) { for (int z = 0; z < ncz; z++) { @@ -221,7 +219,7 @@ FCIMap::FCIMap(Mesh& mesh, Options& options, int offset_, BoundaryRegionPar* bou BoutReal dz = (dR_dx * dZ - dZ_dx * dR) / det; boundary->add_point(x, y, z, x + dx, y + 0.5*offset, z + dz, // Intersection point in local index space - 0.5*coord.dy(x,y), // Distance to intersection + 0.5*dy(x,y), // Distance to intersection PI // Right-angle intersection ); } diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index 6f8c5ed916..4700893f30 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -44,7 +44,7 @@ class FCIMap { public: FCIMap() = delete; - FCIMap(Mesh& mesh, Options& options, int offset, BoundaryRegionPar* boundary, bool + FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset, BoundaryRegionPar* boundary, bool zperiodic); // The mesh this map was created on @@ -71,7 +71,7 @@ public: class FCITransform : public ParallelTransform { public: FCITransform() = delete; - FCITransform(Mesh& mesh, bool zperiodic = true, Options* opt = nullptr) + FCITransform(Mesh& mesh, Field2D dy, bool zperiodic = true, Options* opt = nullptr) : ParallelTransform(mesh, opt) { // check the coordinate system used for the grid data source @@ -86,8 +86,8 @@ public: field_line_maps.reserve(mesh.ystart * 2); for (int offset = 1; offset < mesh.ystart + 1; ++offset) { - field_line_maps.emplace_back(mesh, options, offset, forward_boundary, zperiodic); - field_line_maps.emplace_back(mesh, options, -offset, backward_boundary, zperiodic); + field_line_maps.emplace_back(mesh, dy, options, offset, forward_boundary, zperiodic); + field_line_maps.emplace_back(mesh, dy, options, -offset, backward_boundary, zperiodic); } } From d5721aec6a7a842b5a7b2f0177561cd95816a047 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 16:48:04 +0000 Subject: [PATCH 30/41] Make local variables const --- src/mesh/parallel/fci.cxx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index b9001a6c73..b8e652c855 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -151,7 +151,7 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe interp->calcWeights(xt_prime, zt_prime); } - int ncz = map_mesh.LocalNz; + const int ncz = map_mesh.LocalNz; for (int x = map_mesh.xstart; x <= map_mesh.xend; x++) { for (int y = map_mesh.ystart; y <= map_mesh.yend; y++) { @@ -191,8 +191,8 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe // (dx,dz) is the change in (x,z) index along the field, // and the gradients dR/dx etc. are evaluated at (x,y,z) - BoutReal dR_dx = 0.5 * (R(x + 1, y, z) - R(x - 1, y, z)); - BoutReal dZ_dx = 0.5 * (Z(x + 1, y, z) - Z(x - 1, y, z)); + const BoutReal dR_dx = 0.5 * (R(x + 1, y, z) - R(x - 1, y, z)); + const BoutReal dZ_dx = 0.5 * (Z(x + 1, y, z) - Z(x - 1, y, z)); BoutReal dR_dz, dZ_dz; // Handle the edge cases in Z @@ -209,14 +209,14 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe dZ_dz = 0.5 * (Z(x, y, z + 1) - Z(x, y, z - 1)); } - BoutReal det = dR_dx * dZ_dz - dR_dz * dZ_dx; // Determinant of 2x2 matrix + const BoutReal det = dR_dx * dZ_dz - dR_dz * dZ_dx; // Determinant of 2x2 matrix - BoutReal dR = R_prime(x, y, z) - R(x, y, z); - BoutReal dZ = Z_prime(x, y, z) - Z(x, y, z); + const BoutReal dR = R_prime(x, y, z) - R(x, y, z); + const BoutReal dZ = Z_prime(x, y, z) - Z(x, y, z); // Invert 2x2 matrix to get change in index - BoutReal dx = (dZ_dz * dR - dR_dz * dZ) / det; - BoutReal dz = (dR_dx * dZ - dZ_dx * dR) / det; + const BoutReal dx = (dZ_dz * dR - dR_dz * dZ) / det; + const BoutReal dz = (dR_dx * dZ - dZ_dx * dR) / det; boundary->add_point(x, y, z, x + dx, y + 0.5*offset, z + dz, // Intersection point in local index space 0.5*dy(x,y), // Distance to intersection From 4a5d99d9754649698e562b6ba4bd41d16b6fc66a Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 17:01:19 +0000 Subject: [PATCH 31/41] Convert nested-loop to BOUT_FOR in FCI --- src/mesh/parallel/fci.cxx | 138 +++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index b8e652c855..eae3985fd7 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -153,77 +153,77 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe const int ncz = map_mesh.LocalNz; - for (int x = map_mesh.xstart; x <= map_mesh.xend; x++) { - for (int y = map_mesh.ystart; y <= map_mesh.yend; y++) { - for (int z = 0; z < ncz; z++) { - - // z is periodic, so make sure the z-index wraps around - if (zperiodic) { - zt_prime(x, y, z) = - zt_prime(x, y, z) - - ncz * (static_cast(zt_prime(x, y, z) / static_cast(ncz))); - - if (zt_prime(x, y, z) < 0.0) - zt_prime(x, y, z) += ncz; - } + // Serial loop because call to BoundaryRegionPar::addPoint + // (probably?) can't be done in parallel + BOUT_FOR_SERIAL(i, xt_prime.getRegion("RGN_NOBNDRY")) { + const int x = i.x(); + const int y = i.y(); + const int z = i.z(); + + // z is periodic, so make sure the z-index wraps around + if (zperiodic) { + zt_prime[i] = zt_prime[i] + - ncz * (static_cast(zt_prime[i] / static_cast(ncz))); + + if (zt_prime[i] < 0.0) + zt_prime[i] += ncz; + } - //---------------------------------------- - // Boundary stuff - // - // If a field line leaves the domain, then the forward or backward - // indices (forward/backward_xt_prime and forward/backward_zt_prime) - // are set to -1 - - if (xt_prime(x, y, z) < 0.0) { - // Hit a boundary - - boundary_mask(x, y, z) = true; - - // Need to specify the index of the boundary intersection, but - // this may not be defined in general. - // We do however have the real-space (R,Z) coordinates. Here we extrapolate, - // using the change in R and Z to calculate the change in (x,z) indices - // - // ( dR ) = ( dR/dx dR/dz ) ( dx ) - // ( dZ ) ( dZ/dx dZ/dz ) ( dz ) - // - // where (dR,dZ) is the change in (R,Z) along the field, - // (dx,dz) is the change in (x,z) index along the field, - // and the gradients dR/dx etc. are evaluated at (x,y,z) - - const BoutReal dR_dx = 0.5 * (R(x + 1, y, z) - R(x - 1, y, z)); - const BoutReal dZ_dx = 0.5 * (Z(x + 1, y, z) - Z(x - 1, y, z)); - - BoutReal dR_dz, dZ_dz; - // Handle the edge cases in Z - if (z == 0) { - dR_dz = R(x, y, z + 1) - R(x, y, z); - dZ_dz = Z(x, y, z + 1) - Z(x, y, z); - - } else if (z == map_mesh.LocalNz - 1) { - dR_dz = R(x, y, z) - R(x, y, z - 1); - dZ_dz = Z(x, y, z) - Z(x, y, z - 1); - - } else { - dR_dz = 0.5 * (R(x, y, z + 1) - R(x, y, z - 1)); - dZ_dz = 0.5 * (Z(x, y, z + 1) - Z(x, y, z - 1)); - } - - const BoutReal det = dR_dx * dZ_dz - dR_dz * dZ_dx; // Determinant of 2x2 matrix - - const BoutReal dR = R_prime(x, y, z) - R(x, y, z); - const BoutReal dZ = Z_prime(x, y, z) - Z(x, y, z); - - // Invert 2x2 matrix to get change in index - const BoutReal dx = (dZ_dz * dR - dR_dz * dZ) / det; - const BoutReal dz = (dR_dx * dZ - dZ_dx * dR) / det; - boundary->add_point(x, y, z, - x + dx, y + 0.5*offset, z + dz, // Intersection point in local index space - 0.5*dy(x,y), // Distance to intersection - PI // Right-angle intersection - ); - } + //---------------------------------------- + // Boundary stuff + // + // If a field line leaves the domain, then the forward or backward + // indices (forward/backward_xt_prime and forward/backward_zt_prime) + // are set to -1 + + if (xt_prime[i] < 0.0) { + // Hit a boundary + + boundary_mask(x, y, z) = true; + + // Need to specify the index of the boundary intersection, but + // this may not be defined in general. + // We do however have the real-space (R,Z) coordinates. Here we extrapolate, + // using the change in R and Z to calculate the change in (x,z) indices + // + // ( dR ) = ( dR/dx dR/dz ) ( dx ) + // ( dZ ) ( dZ/dx dZ/dz ) ( dz ) + // + // where (dR,dZ) is the change in (R,Z) along the field, + // (dx,dz) is the change in (x,z) index along the field, + // and the gradients dR/dx etc. are evaluated at (x,y,z) + + const BoutReal dR_dx = 0.5 * (R[i.xp()] - R[i.xm()]); + const BoutReal dZ_dx = 0.5 * (Z[i.xp()] - Z[i.xm()]); + + BoutReal dR_dz, dZ_dz; + // Handle the edge cases in Z + if (z == 0) { + dR_dz = R[i.zp()] - R[i]; + dZ_dz = Z[i.zp()] - Z[i]; + + } else if (z == map_mesh.LocalNz - 1) { + dR_dz = R[i] - R[i.zm()]; + dZ_dz = Z[i] - Z[i.zm()]; + + } else { + dR_dz = 0.5 * (R[i.zp()] - R[i.zm()]); + dZ_dz = 0.5 * (Z[i.zp()] - Z[i.zm()]); } + + const BoutReal det = dR_dx * dZ_dz - dR_dz * dZ_dx; // Determinant of 2x2 matrix + + const BoutReal dR = R_prime[i] - R[i]; + const BoutReal dZ = Z_prime[i] - Z[i]; + + // Invert 2x2 matrix to get change in index + const BoutReal dx = (dZ_dz * dR - dR_dz * dZ) / det; + const BoutReal dz = (dR_dx * dZ - dZ_dx * dR) / det; + boundary->add_point(x, y, z, x + dx, y + 0.5 * offset, + z + dz, // Intersection point in local index space + 0.5 * dy[i], // Distance to intersection + PI // Right-angle intersection + ); } } From 3e08c70f069e04fa0f53e0bcc1abcf64477002e4 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 17:04:17 +0000 Subject: [PATCH 32/41] Invert conditional in FCI boundary calculation --- src/mesh/parallel/fci.cxx | 91 ++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index eae3985fd7..eff2175ac8 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -169,6 +169,11 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe zt_prime[i] += ncz; } + if (xt_prime[i] >= 0.0) { + // Not a boundary + continue; + } + //---------------------------------------- // Boundary stuff // @@ -176,55 +181,51 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe // indices (forward/backward_xt_prime and forward/backward_zt_prime) // are set to -1 - if (xt_prime[i] < 0.0) { - // Hit a boundary + boundary_mask(x, y, z) = true; - boundary_mask(x, y, z) = true; - - // Need to specify the index of the boundary intersection, but - // this may not be defined in general. - // We do however have the real-space (R,Z) coordinates. Here we extrapolate, - // using the change in R and Z to calculate the change in (x,z) indices - // - // ( dR ) = ( dR/dx dR/dz ) ( dx ) - // ( dZ ) ( dZ/dx dZ/dz ) ( dz ) - // - // where (dR,dZ) is the change in (R,Z) along the field, - // (dx,dz) is the change in (x,z) index along the field, - // and the gradients dR/dx etc. are evaluated at (x,y,z) - - const BoutReal dR_dx = 0.5 * (R[i.xp()] - R[i.xm()]); - const BoutReal dZ_dx = 0.5 * (Z[i.xp()] - Z[i.xm()]); - - BoutReal dR_dz, dZ_dz; - // Handle the edge cases in Z - if (z == 0) { - dR_dz = R[i.zp()] - R[i]; - dZ_dz = Z[i.zp()] - Z[i]; - - } else if (z == map_mesh.LocalNz - 1) { - dR_dz = R[i] - R[i.zm()]; - dZ_dz = Z[i] - Z[i.zm()]; - - } else { - dR_dz = 0.5 * (R[i.zp()] - R[i.zm()]); - dZ_dz = 0.5 * (Z[i.zp()] - Z[i.zm()]); - } + // Need to specify the index of the boundary intersection, but + // this may not be defined in general. + // We do however have the real-space (R,Z) coordinates. Here we extrapolate, + // using the change in R and Z to calculate the change in (x,z) indices + // + // ( dR ) = ( dR/dx dR/dz ) ( dx ) + // ( dZ ) ( dZ/dx dZ/dz ) ( dz ) + // + // where (dR,dZ) is the change in (R,Z) along the field, + // (dx,dz) is the change in (x,z) index along the field, + // and the gradients dR/dx etc. are evaluated at (x,y,z) + + const BoutReal dR_dx = 0.5 * (R[i.xp()] - R[i.xm()]); + const BoutReal dZ_dx = 0.5 * (Z[i.xp()] - Z[i.xm()]); + + BoutReal dR_dz, dZ_dz; + // Handle the edge cases in Z + if (z == 0) { + dR_dz = R[i.zp()] - R[i]; + dZ_dz = Z[i.zp()] - Z[i]; + + } else if (z == map_mesh.LocalNz - 1) { + dR_dz = R[i] - R[i.zm()]; + dZ_dz = Z[i] - Z[i.zm()]; + + } else { + dR_dz = 0.5 * (R[i.zp()] - R[i.zm()]); + dZ_dz = 0.5 * (Z[i.zp()] - Z[i.zm()]); + } - const BoutReal det = dR_dx * dZ_dz - dR_dz * dZ_dx; // Determinant of 2x2 matrix + const BoutReal det = dR_dx * dZ_dz - dR_dz * dZ_dx; // Determinant of 2x2 matrix - const BoutReal dR = R_prime[i] - R[i]; - const BoutReal dZ = Z_prime[i] - Z[i]; + const BoutReal dR = R_prime[i] - R[i]; + const BoutReal dZ = Z_prime[i] - Z[i]; - // Invert 2x2 matrix to get change in index - const BoutReal dx = (dZ_dz * dR - dR_dz * dZ) / det; - const BoutReal dz = (dR_dx * dZ - dZ_dx * dR) / det; - boundary->add_point(x, y, z, x + dx, y + 0.5 * offset, - z + dz, // Intersection point in local index space - 0.5 * dy[i], // Distance to intersection - PI // Right-angle intersection - ); - } + // Invert 2x2 matrix to get change in index + const BoutReal dx = (dZ_dz * dR - dR_dz * dZ) / det; + const BoutReal dz = (dR_dx * dZ - dZ_dx * dR) / det; + boundary->add_point(x, y, z, x + dx, y + 0.5 * offset, + z + dz, // Intersection point in local index space + 0.5 * dy[i], // Distance to intersection + PI // Right-angle intersection + ); } interp->setMask(boundary_mask); From 867ece4cb8899b529e3007336fefbe9d180d559a Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 17:10:42 +0000 Subject: [PATCH 33/41] Cache index offsets in FCI boundary loop --- src/mesh/parallel/fci.cxx | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index eff2175ac8..b23aaa51c2 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -156,10 +156,6 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe // Serial loop because call to BoundaryRegionPar::addPoint // (probably?) can't be done in parallel BOUT_FOR_SERIAL(i, xt_prime.getRegion("RGN_NOBNDRY")) { - const int x = i.x(); - const int y = i.y(); - const int z = i.z(); - // z is periodic, so make sure the z-index wraps around if (zperiodic) { zt_prime[i] = zt_prime[i] @@ -174,6 +170,10 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe continue; } + const auto x = i.x(); + const auto y = i.y(); + const auto z = i.z(); + //---------------------------------------- // Boundary stuff // @@ -195,22 +195,28 @@ FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRe // (dx,dz) is the change in (x,z) index along the field, // and the gradients dR/dx etc. are evaluated at (x,y,z) - const BoutReal dR_dx = 0.5 * (R[i.xp()] - R[i.xm()]); - const BoutReal dZ_dx = 0.5 * (Z[i.xp()] - Z[i.xm()]); + // Cache the offsets + const auto i_xp = i.xp(); + const auto i_xm = i.xm(); + const auto i_zp = i.zp(); + const auto i_zm = i.zm(); + + const BoutReal dR_dx = 0.5 * (R[i_xp] - R[i_xm]); + const BoutReal dZ_dx = 0.5 * (Z[i_xp] - Z[i_xm]); BoutReal dR_dz, dZ_dz; // Handle the edge cases in Z if (z == 0) { - dR_dz = R[i.zp()] - R[i]; - dZ_dz = Z[i.zp()] - Z[i]; + dR_dz = R[i_zp] - R[i]; + dZ_dz = Z[i_zp] - Z[i]; } else if (z == map_mesh.LocalNz - 1) { - dR_dz = R[i] - R[i.zm()]; - dZ_dz = Z[i] - Z[i.zm()]; + dR_dz = R[i] - R[i_zm]; + dZ_dz = Z[i] - Z[i_zm]; } else { - dR_dz = 0.5 * (R[i.zp()] - R[i.zm()]); - dZ_dz = 0.5 * (Z[i.zp()] - Z[i.zm()]); + dR_dz = 0.5 * (R[i_zp] - R[i_zm]); + dZ_dz = 0.5 * (Z[i_zp] - Z[i_zm]); } const BoutReal det = dR_dx * dZ_dz - dR_dz * dZ_dx; // Determinant of 2x2 matrix From eede1f0d648441c5684d521633810324ad5a98d9 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 17:28:22 +0000 Subject: [PATCH 34/41] Guard OptionsNetCDF test against legacy netcdf --- bin/bout-config.in | 7 +++++++ tests/integrated/test-options-netcdf/CMakeLists.txt | 1 + tests/integrated/test-options-netcdf/runtest | 2 +- tests/unit/sys/test_options_netcdf.cxx | 2 +- tools/pylib/boutconfig/__init__.py.cin | 1 + tools/pylib/boutconfig/__init__.py.in | 1 + 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bin/bout-config.in b/bin/bout-config.in index dcef809fdc..bfcea24c04 100755 --- a/bin/bout-config.in +++ b/bin/bout-config.in @@ -29,6 +29,7 @@ idlpath="@IDLCONFIGPATH@" pythonpath="@PYTHONCONFIGPATH@" has_netcdf="@BOUT_HAS_NETCDF@" +has_legacy_netcdf="@BOUT_HAS_LEGACY_NETCDF@" has_pnetcdf="@BOUT_HAS_PNETCDF@" has_hdf5="@BOUT_HAS_HDF5@" has_pvode="@BOUT_HAS_PVODE@" @@ -68,6 +69,7 @@ Available values for OPTION include: --python Python path --has-netcdf NetCDF file support + --has-legacy-netcdf Legacy NetCDF file support --has-pnetcdf Parallel NetCDF file support --has-hdf5 HDF5 file support --has-pvode PVODE solver support @@ -103,6 +105,7 @@ all() echo " --python -> $pythonpath" echo echo " --has-netcdf -> $has_netcdf" + echo " --has-legacy-netcdf -> $has_legacy_netcdf" echo " --has-pnetcdf -> $has_pnetcdf" echo " --has-hdf5 -> $has_hdf5" echo " --has-pvode -> $has_pvode" @@ -189,6 +192,10 @@ while test $# -gt 0; do echo $has_netcdf ;; + --has-legacy-netcdf) + echo $has_legacy_netcdf + ;; + --has-pnetcdf) echo $has_pnetcdf ;; diff --git a/tests/integrated/test-options-netcdf/CMakeLists.txt b/tests/integrated/test-options-netcdf/CMakeLists.txt index cfb7a3c016..f2d115d768 100644 --- a/tests/integrated/test-options-netcdf/CMakeLists.txt +++ b/tests/integrated/test-options-netcdf/CMakeLists.txt @@ -3,4 +3,5 @@ bout_add_integrated_test(test-options-netcdf USE_RUNTEST USE_DATA_BOUT_INP REQUIRES BOUT_HAS_NETCDF + CONFLICTS BOUT_HAS_LEGACY_NETCDF ) diff --git a/tests/integrated/test-options-netcdf/runtest b/tests/integrated/test-options-netcdf/runtest index 18d542f6a9..665c9b3b5f 100755 --- a/tests/integrated/test-options-netcdf/runtest +++ b/tests/integrated/test-options-netcdf/runtest @@ -2,7 +2,7 @@ # Note: This test requires NCDF4, whereas on Travis NCDF is used #requires: netcdf -#requires: not travis or fedora +#requires: not travis or fedora or legacy_netcdf from boututils.datafile import DataFile from boututils.run_wrapper import build_and_log, shell, launch diff --git a/tests/unit/sys/test_options_netcdf.cxx b/tests/unit/sys/test_options_netcdf.cxx index faedc4422b..27a7eef565 100644 --- a/tests/unit/sys/test_options_netcdf.cxx +++ b/tests/unit/sys/test_options_netcdf.cxx @@ -2,7 +2,7 @@ #include "bout/build_config.hxx" -#if BOUT_HAS_NETCDF +#if BOUT_HAS_NETCDF && !BOUT_HAS_LEGACY_NETCDF #include "gtest/gtest.h" diff --git a/tools/pylib/boutconfig/__init__.py.cin b/tools/pylib/boutconfig/__init__.py.cin index 138e1c6004..1ec4263b9e 100644 --- a/tools/pylib/boutconfig/__init__.py.cin +++ b/tools/pylib/boutconfig/__init__.py.cin @@ -17,6 +17,7 @@ config = { "idlpath": "@IDLCONFIGPATH@", "pythonpath": "@BOUT_PYTHONPATH@", "has_netcdf": "@BOUT_HAS_NETCDF@", + "has_legacy_netcdf": "@BOUT_HAS_LEGACY_NETCDF@", "has_pnetcdf": "OFF", "has_hdf5": "@BOUT_HAS_HDF5@", "has_pvode": "@BOUT_HAS_PVODE@", diff --git a/tools/pylib/boutconfig/__init__.py.in b/tools/pylib/boutconfig/__init__.py.in index 8d615f0389..f4d41658de 100644 --- a/tools/pylib/boutconfig/__init__.py.in +++ b/tools/pylib/boutconfig/__init__.py.in @@ -16,6 +16,7 @@ config = { "idlpath": "@IDLCONFIGPATH@", "pythonpath": "@PYTHONCONFIGPATH@", "has_netcdf": "@BOUT_HAS_NETCDF@", + "has_legacy_netcdf": "@BOUT_HAS_LEGACY_NETCDF@", "has_pnetcdf": "@BOUT_HAS_PNETCDF@", "has_hdf5": "@BOUT_HAS_HDF5@", "has_pvode": "@BOUT_HAS_PVODE@", From f4f63b64caa364fc12b3c219ee4eb4d35faa976a Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 17:33:25 +0000 Subject: [PATCH 35/41] Pass dy by reference --- src/mesh/parallel/fci.cxx | 2 +- src/mesh/parallel/fci.hxx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index b23aaa51c2..e34f519431 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -47,7 +47,7 @@ #include -FCIMap::FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset_, BoundaryRegionPar* boundary, +FCIMap::FCIMap(Mesh& mesh, const Field2D& dy, Options& options, int offset_, BoundaryRegionPar* boundary, bool zperiodic) : map_mesh(mesh), offset(offset_), boundary_mask(map_mesh), corner_boundary_mask(map_mesh) { diff --git a/src/mesh/parallel/fci.hxx b/src/mesh/parallel/fci.hxx index 4700893f30..3560d7fec9 100644 --- a/src/mesh/parallel/fci.hxx +++ b/src/mesh/parallel/fci.hxx @@ -44,8 +44,8 @@ class FCIMap { public: FCIMap() = delete; - FCIMap(Mesh& mesh, Field2D dy, Options& options, int offset, BoundaryRegionPar* boundary, bool - zperiodic); + FCIMap(Mesh& mesh, const Field2D& dy, Options& options, int offset, + BoundaryRegionPar* boundary, bool zperiodic); // The mesh this map was created on Mesh& map_mesh; @@ -71,7 +71,7 @@ public: class FCITransform : public ParallelTransform { public: FCITransform() = delete; - FCITransform(Mesh& mesh, Field2D dy, bool zperiodic = true, Options* opt = nullptr) + FCITransform(Mesh& mesh, const Field2D& dy, bool zperiodic = true, Options* opt = nullptr) : ParallelTransform(mesh, opt) { // check the coordinate system used for the grid data source From 31b015a1658d613a66c4de05902a1f3c8b3e728d Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 17:33:35 +0000 Subject: [PATCH 36/41] Add braces around conditional body --- src/mesh/parallel/fci.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index e34f519431..c0cb4e8c3e 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -161,8 +161,9 @@ FCIMap::FCIMap(Mesh& mesh, const Field2D& dy, Options& options, int offset_, Bou zt_prime[i] = zt_prime[i] - ncz * (static_cast(zt_prime[i] / static_cast(ncz))); - if (zt_prime[i] < 0.0) + if (zt_prime[i] < 0.0) { zt_prime[i] += ncz; + } } if (xt_prime[i] >= 0.0) { From 19c4c6400aab9265c945a8f8c17ea9ce0c44d1bf Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 6 Jan 2021 17:33:51 +0000 Subject: [PATCH 37/41] clang-format FCIMap constructor --- src/mesh/parallel/fci.cxx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/mesh/parallel/fci.cxx b/src/mesh/parallel/fci.cxx index c0cb4e8c3e..7c16dd27fb 100644 --- a/src/mesh/parallel/fci.cxx +++ b/src/mesh/parallel/fci.cxx @@ -47,22 +47,25 @@ #include -FCIMap::FCIMap(Mesh& mesh, const Field2D& dy, Options& options, int offset_, BoundaryRegionPar* boundary, - bool zperiodic) +FCIMap::FCIMap(Mesh& mesh, const Field2D& dy, Options& options, int offset_, + BoundaryRegionPar* boundary, bool zperiodic) : map_mesh(mesh), offset(offset_), boundary_mask(map_mesh), corner_boundary_mask(map_mesh) { TRACE("Creating FCIMAP for direction {:d}", offset); if (offset == 0) { - throw BoutException("FCIMap called with offset = 0; You probably didn't mean to do that"); + throw BoutException( + "FCIMap called with offset = 0; You probably didn't mean to do that"); } auto& interpolation_options = options["xzinterpolation"]; - interp = XZInterpolationFactory::getInstance().create(&interpolation_options, &map_mesh); + interp = + XZInterpolationFactory::getInstance().create(&interpolation_options, &map_mesh); interp->setYOffset(offset); - interp_corner = XZInterpolationFactory::getInstance().create(&interpolation_options, &map_mesh); + interp_corner = + XZInterpolationFactory::getInstance().create(&interpolation_options, &map_mesh); interp_corner->setYOffset(offset); // Index-space coordinates of forward/backward points @@ -122,8 +125,8 @@ FCIMap::FCIMap(Mesh& mesh, const Field2D& dy, Options& options, int offset_, Bou auto i_zplus = i.zp(); auto i_xzplus = i_zplus.xp(); - if ((xt_prime[i] < 0.0) || (xt_prime[i_xplus] < 0.0) || (xt_prime[i_xzplus] < 0.0) || - (xt_prime[i_zplus] < 0.0)) { + if ((xt_prime[i] < 0.0) || (xt_prime[i_xplus] < 0.0) || (xt_prime[i_xzplus] < 0.0) + || (xt_prime[i_zplus] < 0.0)) { // Hit a boundary corner_boundary_mask(i.x(), i.y(), i.z()) = true; From c80866d749798cae2b897ca1b5df7377f8373c07 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Wed, 6 Jan 2021 19:14:42 +0000 Subject: [PATCH 38/41] Skip test-io when using legacy netCDF interface test-io is expected to fail with the legacy interface. --- tests/integrated/test-io/runtest | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integrated/test-io/runtest b/tests/integrated/test-io/runtest index 33c347bc48..0482acb5a8 100755 --- a/tests/integrated/test-io/runtest +++ b/tests/integrated/test-io/runtest @@ -4,6 +4,7 @@ # Run the test, compare results against the benchmark # # requires: netcdf +# requires: not legacy_netcdf # cores: 4 from boututils.run_wrapper import build_and_log, shell, launch_safe From 24147f8f6db7fb7c6920e95118f4755664e181e4 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Wed, 6 Jan 2021 19:15:27 +0000 Subject: [PATCH 39/41] Add missing brackets in test-options-netcdf --- tests/integrated/test-options-netcdf/runtest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrated/test-options-netcdf/runtest b/tests/integrated/test-options-netcdf/runtest index 665c9b3b5f..6db3184baf 100755 --- a/tests/integrated/test-options-netcdf/runtest +++ b/tests/integrated/test-options-netcdf/runtest @@ -2,7 +2,7 @@ # Note: This test requires NCDF4, whereas on Travis NCDF is used #requires: netcdf -#requires: not travis or fedora or legacy_netcdf +#requires: not (travis or fedora or legacy_netcdf) from boututils.datafile import DataFile from boututils.run_wrapper import build_and_log, shell, launch From 7fb207aec08d1362000e19f7b234b6afb458e1eb Mon Sep 17 00:00:00 2001 From: John Omotani Date: Wed, 6 Jan 2021 19:17:24 +0000 Subject: [PATCH 40/41] Allow test-options-netcdf on Fedora It does work now. --- tests/integrated/test-options-netcdf/runtest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrated/test-options-netcdf/runtest b/tests/integrated/test-options-netcdf/runtest index 6db3184baf..b02f80ddaf 100755 --- a/tests/integrated/test-options-netcdf/runtest +++ b/tests/integrated/test-options-netcdf/runtest @@ -2,7 +2,7 @@ # Note: This test requires NCDF4, whereas on Travis NCDF is used #requires: netcdf -#requires: not (travis or fedora or legacy_netcdf) +#requires: not legacy_netcdf from boututils.datafile import DataFile from boututils.run_wrapper import build_and_log, shell, launch From bb81c0db4a8e4771767f2bc7f26bcc58357c6f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20L=C3=B8iten?= Date: Thu, 7 Jan 2021 21:19:48 +0100 Subject: [PATCH 41/41] Removed bout_runners from repo, and redirected to project site --- README.md | 10 +- bin/bout-pylib-cmd-to-bin | 2 +- examples/bout_runners_example/.gitignore | 12 - .../bout_runners_example/1-basic_driver.py | 11 - .../10-restart_with_resize.py | 49 - .../11-restart_with_scan.py | 68 - .../12-PBS_restart_with_waiting.py | 40 - .../13-restart_w_add_noise.py | 53 - .../2-run_with_simple_post_processing.py | 23 - .../3-override_BOUTinp.py | 80 - .../4-run_with_combinations.py | 40 - .../5-run_with_grid_files.py | 23 - ...ith_MMS_post_processing_specify_numbers.py | 58 - ...-run_with_MMS_post_processing_grid_file.py | 59 - .../bout_runners_example/7-basic_PBS_run.py | 12 - .../8-PBS_run_extra_option.py | 25 - ...-PBS_with_MMS_post_processing_grid_file.py | 73 - examples/bout_runners_example/MMS/BOUT.inp | 119 - examples/bout_runners_example/README.md | 48 - examples/bout_runners_example/data/BOUT.inp | 122 - .../bout_runners_example/diffusion_3D.cxx | 80 - examples/bout_runners_example/makefile | 6 - .../pre_and_post_processing/__init__.py | 12 - .../pre_and_post_processing/grid_generator.py | 99 - .../post_processing_MMS.py | 258 - .../post_processing_show_the_data.py | 36 - .../restart_from_func.py | 72 - manual/sphinx/figs/folder_tree.pdf | Bin 18157 -> 0 bytes manual/sphinx/figs/folder_tree.png | Bin 187787 -> 0 bytes manual/sphinx/user_docs/python.rst | 49 +- manual/sphinx/user_docs/running_bout.rst | 19 +- tools/pylib/bout_runners/README.md | 4 - tools/pylib/bout_runners/__init__.py | 4 - tools/pylib/bout_runners/bout_runners.py | 4418 ----------------- 34 files changed, 23 insertions(+), 5961 deletions(-) delete mode 100644 examples/bout_runners_example/.gitignore delete mode 100755 examples/bout_runners_example/1-basic_driver.py delete mode 100644 examples/bout_runners_example/10-restart_with_resize.py delete mode 100755 examples/bout_runners_example/11-restart_with_scan.py delete mode 100755 examples/bout_runners_example/12-PBS_restart_with_waiting.py delete mode 100755 examples/bout_runners_example/13-restart_w_add_noise.py delete mode 100644 examples/bout_runners_example/2-run_with_simple_post_processing.py delete mode 100644 examples/bout_runners_example/3-override_BOUTinp.py delete mode 100644 examples/bout_runners_example/4-run_with_combinations.py delete mode 100644 examples/bout_runners_example/5-run_with_grid_files.py delete mode 100755 examples/bout_runners_example/6a-run_with_MMS_post_processing_specify_numbers.py delete mode 100644 examples/bout_runners_example/6b-run_with_MMS_post_processing_grid_file.py delete mode 100644 examples/bout_runners_example/7-basic_PBS_run.py delete mode 100644 examples/bout_runners_example/8-PBS_run_extra_option.py delete mode 100644 examples/bout_runners_example/9-PBS_with_MMS_post_processing_grid_file.py delete mode 100644 examples/bout_runners_example/MMS/BOUT.inp delete mode 100644 examples/bout_runners_example/README.md delete mode 100644 examples/bout_runners_example/data/BOUT.inp delete mode 100644 examples/bout_runners_example/diffusion_3D.cxx delete mode 100644 examples/bout_runners_example/makefile delete mode 100755 examples/bout_runners_example/pre_and_post_processing/__init__.py delete mode 100755 examples/bout_runners_example/pre_and_post_processing/grid_generator.py delete mode 100755 examples/bout_runners_example/pre_and_post_processing/post_processing_MMS.py delete mode 100644 examples/bout_runners_example/pre_and_post_processing/post_processing_show_the_data.py delete mode 100644 examples/bout_runners_example/pre_and_post_processing/restart_from_func.py delete mode 100644 manual/sphinx/figs/folder_tree.pdf delete mode 100644 manual/sphinx/figs/folder_tree.png delete mode 100644 tools/pylib/bout_runners/README.md delete mode 100644 tools/pylib/bout_runners/__init__.py delete mode 100755 tools/pylib/bout_runners/bout_runners.py diff --git a/README.md b/README.md index 6a3f6118bd..b136fa8f79 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ equations appearing in a readable form. For example, the following set of equations for magnetohydrodynamics (MHD): -![ddt_rho](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Crho%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%5Crho%20-%20%5Crho%5Cnabla%5Ccdot%5Cmathbf%7Bv%7D) -![ddt_p](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20p%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%20p%20-%20%5Cgamma%20p%5Cnabla%5Ccdot%5Cmathbf%7Bv%7D) -![ddt_v](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bv%7D%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%5Cmathbf%7Bv%7D%20+%20%5Cfrac%7B1%7D%7B%5Crho%7D%28-%5Cnabla%20p%20+%20%28%5Cnabla%5Ctimes%5Cmathbf%7BB%7D%29%5Ctimes%5Cmathbf%7BB%7D%29) -![ddt_B](http://latex.codecogs.com/png.latex?%7B%7B%5Cfrac%7B%5Cpartial%20%5Cmathbf%7BB%7D%7D%7B%5Cpartial%20t%7D%7D%7D%20%3D%20%5Cnabla%5Ctimes%28%5Cmathbf%7Bv%7D%5Ctimes%5Cmathbf%7BB%7D%29) +![ddt_rho](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Crho%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%5Crho%20-%20%5Crho%5Cnabla%5Ccdot%5Cmathbf%7Bv%7D) +![ddt_p](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20p%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%20p%20-%20%5Cgamma%20p%5Cnabla%5Ccdot%5Cmathbf%7Bv%7D) +![ddt_v](http://latex.codecogs.com/png.latex?%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bv%7D%7D%7B%5Cpartial%20t%7D%20%3D%20-%5Cmathbf%7Bv%7D%5Ccdot%5Cnabla%5Cmathbf%7Bv%7D%20+%20%5Cfrac%7B1%7D%7B%5Crho%7D%28-%5Cnabla%20p%20+%20%28%5Cnabla%5Ctimes%5Cmathbf%7BB%7D%29%5Ctimes%5Cmathbf%7BB%7D%29) +![ddt_B](http://latex.codecogs.com/png.latex?%7B%7B%5Cfrac%7B%5Cpartial%20%5Cmathbf%7BB%7D%7D%7B%5Cpartial%20t%7D%7D%7D%20%3D%20%5Cnabla%5Ctimes%28%5Cmathbf%7Bv%7D%5Ctimes%5Cmathbf%7BB%7D%29) can be written simply as: @@ -138,7 +138,6 @@ This directory contains * **boutdata** Routines to simplify accessing BOUT++ output * **boututils** Some useful routines for accessing and plotting data - * **bout_runners** A python wrapper to submit several runs at once (either on a normal computer, or through a PBS system) * **post_bout** Routines for post processing in BOUT++ * **slab** IDL routine for grid generation of a slab @@ -185,3 +184,4 @@ BOUT++ links by default with some GPL licensed libraries. Thus if you compile BOUT++ with any of them, BOUT++ will automatically be licensed as GPL. Thus if you want to use BOUT++ with GPL non-compatible code, make sure to compile without GPLed code. + diff --git a/bin/bout-pylib-cmd-to-bin b/bin/bout-pylib-cmd-to-bin index 850aa6daf8..16217544df 100755 --- a/bin/bout-pylib-cmd-to-bin +++ b/bin/bout-pylib-cmd-to-bin @@ -250,7 +250,6 @@ if __name__ == "__main__": print("Please wait, scanning modules ...") x=boutmodules(["boutcore", "boutdata", - "bout_runners", "boututils", "post_bout", "zoidberg"]) @@ -292,3 +291,4 @@ if __name__ == "__main__": print("Creating failed. To rerun and overwrite the file without asking run:") print("%s %s %s %s -f"%(sys.argv[0],mod,fun,name)) raise + diff --git a/examples/bout_runners_example/.gitignore b/examples/bout_runners_example/.gitignore deleted file mode 100644 index f06dccb407..0000000000 --- a/examples/bout_runners_example/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -data/n* -data/c* -data/t* -data/d* -MMS/MMS-mms_True* -MMS/mms_True* -*run_log* -*tmp* -*.log -*.err -diffusion_3D -grid_files/ diff --git a/examples/bout_runners_example/1-basic_driver.py b/examples/bout_runners_example/1-basic_driver.py deleted file mode 100755 index 4e963c2a73..0000000000 --- a/examples/bout_runners_example/1-basic_driver.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3d_diffusion with the options given in BOUT.inp""" - -from bout_runners import basic_runner - -# Create the instance -my_runs = basic_runner() - -# Do the run -my_runs.execute_runs() diff --git a/examples/bout_runners_example/10-restart_with_resize.py b/examples/bout_runners_example/10-restart_with_resize.py deleted file mode 100644 index 8329c0239e..0000000000 --- a/examples/bout_runners_example/10-restart_with_resize.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python - -"""Driver which resizes the grid after restart""" - -from pre_and_post_processing.post_processing_show_the_data import show_the_data -from bout_runners import basic_runner - -# Initial run -# =========================================================================== -init_run = basic_runner(nz = 8) - -dmp_folder, _ =\ - init_run.execute_runs(\ - post_processing_function = show_the_data,\ - # This function will be called every time after - # performing a run - post_process_after_every_run = True,\ - # Below are the kwargs arguments being passed to - # show_the_data - t = slice(0,None),\ - x = 1,\ - y = slice(0,None),\ - z = slice(0,None)\ - ) -# =========================================================================== - - -# Restart the run after resizing the grid -# =========================================================================== -restart_run = basic_runner(restart = "overwrite" ,\ - restart_from = dmp_folder[0],\ - nx = 22 ,\ - ny = 22 ,\ - nz = 16 ,\ - ) - -restart_run.execute_runs(\ - post_processing_function = show_the_data,\ - # This function will be called every time after - # performing a run - post_process_after_every_run = True,\ - # Below are the kwargs arguments being passed to - # show_the_data - t = slice(0,None),\ - x = 1,\ - y = slice(0,None),\ - z = slice(0,None)\ - ) -# =========================================================================== diff --git a/examples/bout_runners_example/11-restart_with_scan.py b/examples/bout_runners_example/11-restart_with_scan.py deleted file mode 100755 index 4fad630453..0000000000 --- a/examples/bout_runners_example/11-restart_with_scan.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python - -"""Driver which restarts a scan, given a restart function""" - -from pre_and_post_processing.post_processing_show_the_data import show_the_data -from pre_and_post_processing.restart_from_func import restart_from_func -from bout_runners import basic_runner - -scan = (("cst", "D_perp", (1.0,5.5)),\ - ("cst", "D_par", (1.5,2.5)) - ) - -# Given that the runs has already been performed -only_post_process = False - -# Initial runs -# =========================================================================== -init_run = basic_runner(additional = scan) - -dmp_folder, _ =\ - init_run.execute_runs(\ - post_processing_function = show_the_data,\ - # This function will be called every time after - # performing a run - post_process_after_every_run = True,\ - # Below are the kwargs arguments being passed to - # show_the_data - t = slice(0,None),\ - x = 1,\ - y = slice(0,None),\ - z = slice(0,None)\ - ) - -one_of_the_restart_paths_in_scan = dmp_folder[0] -# =========================================================================== - - -# Restart the scan -# =========================================================================== -if only_post_process: - restart = None -else: - restart = "overwrite" - -restart_run = basic_runner(nout = 5 ,\ - restart = restart ,\ - restart_from = restart_from_func,\ - additional = scan ,\ - ) - -restart_run.execute_runs(\ - post_processing_function = show_the_data,\ - # This function will be called every time after - # performing a run - post_process_after_every_run = True,\ - # Below are the kwargs arguments being passed to - # show_the_data - t = slice(0,None),\ - x = 1,\ - y = slice(0,None),\ - z = slice(0,None),\ - # Below are the kwargs given to the - # restart_from_func - one_of_the_restart_paths_in_scan =\ - one_of_the_restart_paths_in_scan,\ - scan_parameters = ["D_perp", "D_par"],\ - ) -# =========================================================================== diff --git a/examples/bout_runners_example/12-PBS_restart_with_waiting.py b/examples/bout_runners_example/12-PBS_restart_with_waiting.py deleted file mode 100755 index 86a21836b2..0000000000 --- a/examples/bout_runners_example/12-PBS_restart_with_waiting.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python - -"""Driver which restarts a scan, given a restart function""" - -from bout_runners import PBS_runner -from pre_and_post_processing.restart_from_func import restart_from_func - -scan = (("cst", "D_perp", (1.0,5.5)),\ - ("cst", "D_par", (1.5,2.5)) - ) - -# Initial runs -# =========================================================================== -init_run = PBS_runner(additional = scan) - -dmp_folder, PBS_ids =\ - init_run.execute_runs() - -one_of_the_restart_paths_in_scan = dmp_folder[0] -# =========================================================================== - - -# Restart the scan -# =========================================================================== -restart_run = PBS_runner(nout = 5 ,\ - restart = "overwrite" ,\ - restart_from = restart_from_func,\ - additional = scan ,\ - ) - -restart_run.execute_runs(\ - # Declare dependencies - job_dependencies = PBS_ids,\ - # Below are the kwargs given to the - # restart_from_func - one_of_the_restart_paths_in_scan =\ - one_of_the_restart_paths_in_scan,\ - scan_parameters = ("D_perp", "D_par"),\ - ) -# =========================================================================== diff --git a/examples/bout_runners_example/13-restart_w_add_noise.py b/examples/bout_runners_example/13-restart_w_add_noise.py deleted file mode 100755 index 9d68430b74..0000000000 --- a/examples/bout_runners_example/13-restart_w_add_noise.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python - -"""Driver which resizes the grid after restart""" - -from pre_and_post_processing.post_processing_show_the_data import show_the_data -from bout_runners import basic_runner - -# Initial run -# =========================================================================== -init_run = basic_runner(nz = 16) - -dmp_folder, _ =\ - init_run.execute_runs(\ - post_processing_function = show_the_data,\ - # This function will be called every time after - # performing a run - post_process_after_every_run = True,\ - # Below are the kwargs arguments being passed to - # show_the_data - t = slice(0,None),\ - x = 1,\ - y = slice(0,None),\ - z = slice(0,None)\ - ) -# =========================================================================== - - -# Restart the run after resizing the grid -# =========================================================================== -restart_run = basic_runner(restart = "overwrite" ,\ - restart_from = dmp_folder[0],\ - nx = 22 ,\ - ny = 22 ,\ - nz = 16 ,\ - # NOTE: This amount if noise is large - # relative to the backgroun, and is - # just added for illustrative purposes - add_noise = {"n": 5e-4} ,\ - ) - -restart_run.execute_runs(\ - post_processing_function = show_the_data,\ - # This function will be called every time after - # performing a run - post_process_after_every_run = True,\ - # Below are the kwargs arguments being passed to - # show_the_data - t = slice(0,None),\ - x = 1,\ - y = slice(0,None),\ - z = slice(0,None)\ - ) -# =========================================================================== diff --git a/examples/bout_runners_example/2-run_with_simple_post_processing.py b/examples/bout_runners_example/2-run_with_simple_post_processing.py deleted file mode 100644 index aca001b972..0000000000 --- a/examples/bout_runners_example/2-run_with_simple_post_processing.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3d_diffusion, and calls the function show_the_data when done""" - -from pre_and_post_processing.post_processing_show_the_data import show_the_data -from bout_runners import basic_runner - - -my_runs = basic_runner() - -# Put this in the post-processing function -my_runs.execute_runs(\ - post_processing_function = show_the_data,\ - # This function will be called every time after - # performing a run - post_process_after_every_run = True,\ - # Below are the kwargs arguments being passed to - # show_the_data - t = slice(0,None),\ - x = 1,\ - y = slice(0,None),\ - z = slice(0,None)\ - ) diff --git a/examples/bout_runners_example/3-override_BOUTinp.py b/examples/bout_runners_example/3-override_BOUTinp.py deleted file mode 100644 index a1fccfd4df..0000000000 --- a/examples/bout_runners_example/3-override_BOUTinp.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3d_diffusion with other options than given in BOUT.inp""" - -from bout_runners import basic_runner - -my_runs = basic_runner(\ - # Number of processors - nproc = 2,\ - # Directory of the inp file - directory = 'data',\ - # Set the solver option - solver = 'rk4',\ - mms = False,\ - atol = 1.0e-8,\ - rtol = 1.0e-8,\ - mxstep = 10000000,\ - # Spatial domain option - nx = 19,\ - ny = 17,\ - nz = 16,\ - # These can be set if needed - zperiod = None,\ - zmin = None,\ - zmax = None,\ - # These are not set here, but the code handles them - # internally - dx = None,\ - dy = None,\ - dz = None,\ - # The same as in BOUT.inp - # (Setting them to a different value doesn't make much sense) - MXG = 1,\ - MYG = 1,\ - # These can also be set - ixseps1 = None,\ - ixseps2 = None,\ - jyseps1_1 = None,\ - jyseps1_2 = None,\ - jyseps2_1 = None,\ - jyseps2_2 = None,\ - symGlobX = None,\ - symGlobY = None,\ - # The differencing option - ddx_first = 'C2',\ - ddx_second = 'C2',\ - ddx_upwind = 'U1',\ - ddx_flux = 'SPLIT',\ - ddy_first = 'C2',\ - ddy_second = 'C2',\ - ddy_upwind = 'U1',\ - ddy_flux = 'SPLIT',\ - ddz_first = 'FFT',\ - ddz_second = 'FFT',\ - ddz_upwind = 'U4',\ - ddz_flux = 'SPLIT',\ - # Temporal domain option - nout = 11,\ - timestep = 0.02,\ - # Additional options - # (An example ofadditional options run in series is found in - # 6a-run_with_MMS_post_processing_specify_numbers.py) - # tuple[0] - section name - # tuple[1] - variable name for the section - # tuple[2] - value of the variable name in the section - additional = (('cst','D_perp',5), ('cst', 'D_par', 0.5)),\ - # Can set this to overwrite or append - restart = None,\ - # Will copy the source file - cpy_source = True,\ - # Will remake the file - make = True,\ - # Code will return an error if False, due to the mismatch - # between nx, ny and nproc - allow_size_modification = True) - -my_runs.execute_runs(\ - # Remove eventually old data - remove_old = True\ - ) diff --git a/examples/bout_runners_example/4-run_with_combinations.py b/examples/bout_runners_example/4-run_with_combinations.py deleted file mode 100644 index 15d4943f55..0000000000 --- a/examples/bout_runners_example/4-run_with_combinations.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3d_diffusion for several different combinations of -the input. """ - -from bout_runners import basic_runner - -# With a few exceptions: All variables in the constructor can be given -# as an iterable. -# When execute_runs is called, bout_runners will run all combinations of -# the member data -my_runs = basic_runner(\ - # nx, ny and nz must be of the same size as they constitute - # one "part" of the combination (i.e. there will be no - # internal combination between the elements in nx, ny and - # nz) - nx = (9, 18),\ - ny = (6, 12),\ - nz = (8, 16),\ - # nout and timestep must be of the same dimension for the - # same reason as mention above - nout = (10, 11, 12),\ - timestep = (0.01, 0.01, 0.01),\ - # The differencing option - ddz_second = ('FFT','C2'),\ - # Additional options - additional = (('cst','D_perp',(1, 2)))\ - ) - -# Execute all the runs -# 2 runs for each combination of nx, ny, nz -# 3 runs for each combination of nout and timestep -# 2 runs for each combination in ddz_second -# 2 runs for each combination of cst:const:value -# In total: 24 runs -my_runs.execute_runs() - -# NOTE: If you feel that the explanation of the combinations was bad, -# have a look at the last lines of data/run_log.txt to see what -# runs have been performed after this run diff --git a/examples/bout_runners_example/5-run_with_grid_files.py b/examples/bout_runners_example/5-run_with_grid_files.py deleted file mode 100644 index 82365a759a..0000000000 --- a/examples/bout_runners_example/5-run_with_grid_files.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3D_diffusion using grid files.""" - -from bout_runners import basic_runner -from pre_and_post_processing.grid_generator import generate_grid -import os - -# Generate a grid -file_name = os.path.join("grid_files","3D_diffusion_grid.nc") -generate_grid(file_name = file_name,\ - inp_path = "data") - -my_runs = basic_runner(\ - grid_file = file_name,\ - # Copy the grid file - cpy_grid = True,\ - # Set the flag in 3D_diffusion that a grid file will be - # used - additional = ('flags', 'use_grid', 'true')\ - ) - -my_runs.execute_runs() diff --git a/examples/bout_runners_example/6a-run_with_MMS_post_processing_specify_numbers.py b/examples/bout_runners_example/6a-run_with_MMS_post_processing_specify_numbers.py deleted file mode 100755 index b50c60cb28..0000000000 --- a/examples/bout_runners_example/6a-run_with_MMS_post_processing_specify_numbers.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3D_diffusion and performs a MMS test by specifying -the grids by hand (see 6b-run_with_MMS_post_processing_grid_file.py to -see how to do the same is done by using grid files)""" - -from bout_runners import basic_runner -from pre_and_post_processing.post_processing_MMS import perform_MMS_test - -my_runs = basic_runner(\ - nproc = 1,\ - # Set the directory - directory = 'MMS',\ - # Set the time domain - nout = 1,\ - timestep = 1,\ - # Set mms to true - mms = True,\ - # Set the spatial domain - nx = (5, 8, 16),\ - ny = (5, 8, 16),\ - nz = (4, 8, 16),\ - # Additional (put here to illustrate the sorting) - series_add = (('cst','D_par',(1,2)), ('cst','D_perp',(0.5,1))),\ - # Since we would like to do a MMS test, we would like to run - # the runs in a particular order. In this example, we would - # like to run all the possible spatial variables before - # doing the test. Hence we would like the spatial domain - # option to be the fastest varying. - # Since we have put post_process_after_every_run = False in - # the run function below, the processing function being - # called when all possibilities of the fastest variable has - # been run. - sort_by = 'spatial_domain'\ - # Some additional sorting examples: - # - # This returns an error, stating the sorting possibilities - # (which will depend on the member data of this object) - # sort_by = 'uncomment_me'\ - # - # In this example cst:D_par will be the fastest varying - # variable, followed by the spatial_domain. The post - # processing function will be called when all possibilities - # of these variables has been run - # sort_by = ('cst:D_par', 'spatial_domain')\ - ) - -# Put this in the post-processing function -my_runs.execute_runs(\ - post_processing_function = perform_MMS_test,\ - # As we need several runs in order to perform the - # MMS test, this needs to be false - post_process_after_every_run = False,\ - # Below are the kwargs arguments being passed to - # perform_MMS_test - extension = 'png',\ - show_plot = True - ) diff --git a/examples/bout_runners_example/6b-run_with_MMS_post_processing_grid_file.py b/examples/bout_runners_example/6b-run_with_MMS_post_processing_grid_file.py deleted file mode 100644 index 3da96197db..0000000000 --- a/examples/bout_runners_example/6b-run_with_MMS_post_processing_grid_file.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3D_diffusion and performs a MMS test by specifying -the grids by using grid_files (see -6a-run_with_MMS_post_processing_specify_numbers.py to see how to do the -same is done by specifying the grid manually)""" - -from bout_runners import basic_runner -from pre_and_post_processing.post_processing_MMS import perform_MMS_test -from pre_and_post_processing.grid_generator import generate_grid -import os - -# Generate the grids -# Specify the grid dimensions -grid_numbers = (5, 8, 16) -# Make an append able list -grid_files = [] -for grid_number in grid_numbers: - file_name = os.path.join("grid_files","grid_file_{}.nc".format(grid_number)) - # Generate the grids - generate_grid(nx = grid_number,\ - ny = grid_number,\ - nz = grid_number,\ - inp_path = 'MMS' ,\ - file_name = file_name) - # Append the grid_files list - grid_files.append(file_name) - -my_runs = basic_runner(\ - nproc = 1,\ - # Set the directory - directory = 'MMS',\ - # Set the time domain - nout = 1,\ - timestep = 1,\ - # Set mms to true - mms = True,\ - # Set the spatial domain - grid_file = grid_files,\ - # Set the flag in 3D_diffusion that a grid file will be - # used - additional = ('flags','use_grid','true'),\ - # Copy the grid file - cpy_grid = True,\ - # Sort the runs by the spatial domain - sort_by = 'grid_file' - ) - -# Put this in the post-processing function -my_runs.execute_runs(\ - post_processing_function = perform_MMS_test,\ - # As we need several runs in order to perform the - # MMS test, this needs to be false - post_process_after_every_run = False,\ - # Below are the kwargs arguments being passed to - # perform_MMS_test - extension = 'png',\ - show_plot = True\ - ) diff --git a/examples/bout_runners_example/7-basic_PBS_run.py b/examples/bout_runners_example/7-basic_PBS_run.py deleted file mode 100644 index 8afd4bb247..0000000000 --- a/examples/bout_runners_example/7-basic_PBS_run.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python - -""" -Driver which runs 3D_diffusion by submitting a job to a Portable Batch System -(PBS) -""" - -from bout_runners import PBS_runner - -my_runs = PBS_runner() - -my_runs.execute_runs() diff --git a/examples/bout_runners_example/8-PBS_run_extra_option.py b/examples/bout_runners_example/8-PBS_run_extra_option.py deleted file mode 100644 index a447e09117..0000000000 --- a/examples/bout_runners_example/8-PBS_run_extra_option.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3D_diffusion by submitting a job to a PBS using -additional options.""" - -from bout_runners import PBS_runner - -my_runs = PBS_runner(\ - # Although nproc is a member of basic_runner, it is used - # together with BOUT_nodes and BOUT_ppn - nproc = 4,\ - # Number of nodes to be used on the cluster - BOUT_nodes = 1,\ - # Specifying processor per node - BOUT_ppn = 4,\ - # The maximum walltime of the run - BOUT_walltime = '0:15:00',\ - # Specify the queue to submit to (if any) - BOUT_queue = None,\ - # Specify a mail to be noticed when the run has finished - BOUT_mail = None\ - ) - -# Put this in the post-processing function -my_runs.execute_runs(remove_old = True) diff --git a/examples/bout_runners_example/9-PBS_with_MMS_post_processing_grid_file.py b/examples/bout_runners_example/9-PBS_with_MMS_post_processing_grid_file.py deleted file mode 100644 index 2b49971005..0000000000 --- a/examples/bout_runners_example/9-PBS_with_MMS_post_processing_grid_file.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python - -"""Driver which runs 3D_diffusion by submitting a job to a PBS and -performs a MMS test by specifying the grids by using grid_files.""" - -from bout_runners import PBS_runner -from pre_and_post_processing.post_processing_MMS import perform_MMS_test -from pre_and_post_processing.grid_generator import generate_grid -import os - -# Generate the grids -# Specify the grid dimensions -grid_numbers = (8, 16, 32) -# Make an append able list -grid_files = [] -for grid_number in grid_numbers: - file_name = os.path.join("grid_files","grid_file_{}.nc".format(grid_number)) - # Generate the grids - generate_grid(nx = grid_number,\ - ny = grid_number,\ - nz = grid_number,\ - inp_path = 'MMS' ,\ - file_name = file_name) - grid_files.append(file_name) - -my_runs = PBS_runner(\ - # Specify the numbers used for the BOUT runs - nproc = 4,\ - BOUT_nodes = 1,\ - BOUT_ppn = 4,\ - BOUT_walltime = '0:15:00',\ - BOUT_queue = None,\ - BOUT_mail = None,\ - # Specify the numbers used for the post processing - post_process_nproc = 1,\ - post_process_nodes = 1,\ - post_process_ppn = 1,\ - post_process_walltime = '0:05:00',\ - post_process_queue = None,\ - post_process_mail = None,\ - # Set the directory - directory = 'MMS',\ - # Set the time domain - nout = 1,\ - timestep = 1,\ - # Set mms to true - mms = True,\ - # Set the spatial domain - grid_file = grid_files,\ - # Set the flag in 3D_diffusion that a grid file will be - # used - additional = ('flags','use_grid','true'),\ - # Add some additional option - series_add = (('cst','D_par' ,(1,2)),\ - ('cst','D_perp',(0.5,1))),\ - # Copy the grid file - cpy_grid = True,\ - # Sort the runs by the spatial domain - sort_by = 'grid_file' - ) - -# Put this in the post-processing function -my_runs.execute_runs(\ - remove_old = True,\ - post_processing_function = perform_MMS_test,\ - # As we need several runs in order to perform the - # MMS test, this needs to be false - post_process_after_every_run = False,\ - # Below are the kwargs arguments being passed to - # perform_MMS_test - extension = 'png',\ - show_plot = False\ - ) diff --git a/examples/bout_runners_example/MMS/BOUT.inp b/examples/bout_runners_example/MMS/BOUT.inp deleted file mode 100644 index 8dd5d141cc..0000000000 --- a/examples/bout_runners_example/MMS/BOUT.inp +++ /dev/null @@ -1,119 +0,0 @@ -# -# Input file for "bout_runners_example/MMS" -# - -# Root option -############################################################################### -nout = 10 # Number of output timesteps -timestep = 0.01 # Time between outputs - -dump_format="nc" # Write NetCDF format files - -# Setting the z coordinate -ZMIN = 0.0 -ZMAX = 1.0 # dz = 2*pi(ZMAX - ZMIN)/(MZ - 1) - -# Number of guard cells -MXG = 1 -MYG = 1 -############################################################################### - - -# Mesh option -############################################################################### -[mesh] -# Puts the boundaries half a step outside the last grid points -symmetricGlobalY=true -symmetricGlobalX=true - -# The spatial dimension -nx = 18 -ny = 16 -nz = 16 - -# Position of the separatrix (-1 is non periodic, >ny is periodic) -# --------Non-periodic---------- -ixseps1 = -1 -ixseps2 = -1 -# ------------------------------ -############################################################################### - - -# Methods option -############################################################################### -# Methods used for the radial (x) derivative terms -[mesh:ddx] -second = C2 # d^2/dx^2 (f) - -# Methods used for parallel (y) derivative terms -[mesh:ddy] -second = C2 # d^2/dy^2 (f) - -#Methods used for the azimuthal (z) derivative terms -[mesh:ddz] -second = FFT # d^2/dz^2 (f) -############################################################################### - - -# Solver settings -############################################################################### -[solver] -type = pvode # Which solver to use (cvode should be same as pvode) -mms = false # false by default - -atol = 1.0e-7 # absolute tolerance -rtol = 1.0e-7 # relative tolerance - -# Max allowed iterations in one step -mxstep = 100000000 -############################################################################### - - -# Specifying the output -############################################################################### -[output] -floats = false # floats = false => output in double -############################################################################### - - -# Additional options -############################################################################### -# Geometry -# ----------------------------------------------------------------------------- -[geom] -Lx = 1.2 # The length of x from boundary to boundary -Ly = 2.0 # The length of y from boundary to boundary -# Setting the spatial variables -yl = y * geom:Ly / (2.0*pi) #y in range [0,Ly] -xl = x * geom:Lx #x in range [0,Lx] -# ----------------------------------------------------------------------------- - -# Constants -# ----------------------------------------------------------------------------- -[cst] -D_par = 1.0 # Parallel diffusion constant -D_perp = 2.0 # Perpendicular diffusion constant -# ----------------------------------------------------------------------------- - -# Flags -# ----------------------------------------------------------------------------- -[flags] -use_grid = false # Whether or not to read from a grid file -# ----------------------------------------------------------------------------- - -# The particle density -# ----------------------------------------------------------------------------- -[n] -# Scaling -scale = 1.0 - -# Source and solution for MMS -solution = t^(2.5) + sin(3+geom:xl^2) + exp(0.5*geom:yl) + 4*cos(z) -source = 2.5*t^(1.5) - cst:D_perp*(2*(cos(geom:xl^2+3)-2*geom:xl^2*sin(geom:xl^2+3)) - 4*cos(z)) - cst:D_par*(0.25*exp(0.5*geom:yl)) - - -# Boundary conditions -# Set the boundary to the initial condition -bndry_all = dirichlet_o4(n:solution) -# ----------------------------------------------------------------------------- -############################################################################## diff --git a/examples/bout_runners_example/README.md b/examples/bout_runners_example/README.md deleted file mode 100644 index bdab947004..0000000000 --- a/examples/bout_runners_example/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# bout_runners_example - -A tutorial on how to use `bout_runners`. - -Extra documentation of `bout_runners` can be found in the -docstring of `bout_runners`. - -## Contents -### The program: - -* `diffusion_3D.cxx` - Simulates 3D diffusion -* `make` - The corresponding make file (notice that the `bout_runner` - calls this, so no make is necessary for the `bout_runner` to work). - -### Folders: - -* `data` - Contains a `BOUT.inp` file -* `MMS` - Contains the `BOUT.inp` file for the MMS runs -* `pre_and_post_processing` - Contains the grid generator and the - post processing functions - -### Examples: - -* `1-basic_driver.py` - How to use `bout_runners` for a basic run -* `2-run_with_simple_post_processing.py` - How couple `bout_runners` - to a post processing routine -* `3-override_BOUTinp.py` - Use `bout_runners` to override settings - in `BOUT.inp` -* `4-run_with_combinations.py` - Use `bout_runners` to change - several settings at once -* `5-run_with_grid_files.py` - Run `bout_runners` with a grid file -* `6a-run_with_MMS_post_processing_specify_numbers.py` - Use - `bout_runners` to MMS the program -* `6b-run_with_MMS_post_processing_grid_file.py` - The same as `6a`, - but using a gride file -* `7-basic_PBS_run.py` - Submit jobs to a cluster using - `bout_runners` -* `8-PBS_run_extra_option.py` - Set the `PBS` option using - `bout_runners` -* `9-PBS_with_MMS_post_processing_grid_file.py` - As 6b, but on a - cluster -* `10-restart_with_resize.py` - Restart a run and re-size the grid - using `bout_runners` -* `11-restart_with_scan.py` - Use `bout_runners` to restart runs - belonging to a parameter scan -* `12-PBS_restart_with_waiting.py` - Runs where the restart waits for jobs to - finish -* `13-restart_w_add_noise.py` - Adds noise to a restart run diff --git a/examples/bout_runners_example/data/BOUT.inp b/examples/bout_runners_example/data/BOUT.inp deleted file mode 100644 index dd530806d6..0000000000 --- a/examples/bout_runners_example/data/BOUT.inp +++ /dev/null @@ -1,122 +0,0 @@ -# -# Input file for "bout_runners_example/data" -# - -# Root option -############################################################################### -nout = 10 # Number of output timesteps -timestep = 0.01 # Time between outputs - -dump_format="nc" # Write NetCDF format files - -# Setting the z coordinate -ZMIN = 0.0 -ZMAX = 1.0 # dz = 2*pi(ZMAX - ZMIN)/(MZ - 1) - -# Number of guard cells -MXG = 1 -MYG = 1 -############################################################################### - - -# Mesh option -############################################################################### -[mesh] -# Puts the boundaries half a step outside the last grid point -symmetricGlobalY=true -symmetricGlobalX=true - -# The spatial dimension -nx = 18 -ny = 16 -nz = 16 - -# Position of the separatrix (-1 is non periodic, >ny is periodic) -# --------Non-periodic---------- -ixseps1 = -1 -ixseps2 = -1 -# ------------------------------ -############################################################################### - - -# Methods option -############################################################################### -# Methods used for the radial (x) derivative terms -[mesh:ddx] -second = C2 # d^2/dx^2 (f) - -# Methods used for parallel (y) derivative terms -[mesh:ddy] -second = C2 # d^2/dy^2 (f) - -#Methods used for the azimuthal (z) derivative terms -[mesh:ddz] -second = FFT # d^2/dz^2 (f) -############################################################################### - - -# Solver settings -############################################################################### -[solver] -type = pvode # Which solver to use (cvode should be same as pvode) -mms = false # false by default - -atol = 1.0e-7 # absolute tolerance -rtol = 1.0e-7 # relative tolerance - -# Max allowed iterations in one step -mxstep = 100000000 -############################################################################### - - -# Specifying the output -############################################################################### -[output] -floats = false # floats = false => output in double -############################################################################### - - -# Additional options -############################################################################### -# Geometry -# ----------------------------------------------------------------------------- -[geom] -Lx = 1.2 # The length of x from boundary to boundary -Ly = 2.0 # The length of y from boundary to boundary -# Setting the spatial variables -yl = y * geom:Ly / (2.0*pi) #y in range [0,Ly] -xl = x * geom:Lx #x in range [0,Lx] -# ----------------------------------------------------------------------------- - -# Constants -# ----------------------------------------------------------------------------- -[cst] -D_par = 1.0 # Parallel diffusion constant -D_perp = 2.0 # Perpendicular diffusion constant -# Options for the Gaussian -x0 = 0.5 * geom:Lx # The x centering of the Gaussian -y0 = 0.5 * geom:Ly # The y centering of the Gaussian -z0 = pi # The z centering of the Gaussian -w = 2 # Width of the Gaussian -# ----------------------------------------------------------------------------- - -# Flags -# ----------------------------------------------------------------------------- -[flags] -use_grid = false # Whether or not to read from a grid file -# ----------------------------------------------------------------------------- - -# The particle density -# ----------------------------------------------------------------------------- -[n] -# Scaling -scale = 1.0 - -# Initial condition (a "spherical" Gaussian centered in the middle of the domain) -function = gauss(geom:xl - cst:x0, cst:w)*gauss(geom:yl - cst:y0, cst:w)*gauss(z - cst:z0, cst:w) - -# Boundary conditions -# Set the boundary to the initial condition -bndry_all = dirichlet_o4(n:function) -# ----------------------------------------------------------------------------- -############################################################################## diff --git a/examples/bout_runners_example/diffusion_3D.cxx b/examples/bout_runners_example/diffusion_3D.cxx deleted file mode 100644 index 27c02a29a8..0000000000 --- a/examples/bout_runners_example/diffusion_3D.cxx +++ /dev/null @@ -1,80 +0,0 @@ -// ******* Simulates 3D diffusion ******* - -#include -#include -// This gives the Laplace(f) options -#include -// Gives PI and TWOPI -#include - -class Diffusion_3d : public PhysicsModel { - Field3D n; // Evolved variable - BoutReal D_par, D_perp; // The diffusion constants - BoutReal Lx, Ly; // The spatial domain size - bool use_grid; // If the spatial size should be loaded from the grid - -protected: - int init(bool UNUSED(restarting)) override { - - // Get the option (before any sections) in the BOUT.inp file - Options* options = Options::getRoot(); - - // Get the diffusion constants - // ************************************************************************ - // Get the section of the variables from [cst] specified in BOUT.inp - // or in the command-line arguments - Options* constants = options->getSection("cst"); - // Storing the variables with the following syntax - // section_name->get("variable_name_in_input", variable_name_in_cxx, - // default_value) - constants->get("D_par", D_par, 1.0); - constants->get("D_perp", D_perp, 1.0); - - // Get domain dimensions - // ************************************************************************ - Options* flags = options->getSection("flags"); - // Get the option - flags->get("use_grid", use_grid, false); - if (use_grid) { - // Loading variables from the grid file so that they can be saved into the - // .dmp file (other variables such as dx, ny etc. are stored - // automatically) - GRID_LOAD2(Lx, Ly); - } else { - // Load from BOUT.inp - Options* geometry = options->getSection("geom"); - geometry->get("Lx", Lx, 1.0); - geometry->get("Ly", Ly, 1.0); - // Calculate the internal number of points - const int internal_x_points = mesh->GlobalNx - 2 * mesh->xstart; - const int internal_y_points = mesh->GlobalNy - 2 * mesh->ystart; - // Calculate dx and dy - // dx = Lx/line_segments_in_x - // On a line with equidistant points there is one less line - // segment than points from the first to the last point. - // The boundary lies (1/2)*dx away from the last point As there - // are 2 boundaries there will effectively add one more line - // segment in the domain. Hence - mesh->getCoordinates()->dx = Lx / (internal_x_points); - mesh->getCoordinates()->dy = Ly / (internal_y_points); - } - - // Specify what values should be stored in the .dmp file - SAVE_ONCE4(Lx, Ly, D_par, D_perp); - - // Tell BOUT++ to solve for n - SOLVE_FOR(n); - - return 0; - } - - int rhs(BoutReal UNUSED(t)) override { - mesh->communicate(n); // Communicate guard cells - - // Density diffusion - ddt(n) = D_par * Laplace_par(n) + D_perp * Laplace_perp(n); - return 0; - } -}; - -BOUTMAIN(Diffusion_3d) diff --git a/examples/bout_runners_example/makefile b/examples/bout_runners_example/makefile deleted file mode 100644 index a3cdcc1c3f..0000000000 --- a/examples/bout_runners_example/makefile +++ /dev/null @@ -1,6 +0,0 @@ - -BOUT_TOP ?= ../.. - -SOURCEC = diffusion_3D.cxx - -include $(BOUT_TOP)/make.config diff --git a/examples/bout_runners_example/pre_and_post_processing/__init__.py b/examples/bout_runners_example/pre_and_post_processing/__init__.py deleted file mode 100755 index dab6b68866..0000000000 --- a/examples/bout_runners_example/pre_and_post_processing/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python - -"""Init file for pre and post processing""" - -import os -import matplotlib.pylab as plt - -# Set proper backend for the display -try: - os.environ["DISPLAY"] -except KeyError: - plt.switch_backend("Agg") diff --git a/examples/bout_runners_example/pre_and_post_processing/grid_generator.py b/examples/bout_runners_example/pre_and_post_processing/grid_generator.py deleted file mode 100755 index 1cfeeeb4da..0000000000 --- a/examples/bout_runners_example/pre_and_post_processing/grid_generator.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python - -"""Generate an input mesh""" - -from boututils.datafile import DataFile -from boututils.options import BOUTOptions -import numpy as np -import os - -# Define pi, in case it is found in the BOUT.inp file -pi = np.pi - -def generate_grid(nx = 20 ,\ - ny = 10 ,\ - nz = 8 ,\ - Lx = None,\ - Ly = None,\ - MXG = None,\ - inp_path = None,\ - file_name = None): - """Generates a grid file based on the input, and on what is being - found in the BOUT.inp file""" - - # Calculate dx and dy - #{{{ Use BOUT.inp if Lx, Ly or MXG is not found - if (Lx == None) or (Ly == None) or (MXG == None): - # Make a BOUTOption object (see documentation of BOUTOption.py) - myOpts = BOUTOptions(inp_path) - - # Appendable list - warnings = [] - - # If Lx is not given - if Lx == None: - # Read 'Lx' from the 'geom' section - Lx = myOpts.geom['Lx'] - # Lx is now a string, we let python evaluate the string - Lx = eval(Lx) - # Append a warning - warnings.append('Lx') - # If Ly is not given - if Ly == None: - Ly = myOpts.geom['Ly'] - Ly = eval(Ly) - warnings.append('Ly') - # If MXG is not given - if MXG == None: - MXG = myOpts.root['MXG'] - MXG = eval(MXG) - warnings.append('MXG') - - # Print warnings - for warning in warnings: - print("\n"*2 + "!"*137) - print("WARNING!!! " + warning + " not given in generate_grid") - print("Will use value from BOUT.inp to calculate gridspacing, but"+\ - " note that this would be inconsistent if '" + warning +\ - "' is given in a bout_runner object") - print("!"*137 + "\n"*2) - #}}} - - # Calculate dx and dy - internal_x_points = nx - 2*MXG - internal_y_points = ny - # Since the grid points lay half between the grid - # (There is one less line segment than points, and there is one more - # "internal" in the grid due to 2 half grid points out to the - # boundary) - dx = Lx / (internal_x_points) - dy = Ly / (internal_y_points) - - # Set ixseps - ixseps1 = -1 - ixseps2 = -2 - - # Check that folder exists - grid_folder = os.path.split(file_name)[0] - if grid_folder != "": - if not os.path.exists(grid_folder): - os.makedirs(grid_folder) - - # Write the grid file - with DataFile(file_name, write=True, create=True) as grid: - # Write the dimensions to the grid file - grid.write("nx", nx) - grid.write("ny", ny) - grid.write("nz", nz) - - # Write the grid sizes to the grid file - grid.write("dx", dx) - grid.write("dy", dy) - - # Write the lengths to the grid file - grid.write("Lx", Lx) - grid.write("Ly", Ly) - - # Write the ixseps - grid.write("ixseps1", ixseps1) - grid.write("ixseps2", ixseps2) diff --git a/examples/bout_runners_example/pre_and_post_processing/post_processing_MMS.py b/examples/bout_runners_example/pre_and_post_processing/post_processing_MMS.py deleted file mode 100755 index f734b088fa..0000000000 --- a/examples/bout_runners_example/pre_and_post_processing/post_processing_MMS.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env python - -"""Post processing which performs MMS""" - -from boutdata.collect import collect -import matplotlib.pyplot as plt -import numpy as np - -#{{{perform_MMS_test -def perform_MMS_test(paths, extension='.pdf', show_plot=False): - """Collects the data members belonging to a convergence plot""" - - # Make a variable to store the errors and the spacing - data = {'error_2':[], 'error_inf':[], 'spacing':[]} - - # Loop over the runs in order to collect - for path in paths: - # Collect n_solution - n_numerical - error_array = collect('E_n', path=path, info=False,\ - xguards = False, yguards = False) - # Pick the last time point - error_array = error_array[-1] - - # The error in the 2-norm and infintiy-norm - data['error_2'] .append( np.sqrt(np.mean( error_array**2.0 )) ) - data['error_inf'].append( np.max(np.abs( error_array )) ) - - # Collect the spacings - dx_spacing = collect("dx", path=path, info=False,\ - xguards = False, yguards = False) - dy_spacing = collect("dy", path=path, info=False,\ - xguards = False, yguards = False) - dz_spacing = collect("dz", path=path, info=False,\ - xguards = False, yguards = False) - # We are interested in the max of the spacing - dx_spacing = np.max(dx_spacing) - dy_spacing = np.max(dy_spacing) - dz_spacing = np.max(dz_spacing) - - # Store the spacing in the data - data['spacing'].append(np.max([dx_spacing, dy_spacing, dz_spacing])) - - # Sort the data - data = sort_data(data) - - # Find the order of convergence in the 2 norm and infinity norm - order_2, order_inf = get_order(data) - - # Get the root name of the path (used for saving files) - root_folder = paths[0].split('/')[0] + '/' - - # Get the name of the plot based on the first folder name - name = paths[0].split('/') - # Remove the root folder and put 'MMS-' in front - name = 'MMS-' + '_'.join(name[1:]) - - # Print the convergence rate - print_convergence_rate(data, order_2, order_inf, root_folder, name) - - # Plot - # We want to show the lines of the last orders, so we send in - # order_2[-1] and order_inf[-1] - do_plot(data, order_2[-1], order_inf[-1],\ - root_folder, name, extension, show_plot) -#}}} - -# Help functions -#{{{sort_data -def sort_data(data): - """Sorts the data after highest grid spacing""" - - # Sort the data in case it is unsorted - list_of_tuples_to_be_sorted =\ - list(zip(data['spacing'], data['error_inf'], data['error_2'])) - - # Sort the list - # Note that we are sorting in reverse order, as we want the - # highest grid spacing first - sorted_list = sorted(list_of_tuples_to_be_sorted, reverse = True) - # Unzip the sorted list - data['spacing'], data['error_inf'], data['error_2'] =\ - list(zip(*sorted_list)) - - return data -#}}} - -#{{{get_order -def get_order(data): - # TODO: Check this - # Initialize the orders - order_2 = [np.nan] - order_inf = [np.nan] - - # The order will be found by finding a linear fit between two - # nearby points in the error-spacing plot. Hence, we must let - # the index in the for loop run to the length minus one - for index in range(len(data['spacing']) - 1): - # p = polyfit(x,y,n) finds the coefficients of a polynomial p(x) - # of degree that fits the data, p(x(i)) to y(i), in a least squares - # sense. - # The result p is a row vector of length n+1 containing the - # polynomial coefficients in descending powers - spacing_start = np.log(data['spacing'][index]) - spacing_end = np.log(data['spacing'][index + 1]) - error_start_2 = np.log(data['error_2'][index]) - error_end_2 = np.log(data['error_2'][index + 1]) - error_start_inf = np.log(data['error_inf'][index]) - error_end_inf = np.log(data['error_inf'][index + 1]) - # Finding the order in the two norm - order = np.polyfit([spacing_start, spacing_end],\ - [error_start_2, error_end_2], 1) - # Append it to the order_2 - order_2.append(order[0]) - - # Finding the infinity order - order = np.polyfit([spacing_start, spacing_end],\ - [error_start_inf, error_end_inf], 1) - # Append it to the order_inf - order_inf.append(order[0]) - - return order_2, order_inf -#}}} - -#{{{print_convergence_rate -def print_convergence_rate(data, order_2, order_inf, root_folder, name): - "Prints the convergence rates to the screen and to a file" - outstring = list(zip(data['spacing'],\ - data['error_2'],\ - order_2,\ - data['error_inf'],\ - order_inf)) - header = ['#spacing', 'error_2 ', 'order_2 ', 'error_inf ', 'order_inf'] - # Format on the rows (: accepts the argument, < flushes left, - # 20 denotes character width, .10e denotes scientific notation with - # 10 in precision) - header_format = "{:<20}" * (len(header)) - number_format = "{:<20.10e}" * (len(header)) - # * in front of header unpacks - header_string = header_format.format(*header) - text = header_string - for string in outstring: - text += '\n' + number_format.format(*string) - print('\nNow printing the results of the convergence test:') - print(text) - # Write the found orders - with open(root_folder + name + '.txt', 'w' ) as f: - f.write(text) - print('\n') -#}}} - -#{{{do_plot -def do_plot(data, order_2, order_inf, root_folder, name, extension, show_plot): - """Function which handles the actual plotting""" - - # Plot errors - # Set the plotting style - title_size = 30 - plt.rc("font", size = 30) - plt.rc("axes", labelsize = 25, titlesize = title_size) - plt.rc("xtick", labelsize = 25) - plt.rc("ytick", labelsize = 25) - plt.rc("legend", fontsize = 30) - plt.rc("lines", linewidth = 3.0) - plt.rc("lines", markersize = 20.0) - plt_size = (10, 7) - fig_no = 1 - # Try to make a figure with the current backend - try: - fig = plt.figure(fig_no, figsize = plt_size) - except: - # Switch if a backend needs the display - plt.switch_backend('Agg') - fig = plt.figure(fig_no, figsize = plt_size) - - ax = fig.add_subplot(111) - - # Plot errore - # Plot the error-space plot for the 2-norm - ax.plot(data['spacing'], data['error_2'], 'b-o', label=r'$L_2$') - # Plot the error-space plot for the inf-norm - ax.plot(data['spacing'], data['error_inf'], 'r-^', label=r'$L_\infty$') - - # Plot the order - #{{{ Explanaition of the calculation - # In the log-log plot, we have - # ln(y) = a*ln(x) + ln(b) - # y = error - # x = spacing (found from linear regression) - # a = order - # b = where the straight line intersects with the ordinate - # - # Using the logarithmic identities - # ln(x^a) = a*ln(x) - # ln(x*y) = ln(x) + ln(y) - # ln(x/y) = ln(x) - ln(y) - # - # We usually find b for x = 0. Here, on the other hand, we find it - # by solving the equation for the smallest grid point: - # ln[y(x[-1])] = a*ln(x[-1]) + ln(b), - # so - # ln(b) = ln[y(x[-1])] - a*ln(x[-1]) - # => - # ln(y) = a*ln(x) - a*ln(x[-1]) + ln[y(x[-1])] - # = a*[ln(x)-ln(x[-1])] + ln[y(x[-1])] - # = a*[ln(x/x[-1])] + ln[ys(x[-1])] - # = ln[(x/x[-1])^a*y(x[-1])] - #}}} - # Order in the inf norm - ax.plot(\ - (data['spacing'][-1],\ - data['spacing'][0]),\ - (\ - ((data['spacing'][-1] / data['spacing'][-1])**order_inf)*\ - data['error_inf'][-1],\ - ((data['spacing'][0] / data['spacing'][-1])**order_inf)*\ - data['error_inf'][-1]\ - ),\ - 'm--',\ - label=r"$\mathcal{O}_{L_\infty}="+"%.2f"%(order_inf)+r"$") - # Order in the 2 norm - ax.plot(\ - (data['spacing'][-1],\ - data['spacing'][0]),\ - (\ - ((data['spacing'][-1] / data['spacing'][-1])**order_2)*\ - data['error_2'][-1],\ - ((data['spacing'][0] / data['spacing'][-1])**order_2)*\ - data['error_2'][-1]\ - ),\ - 'c--',\ - label=r"$\mathcal{O}_{L_2}="+"%.2f"%(order_2)+r"$") - - # Set logaraithmic scale - ax.set_yscale('log') - ax.set_xscale('log') - - # Set axis label - ax.set_xlabel("Mesh spacing") - ax.set_ylabel("Error norm") - - # Make the plot look nice - # Plot the legend - leg = ax.legend(loc="best", fancybox = True, numpoints=1) - leg.get_frame().set_alpha(0.5) - # Plot the grid - ax.grid() - # Includes the xlabel if outside - plt.tight_layout() - - # Save the plot - plt.savefig(root_folder + name + '.' + extension) - print('\nPlot saved to ' + name + '.' + extension + '\n'*2) - - if show_plot: - plt.show() - - plt.close() -#}}} diff --git a/examples/bout_runners_example/pre_and_post_processing/post_processing_show_the_data.py b/examples/bout_runners_example/pre_and_post_processing/post_processing_show_the_data.py deleted file mode 100644 index ffa1853561..0000000000 --- a/examples/bout_runners_example/pre_and_post_processing/post_processing_show_the_data.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python - -"""Post processing routine which shows the data""" - -from boutdata.collect import collect -from boututils.showdata import showdata - -# All post processing functions called by bout_runners must accept the -# first argument from bout_runners (called 'folder' in -# __call_post_processing_function) -def show_the_data(paths, t=None, x=None, y=None, z=None, **kwargs): - """Function which plots the data. - - Parameters - ---------- - paths : tuple - The paths of the runs - t : slice - The desired t slice of showdata - x : slice - The desired x slice of showdata - y : slice - The desired y slice of showdata - z : slice - The desired z slice of showdata - **kwargs : key word arguments - Not used here, but acts like a "dumpster" for additional keyword - arguments - """ - - for path in paths: - print("Showing data from {}".format(path)) - n = collect('n', xguards=False, yguards=False, path=path, info=False) - - # Show the data - showdata(n[t,x,y,z]) diff --git a/examples/bout_runners_example/pre_and_post_processing/restart_from_func.py b/examples/bout_runners_example/pre_and_post_processing/restart_from_func.py deleted file mode 100644 index 7438b530d5..0000000000 --- a/examples/bout_runners_example/pre_and_post_processing/restart_from_func.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python - -"""Contains restart_from_func""" - -import re - -def restart_from_func(dmp_folder,\ - one_of_the_restart_paths_in_scan = None,\ - scan_parameters = None,\ - **kwargs): - """ - Function which returns the restart from dmp_folder and - one_of_the_restart_paths_in_scan - NOTE: This will not work if the values of one of the scan parameters - contains an underscore, or if the initial hit is in the root - folder. - Parameters - ---------- - dmp_folder : str - Given by the bout_runners. Used to find the current scan - values. - one_of_the_restart_paths_in_scan : str - One of the restart paths from a previously run scan. - scan_parameters : list - List of strings of the names of the scan paramemters. - kwargs : dict - Dictionary with additional keyword arguments, given by - bout_runners. - Returns - ------- - restart_from : str - String which gives path to restart from - """ - - # Make a template string of one_of_the_restart_paths_in_scan - restart_template = one_of_the_restart_paths_in_scan - for scan_parameter in scan_parameters: - hits = [m.start() for m in \ - re.finditer(scan_parameter, restart_template)] - while(len(hits) > 0): - # Replace the values with {} - # The value is separated from the value by 1 character - value_start = hits[0] + len(scan_parameter) + 1 - # Here we assume that the value is not separated by an - # underscore - value_len = len(restart_template[value_start:].split("_")[0]) - value_end = value_start + value_len - # Replace the values with {} - restart_template =\ - "{}{{0[{}]}}{}".format(\ - restart_template[:value_start],\ - scan_parameter,\ - restart_template[value_end:]) - # Update hits - hits.remove(hits[0]) - - # Get the values from the current dmp_folder - values = {} - for scan_parameter in scan_parameters: - hits = [m.start() for m in \ - re.finditer(scan_parameter, dmp_folder)] - # Choose the first hit to get the value from (again we assume - # that the value does not contain a _) - value_start = hits[0] + len(scan_parameter) + 1 - # Here we assume that the value is not separated by an - # underscore - values[scan_parameter] = dmp_folder[value_start:].split("_")[0] - - # Insert the values - restart_from = restart_template.format(values) - - return restart_from diff --git a/manual/sphinx/figs/folder_tree.pdf b/manual/sphinx/figs/folder_tree.pdf deleted file mode 100644 index 8915cc79f78784f18ec2d4d509bfcee95d8f5154..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18157 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 {pCYme9g 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 Mx5ci49f!{Te<=8HQUCw| diff --git a/manual/sphinx/user_docs/python.rst b/manual/sphinx/user_docs/python.rst index 7c64df457d..7141d76861 100644 --- a/manual/sphinx/user_docs/python.rst +++ b/manual/sphinx/user_docs/python.rst @@ -24,7 +24,7 @@ boututils - ``linear_regression()`` -- ``showdata()`` visualises and animates 2D data (time + 1 spatial dimension) or 3D data (time + 2 spatial dimensions). The animation object can be returned, or the animation can be saved to a file or displayed on screen. +- ``showdata()`` visualises and animates 2D data (time + 1 spatial dimension) or 3D data (time + 2 spatial dimensions). The animation object can be returned, or the animation can be saved to a file or displayed on screen. - ``boutwarnings`` contains functions to raise warning messages. ``alwayswarn()`` by default prints the warning every time it is called. @@ -60,50 +60,3 @@ boutdata :members: :undoc-members: -.. _sec-bout_runners: - -bout_runners ------------- - -``bout_runners`` contains classes which gives an alternative way of -running BOUT++ simulations either normally using the class -``basic_runner`` , or on a cluster through a generated Portable Batch -System (PBS) script using the child class ``PBS_runner`` . Examples can -be found in ``examples/bout_runners_example/``. - -``bout_runners`` is especially useful if one needs to make several runs -with only small changes in the options (which is normally written in -``BOUT.inp`` or in the command-line), as is the case when performing a -parameter scan, or when performing a MMS test. - -Instead of making several runs with several different input files with -only small changes in the option, one can with ``bout_runners`` specify -the changes as member data of an instance of the appropriate -``bout_runners`` class. One way to do this is to write a *driver* in the -same directory as the executable. The *driver* is just a python script -which imports ``bout_runners`` , creates an instance, specifies the -running option as member data of that instance and finally calls the -member function ``self.execute_runs()`` . - -In addition, the ``bout_runners`` provides a way to run any python -post-processing script after finished simulations (as long as it accept -at least one parameter containing the folder name(s) of the run(s)). If -the simulations have been performed using the ``PBS_runner`` , the -post-processing function will be submitted to the cluster (although it -is possible to submit it to a different queue, using a different amount -of nodes etc.). - -When the function ``self.execute_runs()`` is executed, a folder -structure like the one presented in :numref:`fig-folder-tree` is -created. ``BOUT.inp`` is copied to the folder of execution, where the -``BOUT.*.dmp`` files are stored. Secondly a list of combination of the -options specified in the driver is made. Eventually unset options are -obtained from ``BOUT.inp`` or given a default value if the option is -nowhere to be found. - -.. _fig-folder-tree: -.. figure:: ../figs/folder_tree.* - :alt: Longest possible folder tree - - Longest possible folder tree made by the ``self.execute_runs()`` - function. diff --git a/manual/sphinx/user_docs/running_bout.rst b/manual/sphinx/user_docs/running_bout.rst index 14f29ad456..6493817c75 100644 --- a/manual/sphinx/user_docs/running_bout.rst +++ b/manual/sphinx/user_docs/running_bout.rst @@ -82,7 +82,7 @@ run, and produce a bunch of files in the ``data/`` subdirectory. if needed. In some cases the options used have documentation, with a brief explanation of how they are used. In most cases the type the option is used as (e.g. ``int``, ``BoutReal`` or ``bool``) is given. - + - ``BOUT.restart.*.nc`` are the restart files for the last time point. Currently each processor saves its own state in a separate file, but there is experimental support for parallel I/O. For the settings, see @@ -110,9 +110,21 @@ To see some of the other command-line options try "-h":: and see the section on options (:ref:`sec-options`). +There is also a python tool called |bout_runners|_ which can be used for executing ``BOUT++`` runs. +In addition, this tool can be used to + +- programmatically change parameters of a project in python + +- keep track of all the metadata of the runs of the project + +- automate the orchestration (including pre- and post-processing routines) of chains of runs locally or on a cluster + To analyse the output of the simulation, cd into the ``data`` subdirectory and start python or IDL (skip to :ref:`Using IDL ` for IDL). +.. |bout_runners| replace:: ``bout_runners`` +.. _bout_runners: https://pypi.org/project/bout-runners/ + Analysing the output using Python --------------------------------- @@ -129,7 +141,7 @@ will first need to have set up python to use the BOUT++ libraries ``boutdata`` and ``boututils``; see section :ref:`sec-config-python` for how to do this. The analysis routines have some requirements such as SciPy; see section -:ref:`sec-python-requirements` for details. +:ref:`sec-python-requirements` for details. To print a list of variables in the output files, one way is to use the ``DataFile`` class. This is a wrapper around the various NetCDF and HDF5 libraries for python: @@ -221,7 +233,7 @@ and to make this a coloured contour plot IDL> showdata, T[*,*,0,*], /cont -The equivalent commands in Python are as follows. +The equivalent commands in Python are as follows. .. _sec-run-nls: @@ -744,3 +756,4 @@ then the BOUT++ restart will fail. **Note** It is a good idea to set ``nxpe`` in the ``BOUT.inp`` file to be consistent with what you set here. If it is inconsistent then the restart will fail, but the error message may not be particularly enlightening. + diff --git a/tools/pylib/bout_runners/README.md b/tools/pylib/bout_runners/README.md deleted file mode 100644 index da164a57d8..0000000000 --- a/tools/pylib/bout_runners/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# bout-runners - -Python-wrapper for doing simulation runs with BOUT++. -See [bout_runners_example](../../../examples/bout_runners_example) for examples diff --git a/tools/pylib/bout_runners/__init__.py b/tools/pylib/bout_runners/__init__.py deleted file mode 100644 index 9bcbc7acef..0000000000 --- a/tools/pylib/bout_runners/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Packages to be imported when writing from boutdata import * -__all__ = ["bout_runners"] - -from .bout_runners import basic_runner, PBS_runner diff --git a/tools/pylib/bout_runners/bout_runners.py b/tools/pylib/bout_runners/bout_runners.py deleted file mode 100755 index 76e2a3a546..0000000000 --- a/tools/pylib/bout_runners/bout_runners.py +++ /dev/null @@ -1,4418 +0,0 @@ -#!/usr/bin/env python3 - -""" -Classes for running one or several mpi-runs with BOUT++ at once. -Read the docstring of "basic_runner", or refer to the user manual of -BOUT++ for more info. Examples can be found in -BOUT/examples/bout_runners_example. -""" - -# NOTE: This document uses folding. A hash-symbol followed by three {'s -# denotes the start of a fold, and a hash-symbol followed by three -# }'s denotes the end of a fold -# NOTE: Improvement suggestions: -# It would be beneficial to refactor bout_runners -# 1. Better design: Shorter functions -# 2. Better input parsing: The input for the constructors are rather long. -# One alternative could be to have setters for a grouping of -# parameters -__authors__ = "Michael Loeiten" -__version__ = "1.08" -__date__ = "2018.01.07" - -import os -import sys -import re -import itertools -import glob -import timeit -import datetime -import time -import shutil -from numbers import Number -import numpy as np -from boututils.run_wrapper import shell, launch, getmpirun -from boututils.options import BOUTOptions -from boututils.datafile import DataFile -from boutdata.restart import redistribute, addnoise, resizeZ, resize - -#{{{class basic_runner -# As a child class uses the super function, the class must allow an -# object as input - - -class basic_runner(object): - #{{{docstring - """ - basic_runner - ------------ - - Class for mpi running one or several runs with BOUT++. - Calling self.execute_runs() will run your BOUT++ program with the possible - combinations given in the member data using the mpi runner. - - Before each run basic_runner will: - * Create a folder system, based on the member data, rooted in - self._directory. - * Copy BOUT.inp of self._directory to the execution folder. - * Check that the domain split is sane (suggest a split if a bad - domain split is given) - - If the restart option is checked, bout_runners will - * Put old data into a restart folder (so that nothing is lost - upon restart) - * Resize the mesh if new sizes are detected - - A log-file for the run is stored in self._directory - - By default self._directory = "data", self._nproc = 1 and - self._allow_size_modification = False - - self._program_name is by default set to the same name as any .o files in - thefolder where an instance of the object is created. If none is found the - constructor tries to run make. - - All other data members are set to None by default. - - The data members will override the corresponding options given in - self._directory/BOUT.inp. - - See the doctring of the constructor (__int__) for options. - See BOUT/examples/bout_runners_example for examples. - """ -#}}} - -#{{{__init__ - def __init__(self, - nproc=1, - directory="data", - prog_name=None, - solver=None, - mms=None, - atol=None, - rtol=None, - mxstep=None, - grid_file=None, - nx=None, - ny=None, - nz=None, - zperiod=None, - zmin=None, - zmax=None, - dx=None, - dy=None, - dz=None, - MXG=None, - MYG=None, - NXPE=None, - ixseps1=None, - ixseps2=None, - jyseps1_1=None, - jyseps1_2=None, - jyseps2_1=None, - jyseps2_2=None, - symGlobX=None, - symGlobY=None, - ddx_first=None, - ddx_second=None, - ddx_upwind=None, - ddx_flux=None, - ddy_first=None, - ddy_second=None, - ddy_upwind=None, - ddy_flux=None, - ddz_first=None, - ddz_second=None, - ddz_upwind=None, - ddz_flux=None, - nout=None, - timestep=None, - additional=None, - series_add=None, - restart=None, - restart_from=None, - redistribute=None, - use_expand=False, - max_proc=None, - intrp_method=None, - add_noise=None, - cpy_source=None, - cpy_grid=None, - sort_by=None, - make=None, - allow_size_modification=False): - #{{{docstring - """ - basic_runner constructor - ------------------------ - - All the member data is set to None by default. If the - data members are not set, the values from BOUT.inp will be used. - The exception is nproc (default = 1), directory (default = - "data"), use_expand (default = False) and - allow_size_modification (default = False), which always needs to - be set. - - Parameters - ---------- - nproc : int - Number of processors to use in the mpirun - directory : str - The directory of the BOUT.inp file - prog_name : str or iterable - Name of the excecutable. If none is set the name will be set from - the *.o file. - solver : str or iterable - The solver to be used in the runs - mms : bool - Whether or not mms should be run - atol : number or iterable - Absolute tolerance - rtol : number or iterable - Relative tolerance - mxstep : int or iterable - Max internal step pr output step - grid_file : str or iterable - The grid file - nx : int or iterable - Number of nx in the run - ny : int or iterable - Number of ny in the run - nz : int or iterable - Number of nz in the run - zperiod : int or iterable - Domain size in multiple of fractions of 2*pi - zmin : number - Minimum range of the z domain - zmax : number - Maximum range of the z domain - dx : number or iterable - Grid size in the x direction - dy : number or iterable - Grid size in the x direction number or iterable - dz : number or iterable - Grid size in the x direction number or iterable - MXG : int or iterable - The number of guard cells in the x direction - MYG : int or iterable - The number of guard cells in the y direction - NXPE : int or iterable - Numbers of processors in the x direction - ixseps1 : int or iterable - Separatrix location for "upper" divertor - ixseps2 : int or iterable - Separatrix location for "lower" divertor - jyseps1_1 : int or iterable - Branch cut location 1_1 (see user's manual for details) - jyseps1_2 : int or iterable - Branch cut location 1_2 (see user's manual for details) - jyseps2_1 : int or iterable - Branch cut location 2_1 (see user's manual for details) - jyseps2_2 : int or iterable - Branch cut location 2_2 (see user's manual for details) - symGlobX : bool - Whether or not to use symmetricGLobalX (x defined - symmetrically between 0 and 1) - symGlobY : bool - Whether or not to use symmetricGLobalY (y defined - symmetrically) - ddx_first : str or iterable - Method used for for first ddx terms - ddx_second : str or iterable - Method used for for second ddx terms - ddx_upwind : str or iterable - Method used for for upwind ddx terms - ddx_flux : str or iterable - Method used for for flux ddx terms - ddy_first : str or iterable - Method used for for first ddy terms - ddy_second : str or iterable - Method used for for second ddy terms - ddy_upwind : str or iterable - Method used for for upwind ddy terms - ddy_flux : str or iterable - Method used for for flux ddy terms - ddz_first : str or iterable - Method used for for first ddz terms - ddz_second : str or iterable - Method used for for second ddz terms - ddz_upwind : str or iterable - Method used for for upwind ddz terms - ddz_flux : str or iterable - Method used for for flux ddz terms - nout : int or iterable - Number of outputs stored in the *.dmp.* files - timestep : int or iterable - The time between each output stored in the *.dmp.* files - additional : tuple or iterable - Additional option for the run given on the form - - >>> ("section_name","variable name", values) - - or as iterable on the same form, where values can be any - value or string or an iterable of those - series_add : tuple or iterable - The same as above, with the exception that no combination - will be performed between the elements during a run - restart : str - Wheter or not to use the restart files. Must be either - "overwrite" or "append" if set - restart_from : [str | function] - Path to restart if string. If function: A function which - takes the current dmp_folder and kwargs (given to - execute_runs) as input and returns the restart path. The - function is handy when restarting from jobs while doing a - parameter scan. - redistribute : int - The number of processors the redistribute the restart files - to. Calls the redistribute function in boutdata.restart. - Will only be effective if "restart" is not None - use_expand : bool - Only used when restarting. - If there is a mismatch in nz between the requested nz and - the nz found in the restart file, boutdata.restart.resizeZ - will be used if use_expand = True, if not - boutdata.restart.resize will be used - max_proc : int - Only used when restarting. - Max processors used when calling boutdata.restart.resize - intrp_method: str - Only used when restarting, and when the mesh is resizied. - Sets the method used in the interpolation. - add_noise : dict - Adding noise to the restart files by calling the addnoise - function in boutdata.restart. Will only be effective if - "restart" is not None. Must be given as a dict with "var" - and 'scale" as keys if used. The value of "var" must be a - string or None. If set to None, then all the evolved - variables will be added noise to. The value of "scale" will - be the scale of the noise, if set to None the default value - will be used. - Example: - - >>> add_noise = {"n":1e-4, "Te":1e-5} - - cpy_source : bool - Wheter or not to copy the source files to the folder of the - *.dmp.* files - cpy_grid : bool - Whether or not to copy the grid files to the folder of the - *.dmp.* files - sort_by : str - Defining what will be the fastest running variable in the - run, which can be useful if one for example would like to - "group" the runs before sending it to a post processing - function (see the docstring of the run function for more - info). The possibilities are - - * "spatial_domain" - * "temporal_domain" - * "solver" - * "ddx_first" - * "ddx_second" - * "ddx_upwind" - * "ddx_flux" - * "ddy_first" - * "ddy_second" - * "ddy_upwind" - * "ddy_flux" - * "ddz_first" - * "ddz_second" - * "ddz_upwind" - * "ddz_flux" - * Any "variable_name" from additional or series_add - * An iterable consisting of several of these. - - If an iterable is given, then the first element is going to - be the fastest varying variable, the second element is going - to be the second fastest varying variable and so on. - make : bool - Whether or not to make the program - allow_size_modification : bool - Whether or not to allow bout_runners modify nx and ny in - order to find a valid split of the domain - """ - #}}} - - # Setting the member data - self._nproc = nproc - self._directory = directory - self._solver = self._set_member_data(solver) - self._mms = mms - self._atol = self._set_member_data(atol) - self._rtol = self._set_member_data(rtol) - self._mxstep = self._set_member_data(mxstep) - self._grid_file = self._set_member_data(grid_file) - self._nx = self._set_member_data(nx) - self._ny = self._set_member_data(ny) - self._nz = self._set_member_data(nz) - self._zperiod = self._set_member_data(zperiod) - self._zmin = self._set_member_data(zmin) - self._zmax = self._set_member_data(zmax) - self._dx = self._set_member_data(dx) - self._dy = self._set_member_data(dy) - self._dz = self._set_member_data(dz) - self._MXG = MXG - self._MYG = MYG - self._NXPE = self._set_member_data(NXPE) - self._ixseps1 = self._set_member_data(ixseps1) - self._ixseps2 = self._set_member_data(ixseps2) - self._jyseps1_1 = self._set_member_data(jyseps1_1) - self._jyseps1_2 = self._set_member_data(jyseps1_2) - self._jyseps2_1 = self._set_member_data(jyseps2_1) - self._jyseps2_2 = self._set_member_data(jyseps2_2) - self._symGlobX = symGlobX - self._symGlobY = symGlobY - self._ddx_first = self._set_member_data(ddx_first) - self._ddx_second = self._set_member_data(ddx_second) - self._ddx_upwind = self._set_member_data(ddx_upwind) - self._ddx_flux = self._set_member_data(ddx_flux) - self._ddy_first = self._set_member_data(ddy_first) - self._ddy_second = self._set_member_data(ddy_second) - self._ddy_upwind = self._set_member_data(ddy_upwind) - self._ddy_flux = self._set_member_data(ddy_flux) - self._ddz_first = self._set_member_data(ddz_first) - self._ddz_second = self._set_member_data(ddz_second) - self._ddz_upwind = self._set_member_data(ddz_upwind) - self._ddz_flux = self._set_member_data(ddz_flux) - self._nout = self._set_member_data(nout) - self._timestep = self._set_member_data(timestep) - self._additional = additional - self._series_add = series_add - self._restart = restart - self._restart_from = restart_from - self._redistribute = redistribute - self._use_expand = use_expand - self._max_proc = max_proc - self._intrp_method = intrp_method - self._add_noise = add_noise - self._cpy_source = cpy_source - self._cpy_grid = cpy_grid - self._sort_by = self._set_member_data(sort_by) - self._make = make - self._allow_size_modification = allow_size_modification - - # Make some space to distinguish from the rest of the terminal - print("\n") - - # Initializing self._warnings and self._error - # self._warnings will be filled with warnings - # self._errors will be filled with errors - # The warnings and errors will be printed when the destructor is called - self._warnings = [] - self._errors = [] - - # Check if make is a boolean - if self._make is not None: - if not isinstance(self._make, bool): - self._errors.append("TypeError") - raise TypeError("make must be boolean if set") - - # Set self._program_name - self._set_program_name(prog_name) - - # Make the file if make is True - if self._make: - self._run_make() - - # Obtain the MPIRUN - self._MPIRUN = getmpirun() - - # The run type is going to be written in the run.log file - self._run_type = "basic" - - #{{{ Set self._additional and self._series_add correctly - # self._additional must be on a special form (see - # basic_error_checker). - if self._additional is not None: - if not(hasattr(self._additional, "__iter__")) or\ - (isinstance(self._additional, str)) or\ - (isinstance(self._additional, dict)): - # Put additional as a double iterable - self._additional = ((self._additional),) - else: - if not(hasattr(self._additional[0], "__iter__")) or\ - (isinstance(self._additional[0], str)) or\ - (isinstance(self._additional, dict)): - # Put self._additional as an iterable - self._additional = (self._additional,) - # Do the same for series_add - if self._series_add is not None: - if not(hasattr(self._series_add, "__iter__")) or\ - (isinstance(self._series_add, str)) or\ - (isinstance(self._series_add, dict)): - # Put series_add as a double iterable - self._series_add = ((self._series_add),) - else: - if not(hasattr(self._series_add[0], "__iter__")) or\ - (isinstance(self._series_add[0], str)) or\ - (isinstance(self._series_add, dict)): - # Put self._series_add as an iterable - self._series_add = (self._series_add,) - #}}} - - # Check that nproc is given correctly - if not isinstance(self._nproc, int): - message = ("nproc is of wrong type\n" - "nproc must be given as an int") - self._errors.append("TypeError") - raise TypeError(message) - - #{{{ Set NYPE from NXPE and nproc - if self._NXPE is not None: - # Make self._NYPE as an appendable list - self._NYPE = [] - - # Check that NXPE is of correct type - check_if_int = ( - (self._NXPE, "NXPE"), - ) - self._check_for_correct_type(var=check_if_int, - the_type=int, - allow_iterable=True) - - # Check that NXPE and nproc is consistent - for cur_NXPE in self._NXPE: - if (self._nproc % cur_NXPE) != 0: - self._errors.append("RuntimeError") - message = "nproc =" + str(self._nproc) +\ - " not divisible by" +\ - " NXPE = " + str(cur_NXPE) +\ - " (the number of processors in the x direction)" - raise RuntimeError(message) - - # Append NYPE - self._NYPE.append(int(self._nproc / cur_NXPE)) - else: - self._NYPE = None - #}}} - - # Check if the instance is set correctly - self._check_for_basic_instance_error() - - # We need to find options in BOUT.inp. We use BOUTOption for this - # Object initialization - self._inputFileOpts = BOUTOptions(self._directory) - # Convert indices to lowercase - self._inputFileOpts.root = dict( - (key.lower(), value) for key, value in self._inputFileOpts.root.items()) - self._inputFileOpts.mesh = dict( - (key.lower(), value) for key, value in self._inputFileOpts.mesh.items()) - - # Initialize outputs from execute runs - self._PBS_id = [] - self._dmp_folders = [] -#}}} - -#{{{__del__ - def __del__(self): - """The destructor will print all the warning and error messages""" - - # Switch to see if error occured - error_occured = False - - # If errors occured - if len(self._errors) > 0: - message = "! A {} occurred. !".format(self._errors[0]) - # Find the boarder length - len_boarder = len(message) - # Print the message - print("{0}{1}\n{2}\n{1}{0}". - format("\n" * 2, "!" * len_boarder, message)) - error_occured = True - if len(self._warnings) > 0: - print("{}The following WARNINGS were detected:\n{}". - format("\n" * 3, "-" * 80)) - for warning in self._warnings: - print(warning + "\n") - print("{}{}".format("-" * 80, "\n" * 3)) - elif len(self._warnings) > 0 and not(error_occured): - print("{} {}".format("\n" * 3, "~" * 69)) - print(("| No WARNINGS detected before instance destruction in " - "'bout_runners'. |")) -#}}} - -#{{{execute_runs - def execute_runs(self, - job_dependencies=None, - remove_old=False, - post_processing_function=None, - post_process_after_every_run=False, - **kwargs): - #{{{docstring - """ - Makes a run for each of the combination given by the member data. - - Parameters - ---------- - job_dependencies : [None | sequence (not str)], default: None - If the jobs should be run after other jobs. This input is - only effective if the object calling the function is a - PBS_runner. - remove_old : bool, default : False - Whether old run files should be deleted or not - post_processing_function : callable - A function to be called after one or several run. This - function must accept the string of self._dmp_folder if - post_process_after_each_run is True, and a tuple of dmp - folders if post_process_after_each_run is False - post_process_after_each_run : bool, default: False - Boolean telling whether post_processing_function should be - called after each run (if True), or after the number of runs - decided by self._sort_by (see the constructor of - basic_runner for more info) - **kwargs : any - Parameters to be passed to the post_processing_function and - self._restart_from function (if any) - - Returns - ------- - self._dmp_folders : sequence (not str) - A sequence of the folder locations made from the runner - self._PBS_id : sequence (not str) - A sequence of the PBS ids is returned. - """ - #}}} - - if self.__class__.__name__ == "PBS_runner": - # Wait for jobs to finish - if job_dependencies is not None: - # Ensure that job_dependencies are just numbers - job_dependencies = [int(re.match('\d+', j).group(0)) - for j in job_dependencies - if re.match('\d+', j) is not None] - if len(job_dependencies) != 0: - print("\nWill now wait for these jobs to finish\n{}\n". - format("\n".join([str(j) for j in job_dependencies]))) - while len(job_dependencies) > 0: - # Get current jobs - status, output = shell("qstat", pipe=True) - job_queue = output.split("\n") - # Find the jobIds - job_queue = [int(re.match('\d+', j).group(0)) - for j in job_queue - if re.match('\d+', j) is not None] - # These jobs will be removed from job_dependencies - pop_jobs = [] - for job in job_dependencies: - if job not in job_queue: - pop_jobs.append(job) - - for job in pop_jobs: - job_dependencies.remove(job) - - time.sleep(60) - - # Check for errors in the run function - self._error_check_for_run_input(remove_old, - post_processing_function, - post_process_after_every_run) - - # Create the run log - self._create_run_log() - - # We check that the given combination of nx and ny is - # possible to perform with the given nproc - self._get_correct_domain_split() - - # Get the combinations of the member functions - possibilities = self._get_possibilities() - combinations = self._get_combinations(possibilities) - - # If we are not running the post processing function after every - # run, make an appendable list over all the runs which will be - # passed as an input parameter to the post processing function - if not(post_process_after_every_run): - seq_of_dmp_folders = [] - - # Print either "now running" or "now submitting" - self._print_run_or_submit() - - # Set self._len_group if post_processing_function is set, but - # self._sort_by is None - if (post_processing_function is not None) and\ - (not(post_process_after_every_run)) and\ - (self._len_group is None): - # self._len_group is to a number by _get_swapped_input_list - # (which is called if self._sort_by is not None) - # If there are no sorting, self._len_group will be None - # We will make self._len_group the length of the - # number of runs here - self._len_group = len(combinations) - - # The run - for run_no, combination in enumerate(combinations): - - # Get the folder to store the data - do_run = self._prepare_dmp_folder(combination, **kwargs) - if not(do_run): - # Skip this run - continue - - if remove_old: - # Remove old data - self._remove_data() - - # Copy the grid (if any) if cpy_grid files is True - if (self._cpy_grid) and (self._grid_file is not None): - combination_list = combination.split() - # Loop through all the combinations - for elem in combination_list: - # Find the grid - if elem[0:4] == "grid": - # Remove grid=, so that only the path remains - cur_grid = elem.replace("grid=", "") - # Copy the grid file - shutil.copy2(cur_grid, self._dmp_folder) - - # Check if the run has been performed previously - do_run = self._check_if_run_already_performed() - # Do the actual runs - if do_run: - # Call the driver for a run - self._run_driver(combination, run_no) - - # If we would like to call a post_processing function - if post_processing_function is not None: - if post_process_after_every_run: - # Call the post processing function - self._call_post_processing_function( - function=post_processing_function, - folders=(self._dmp_folder,), - **kwargs) - else: - # Append the dmp folder to the list of dmp folders - seq_of_dmp_folders.append(self._dmp_folder) - # If the run_no+1 is divisible by self._len_group - if ((run_no + 1) % self._len_group == 0): - # Call the post processing function - self._call_post_processing_function( - function=post_processing_function, - folders=tuple(seq_of_dmp_folders), - **kwargs) - # Reset the seq_of_dmp_folders - seq_of_dmp_folders = [] - - # Cast to tuple - self._PBS_id = tuple(self._PBS_id) - if hasattr(self._dmp_folders, "__iter__")\ - and not isinstance(self._dmp_folders, str): - self._dmp_folders = tuple(el for el in self._dmp_folders) - else: - self._dmp_folders = (self._dmp_folders,) - - return self._dmp_folders, self._PBS_id -#}}} - -#{{{_run_driver - def _run_driver(self, combination, run_no): - """ - The machinery which actually performs the runs. - """ - - # Get the time when the run starts - start = datetime.datetime.now() - # Do the run - output, run_time = self._single_run(combination) - # Print info to the log file for the runs - self._append_run_log(start, run_no, run_time) - print("\n") -#}}} - -#{{{ Functions called by the constructor -#{{{_set_member_data - def _set_member_data(self, input_parameter): - """ - Returns the input_parameter as a tuple if it is different than None, - and if it is not iterable - """ - - # If the input_data is not set, the value in BOUT.inp will - # be used - if input_parameter is not None: - # If the input_data is not an iterable, or if it is a - # string: Put it to a tuple - if not(hasattr(input_parameter, "__iter__")) or\ - (type(input_parameter)) == str: - input_parameter = (input_parameter,) - - return input_parameter -#}}} - -#{{{_set_program_name - def _set_program_name(self, prog_name=None): - """ - Will set self._program_name and make the program if the - prog_name.o file is not found. - - Parameters - ---------- - prog_name : str - Name of the exceutable. If None, the name will be set from - the *.o file. - """ - - if prog_name is not(None): - # Check that a string is given - if not isinstance(prog_name, str): - message = "prog_name must be given as a string" - self._errors.append("TypeError") - raise TypeError(message) - # Search for file - if os.path.isfile(prog_name): - self._program_name = prog_name - else: - print("{} not found, now making:".format(prog_name)) - # File not found, make - self._run_make() - # Set the make flag to False, so it is not made again - self._make = False - # Search for file - if not(os.path.isfile(prog_name)): - message = ("{} could not be found after make. " - "Please check for spelling mistakes").\ - format(prog_name) - self._errors.append("RuntimeError") - raise RuntimeError(message) - else: - self._program_name = prog_name - else: - # Find the *.o file - o_files = glob.glob("*.o") - if len(o_files) > 1: - message = ("More than one *.o file found. " - "The first *.o file is chosen. " - "Consider setting 'prog_name'.") - self._warning_printer(message) - self._warnings.append(message) - self._program_name = o_files[0].replace(".o", "") - elif len(o_files) == 1: - # Pick the first instance as the name - self._program_name = o_files[0].replace(".o", "") - else: - # Check if there exists a make - make_file = glob.glob("*make*") - if len(make_file) > 0: - # Run make - self._run_make() - # Set the make flag to False, so it is not made again - self._make = False - # Search for the .o file again - o_files = glob.glob("*.o") - if len(o_files) > 0: - self._program_name = o_files[0].replace(".o", "") - else: - self._program_name = False - message = ("The constructor could not make your" - " program") - self._errors.append("RuntimeError") - raise RuntimeError(message) - else: - self._errors.append("RuntimeError") - raise RuntimeError( - "No make file found in current directory") -#}}} - -#{{{_check_for_basic_instance_error - def _check_for_basic_instance_error(self): - """Check if there are any type errors when creating the object""" - - #{{{Check if nproc has the correct type - if not isinstance(self._nproc, int): - message = ("nproc is of wrong type\n" - "nproc must be given as an int") - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check if directory has the correct type - if not isinstance(self._directory, str): - message = ("directory is of wrong type\n" - "directory must be given as a str") - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check if MXG and MYG has the correct type - # Check if MXG and MYG is given as a single int - # One should not be allowed to specify MXG and MYG as an - # iterable, as MXG is used to find the correct split, and - # because it in principle could be incompatible with the method - # (first, second etc.) used - check_if_int = ( - (self._MXG, "MXG"), - (self._MYG, "MYG"), - ) - self._check_for_correct_type(var=check_if_int, - the_type=int, - allow_iterable=False) - #}}} - - #{{{Check if BOUT.inp exsists in the self._directory - # Check if there are any BOUT.inp files in the self._directory - inp_file = glob.glob(os.path.join(self._directory, "BOUT.inp")) - if len(inp_file) == 0: - self._errors.append("RuntimeError") - raise RuntimeError("No BOUT.inp files found in '{}'". - format(self._directory)) - #}}} - - #{{{Check grid_file are strings, that they exsist, and one can sort - if self._grid_file is not None: - # Set a variable which is has length over one if the test fails - not_found = [] - if isinstance(self._grid_file, str): - # See if the grid_file can be found - grid_file = glob.glob(self._grid_file) - # The grid_file cannot be found - if len(grid_file) == 0: - not_found.append(self._grid_file) - # If several grid files are given - elif hasattr(self._grid_file, "__iter__"): - for elem in self._grid_file: - # See if the grid_file can be found - grid_file = glob.glob(elem) - # The grid_file cannot be found - if len(grid_file) == 0: - not_found.append(elem) - if len(not_found) > 0: - message = ("The following grid files were not found\n" - "{}".format("\n".join(not_found))) - self._errors.append("RuntimeError") - raise RuntimeError(message) - if (self._sort_by is not None) and ("grid_file" in self._sort_by): - # Set a success flag - success = True - # The start name of the files - start_name = "grid_file" - # Check if grid file is iterable - if hasattr(self._grid_file, "__iter__"): - for grid in grid_file: - if grid[0:len(start_name)] != start_name: - success = False - else: - # Only one grid file - if self._grid_file[0:len(start_name)] != start_name: - success = False - if not(success): - message = ("The name of the grid file must start with" - " 'grid_file' in order to sort by them.") - self._errors.append("RuntimeError") - raise RuntimeError(message) - - #}}} - - #{{{Check nx, ny, nz, zperiod, nout, mxstep, separatrix are int/iterable - check_if_int = ( - (self._nx, "nx"), - (self._ny, "ny"), - (self._nz, "nz"), - (self._zperiod, "zperiod"), - (self._nout, "nout"), - (self._mxstep, "mxstep"), - (self._ixseps1, "ixseps1"), - (self._ixseps2, "ixseps2"), - (self._jyseps1_1, "jyseps1_1"), - (self._jyseps1_2, "jyseps1_2"), - (self._jyseps2_1, "jyseps2_1"), - (self._jyseps2_2, "jyseps2_2"), - ) - - self._check_for_correct_type(var=check_if_int, - the_type=int, - allow_iterable=True) - #}}} - - #{{{Check timestep, atol, rtol, zmin/max, dx, dy, dz is Number/iterable - # Check if the following is a number - check_if_number = ( - (self._timestep, "timestep"), - (self._zmin, "zmin"), - (self._zmax, "zmax"), - (self._dx, "dx"), - (self._dy, "dy"), - (self._dz, "dz"), - (self._atol, "atol"), - (self._rtol, "rtol") - ) - - self._check_for_correct_type(var=check_if_number, - the_type=Number, - allow_iterable=True) - #}}} - - #{{{Check if solver, grid_file, methods and sort_by is str/tuple of str - # Check if instance is string, or an iterable containing strings - check_if_string = ( - (self._solver, "solver"), - (self._grid_file, "grid_file"), - (self._ddx_first, "ddx_first"), - (self._ddx_second, "ddx_second"), - (self._ddx_upwind, "ddx_upwind"), - (self._ddx_flux, "ddx_flux"), - (self._ddy_first, "ddy_first"), - (self._ddy_second, "ddy_second"), - (self._ddy_upwind, "ddy_upwind"), - (self._ddy_flux, "ddy_flux"), - (self._ddz_first, "ddz_first"), - (self._ddz_second, "ddz_second"), - (self._ddz_upwind, "ddz_upwind"), - (self._ddz_flux, "ddz_flux"), - (self._sort_by, "sort_by") - ) - - self._check_for_correct_type(var=check_if_string, - the_type=str, - allow_iterable=True) - #}}} - - #{{{Check if solver is set to the correct possibility - # Check if the solver is possible - # From /include/bout/solver.hxx - possible_solvers = ( - "cvode", - "pvode", - "ida", - "petsc", - "slepc", - "karniadakis", - "rk4", - "euler", - "rk3ssp", - "power", - "arkode", - "imexbdf2", - "snes", - "rkgeneric", - ) - - # Do the check if the solver is set - if self._solver is not None: - self._check_if_set_correctly(var=(self._solver, "solver"), - possibilities=possible_solvers) - #}}} - - #{{{Check if the methods is set to the correct possibility - # Check if ddx or ddy is possible - possible_method = [ - "C2", - "C4", - ] - - # Make a tuple of the variables - the_vars = ( - (self._ddx_first, "ddx_first"), - (self._ddx_second, "ddx_second"), - (self._ddy_first, "ddy_first"), - (self._ddy_second, "ddy_second") - ) - - for var in the_vars: - # Do the check if the method is set - if var[0] is not None: - self._check_if_set_correctly(var=var, - possibilities=possible_method) - - # Check if ddz is possible - possible_method.append("FFT") - - # Make a tuple of the variables - the_vars = ( - (self._ddz_first, "ddz_first"), - (self._ddz_second, "ddz_second") - ) - - for var in the_vars: - # Do the check if the method is set - if var[0] is not None: - self._check_if_set_correctly(var=var, - possibilities=possible_method) - - # Check for upwind terms - possible_method = ( - "U1", - "U2", - "U4", - "W2", - "W3", - ) - - # Make a tuple of the variables - the_vars = ( - (self._ddx_upwind, "ddx_upwind"), - (self._ddy_upwind, "ddy_upwind"), - (self._ddz_upwind, "ddz_upwind") - ) - - for var in the_vars: - # Do the check if the method is set - if var[0] is not None: - self._check_if_set_correctly(var=var, - possibilities=possible_method) - - # Check for flux terms - possible_method = ( - "SPLIT", - "NND" - ) - - # Make a tuple of the variables - the_vars = ( - (self._ddx_flux, "ddx_flux"), - (self._ddy_flux, "ddy_flux"), - (self._ddz_flux, "ddz_flux") - ) - - for var in the_vars: - # Do the check if the method is set - if var[0] is not None: - self._check_if_set_correctly(var=var, - possibilities=possible_method) - #}}} - - #{{{Check if sort_by is set to the correct possibility - # Appendable list - possible_sort_by = [] - - # Append the 1st element of sort_checks if the 0th elements of - # sort_checks is not None - sort_checks = ( - (self._nx, "spatial_domain"), - (self._ny, "spatial_domain"), - (self._nz, "spatial_domain"), - (self._dx, "spatial_domain"), - (self._dy, "spatial_domain"), - (self._dz, "spatial_domain"), - (self._ixseps1, "spatial_domain"), - (self._ixseps2, "spatial_domain"), - (self._jyseps1_1, "spatial_domain"), - (self._jyseps1_2, "spatial_domain"), - (self._jyseps2_1, "spatial_domain"), - (self._jyseps2_2, "spatial_domain"), - (self._symGlobX, "spatial_domain"), - (self._symGlobY, "spatial_domain"), - (self._timestep, "temporal_domain"), - (self._nout, "temporal_domain"), - (self._solver, "solver"), - (self._mms, "solver"), - (self._atol, "solver"), - (self._rtol, "solver"), - (self._mxstep, "solver"), - (self._ddx_first, "ddx_first"), - (self._ddx_second, "ddx_second"), - (self._ddx_upwind, "ddx_upwind"), - (self._ddx_flux, "ddx_flux"), - (self._ddy_first, "ddy_first"), - (self._ddy_second, "ddy_second"), - (self._ddy_upwind, "ddy_upwind"), - (self._ddy_flux, "ddy_flux"), - (self._ddz_first, "ddz_first"), - (self._ddz_second, "ddz_second"), - (self._ddz_upwind, "ddz_upwind"), - (self._ddz_flux, "ddz_flux"), - (self._grid_file, "grid_file") - ) - - for sort_check in sort_checks: - if sort_check[0] is not None: - if not(sort_check[1] in possible_sort_by): - possible_sort_by.append(sort_check[1]) - - # Append the additionals and series_add - # If additional is set - if self._additional is not None: - for additional in self._additional: - # The additional now contains a tuple of three elements - # We would like to extract the section (if any) and variable - # and append them to the possibilities list - # If the section is empty - if additional[0] == "": - section = "" - else: - section = additional[0] + ":" - possible_sort_by.append(section + additional[1]) - # Do the same for series_add - if self._series_add is not None: - for series_add in self._series_add: - if series_add[0] == "": - section = "" - else: - section = series_add[0] + ":" - possible_sort_by.append(section + series_add[1]) - - # Make a tuple of the variables - the_vars = ( - (self._sort_by, "sort_by"), - ) - - for var in the_vars: - # Do the check if the method is set - if var[0] is not None: - self._check_if_set_correctly(var=var, - possibilities=possible_sort_by) - #}}} - - #{{{Check if restart is set correctly - if self._restart is not None: - if not isinstance(self._restart, str): - self._errors.append("TypeError") - raise TypeError("restart must be set as a string when set") - - possible_method = ( - "overwrite", - "append" - ) - - # Make a tuple of the variables - the_vars = ( - (self._restart, "restart"), - ) - - for var in the_vars: - # Do the check if the method is set - if var[0] is not None: - self._check_if_set_correctly(var=var, - possibilities=possible_method) - #}}} - - #{{{Check if restart_from is set correctly - if self._restart_from is not None: - # Throw warning if restart is None - if self._restart is None: - message = "restart_from will be ignored as restart = None" - self._warning_printer(message) - self._warnings.append(message) - - if not isinstance(self._restart_from, str)\ - and not(hasattr(self._restart_from, "__call__")): - self._errors.append("TypeError") - message = ("restart_from must be set as a string or a " - "function returning the restart path when set") - raise TypeError(message) - #}}} - - #{{{Check if redistribute is set correctly - if self._redistribute is not None: - # Throw warning if restart is None - if self._restart is None: - message = "redistribute will be ignored as restart = None" - self._warning_printer(message) - self._warnings.append(message) - # Throw a warning if restart is append - elif self._restart == "append": - message = ("redistribute is not None and restart = 'append' is" - " currently incompatible, setting restart to" - " 'overwrite'") - if not(self._restart_from): - message += " (previous files will be saved)" - self._warning_printer(message) - self._warnings.append(message) - self._restart = "overwrite" - if not isinstance(self._redistribute, int): - self._errors.append("TypeError") - message = "redistribute must be set as an integer when set" - raise TypeError(message) - # If nproc is set, and this is incompatible with NPES - if self._nproc != self._redistribute: - raise RuntimeError("nproc and redistribute must be equal") - #}}} - - #{{{Check if max_proc has the correct type - if self._restart is not None and self._max_proc is not None: - if not isinstance(self._max_proc, int): - message = ("max_proc is of wrong type\n" - "max_proc must be given as an int") - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check if intrp_method has the correct type - if self._restart is not None and self._intrp_method is not None: - if not isinstance(self._intrp_method, str): - message = ("intrp_method is of wrong type\n" - "intrp_method must be given as a string") - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check if add_noise is set correctly - if self._add_noise is not None: - # Throw warning if restart is None - if self._restart is None: - message = "add_noise will be ignored as restart = None" - self._warning_printer(message) - self._warnings.append(message) - - raise_error = False - is_key_none = False - if isinstance(self._add_noise, dict): - for var, scale in self._add_noise.items(): - if not isinstance(var, str): - if var is not(None): - raise_error = True - break - else: - is_key_none = True - if not(isinstance(scale, Number) or (scale is None)): - raise_error = True - break - if is_key_none and len(self._add_noise.keys()) > 1: - raise_error = True - else: - raise_error = True - - if raise_error: - self._errors.append("TypeError") - message = ("add_noise must be on the form " - "{'var1': number_or_none," - " 'var2': number_or_none, ...}'\n" - "or\n" - "{None: number_or_none}" - ) - raise TypeError(message) - #}}} - - #{{{Check for options set in both member data and in the grid file - if self._grid_file is not None: - # Check if the following variables are found in the grid - # file - check_if_in_grid = ( - (self._nx, "nx"), - (self._ny, "ny"), - (self._nz, "nz"), - (self._dx, "dx"), - (self._dy, "dy"), - (self._dz, "dz"), - (self._MXG, "MXG"), - (self._MYG, "MYG"), - (self._NXPE, "NXPE"), - (self._NYPE, "NYPE"), - (self._ixseps1, "ixseps1"), - (self._ixseps2, "ixseps2"), - (self._jyseps1_1, "jyseps1_1"), - (self._jyseps1_2, "jyseps1_2"), - (self._jyseps2_1, "jyseps2_1"), - (self._jyseps2_2, "jyseps2_2"), - (self._symGlobX, "symmmetricGlobalX"), - (self._symGlobY, "symmmetricGlobalY") - ) - for var in check_if_in_grid: - # If the variable is set - if var[0] is not None: - # Loop through the grid files - for grid_file in self._grid_file: - # Open (and automatically close) the grid files - f = DataFile(grid_file) - # Search for mesh data in the grid file - grid_variable = f.read(var[1]) - # If the variable is found - if grid_variable is not None: - self._errors.append("TypeError") - message = ("{0} was specified both in the " - "driver and in the grid file.\n" - "Please remove {}" - " from the driver if you would " - "like to run with a grid file.") - raise TypeError(message.format(var[1])) - #}}} - - #{{{If grid files are set: Use nx, ny and nz values in the grid file - if self._grid_file is not None: - # Make a dict of appendable lists - spatial_domain = {"nx": [], "ny": [], "nz": []} - for grid_file in self._grid_file: - # Open (and automatically close) the grid files - f = DataFile(grid_file) - # Search for nx, ny and nz in the grid file - mesh_types = ("nx", "ny", "nz") - for mesh_type in mesh_types: - grid_variable = f.read(mesh_type) - # If the variable is found - if grid_variable is not None: - spatial_domain[mesh_type].append(grid_variable) - # Check that the lengths of nx, ny and nz are the same - # unless they are not found - len_nx = len(spatial_domain["nx"]) - len_ny = len(spatial_domain["ny"]) - len_nz = len(spatial_domain["nz"]) - if len_nx != 0: - self._nx = spatial_domain["nx"] - if len_ny != 0: - self._ny = spatial_domain["ny"] - if len_nz != 0: - self._nz = spatial_domain["nz"] - #}}} - - #{{{Check that nx, ny and nz are of the same length - if self._nx is not None and self._ny is not None: - self._check_if_same_len((self._nx, "nx"), (self._ny, "ny")) - if self._nx is not None and self._nz is not None: - self._check_if_same_len((self._nx, "nx"), (self._nz, "nz")) - if self._ny is not None and self._nz is not None: - self._check_if_same_len((self._ny, "ny"), (self._nz, "nz")) - #}}} - - #{{{Check that NXPE and NYPE are of the same length as nx, ny, nz - if self._nx is not None and self._NXPE is not None: - self._check_if_same_len((self._nx, "nx"), (self._NXPE, "NXPE")) - if self._ny is not None and self._NXPE is not None: - self._check_if_same_len((self._ny, "ny"), (self._NXPE, "NXPE")) - if self._nz is not None and self._NXPE is not None: - self._check_if_same_len((self._nz, "nz"), (self._NXPE, "NXPE")) - - if self._nx is not None and self._NYPE is not None: - self._check_if_same_len((self._nx, "nx"), (self._NYPE, "NYPE")) - if self._ny is not None and self._NYPE is not None: - self._check_if_same_len((self._ny, "ny"), (self._NYPE, "NYPE")) - if self._nz is not None and self._NYPE is not None: - self._check_if_same_len((self._nz, "nz"), (self._NYPE, "NYPE")) - #}}} - - #{{{Check (zperiod), (zmin, zmax) and (dz) is not set simultaneously - if (self._zperiod is not None and - (self._zmin is not None or self._zmax is not None)): - self._errors.append("TypeError") - message = "zperiod and zmin or zmax cannot be set simultaneously." - raise TypeError(message) - elif (self._dz is not None and - (self._zmin is not None or self._zmax is not None)): - self._errors.append("TypeError") - message = "dz and zmin or zmax cannot be set simultaneously." - raise TypeError(message) - elif (self._zperiod is not None and self._dz): - self._errors.append("TypeError") - message = "dz and zperiod cannot be set simultaneously." - raise TypeError(message) - #}}} - - #{{{Check that dz is not set - # dz is currently set throught zmin and zmax - if self._dz is not None: - self._errors.append("TypeError") - message = ("dz can currently just be set through zmin and zmax\n" - "dz = 2*pi*(zmax-zmin)/(MZ)") - raise TypeError(message) - #}}} - - #{{{Check that dx, dy and dz are of the same length - if self._dx is not None and self._dy is not None: - self._check_if_same_len((self._dx, "dx"), (self._dy, "dy")) - if self._dx is not None and self._dz is not None: - self._check_if_same_len((self._dx, "dx"), (self._dz, "dz")) - if self._dy is not None and self._dz is not None: - self._check_if_same_len((self._dy, "dy"), (self._dz, "dz")) - #}}} - - #{{{Check that (dx, nx), (dy, ny) and (dz,nz) are of the same length - if self._dx is not None and self._nx is not None: - self._check_if_same_len((self._dx, "dx"), (self._nx, "nx")) - if self._dy is not None and self._ny is not None: - self._check_if_same_len((self._dy, "dy"), (self._ny, "ny")) - if self._nz is not None and self._dz is not None: - self._check_if_same_len((self._dz, "dz"), (self._nz, "nz")) - #}}} - - #{{{ Check that timestep and nout have the same len - if self._timestep is not None and self._nout is not None: - self._check_if_same_len((self._timestep, "timestep"), - (self._nout, "nout")) - #}}} - - #{{{Check that additional and series_add are on the correct form - self._error_check_additional((self._additional, "additional")) - self._error_check_additional((self._series_add, "series_add")) - #}}} - - #{{{Check that self._series_add[:][2] have the same length - if self._series_add is not None: - # Make the second indices iterable if they are not already - # Start by converting to list, so that self._series becomes - # modifyable - self._series_add = list(list(el) for el in self._series_add) - for index in range(len(self._series_add)): - if not(hasattr(self._series_add[index][2], "__iter__")): - self._series_add[index][2] = (self._series_add[index][2],) - # Conver to tuple - self._series_add = tuple(tuple(el) for el in self._series_add) - - # Collect all second indices - third_indicies = tuple(elems[2] for elems in self._series_add) - # Find the length of the second indices - lengths = tuple( - len(elem) for elem in third_indicies if ( - not isinstance( - elem, str) and not isinstance( - elem, dict))) - - # Check that the length of the second indices are the same - # L.count(value) -> integer -- return number of occurrences - # of value - # stackoverflow.com/questions/3844801/check-if-all-elements-in-a-list-are-identical - if not(lengths.count(lengths[0]) == len(lengths)): - message = ("The length of the third index of the elements" - " of series_add must be the same") - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check mms, symGlobX, symGlobY, cpy_src/grid, use_expand and - # allow_size_mod is bool - check_if_bool = ( - (self._mms, "mms"), - (self._symGlobX, "symGlobX"), - (self._symGlobY, "symGlobY"), - (self._cpy_source, "cpy_source"), - (self._cpy_grid, "cpy_grid"), - (self._use_expand, "use_expand"), - (self._allow_size_modification, "allow_size_modification") - ) - - self._check_for_correct_type(var=check_if_bool, - the_type=bool) - #}}} - - #{{{Check grid_file is None if cpy_grid==True - if (self._grid_file is None) and (self._cpy_grid): - # Raise error - self._errors.append("TypeError") - message = ("Cannot copy the grid files if none exists in " - " 'grid_file'") - raise TypeError(message) - #}}} - - #{{{Check that zmin and zmax has the same length - if (self._zmin is not None) and (self._zmax is not None): - self._check_if_same_len((self._zmin, "zmin"), - (self._zmax, "zmax")) - - #}}} -#}}} -#}}} - -#{{{Functions called by _check_for_basic_instance_error - #{{{_error_check_additional - def _error_check_additional(self, input_member): - #{{{docstring - """ - Checks that the input_member is on the following form: - - >>> input_member = ((section1, name1, (value1-1, value1-2, ...)), - (section2, name2, (value2-1, value2-2, ...)), - ...) - - Parameters - ---------- - input member: [self._additional | self._series_add] - input_member[0] is the input data and - input_member[1] is the name of the input data - """ - #}}} - - # If input_member is set - if input_member[0] is not None: - # Set a success variable that will fail if anything goes - # wrong - success = True - - # Loop through all elements in input_member - for elem in input_member[0]: - # Check if self._addition is iterable, but not a string - # or dict - if (hasattr(elem, "__iter__")) and\ - (not isinstance(elem, str)) and\ - (not isinstance(elem, dict)): - if isinstance(elem[0], str): - # Check that the second element (the name) is a - # string - if not isinstance(elem[1], str): - success = False - # If more than three elements are given - if len(elem) != 3: - success = False - # elem[0] is not a string - else: - success = False - # elem is not iterable or is a dict or a string - else: - success = False - if not(success): - message =\ - ("{0} is on the wrong form.\n" - "{0} should be on the form\n" - "{0}=\ \n" - " ((section1, name1, (value1-1, value1-2,...)),\ \n" - " (section2, name2, (value2-1, value2-2,...)),\ \n" - " ...))\n").format(input_member[1]) - self._errors.append("TypeError") - raise TypeError(message) - #}}} -#}}} - -#{{{ Functions called by the execute_runs function -#{{{_error_check_for_run_input - def _error_check_for_run_input(self, - remove_old, - post_processing_function, - post_process_after_every_run - ): - """ - Check if there are any type errors in input for the run function - """ - - #{{{Check if remove_old is of the correct type - check_if_bool = ( - (remove_old, "remove_old"), - ) - - self._check_for_correct_type(var=check_if_bool, - the_type=bool) - #}}} - - #{{{Check if remove_old and restart is set on the same time - if remove_old and self._restart is not None: - self._errors.append("RuntimeError") - raise RuntimeError("You should not remove old data if you" - " want a restart run") - #}}} - - #{{{Check that the post_processing_function is a fuction - if (post_processing_function is not None) and\ - (not(hasattr(post_processing_function, "__call__"))): - self._errors.append("RuntimeError") - message = ("post_process_after_every_run must be a" - " function") - raise RuntimeError(message) - #}}} - - #{{{Check that the post_process_after_every_run is not set alone - if (post_process_after_every_run and - post_processing_function is None): - self._errors.append("RuntimeError") - message = ("post_process_after_every_run can only be set if" - " post_processing_function is given") - raise RuntimeError(message) - #}}} - - #{{{Check that the post_process_after_every_run is a boolean - if (post_process_after_every_run is not None) and\ - (not isinstance(post_process_after_every_run, bool)): - self._errors.append("RuntimeError") - message = ("post_process_after_every_run must be set to" - " a boolean when set") - raise RuntimeError(message) - #}}} - - # Check for errors in a child class - self._check_for_child_class_errors( - remove_old, - post_processing_function, - post_process_after_every_run - ) -#}}} - -#{{{_create_run_log - def _create_run_log(self): - """Makes a run_log file if it doesn't exists""" - - # Checks if run_log exists - self._run_log = os.path.join(self._directory, "run_log.txt") - if os.path.isfile(self._run_log) == False: - # The header - header = ("start_time", "run_type", "run_no", - "run_time_H:M:S", "dump_folder") - header_format = "{:<19} {:<9} {:<6} {:<17} {:<}" - # Create the log file, and print the header - with open(self._run_log, "w") as f: - f.write(header_format.format(*header) + "\n") - - # Preparation of the run - print("\nRunning with inputs from '{}'".format(self._directory)) -#}}} - -#{{{_get_correct_domain_split - def _get_correct_domain_split(self): - """ - Checks that the grid can be split in the correct number of - processors. - - If not, vary the number of points until value is found. - """ - - if (self._nx is None) and (self._ny is None): - #{{{ Set local_nx and local_ny from input - # Set the local nx value - local_nx = [self._get_dim_from_input("nx")] - - # Set the local ny value - local_ny = [self._get_dim_from_input("ny")] - #}}} - elif (self._nx is None): - #{{{ Set local_nx from input - # ny is given, so we only need to find nx - local_ny = list(self._ny) - - # Set the local nx value - local_nx = [self._get_dim_from_input("nx")] - - # Get the same length on nx and ny - local_nx = local_nx * len(local_ny) - #}}} - elif (self._ny is None): - #{{{ Set local_ny from input - # nx is given, so we only need to find ny - local_nx = list(self._nx) - - # Set the local ny value - local_ny = [self._get_dim_from_input("ny")] - - # Get the same length on nx and ny - local_ny = local_ny * len(local_nx) - #}}} - else: - local_nx = list(self._nx) - local_ny = list(self._ny) - - # If NXPE is not set, we will try to find a optimal grid size - # Flag to determine if a warning should be printed - produce_warning = False - print("\nChecking the grid split for the meshes\n") - # Obtain MXG - MXG, _MYG = self._get_MXG_MYG() - if self._NXPE is None: - #{{{ If NXPE is not set - for size_nr in range(len(local_nx)): - print("Checking nx={} and ny={}". - format(local_nx[size_nr], local_ny[size_nr])) - # Check to see if succeeded - init_split_found = False - cur_split_found = False - add_number = 1 - # Counter to see how many times the while loop has been - # called - count = 0 - - #{{{While cur_split_found == False - while cur_split_found == False: - # The same check as below is performed internally in - # BOUT++ (see boutmesh.cxx under - # if(options->isSet("NXPE"))) - for i in range(1, self._nproc + 1, 1): - MX = local_nx[size_nr] - 2 * MXG - # self._nproc is called NPES in boutmesh - if (self._nproc % i == 0) and \ - (MX % i == 0) and \ - (local_ny[size_nr] % (self._nproc / i) == 0): - # If the test passes - cur_split_found = True - - # Check if cur_split_found is true, eventually - # update the add_number - local_nx, local_ny, add_number, produce_warning\ - = self._check_cur_split_found(cur_split_found, - produce_warning, - add_number, - size_nr, - local_nx, - local_ny, - using_nx=True, - using_ny=True) - - #{{{ Check if the split was found the first go. - # This will be used if self_allow_size_modification is - # off, or if we are using a grid file - if count == 0 and cur_split_found: - init_split_found = True - #}}} - - # Add one to the counter - count += 1 - #}}} - - # Check if initial split succeeded - self._check_init_split_found(init_split_found, - size_nr, - local_nx, - local_ny, - test_nx=True, - test_ny=True, - produce_warning=produce_warning) - #}}} - else: - #{{{ If NXPE is set - # Check if NXPE and NYPE is set consistently with nproc - self._check_NXPE_or_NYPE(local_nx, - local_ny, - type_str="NXPE", - MXG=MXG) - self._check_NXPE_or_NYPE(local_nx, - local_ny, - type_str="NYPE") - #}}} -#}}} - -#{{{_get_possibilities - def _get_possibilities(self): - """ - Returns the list of the possibilities. In get_combinations - the elements of this list is going to be put together to a list - of strings which will be used when making a run. - """ - - #{{{Set combination of nx, ny and nz (if not set in grid_file) - # Appendable list - spatial_grid_possibilities = [] - if (self._grid_file is None): - # Dictionary where - # - the first element is the variable itself - # - the second element is the section of the variable - # - the third element is an appendable list - spatial_grid_str = { - "nx": (self._nx, "mesh:", []), - "ny": (self._ny, "mesh:", []), - "nz": (self._nz, "mesh:", []), - "dx": (self._dx, "mesh:", []), - "dy": (self._dy, "mesh:", []), - "dz": (self._dz, "mesh:", []), - "zperiod": (self._zperiod, "", []), - "zmin": (self._zmin, "", []), - "zmax": (self._zmax, "", []), - } - # Store the keys as an own variable - keys = tuple(spatial_grid_str.keys(), ) - # Append the different dimension to the list of strings - for key in keys: - # If the variable is not empty - if spatial_grid_str[key][0] is not None: - # Fill the appendable list with the elements from - # the variable - for elem in spatial_grid_str[key][0]: - spatial_grid_str[key][2].append( - "{}{}={}". - format(spatial_grid_str[key][1], key, elem) - ) - - # The goal is to combine the these strings to one string - # Find the largest length - lengths = tuple(len(spatial_grid_str[key][2]) for key in keys) - max_len = np.max(lengths) - # Make the strings the same length - for key in keys: - # We do this by filling it with empty strings - while len(spatial_grid_str[key][2]) <= max_len: - spatial_grid_str[key][2].append("") - - # Append this to the spatial grid possibilities as a string - for number in range(max_len): - # Make a tuple - current_grid = tuple(spatial_grid_str[key][2][number] - for key in keys) - # Join the strings in the list and append - spatial_grid_possibilities.append(" ".join(current_grid)) - #}}} - - #{{{Set the combination of timestep and nout if is not None - # Appendable lists - temporal_grid_possibilities = [] - timestep_str = [] - nout_str = [] - # Append the different time options to the list of strings - if self._timestep is not None: - for timestep in self._timestep: - timestep_str.append("timestep={}".format(timestep)) - if self._nout is not None: - for nout in self._nout: - nout_str.append("nout={}".format(nout)) - # Combine the strings to one string - # Find the largest length - max_len = np.max([len(timestep_str), len(nout_str)]) - # Make the strings the same length - if len(timestep_str) < max_len: - timestep_str.append("") - if len(nout_str) < max_len: - nout_str.append("") - # Append the temporal grid possibilities as a string - for number in range(max_len): - # Make a tuple - current_times = (timestep_str[number], - nout_str[number] - ) - # Join the strings in the list and append - temporal_grid_possibilities.append(" ".join(current_times)) - #}}} - - #{{{Set the combination of the series_add option if is not None - # Appendable list - series_add_possibilities = [] - if self._series_add is not None: - # Dictionary to handle the data, where the key is going to - # be the element number in self._series_add, and the values - # are going to be the sub dictionary defined below - all_info = {} - # Loop through all elements and fill the dictionary - for nr, elem in enumerate(self._series_add): - # Put in the sub dictionary - all_info[nr] = {"values": None, - "section_and_var": None, - "sec_var_vals": []} - # Fill the values - all_info[nr]["values"] = elem[2] - # Fill the section and variable key - all_info[nr]["section_and_var"] = "{}:{}=".\ - format(elem[0], elem[1]) - # Fill in the combinations - for val in all_info[nr]["values"]: - all_info[nr]["sec_var_vals"].append( - all_info[nr]["section_and_var"] + str(val) - ) - - # Make an appendable list - all_sec_var_vals = [] - for key in all_info.keys(): - all_sec_var_vals.append(all_info[key]["sec_var_vals"]) - - # Zip the sec_var_vals together (* unpacks), join them with - # a space, and append them to series_add_possibilities - for one_possibility in zip(*all_sec_var_vals): - series_add_possibilities.append(" ".join(one_possibility)) - #}}} - - #{{{Put non-iterable variables into a list if they are not set to None - # This makes the member data iterable, and usable in - # generate_possibilities - if self._MXG is not None: - self._MXG = (self._MXG,) - if self._MYG is not None: - self._MYG = (self._MYG,) - if self._mms is not None: - self._mms = (self._mms,) - if self._symGlobX is not None: - self._symGlobX = (self._symGlobX,) - if self._symGlobY is not None: - self._symGlobY = (self._symGlobY,) - #}}} - - #{{{tuple of tuple of variables to generate possibilities from - tuple_of_variables = [ - (self._solver, "solver", "type"), - (self._mms, "solver", "mms"), - (self._atol, "solver", "atol"), - (self._rtol, "solver", "rtol"), - (self._mxstep, "solver", "mxstep"), - (self._MXG, "", "MXG"), - (self._MYG, "", "MYG"), - (self._NXPE, "", "NXPE"), - (self._NYPE, "", "NYPE"), - (self._grid_file, "", "grid"), - (self._ddx_first, "ddx", "first"), - (self._ddx_second, "ddx", "second"), - (self._ddx_upwind, "ddx", "upwind"), - (self._ddx_flux, "ddx", "flux"), - (self._ddy_first, "ddy", "first"), - (self._ddy_second, "ddy", "second"), - (self._ddy_upwind, "ddy", "upwind"), - (self._ddy_flux, "ddy", "flux"), - (self._ddz_first, "ddz", "first"), - (self._ddz_second, "ddz", "second"), - (self._ddz_upwind, "ddz", "upwind"), - (self._ddz_flux, "ddz", "flux"), - (self._ixseps1, "mesh", "ixseps1"), - (self._ixseps2, "mesh", "ixseps2"), - (self._jyseps1_1, "mesh", "jyseps1_1"), - (self._jyseps1_2, "mesh", "jyseps1_2"), - (self._jyseps2_1, "mesh", "jyseps2_1"), - (self._jyseps2_2, "mesh", "jyseps2_2"), - (self._symGlobX, "mesh", "symmetricGlobalX"), - (self._symGlobY, "mesh", "symmetricGlobalY") - ] - #}}} - - #{{{Append the additional option to tuple of variables if set - if self._additional is not None: - for additional in self._additional: - # If the last element of additional is not iterable we need - # put them into a tuple to make them iterable (in order to - # use them in generate_possibilities) - if (not(hasattr(additional[2], "__iter__"))) or\ - (isinstance(additional[2], str)): - # We have to specify the whole additional, as this can - # be given as a tuple, and tuples does not support item - # assignments - additional = (additional[0], - additional[1], - (additional[2],)) - # Append the additional to tuple of variables - tuple_of_variables.append( - (additional[2], - additional[0], - additional[1]) - ) - #}}} - - #{{{List of the possibilities of the variables - # Start out with the already generated - # spatial_grid_possibilities and temporal_grid_possibilities - list_of_possibilities = [spatial_grid_possibilities, - temporal_grid_possibilities, - series_add_possibilities] - - # Append the possibilities to the list of possibilities - for var in tuple_of_variables: - list_of_possibilities.append( - self._generate_possibilities(var[0], var[1], var[2]) - ) - #}}} - - # Return the list_of possibilities - return list_of_possibilities -#}}} - -#{{{_get_combinations - def _get_combinations(self, input_list): - """ - The input_list is a list with lists as element. - Returns a list of all combinations between the elements of the - input_list. - """ - - # Remove empty elements in input_list in order for - # itertools.product to work - input_list = [elem for elem in input_list if elem != []] - - # If we would like to sort the input list (choose which variable - # to be the fastest varying) - if self._sort_by is not None: - # Swap the list corresponding to the sort_by statement so - # that that list will be the last. The itertools.product - # will then make that list the fastest varying in the list - input_list = self._get_swapped_input_list(input_list) - else: - # Initialize this member data to None - self._len_group = None - - # The last element in the input_list will be the fastest varying - # element - all_combinations_as_tuple = list(itertools.product(*input_list)) - - # all_combination_as_tuple is a list with tuples as elements - # We would like to combine the elements in these tuples to one - # string - # Make an appendable list - all_combinations_as_strings = [] - - # Loop over the elements in the list containing tuples - for a_tuple in all_combinations_as_tuple: - # Join the elements in a tuple and store it - all_combinations_as_strings.append(" ".join(a_tuple)) - - return all_combinations_as_strings -#}}} - -#{{{_print_run_or_submit - def _print_run_or_submit(self): - """Prints "Now running" """ - print("\nNow running:") -#}}} - -#{{{_prepare_dmp_folder - def _prepare_dmp_folder(self, combination, **kwargs): - """ - Prepare the dump folder for runs - - - Obtain the folder name to restart from - - Obtain folder name and copy the input file to the final folder. - - Check if restart files are present if restart is set (set - restart to None if not found). - - Find appropriate mxg and myg if redistribute is set. - - Copy restart files if restart_from and/or redistribute is set - - Redistribute restart files if redistribute and restart is set - - Resize the runs (change nx, ny and/or nz) if the dimension is - changed. - - resizeZ if nz is set and it deviates from what is found in the - restart files. - - Add noise to the restart files if add_noise and restart is set - - Copy files if restart is set to overwrite - - Copy the source files to the final folder is cpy_source is True. - - Parameters - ---------- - combination : sequence (not str) - The current combination to be run - **kwargs : any - Extra parameters given to self._restart_from from function (if - any) - - Returns do_run = False if there are any troubles with the copying - """ - - # do_run is set to True by default - do_run = True - - #{{{ Obtain folder name and copy the input file - folder_name = self._get_folder_name(combination) - self._dmp_folder = os.path.join(self._directory, folder_name) - # If the last character is "/", then remove it - if self._dmp_folder[-1] == "/": - self._dmp_folder = self._dmp_folder[:-1] - - # Create folder if it doesn't exists - self._create_folder(self._dmp_folder) - - if not isinstance(self._dmp_folders, tuple): - # If self._dmp_folders is a tuple, it means that execute runs - # is called more then once. - # self._dmp_folders should then not be appended - self._dmp_folders.append(self._dmp_folder) - - # If self._dmp_folder contains anything other than - # self._directory - if self._dmp_folder != self._directory: - # Copy the input file into this folder - src = os.path.join(self._directory, "BOUT.inp") - shutil.copy2(src, self._dmp_folder) - #}}} - - #{{{ Obtain the folder name to restart from - if self._restart_from is not None: - - if isinstance(self._restart_from, str): - self._cur_restart_from = self._restart_from - elif hasattr(self._restart_from, "__call__"): - self._cur_restart_from =\ - self._restart_from(self._dmp_folder, **kwargs) - if not isinstance(self._cur_restart_from, str): - message = ("The restart_from from function must " - "return a string") - raise ValueError(message) - - # Check if any restart files are present - # This check is performed after waiting for other runs to finish - if len(glob.glob( - os.path.join(self._cur_restart_from, "*restart*"))) == 0: - self._errors.append("FileNotFoundError") - raise FileNotFoundError("No restart files found in " + - self._cur_restart_from) - - else: - self._cur_restart_from = None - #}}} - - #{{{ Toggle restart - dmp_files = glob.glob(os.path.join(self._dmp_folder, "*.restart.*")) - # If no dump files are found, set restart to "None" - if len(dmp_files) == 0 and\ - self._restart is not None and\ - self._cur_restart_from is None: - message = ("'restart' was set to {}" - ", but no restart files found." - " Setting 'restart' to None").format(self._restart) - self._restart = None - self._warning_printer(message) - self._warnings.append(message) - #}}} - - #{{{ Find the appropriate mxg and myg if redistribute is set - if self._redistribute: - redistribute_MXG, redistribute_MYG = self._get_MXG_MYG() - #}}} - - #{{{ Copy restart files if restart_from and/or redistribute is set - if self._restart and self._cur_restart_from: - if self._redistribute: - # Use the redistribute function to copy the restart file - do_run = self._check_if_run_already_performed( - restart_file_search_reason="redistribute") - - if do_run: - print("\nCopying files from {0} to {1}\n". - format(self._cur_restart_from, self._dmp_folder)) - do_run = redistribute(self._redistribute, - path=self._cur_restart_from, - output=self._dmp_folder, - mxg=redistribute_MXG, - myg=redistribute_MYG, - ) - if not do_run: - message = "Redistribute failed, run skipped" - self._warning_printer(message) - self._warnings.append(message) - else: - # Copy the files to restart - do_run = self._copy_run_files() - - elif self._restart and self._redistribute: - # Save the files from previous runs - dst = self._move_old_runs(folder_name="redistribute", - include_restart=True) - - do_run = redistribute(self._redistribute, - path=dst, - output=self._dmp_folder, - mxg=redistribute_MXG, - myg=redistribute_MYG, - ) - #}}} - - #{{{ Save files if restart is set to "overwrite" - # NOTE: This is already done if self._redistribute is set - if self._restart == "overwrite" and not(self._redistribute) and do_run: - self._move_old_runs(folder_name="restart", - include_restart=False) - #}}} - - #{{{Finding cur_nz - if self._restart and do_run: - if self._nz: - # The current nz should be in the second index as any - # eventual other names would come from additional or - # series_add - cur_nz = int(self._dmp_folder. - split("nz")[1]. - split("/")[0]. - replace("_", "")) - else: - # The nz size is not changed, will use the one from - # the input file - try: - cur_nz = self._get_dim_from_input("nz") - except KeyError: - cur_nz = self._get_dim_from_input("mz") - - # Make sure cur_nz is divisible by 2 if cur_nz != 1 - if cur_nz != 1: - if cur_nz % 2 != 0: - old_cur_nz = cur_nz - cur_nz += 1 - if cur_nz % 2 != 0: - cur_nz = old_cur_nz - 1 - else: - message = "nz = {} not a power of 2".format(cur_nz) - raise RuntimeError(message) - #}}} - - # Flag to check if the mesh has been resized - resized = False - - #{{{ Resize nx, ny and nz of the evolved fields - if self._restart and do_run and (self._nx or self._ny or self._nz): - # Obtain MXG and MYG - MXG, MYG = self._get_MXG_MYG() - - # Checking if the sizes are changed - # Finding the current sizes - # The current sizes should be in the second index as any - # eventual other names would come from additional or - # series_add - # Finding nx - if self._nx: - cur_nx = int(self._dmp_folder. - split("nx")[1]. - split("/")[0]. - split("_")[1]) - else: - # The nx size is not changed, will use the one from - # the input file - cur_nx = self._get_dim_from_input("nx") - # Finding ny - if self._ny: - cur_ny = int(self._dmp_folder. - split("ny")[1]. - split("/")[0]. - split("_")[1]) - else: - # The ny size is not changed, will use the one from - # the input file - cur_ny = self._get_dim_from_input("ny") - - # Finding the sizes in the restart files - file_name = glob.glob( - os.path.join(self._dmp_folder, "BOUT.restart.0.*"))[0] - - with DataFile(file_name) as f: - # Loop over the variables in the file - NYPE = f.read("NYPE") - NXPE = f.read("NXPE") - for var in f.list(): - # Read the data - data = f.read(var) - - # Find 3D variables - if f.ndims(var) == 3: - local_nx, local_ny, nz = data.shape - MXSUB = local_nx - 2 * MXG - MYSUB = local_ny - 2 * MYG - nx = NXPE * MXSUB + 2 * MXG - ny = NYPE * MYSUB - - if nx == cur_nx and ny == cur_ny and nz == cur_nz: - call_resize = False - break - elif nx == cur_nx and ny == cur_ny and nz != cur_nz: - if nz == 1: - # Override user specification to save time - self._use_expand = True - if self._use_expand: - call_resize = False - else: - call_resize = True - break - else: - call_resize = True - if self._restart == "append": - message = ("Cannot change nx, ny and/or nz " - "when appending\n") - # Extra plane in nz - message += ( - "Requested nx = {}, nx in restart file = {}\n" - "Requested ny = {}, ny in restart file = {}\n" - "Requested nz = {}, nz in restart file = {}\n" - "Resizing:\n"). format( - cur_nx, nx, cur_ny, ny, cur_nz, nz) - raise IOError(message) - else: - break - - if call_resize: - # Move runs - dst = self._move_old_runs(folder_name="beforeResize", - include_restart=True) - - # Redistributing the data to one file - # Redistribute - success = redistribute(1, - path=dst, - output=self._dmp_folder, - mxg=MXG, - myg=MYG, - ) - if not success: - message = ("Failed to redistribute to one file when " - "resizing evolved variables") - raise RuntimeError(message) - - # Move the redistributed to the resize folder - file_name = glob.glob(os.path.join(self._dmp_folder, - "BOUT.restart.0.*"))[0] - path, name = os.path.split(file_name) - before_resize_dir = os.path.join(path, "beforeResizingOneFile") - self._create_folder(before_resize_dir) - shutil.move(file_name, before_resize_dir) - - if self._use_expand: - print("\nDimension change found:\n" - "Requested nx = {}, nx in restart file = {}\n" - "Requested ny = {}, ny in restart file = {}\n" - "Resizing:\n" - .format(cur_nx, nx, cur_ny, ny)) - the_nz = nz - else: - print("\nDimension change found:\n" - "Requested nx = {}, nx in restart file = {}\n" - "Requested ny = {}, ny in restart file = {}\n" - "Requested nz = {}, nz in restart file = {}\n" - "Resizing:\n" - .format(cur_nx, nx, cur_ny, ny, cur_nz, nz)) - the_nz = cur_nz - - # NOTE: Different definition for nx and ny - success = resize(cur_nx, cur_ny + 2 * MYG, the_nz, - mxg=MXG, - myg=MYG, - path=before_resize_dir, - output=self._dmp_folder, - method=self._intrp_method, - maxProc=self._max_proc) - print("\n") - - if not success: - do_run = False - if self._cur_restart_from: - print("Something went wrong: Reomving {}\n". - format(os.path.split(dst)[0], "\n")) - shutil.rmtree(os.path.split(dst)[0]) - message = "Resize failed, skipping run." - self._warnings.append(message) - self._warning_printer(message) - - # Move the resized restart file - path, name = os.path.split(file_name) - # Create a temporary file which "redistribute" can read - # from - after_resize_dir = os.path.join(path, "afterResizingOneFile") - self._create_folder(after_resize_dir) - shutil.move(file_name, after_resize_dir) - - # Redistribute to original split - if self._redistribute: - nproc = self._redistribute - else: - nproc = self._nproc - - success = redistribute(nproc, - path=after_resize_dir, - output=self._dmp_folder, - mxg=MXG, - myg=MYG, - ) - - if not success: - message = ("Failed to redistribute after " - "resizing evolved variables") - if self._cur_restart_from: - print("Something went wrong: Reomving {}\n". - format(os.path.split(dst)[0], "\n")) - shutil.rmtree(os.path.split(dst)[0]) - raise RuntimeError(message) - - resized = True - #}}} - - #{{{ Resize nz only - if self._restart and do_run\ - and self._nz and not resized and self._use_expand: - # The current nz should be in the second index as any - # eventual other names would come from additional or - # series_add - cur_nz = int(self._dmp_folder. - split("nz")[1]. - split("/")[0]. - split("_")[1]) - if self._restart == "append": - # Check if nz is the same as in the restart files - # Start by opening the 0th restart file - file_name = glob.glob(os.path.join(self._dmp_folder, - "BOUT.restart.0.*"))[0] - with DataFile(file_name) as f: - # Loop over the variables in the file - for var in f.list(): - # Read the data - data = f.read(var) - - # Find 3D variables - if f.ndims(var) == 3: - _nx, _ny, nz = data.shape - - if nz != cur_nz: - message = ("Cannot change nz when appending\n" - "nz in restart file = {}\n" - "current run nz = {}").\ - format(nz, cur_nz) - raise IOError(message) - else: - break - - # Get the folder of the restart files - elif resized: - # Copy the files to afterResizeRedistr - after_resize_dir = os.path.join(path, "afterResizeRedistr") - self._create_folder(after_resize_dir) - file_names = glob.glob( - os.path.join(self._dmp_folder, "BOUT.restart.*")) - for file_name in file_names: - shutil.copy2(file_name, after_resize_dir) - # The restart files are stored in the resize folder - folder = "afterResizeRedistr*" - elif self._restart == "overwrite" and not(self._redistribute): - # The restart files are stored in the restart folder - folder = "restart*" - elif self._restart == "overwrite" and self._redistribute: - if self._cur_restart_from: - _ = self._move_old_runs(folder_name="redistribute", - include_restart=True) - - # The restart files are stored in the restart folder - folder = "redistribute*" - - if self._restart == "overwrite": - # Find the restart files - location = sorted( - glob.glob( - os.path.join( - self._dmp_folder, - folder))) - location = location[-1] - - # Check whether nz is changing or not - file_name = glob.glob( - os.path.join(location, "BOUT.restart.0.*"))[0] - - with DataFile(file_name) as f: - # Loop over the variables in the file - for var in f.list(): - # Read the data - data = f.read(var) - - # Find 3D variables - if f.ndims(var) == 3: - nx, ny, nz = data.shape - - if nz == cur_nz: - call_expand = False - else: - if nz < cur_nz: - call_expand = True - else: - if self._cur_restart_from: - print(("Something went wrong: " - "Reomving {}\n"). - format(os.path.split(location)[0])) - shutil.rmtree( - os.path.split(location)[0]) - message = ("Cannot decrease nz from {} to" - " {} in a restart").\ - format(nz, cur_nz) - raise IOError(message) - - if call_expand: - print("\nnz is bigger than in restart file, expanding:\n") - success = resizeZ(cur_nz, - path=location, - output=self._dmp_folder) - print("\n") - - if not success: - do_run = False - if self._cur_restart_from: - print("Something went wrong: Reomving {}\n". - format(os.path.split(location)[0])) - shutil.rmtree(os.path.split(location)[0]) - message = "resizeZ failed, skipping run." - self._warnings.append(message) - self._warning_printer(message) - #}}} - - #{{{ Add noise - if self._restart and self._add_noise and do_run: - print("Now adding noise\n") - for var, scale in self._add_noise.items(): - if scale is None: - scale = 1e-5 - print("No scale set for '{}', setting to {}\n". - format(var, scale)) - try: - addnoise(path=self._dmp_folder, - var=var, - scale=scale) - except Exception as ex: - print("{0}{1}addnoise failed with the following error:{0}". - format("\n" * 4, "!" * 3)) - raise ex - print("\n") - #}}} - - #{{{ Copy the source files if cpy_source is True - if self._cpy_source and do_run: - # This will copy all C++ files to the dmp_folder - cpp_extension = (".cc", ".cpp", ".cxx", ".C", ".c++", - ".h", ".hpp", ".hxx", ".h++") - # Copy for all files in the extension - for extension in cpp_extension: - file_names = glob.glob("*" + extension) - for a_file in file_names: - shutil.copy2(a_file, self._dmp_folder) - #}}} - - return do_run -#}}} - -#{{{_remove_data - def _remove_data(self): - """ - Removes dmp.*, fail.*, restart.*, log.* and *.cpy files from the - dump directory - """ - - print("Removing old data") - remove_extensions = ("dmp.*", "fail.*", "restart.*", "log.*", "cpy") - files_to_rm = [] - for extension in remove_extensions: - files_to_rm.extend( - glob.glob( - os.path.join(self._dmp_folder, "*." + extension))) - - # Cast to set (unique values) - files_to_rm = set(files_to_rm) - for f in files_to_rm: - os.remove(f) - - # Remove dirs - folder_to_rm = glob.glob( - os.path.join(self._dmp_folder, "before_redistribution_*")) - folder_to_rm.extend(glob.glob(os.path.join(self._dmp_folder, "run_*"))) - # Filter to only inlcude folders - folder_to_rm = tuple(f for f in folder_to_rm if os.path.isdir(f)) - for f in folder_to_rm: - shutil.rmtree(f) -#}}} - -#{{{_check_if_run_already_performed - def _check_if_run_already_performed(self, - restart_file_search_reason=None): - """ - Checks if the run has been run previously. - - Parameters - ---------- - restart_file_search_reason : ["restart_from" | "redistribute" | None ] - Reason to check for restart files if not None. - - Returns - ------- - bool : [True|False] - If true is returned, the run will be performed, if not the - run will not be performed - """ - - dmp_files = glob.glob(os.path.join(self._dmp_folder, "*.dmp.*")) - - if restart_file_search_reason: - restart_files =\ - glob.glob(os.path.join(self._dmp_folder, "*.restart.*")) - # Check if dmp or restart files are found - if len(dmp_files) != 0 or len(restart_files) != 0: - message = ("Restart or dmp files was found in {}" - " when {}" - " was set. Run skipped.").\ - format(self._dmp_folder, restart_file_search_reason) - self._warning_printer(message) - self._warnings.append(message) - return False - else: - return True - # Check if dmp files are found if restart is None - elif len(dmp_files) != 0 and self._restart is None: - print("Skipping the run as *.dmp.* files was found in " - + self._dmp_folder) - print(("To overwrite old files, run with" - " self.execute_runs(remove_old=True)\n")) - return False - else: - return True -#}}} - -#{{{_call_post_processing_function - def _call_post_processing_function( - self, - function=None, - folders=None, - **kwargs): - """Function which calls the post_processing_function""" - - function(folders, **kwargs) - -#}}} -#}}} - -#{{{Functions called by _error_check_for_run_input - #{{{_check_for_child_class_errors - def _check_for_child_class_errors( - self, - remove_old, - post_processing_function, - post_process_after_every_run - ): - """ - Function which check for errors in a child class. - - Here a virtual function - """ - pass - #}}} -#}}} - -#{{{Function called by _set_program_name -#{{{_run_make - def _run_make(self): - """Make cleans and makes the .cxx program""" - - print("Make clean eventually previously compiled\n") - command = "make clean" - status, output = shell(command, pipe=True) - print("Making the .cxx program\n") - command = "make" - status, output = shell(command, pipe=True) - print(output) - # Check if any errors occurred - if status != 0: - self._errors.append("RuntimeError") - raise RuntimeError("Error encountered during make.") -#}}} -#}}} - -#{{{ Functions called by the basic_error_checker -#{{{_check_for_correct_type - def _check_for_correct_type(self, - var=None, - the_type=None, - allow_iterable=None): - """ - Checks if a variable has the correct type - - Parameters - ---------- - var : tuple - var[0] - the variable (a data member) - - var[1] - the name of the variable given as a string - the_type : type - The data type to be checked - allow_iterable : bool - If an iterable with the element as type is allowed - """ - - # Set a variable which is False if the test fails - success = True - for cur_var in var: - # There is an option that the variable could be set to None, - # and that the default value from BOUT.inp will be used - if cur_var[0] is not None: - # Check for the correct type - if isinstance(cur_var[0], the_type) == False: - # Check if it is an iterable if iterables are - # allowed - if allow_iterable and\ - hasattr(cur_var[0], "__iter__") and\ - not isinstance(cur_var[0], dict): - for elem in cur_var[0]: - # Check for the correct type - if isinstance(elem, the_type) == False: - success = False - else: - # Neither correct type, nor iterable - success = False - if not(success): - message = ("{} is of wrong type\n" - "{} must be {}").\ - format(cur_var[1], the_type.__name__) - if allow_iterable: - # If iterable is allowed, then add this - message += (" or an iterable with {}" - " as elements.").format(the_type.__name__) - self._errors.append("TypeError") - raise TypeError(message) -#}}} - -#{{{_check_if_set_correctly - def _check_if_set_correctly(self, - var=None, - possibilities=None): - """ - Check if a variable is set to a possible variable. - Called by the error checkers - """ - - # Set a variable which is False if the test fails - success = True - - # Due to the check done in check_for_correct_type: If the - # variable is not a string it will be an iterable - if not isinstance(var[0], str): - for elem in var[0]: - # Check if the element is contained in the possibilities - if not(elem in possibilities): - success = False - else: - # The variable was a string - if not(var[0] in possibilities): - success = False - - if not(success): - message = ("{} was not set to a possible option.\n" - "The possibilities are \n{}").\ - format(var[1], "\n".join(possibilities)) - self._errors.append("TypeError") - raise TypeError(message) -#}}} - -#{{{_check_if_same_len - def _check_if_same_len(self, object1=None, object2=None): - """Checks if object1 and object2 has the same length - - Input: - object1 - a tuple of the object [0] and its name [1] - object2 - a tuple an object [0] different than object1 together with - its name [1] - """ - - try: - len_dim1 = len(object1[0]) - # If object1 does not have length - except TypeError: - len_dim1 = 1 - try: - len_dim2 = len(object2[0]) - # If object2 does not have length - except TypeError: - len_dim2 = 1 - - if len_dim1 != len_dim2: - message = ("{} and {} must have the same" - " length when specified").format(object1[1], object2[1]) - self._errors.append("RuntimeError") - raise RuntimeError(message) -#}}} -#}}} - -#{{{ Functions called by _get_correct_domain_split - #{{{_check_cur_split_found - def _check_cur_split_found(self, - cur_split_found, - produce_warning, - add_number, - size_nr, - local_nx, - local_ny, - using_nx=None, - using_ny=None): - #{{{docstring - """ - Checks if the current split is found. - - Will add a number if not found. - - Parameters - ---------- - cur_split_found : bool - Whether or not the current split was found - produce_warning : bool - If a warning should be produced - add_number : int - The number added to nx and/or ny - local_nx : [int|sequence of int] - Sequence of values of nx (a local value is used in order not to - alter self._nx) - local_ny : [int|sequence of int] - Sequence of values of ny (a local value is used in order not to - alter self._ny) - size_nr : int - Index of the current nx and/or ny - using_nx : bool - If add_number should be added to nx - using_ny : bool - if add_number should be added to ny - - Returns - ------- - local_nx : [int|sequence of int] - Sequence of values of nx - local_ny : [int|sequence of int] - Sequence of values of ny - add_number : int - The number to eventually be added the next time - produce_warning : bool - Whether or not a warning should be produced - """ - #}}} - - # If the value tried is not a good value - if not cur_split_found: - # Produce a warning - produce_warning = True - if using_nx: - local_nx[size_nr] += add_number - if using_ny: - local_ny[size_nr] += add_number - - print("Mismatch, trying {}*{}". - format(local_nx[size_nr], local_ny[size_nr])) - - # FIXME: This is a crude approach as we are adding one to - # both nx and ny - # Consider: Something like this - # nx+1 ny - # nx ny+1 - # nx-1 ny - # nx ny-1 - # nx+2 ny - # nx ny+2 - # nx-2 ny - # nx ny-2 - # ... - add_number = (-1)**(abs(add_number))\ - * (abs(add_number) + 1) - else: - # If no warnings has been produced so far - if not(produce_warning): - produce_warning = False - - return local_nx, local_ny, add_number, produce_warning - #}}} - - #{{{_check_init_split_found - def _check_init_split_found(self, - init_split_found, - size_nr, - local_nx, - local_ny, - test_nx=None, - test_ny=None, - produce_warning=None): - #{{{docstring - """ - Check if the initial split was a good choice when checking the grids. - - Will raise eventual errors. - - Parameters - ---------- - init_split_found : bool - Whether or not a good split was found on the first trial - size_nr : int - The index of the current nx, ny or NXPE under consideration - local_nx : [int|sequence of int] - Sequence of values of nx (a local value is used in order not to - alter self._nx) - local_ny : [int|sequence of int] - Sequence of values of ny (a local value is used in order not to - alter self._ny) - test_nx : bool - whether or not the test was run on nx - test_ny : bool - whether or not the test was run on ny - produce_warning : bool - whether or not a warning should be produced - """ - #}}} - - #{{{ If the initial split did not succeed - if not(init_split_found): - # If modification is allowed - if not(self._allow_size_modification) or\ - (self._grid_file is not None): - # If the split fails and the a grid file is given - if self._grid_file is not None: - self._errors.append("RuntimeError") - message = ("The grid can not be split using the" - " current number of nproc.\n" - "Suggest using ") - if test_nx: - message += "nx = {} ".format(self._nx[size_nr]) - if test_ny: - message += "ny = {} ".format(self._ny[size_nr]) - message += " with the current nproc" - raise RuntimeError(message) - # If the split fails and no grid file is given - else: - self._errors.append("RuntimeError") - message = ("The grid can not be split using the" - " current number of nproc.\n" - "Setting allow_size_modification = True" - " will allow modification of the grid" - " so that it can be split with the" - " current number of nproc") - raise RuntimeError(message) - else: - # Set nx and ny - self._nx = local_nx - self._ny = local_ny - #}}} - - #{{{ When the good value is found - print("Successfully found the following good values for the mesh:") - message = "" - if test_nx: - message += "nx = {} ".format(local_nx[size_nr]) - if test_ny: - message += "ny = {} ".format(local_ny[size_nr]) - - print(message + "\n") - #}}} - - #{{{ Make the warning if produced - if produce_warning: - message = "The mesh was changed to allow the split given by nproc" - self._warning_printer(message) - self._warnings.append(message) - #}}} - #}}} - - #{{{_check_NXPE_or_NYPE - def _check_NXPE_or_NYPE(self, - local_nx, - local_ny, - type_str=None, - MXG=None, - produce_warning=None, - ): - #{{{docstring - """ - Check if NXPE or NYPE is consistent with nproc - - Parameters - ---------- - - local_nx : [int|sequence of int] - Sequence of values of nx (a local value is used in order not to - alter self._nx) - local_ny : [int|sequence of int] - Sequence of values of ny (a local value is used in order not to - alter self._ny) - type_str : ["NXPE" | "NYPE"] - Can be either "NXPE" or "NYPE" and is specifying whether - NXPE or NYPE should be checked - MXG : int - The current MXG - produce_warning : bool - Whether or not a warning should be produced - """ - #}}} - - for size_nr in range(len(local_nx)): - # Check the type - if type_str == "NXPE": - print("Checking nx = {} with NXPE = {}". - format(local_nx[size_nr], self._NXPE[size_nr])) - elif type_str == "NYPE": - print("Checking ny = {} with NYPE = {}". - format(local_ny[size_nr], self._NYPE[size_nr])) - # Check to see if succeeded - init_split_found = False - cur_split_found = False - add_number = 1 - # Counter to see how many times the while loop has been - # called - count = 0 - - #{{{While cur_split_found == False - while cur_split_found == False: - # The same check as below is performed internally in - # BOUT++ (see boutmesh.cxx under - # if((MX % NXPE) != 0) - # and - # if((MY % NYPE) != 0) - if type_str == "NXPE": - MX = local_nx[size_nr] - 2 * MXG - # self._nproc is called NPES in boutmesh - if (MX % self._NXPE[size_nr]) == 0: - # If the test passes - cur_split_found = True - # Check if cur_split_found is true, eventually - # update the add_number - local_nx, local_ny, add_number, produce_warning\ - = self._check_cur_split_found(cur_split_found, - produce_warning, - add_number, - size_nr, - local_nx, - local_ny, - using_nx=True, - using_ny=False) - elif type_str == "NYPE": - MY = local_ny[size_nr] - # self._nproc is called NPES in boutmesh - if (MY % self._NYPE[size_nr]) == 0: - # If the test passes - cur_split_found = True - # Check if cur_split_found is true, eventually - # update the add_number - local_nx, local_ny, add_number, produce_warning\ - = self._check_cur_split_found(cur_split_found, - produce_warning, - add_number, - size_nr, - local_nx, - local_ny, - using_nx=False, - using_ny=True) - - #{{{ Check if the split was found the first go. - # This will be used if self_allow_size_modification is - # off, or if we are using a grid file - if count == 0 and cur_split_found: - init_split_found = True - #}}} - - # Add one to the counter - count += 1 - #}}} - - # Check if initial split succeeded - if type_str == "NXPE": - self._check_init_split_found(init_split_found, - size_nr, - local_nx, - local_ny, - test_nx=True, - test_ny=False, - produce_warning=produce_warning) - elif type_str == "NYPE": - self._check_init_split_found(init_split_found, - size_nr, - local_nx, - local_ny, - test_nx=False, - test_ny=True, - produce_warning=produce_warning) - #}}} -#}}} - -#{{{Function called by _prepare_dmp_folder -#{{{_get_folder_name - def _get_folder_name(self, combination): - """ - Returning the folder name where the data will be stored. - - If all options are given the folder structure should be on the - form solver/method/nout_timestep/mesh/additional/grid - """ - - # Combination is one of the combination of the data members - # which is used as the command line arguments in the run - combination = combination.split() - - #{{{Append from eventual grid file - # FIXME: The grid-file names can become long if adding these, - # consider using just path name to gridfile - # If there is a grid file, we will extract the values from the - # file, and put it into this local combination variable, so that - # a proper dmp folder can be made on basis on the variables - # A flag to see whether or not the grid file was found - grid_file_found = False - # Check if grid is in element, and extract its path - for elem in combination: - if elem[0:5] == "grid=": - cur_grid = elem.replace("grid=", "") - grid_file_found = True - - # If the grid file is found, open it - if grid_file_found: - # Open (and automatically close) the grid files - f = DataFile(cur_grid) - # Search for mesh types in the grid file - mesh_types = ( - ("mesh:", "nx"), - ("mesh:", "ny"), - ("mesh:", "nz"), - ("mesh:", "zperiod"), - ("mesh:", "zmin"), - ("mesh:", "zmax"), - ("mesh:", "dx"), - ("mesh:", "dy"), - ("mesh:", "dz"), - ("mesh:", "ixseps1"), - ("mesh:", "ixseps2"), - ("mesh:", "jyseps1_1"), - ("mesh:", "jyseps1_2"), - ("mesh:", "jyseps2_1"), - ("mesh:", "jyseps2_2"), - ("", "MXG"), - ("", "MYG"), - ) - for mesh_type in mesh_types: - grid_variable = f.read(mesh_type[1]) - # If the variable is found - if grid_variable is not None: - if len(grid_variable.shape) > 0: - # Chosing the first - grid_variable =\ - "{:.2e}".format(grid_variable.flatten()[0]) - # Append it to the combinations list - combination.append("{}{}={}".format(mesh_type[0], - mesh_type[1], - grid_variable)) - #}}} - - # Make lists for the folder-type, so that we can append the - # elements in the combination folders if it is found - solver = [] - method = [] - nout_timestep = [] - mesh = [] - additional = [] - grid_file = [] - - # We will loop over the names describing the methods used - # Possible directional derivatives - dir_derivatives = ("ddx", "ddy", "ddz") - - # Check trough all the elements of combination - for elem in combination: - - # If "solver" is in the element - if "solver" in elem: - # Remove 'solver:' and append it to the mesh folder - cur_solver = elem.replace("solver:", "") - cur_solver = cur_solver.replace("=", "_") - # Append it to the solver folder - solver.append(cur_solver) - - # If nout or timestep is in the element - elif ("nout" in elem) or\ - ("timestep" in elem): - # Remove "=", and append it to the - # nout_timestep folder - nout_timestep.append(elem.replace("=", "_")) - - # If any quantity related to mesh is in the combination - elif ("mesh" in elem) or\ - ("MXG" in elem) or\ - ("MYG" in elem) or\ - ("NXPE" in elem) or\ - ("NYPE" in elem) or\ - ("zperiod" in elem) or\ - ("zmin" in elem) or\ - ("zmax" in elem) or\ - (("dx" in elem) and not("ddx" in elem)) or\ - (("dy" in elem) and not("ddy" in elem)) or\ - (("dz" in elem) and not("ddz" in elem)): - # Remove "mesh:", and append it to the mesh folder - cur_mesh = elem.replace("mesh:", "") - cur_mesh = cur_mesh.replace("=", "_") - # Simplify the mesh spacing - if ("dx" in elem) or ("dy" in elem) or ("dz" in elem): - cur_mesh = cur_mesh.split("_") - cur_mesh = "{}_{:.2e}".format( - cur_mesh[0], float(cur_mesh[1])) - mesh.append(cur_mesh) - - # If a grid file is in the combination - elif (elem[0:4] == "grid"): - # Remove .grd .nc and = - cur_grid = elem.replace(".grd", "") - cur_grid = cur_grid.replace(".nc", "") - cur_grid = cur_grid.replace("=", "_") - grid_file.append(cur_grid) - - # If the element is none of the above - else: - # It could either be a dir derivative - # Set a flag to state if any of the dir derivative was - # found in the combination - dir_derivative_set = False - # If any of the methods are in combination - for dir_derivative in dir_derivatives: - if dir_derivative in elem: - # Remove ":", and append it to the - # method folder - cur_method = elem.replace(":", "_") - cur_method = cur_method.replace("=", "_") - method.append(cur_method) - dir_derivative_set = True - - # If the dir_derivative_set was not set, the only - # possibility left is that the element is an - # "additional" option - if not(dir_derivative_set): - # Replace ":" and "=" and append it to the - # additional folder - cur_additional = elem.replace(":", "_") - cur_additional = cur_additional.replace("=", "_") - cur_additional = cur_additional.replace('"', "-") - cur_additional = cur_additional.replace("'", "-") - cur_additional = cur_additional.replace("(", ",") - cur_additional = cur_additional.replace(")", ",") - additional.append(cur_additional) - - # We sort the elements in the various folders alphabetically, - # to ensure that the naming convention is always the same, no - # matter how the full combination string looks like - # Sort alphabetically - solver.sort() - #{{{ Manual sort solver - # We want "type" to be first, and "atol" and "rtol" to be last - sort_these = ( - ("type", 0), - ("atol", -1), - ("rtol", -1) - ) - # Loop through everything we want to sort - for sort_this in sort_these: - # Flag to check if found - found_string = False - for elem_nr, elem in enumerate(solver): - if sort_this[0] in elem: - swap_nr = elem_nr - # Set the flag that the string is found - found_string = True - # If type was found - if found_string: - # Swap the elements in the solver - solver[sort_this[1]], solver[swap_nr] =\ - solver[swap_nr], solver[sort_this[1]] - #}}} - method.sort() - nout_timestep.sort() - mesh.sort() - additional.sort() - grid_file.sort() - - # Combine the elements in the various folders - solver = ("_".join(solver),) - method = ("_".join(method),) - nout_timestep = ("_".join(nout_timestep),) - mesh = ("_".join(mesh),) - additional = ("_".join(additional),) - grid_file = ("_".join(grid_file),) - - # Put all the folders into the combination_folder - combination_folder = ( - solver, - method, - nout_timestep, - mesh, - additional, - grid_file - ) - # We access the zeroth element (if given) as the folders are - # given as a sequence - combination_folder = tuple(folder[0] for folder in combination_folder - if (len(folder) != 0) and not("" in folder)) - - # Make the combination folder as a string - combination_folder = "/".join(combination_folder) - - return combination_folder -#}}} - -#{{{_create_folder - def _create_folder(self, folder): - """Creates a folder if it doesn't exists""" - - if not os.path.exists(folder): - os.makedirs(folder) - print(folder + " created\n") -#}}} - -#{{{_copy_run_files - def _copy_run_files(self): - """ - Function which copies run files from self._cur_restart_from - """ - - do_run =\ - self._check_if_run_already_performed( - restart_file_search_reason="restart_from") - - if do_run: - print("\nCopying files from {0} to {1}\n". - format(self._cur_restart_from, self._dmp_folder)) - - # Files with these extension will be given the - # additional extension .cpy when copied to the destination - # folder - extensions_w_cpy = ["inp"] - # When the extension is not a real extension - has_extensions_w_cpy = ["log.*"] - - if self._cpy_source: - extensions_w_cpy.extend(["cc", "cpp", "cxx", "C", "c++", - "h", "hpp", "hxx", "h++"]) - - # Python 3 syntax (not python 2 friendly) - # extensions =\ - # (*extensions_w_cpy, *has_extensions_w_cpy, "restart.*") - extensions = extensions_w_cpy - for item in has_extensions_w_cpy: - extensions.append(item) - extensions.append("restart.*") - - if self._restart == "append": - extensions.append("dmp.*") - - # Copy for all files in the extension - for extension in extensions: - file_names = glob.glob( - os.path.join( - self._cur_restart_from, - "*." + extension)) - for cur_file in file_names: - # Check if any of the extensions matches the current - # string - if any([cur_file.endswith(ewc) - for ewc in extensions_w_cpy]): - # Add ".cpy" to the file name (without the path) - name = os.path.split(cur_file)[-1] + ".cpy" - shutil.copy2(cur_file, - os.path.join(self._dmp_folder, name)) - # When the extension is not a real extension we must - # remove "*" in the string as shutil doesn't accept - # wildcards - elif any([hewc.replace("*", "") in cur_file - for hewc in has_extensions_w_cpy]): - # Add ".cpy" to the file name (without the path) - name = os.path.split(cur_file)[-1] + ".cpy" - shutil.copy2(cur_file, - os.path.join(self._dmp_folder, name)) - else: - shutil.copy2(cur_file, self._dmp_folder) - - return do_run -#}}} - -#{{{_move_old_runs - def _move_old_runs(self, folder_name="restart", include_restart=False): - """Move old runs, return the destination path""" - - # Check for folders in the dmp directory - directories = tuple( - name for name in - os.listdir(self._dmp_folder) if - os.path.isdir(os.path.join( - self._dmp_folder, name)) - ) - # Find occurrences of "folder_name", split, and cast result to number - restart_nr = tuple(int(name.split("_")[-1]) for name in directories - if folder_name in name) - # Check that the sequence is not empty - if len(restart_nr) != 0: - # Sort the folders in ascending order - restart_nr = sorted(restart_nr) - # Pick the last index - restart_nr = restart_nr[-1] - # Add one to the restart_nr, as we want to create - # a new directory - restart_nr += 1 - else: - # Set the restart_nr - restart_nr = 0 - # Create the folder for the previous runs - self._create_folder(os.path.join( - self._dmp_folder, - "{}_{}".format(folder_name, restart_nr))) - - extensions_to_move = ["cpy", "log.*", "dmp.*", - "cc", "cpp", "cxx", "C", "c++", - "h", "hpp", "hxx", "h++"] - - if include_restart: - extensions_to_move.append("restart.*") - - dst = os.path.join(self._dmp_folder, - "{}_{}".format(folder_name, restart_nr)) - - print("Moving old runs to {}\n".format(dst)) - - for extension in extensions_to_move: - file_names =\ - glob.glob(os.path.join(self._dmp_folder, "*." + extension)) - - # Cast to unique file_names - file_names = set(file_names) - - # Move the files - for cur_file in file_names: - shutil.move(cur_file, dst) - - if not(include_restart): - # We would like to save the restart files as well - print("Copying restart files to {}\n".format(dst)) - file_names =\ - glob.glob(os.path.join(self._dmp_folder, "*.restart.*")) - - # Cast to unique file_names - file_names = set(file_names) - - # Copy the files - for cur_file in file_names: - shutil.copy2(cur_file, dst) - - return dst -#}}} -#}}} - -#{{{Function called by _run_driver -#{{{_single_run - def _single_run(self, combination): - """Makes a single MPIRUN of the program""" - - # Get the command to be used - command = self._get_command_to_run(combination) - - # Time how long the time took - tic = timeit.default_timer() - - # Launch the command - status, out = launch(command, - runcmd=self._MPIRUN, - nproc=self._nproc, - pipe=True, - verbose=True) - - # If the run returns an exit code other than 0 - if status != 0: - message = "! An error occurred. Printing the output to stdout !" - print("{0}{1}{2}{1}{0}{3}". - format("\n", "!" * len(message), message, out)) - self._errors.append("RuntimeError") - message = ("An error occurred the run." - " Please see the output above for details.") - # Search if parantheses are present, but without ' or " - if ("(" in combination and - not(re.search(r'\"(.*)\(', combination) - or re.search(r"\'(.*)\(", combination)))\ - or (")" in combination and - not(re.search(r'\)(.*)\"', combination) - or re.search(r"\)(.*)\'", combination))): - message = ( - "A '(' and/or ')' symbol seem to have appeared in the" - " command line.\nIf this true, you can avoid" - " this problem by adding an extra set of" - " quotation marks. For example\n\n" - "additional=('variable', 'bndry_xin'," - " '\"dirichlet_o4(0.0)\")'\n" - "rather than\n" - "additional=('variable', 'bndry_xin'," - " 'dirichlet_o4(0.0))'") - else: - message = ("An error occurred the run." - " Please see the output above for details.") - raise RuntimeError(message) - - # Estimate elapsed time - toc = timeit.default_timer() - elapsed_time = toc - tic - - return out, elapsed_time -#}}} - -#{{{_append_run_log - def _append_run_log(self, start, run_no, run_time): - """Appends the run_log""" - - # Convert seconds to H:M:S - run_time = str(datetime.timedelta(seconds=run_time)) - - start_time = "{}-{}-{}-{}:{}:{}".\ - format(start.year, start.month, start.day, - start.hour, start.minute, start.second) - - # If the run is restarted with initial values from the last run - if self._restart: - dmp_line = "{}-restart-{}".format(self._dmp_folder, self._restart) - if self._cur_restart_from: - dmp_line += " from " + self._cur_restart_from - else: - dmp_line = self._dmp_folder - - # Line to write - line = (start_time, self._run_type, run_no, run_time, dmp_line) - # Opens for appending - log_format = "{:<19} {:^9} {:^6} {:<17} {:<}" - with open(self._run_log, "a") as f: - f.write(log_format.format(*line) + "\n") -#}}} -#}}} - -#{{{Function called by _get_possibilities -#{{{_generate_possibilities - def _generate_possibilities(self, variables=None, section=None, name=None): - """Generate the list of strings of possibilities""" - - if variables is not None: - # Set the section name correctly - if section != "": - section = section + ":" - else: - section = "" - # Set the combination of the variable - var_possibilities = [] - # Find the number of different dimensions - - for var in variables: - var_possibilities.append("{}{}={}".format(section, name, var)) - else: - var_possibilities = [] - - return var_possibilities -#}}} -#}}} - -#{{{Functions called by _get_combinations -#{{{_get_swapped_input_list - def _get_swapped_input_list(self, input_list): - """ - Finds the element in the input list, which corresponds to the - self._sort_by criterion. The element is swapped with the last - index, so that itertools.product will make this the fastest - varying variable - """ - - # We make a sort list containing the string to find in the - # input_list - sort_list = [] - - # We loop over the elements in self._sort_by to find what - # string we need to be looking for in the elements of the lists - # in input_list - for sort_by in self._sort_by: - # Find what list in the input_list which contains what we - # would sort by - - #{{{ If we would like to sort by the spatial domain - if sort_by == "spatial_domain": - # nx, ny and nz are all under the section "mesh" - find_in_list = "mesh" - #}}} - - #{{{ If we would like to sort by the temporal domain - elif sort_by == "temporal_domain": - # If we are sorting by the temporal domain, we can either - # search for timestep or nout - if self._timestep is not None: - find_in_list = "timestep" - elif self._nout is not None: - find_in_list = "nout" - #}}} - - #{{{ If we would like to sort by the method - elif (sort_by == "ddx_first") or\ - (sort_by == "ddx_second") or\ - (sort_by == "ddx_upwind") or\ - (sort_by == "ddx_flux") or\ - (sort_by == "ddy_first") or\ - (sort_by == "ddy_second") or\ - (sort_by == "ddy_upwind") or\ - (sort_by == "ddy_flux") or\ - (sort_by == "ddz_first") or\ - (sort_by == "ddz_second") or\ - (sort_by == "ddz_upwind") or\ - (sort_by == "ddz_flux"): - find_in_list = sort_by.replace("_", ":") - #}}} - - #{{{ If we would like to sort by the solver - elif sort_by == "solver": - find_in_list = sort_by - #}}} - - #{{{ If we would like to sort by anything else - else: - find_in_list = sort_by - #}}} - - # Append what to be found in the input_list - sort_list.append(find_in_list) - - # For all the sort_list, we would like check if the match - # can be found in any of the elements in input_list - # Appendable list - lengths = [] - for sort_nr, sort_by_txt in enumerate(sort_list): - # Make a flag to break the outermost loop if find_in_list is - # found - break_outer = False - # Loop over the lists in the input_list to find the match - for elem_nr, elem in enumerate(input_list): - # Each of the elements in this list is a string - for string in elem: - # Check if fins_in_list is in the string - if sort_by_txt in string: - # If there is a match, store the element number - swap_from_index = elem_nr - # Check the length of the element (as this is - # the number of times the run is repeated, only - # changing the values of sort_by [defining a - # group]) - lengths.append(len(elem)) - # Break the loop to save time - break_outer = True - break - # Break the outer loop if find_in_list_is_found - if break_outer: - break - - # As it is the last index which changes the fastest, we swap the - # element where the find_in_list was found with the last element - input_list[swap_from_index], input_list[-(sort_nr + 1)] =\ - input_list[-(sort_nr + 1)], input_list[swap_from_index] - - # The number of runs in one "group" - # Initialize self._len_group with one as we are going to - # multiply it with all the elements in lengths - self._len_group = 1 - for elem in lengths: - self._len_group *= elem - - return input_list -#}}} -#}}} - -#{{{Function called by _single_run -#{{{_get_command_to_run - def _get_command_to_run(self, combination): - """ - Returns a string of the command which will run the BOUT++ - program - """ - - # Creating the arguments - arg = " -d {} {}".format(self._dmp_folder, combination) - - # If the run is set to overwrite - if self._restart == "overwrite": - arg += " restart" - elif self._restart == "append": - arg += " restart append" - - # Replace excessive spaces with a single space - arg = " ".join(arg.split()) - command = "./{} {}".format(self._program_name, arg) - - return command -#}}} -#}}} - -#{{{Functions called from several places in the code -#{{{_get_MXG_MYG - def _get_MXG_MYG(self): - """Function which returns the MXG and MYG""" - - if self._MXG is None: - try: - MXG = eval(self._inputFileOpts.root["mxg"]) - except KeyError: - message = ("Could not find 'MXG' or 'mxg' " - "in the input file. " - "Setting MXG = 2") - self._warning_printer(message) - self._warnings.append(message) - MXG = 2 - else: - MXG = self._MXG - if self._MYG is None: - try: - MYG = eval(self._inputFileOpts.root["myg"]) - except KeyError: - message = ("Could not find 'MYG' or 'myg' " - "in the input file. " - "Setting MYG = 2") - self._warning_printer(message) - self._warnings.append(message) - MYG = 2 - else: - MYG = self._MYG - - return MXG, MYG -#}}} - -#{{{_get_dim_from_input - def _get_dim_from_input(self, direction): - """ - Get the dimension from the input - - Parameters - ---------- - direction : ["nx"|"ny"|"nz"|"mz"] - The direction to read - - Returns - ------- - Number of points in the given direction - """ - - # If nx and ny is a function of MXG and MYG - MXG, MYG = self._get_MXG_MYG() - # NOTE: MXG may seem unused, but it needs to be in the current - # namespace if eval(self._inputFileOpts.mesh["nx"]) depends on - # MXG - - if self._grid_file: - # Open the grid file and read it - with DataFile(self._grid_file) as f: - # Loop over the variables in the file - n_points = f.read(direction) - else: - try: - n_points = eval(self._inputFileOpts.mesh[direction]) - except NameError: - message = "Could not evaluate\n" - message += self._inputFileOpts.mesh[direction] - message += "\nfound in {} in [mesh] in the input file.".\ - format(direction) - raise RuntimeError(message) - - return n_points -#}}} - - #{{{_warning_printer - def _warning_printer(self, message): - """Function for printing warnings""" - - print("{}{}WARNING{}".format("\n" * 3, "*" * 37, "*" * 36)) - # Makes sure that no more than 80 characters are printed out at - # the same time - for chunk in self._message_chunker(message): - rigth_padding = " " * (76 - len(chunk)) - print("* {}{} *".format(chunk, rigth_padding)) - print("*" * 80 + "\n" * 3) - #}}} - - #{{{_message_chunker - def _message_chunker(self, message, chunk=76): - """Generator used to chop a message so it doesn't exceed some - width""" - - for start in range(0, len(message), chunk): - yield message[start:start + chunk] - #}}} -#}}} -#}}} - -#{{{class PBS_runner - - -class PBS_runner(basic_runner): - #{{{docstring - """ - pbs_runner - ---------- - - Class for mpi running one or several runs with BOUT++. - Works like the basic_runner, but submits the jobs to a Portable - Batch System (PBS). - - For the additional member data, see the docstring of __init__. - - For more info check the docstring of bout_runners. - """ -#}}} - -# The constructor -#{{{__init__ - def __init__(self, - BOUT_nodes=1, - BOUT_ppn=1, - BOUT_walltime=None, - BOUT_queue=None, - BOUT_mail=None, - BOUT_run_name=None, - BOUT_account=None, - post_process_nproc=None, - post_process_nodes=None, - post_process_ppn=None, - post_process_walltime=None, - post_process_queue=None, - post_process_mail=None, - post_process_run_name=None, - post_process_account=None, - **kwargs): - #{{{docstring - """ - PBS_runner constructor - ---------------------- - - All the member data is set to None by default, with the - exception of BOUT_nodes (default=1) and BOUT_ppn (default = 4). - - Parameters - ---------- - - BOUT_nodes : int - Number of nodes for one submitted BOUT job - BOUT_ppn : int - Processors per node for one submitted BOUT job - BOUT_walltime : str - Maximum wall time for one submitted BOUT job - BOUT_queue : str - The queue to submit the BOUT jobs - BOUT_mail : str - Mail address to notify when a BOUT job has finished - BOUT_run_name : str - Name of the BOUT run on the cluster (optional) - BOUT_account : str - Account number to use for the run (optional) - post_process_nproc : int - Total number of processors for one submitted post processing - job - post_process_nodes : int - Number of nodes for one submitted post processing job - post_process_ppn : int - Processors per node for one submitted BOUT job - post_process_walltime : str - Maximum wall time for one submitting post processing job - post_process_queue : str - The queue to submit the post processing jobs - post_process_mail : str - Mail address to notify when a post processing job has - finished - post_process_run_name : str - Name of the post processing run on the cluster (optional) - post_process_account : str - Account number to use for the post processing (optional) - **kwargs : any - As the constructor of bout_runners is called, this - additional keyword makes it possible to specify the member - data of bout_runners in the constructor of PBS_runner (i.e. - nprocs = 1 is an allowed keyword argument in the constructor - of PBS_runner). - - For a full sequence of possible keywords, see the docstring of - the bout_runners constructor. - """ - #}}} - - # Note that the constructor accepts additional keyword - # arguments (**kwargs). These must match the keywords of the - # parent class "basic_runner", which is called by the "super" - # function below - - # Call the constructor of the superclass - super(PBS_runner, self).__init__(**kwargs) - - # Options set for the BOUT runs - self._BOUT_nodes = BOUT_nodes - self._BOUT_ppn = BOUT_ppn - self._BOUT_walltime = BOUT_walltime - self._BOUT_mail = BOUT_mail - self._BOUT_queue = BOUT_queue - self._BOUT_run_name = BOUT_run_name - self._BOUT_account = BOUT_account - # Options set for the post_processing runs - self._post_process_nproc = post_process_nproc - self._post_process_nodes = post_process_nodes - self._post_process_ppn = post_process_ppn - self._post_process_walltime = post_process_walltime - self._post_process_mail = post_process_mail - self._post_process_queue = post_process_queue - self._post_process_run_name = post_process_run_name - self._post_process_account = post_process_account - - # Options set for all runs - self._run_type = "basic_PBS" - - # Error check the input data - self._check_for_PBS_instance_error() - - # Initialize the jobid returned from the PBS - self._PBS_id = [] -#}}} - -# The run_driver -#{{{_run_driver - def _run_driver(self, combination, run_no): - """The machinery which actually performs the run""" - - # Submit the job to the queue - self._single_submit(combination, run_no, append_to_run_log=True) -#}}} - -#{{{Functions called by the constructor - #{{{_check_for_PBS_instance_error - def _check_for_PBS_instance_error(self): - """Check if there are any type errors when creating the object""" - - #{{{Check if BOUT_ppn and BOUT_nodes have the correct type - # BOUT_ppn and BOUT_nodes are set by default, however, we must check - # that the user has not given them as wrong input - if not isinstance(self._BOUT_ppn, int): - message = ("BOUT_ppn is of wrong type\n" - "BOUT_ppn must be given as a int") - self._errors.append("TypeError") - raise TypeError(message) - if not isinstance(self._BOUT_nodes, int): - message = ("BOUT_nodes is of wrong type\n" - "BOUT_nodes must be given as a int") - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check that nprocs, BOUT_nodes and BOUT_ppn is consistent - if self._nproc > (self._BOUT_nodes * self._BOUT_ppn): - message = "Must have nproc <= BOUT_nodes * BOUT_ppn" - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check all the proper post_process data is set if any is set - check_if_set = ( - self._post_process_nproc, - self._post_process_nodes, - self._post_process_ppn, - ) - # All elements of check_if_set must be set if any is set - not_None = 0 - for check in check_if_set: - if check is not None: - not_None += 1 - - if (not_None != 0) and (not_None != len(check_if_set)): - message = ("If any of post_process_nproc, post_process_nodes," - " post_process_ppn and post_process_walltime is" - " set, all others must be set as well.") - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check if post_process_ppn and post_process_nodes is int if set - check_if_int = ( - (self._post_process_nodes, "post_process_nodes"), - (self._post_process_ppn, "post_process_ppn") - ) - self._check_for_correct_type(var=check_if_int, - the_type=int, - allow_iterable=False) - #}}} - - #{{{Check that post_process_nprocs,nodes,ppn is consistent if set - if self._post_process_nproc is not None: - if self._post_process_nproc > \ - (self._post_process_nodes * self._post_process_ppn): - message = ("Must have post_process_nproc <= " - "post_process_nodes * post_process_ppn") - self._errors.append("TypeError") - raise TypeError(message) - #}}} - - #{{{Check if walltime, mail and queue is a string if set - check_if_str = ( - (self._BOUT_walltime, "BOUT_walltime"), - (self._BOUT_mail, "BOUT_mail"), - (self._BOUT_queue, "BOUT_queue"), - (self._BOUT_run_name, "BOUT_run_name"), - (self._BOUT_account, "BOUT_account"), - (self._post_process_walltime, "BOUT_walltime"), - (self._post_process_mail, "post_process_mail"), - (self._post_process_queue, "post_process_queue"), - (self._post_process_run_name, "post_process_run_name"), - (self._post_process_account, "post_process_account"), - ) - self._check_for_correct_type(var=check_if_str, - the_type=str, - allow_iterable=False) - #}}} - - #{{{Check that walltime is on correct format - # A list to loop over - walltimes = [] - # Append the walltimes if set - if self._BOUT_walltime is not None: - walltimes.append((self._BOUT_walltime, - "BOUT_walltime")) - if self._post_process_walltime is not None: - walltimes.append((self._post_process_walltime, - "post_process_walltime")) - - # Loop over the walltimes - for walltime in walltimes: - # Set a flag which states whether or not the check was - # successful - success = True - # Split the walltime string - walltime_list = walltime[0].split(":") - # Check that the list has three elements - if len(walltime_list) == 3: - - # Check that seconds is on the format SS - if len(walltime_list[2]) == 2: - # Check that the last element (seconds) is a digit (int) - if walltime_list[2].isdigit(): - # Check that the element is less than 59 - if int(walltime_list[2]) > 59: - success = False - # Seconds is not a digit - else: - success = False - # Seconds is not on the format SS - else: - success = False - - # Do the same for the second last element (minutes) - if len(walltime_list[1]) == 2: - # Check that the last element (seconds) is a digit (int) - if walltime_list[1].isdigit(): - if int(walltime_list[1]) > 59: - success = False - # Minutes is not a digit - else: - success = False - # Seconds is not on the format SS - else: - success = False - - # Check that the first element (hours) is a digit - if not(walltime_list[0].isdigit()): - success = False - - # walltime_list does not have three elements - else: - success = False - - if not(success): - message = walltime[1] + " must be on the form H...H:MM:SS" - self._errors.append("TypeError") - raise TypeError(message) - #}}} - #}}} -#}}} - -#{{{Functions called by _error_check_for_run_input - #{{{_check_for_child_class_errors - def _check_for_child_class_errors( - self, - remove_old, - post_processing_function, - post_process_after_every_run - ): - """Function which check for errors in a child class.""" - - # Check member data is set if post_processing_function is not None - if post_processing_function is not None: - check_if_set = ( - self._post_process_nproc, - self._post_process_nodes, - self._post_process_ppn, - ) - # All elements of check_if_set must be set if any is set - not_None = 0 - for check in check_if_set: - if check is not None: - not_None += 1 - - if (not_None != 0) and (not_None != len(check_if_set)): - message = ("post_process_nproc, post_process_nodes," - " and post_process_ppn and must" - " be set if post_processing_function is set.") - self._errors.append("TypeError") - raise TypeError(message) - #}}} -#}}} - -#{{{Functions called by the execute_runs - #{{{ _print_run_or_submit - def _print_run_or_submit(self): - """Prints "submitting" """ - print("\nSubmitting:") - #}}} -#}}} - -#{{{Functions called by _run_driver - #{{{_single_submit - def _single_submit(self, combination, run_no, append_to_run_log=None): - """Submit a single BOUT job and submit the jobid to self._PBS_id""" - - # Get the script (as a string) which is going to be - # submitted - job_string = self._get_job_string(run_no, - combination, - append_to_run_log) - - # The submission - PBS_id = self._submit_to_PBS(job_string) - self._PBS_id.append(PBS_id) - #}}} - - #{{{_call_post_processing_function - def _call_post_processing_function( - self, - function=None, - folders=None, - **kwargs - ): - """ - Function which submits the post processing to the PBS - - This is done by making a self deleting temporary python file - that will be called by a PBS script. - """ - - #{{{ Create a python script, calling the post-processing function - # Get the start_time (to be used in the name of the file) - start_time = self._get_start_time() - - # The name of the file - python_name = "tmp_{}_{}.py".format(function.__name__, start_time) - - # Make the script - python_tmp = "#!/usr/bin/env python3\n" - python_tmp += "import os, sys\n" - # Set the python path - python_tmp += "sys.path = {}\n".format(sys.path) - # Import the post processing function - python_tmp += "from {} import {}\n".\ - format(function.__module__, function.__name__) - # Convert the keyword args to proper arguments - # Appendable list - arguments = [] - for key in kwargs.keys(): - if not isinstance(kwargs[key], str): - # If the value is not a string, we can append it directly - arguments.append("{}={}".format(key, kwargs[key])) - else: - # If the value is a string, we need to put quotes around - arguments.append("{}='{}'".format(key, kwargs[key])) - - # Put a comma in between the arguments - arguments = ", ".join(arguments) - # Call the post processing function - if hasattr(folders, "__iter__") and not isinstance(folders, str): - python_tmp += "{}({},{})\n".\ - format(function.__name__, tuple(folders), arguments) - elif isinstance(folders, str): - python_tmp += "{}(('{}',),{})\n".\ - format(function.__name__, folders, arguments) - # When the script has run, it will delete itself - python_tmp += "os.remove('{}')\n".format(python_name) - - # Write the python script - with open(python_name, "w") as f: - f.write(python_tmp) - #}}} - - #{{{Create and submit the shell script - # Creating the job string - if self._post_process_run_name is None: - job_name = "post_process_{}_".format(function.__name__, start_time) - else: - job_name = self._post_process_run_name - - # Get core of the job string - job_string = self._create_PBS_core_string( - job_name=job_name, - nodes=self._post_process_nodes, - ppn=self._post_process_ppn, - walltime=self._post_process_walltime, - mail=self._post_process_mail, - queue=self._post_process_queue, - account=self._post_process_account, - ) - # Call the python script in the submission - - job_string += "python {}\n".format(python_name) - job_string += "exit" - - # Create the dependencies - dependencies = ":".join(self._PBS_id) - # Submit the job - print("\nSubmitting the post processing function '{}'\n". - format(function.__name__)) - self._submit_to_PBS(job_string, dependent_job=dependencies) - #}}} - #}}} -#}}} - -#{{{ Functions called by _single_submit - #{{{_get_job_string - def _get_job_string(self, run_no, combination, append_to_run_log): - """ - Make a string which will saved as a shell script before being - sent to the PBS queue. - """ - - #{{{Make the job name based on the combination - # Split the name to a list - combination_name = combination.split(" ") - # Remove whitespace - combination_name = tuple(element for element in combination_name - if element != "") - # Collect the elements - combination_name = "_".join(combination_name) - # Replace bad characters - combination_name = combination_name.replace(":", "") - combination_name = combination_name.replace("=", "-") - - # Name of job - if self._BOUT_run_name is None: - job_name = "{}_{}_{}".\ - format(combination_name, self._directory, run_no) - else: - job_name = self._BOUT_run_name - #}}} - - #{{{Make the main command that will be used in the PBS script - command = self._get_command_to_run(combination) - command = "mpirun -np {} {}".format(self._nproc, command) - - # Print the command - print(command + "\n") - #}}} - - #{{{ Creating the core job string - job_string = self._create_PBS_core_string( - job_name=job_name, - nodes=self._BOUT_nodes, - ppn=self._BOUT_ppn, - walltime=self._BOUT_walltime, - mail=self._BOUT_mail, - queue=self._BOUT_queue, - account=self._BOUT_account, - ) - #}}} - - if append_to_run_log: - #{{{ Get the time for start of the submission - start = datetime.datetime.now() - start_time = "{}-{}-{}-{}:{}:{}".\ - format(start.year, start.month, start.day, - start.hour, start.minute, start.second) - #}}} - - #{{{ Start the timer - job_string += "start=`date +%s`\n" - # Run the bout program - job_string += command + "\n" - # end the timer - job_string += "end=`date +%s`\n" - # Find the elapsed time - job_string += "time=$((end-start))\n" - # The string is now in seconds - # The following procedure will convert it to H:M:S - job_string += "h=$((time/3600))\n" - job_string += "m=$((($time%3600)/60))\n" - job_string += "s=$((time%60))\n" - #}}} - - #{{{ Append to the run log - # Ideally we would check if any process were writing to - # run_log.txt - # This could be done with lsof command as described in - # http://askubuntu.com/questions/14252/how-in-a-script-can-i-determine-if-a-file-is-currently-being-written-to-by-ano - # However, lsof is not available on all clusters - - # Using the same formatting as in _append_run_log, we are going - # to echo the following to the run_log when the run is finished - job_string += "echo '" +\ - "{:<19}".format(start_time) + " " * 3 +\ - "{:^9}".format(self._run_type) + " " * 3 +\ - "{:^6}".format(str(run_no)) + " " * 3 +\ - "'$h':'$m':'$s" + " " * 10 +\ - "{:<}".format(self._dmp_folder) + " " * 3 +\ - " >> $PBS_O_WORKDIR/" + self._directory +\ - "/run_log.txt\n" - #}}} - - # Exit the qsub - job_string += "exit" - - return job_string - #}}} -#}}} - -#{{{Functions called by _submit_to_PBS -#{{{_get_start_time - def _get_start_time(self): - """ - Returns a string of the current time down to micro precision - """ - - # The time is going to be appended to the job name and python name - # In case the process is really fast, so that more than one job - # is submitted per second, we add a microsecond in the - # names for safety - time_now = datetime.datetime.now() - start_time = "{}-{}-{}-{}".format(time_now.hour, - time_now.minute, - time_now.second, - time_now.microsecond, - ) - return start_time -#}}} -#}}} - -#{{{Functions called by several functions -#{{{_create_PBS_core_string - def _create_PBS_core_string( - self, - job_name=None, - nodes=None, - ppn=None, - walltime=None, - mail=None, - queue=None, - account=None, - ): - """ - Creates the core of a PBS script as a string - """ - - # Shebang line - job_string = "#!/bin/bash\n" - # The job name - job_string += "#PBS -N {}\n".format(job_name) - job_string += "#PBS -l nodes={}:ppn={}\n".format(nodes, ppn) - # If walltime is set - if walltime is not None: - # Wall time, must be in format HOURS:MINUTES:SECONDS - job_string += "#PBS -l walltime={}\n".format(walltime) - # If submitting to a specific queue - if queue is not None: - job_string += "#PBS -q {}\n".format(queue) - job_string += "#PBS -o {}.log\n".\ - format(os.path.join(self._dmp_folder, job_name)) - job_string += "#PBS -e {}.err\n".\ - format(os.path.join(self._dmp_folder, job_name)) - if account is not None: - job_string += "#PBS -A {}\n".format(account) - # If we want to be notified by mail - if mail is not None: - job_string += "#PBS -M {}\n".format(mail) - # #PBS -m abe - # a=aborted b=begin e=ended - job_string += "#PBS -m e\n" - # cd to the folder you are sending the qsub from - job_string += "cd $PBS_O_WORKDIR\n" - - return job_string -#}}} - -#{{{_submit_to_PBS - def _submit_to_PBS(self, job_string, dependent_job=None): - """ - Saves the job_string as a shell script, submits it and deletes - it. Returns the output from PBS as a string - """ - - # Create the name of the temporary shell script - # Get the start_time used for the name of the script - start_time = self._get_start_time() - script_name = "tmp_{}.sh".format(start_time) - - # Save the string as a script - with open(script_name, "w") as shell_script: - shell_script.write(job_string) - - # Submit the jobs - if dependent_job is None: - # Without dependencies - command = "qsub ./" + script_name - status, output = shell(command, pipe=True) - else: - # If the length of the depend job is 0, then all the jobs - # have completed, and we can carry on as usual without - # dependencies - if len(dependent_job) == 0: - command = "qsub ./" + script_name - status, output = shell(command, pipe=True) - else: - # With dependencies - command = "qsub -W depend=afterok:{} ./{}".\ - format(dependent_job, script_name) - status, output = shell(command, pipe=True) - - # Check for success - if status != 0: - if status == 208: - message = ("Runs finished before submission of the post" - " processing function. When the runs are done:" - " Run again with 'remove_old = False' to submit" - " the function.") - self._warnings.append(message) - else: - print("\nSubmission failed, printing output\n") - print(output) - self._errors.append("RuntimeError") - message = ("The submission failed with exit code {}" - ", see the output above").format(status) - raise RuntimeError(message) - - # Trims the end of the output string - output = output.strip(" \t\n\r") - - # Delete the shell script - try: - os.remove(script_name) - except FileNotFoundError: - # Do not raise an error - pass - - return output -#}}} -#}}} -#}}} - - -#{{{if __name__ == "__main__": -if __name__ == "__main__": - - print(("\n\nTo find out about the bout_runners, please read the user's " - "manual, or have a look at 'BOUT/examples/bout_runners_example', " - "or have a look at the documentation")) -#}}}