diff --git a/.github/workflows/test_paid.yml b/.github/workflows/test_paid.yml index 878336f7f..c8fe34c03 100644 --- a/.github/workflows/test_paid.yml +++ b/.github/workflows/test_paid.yml @@ -257,12 +257,15 @@ jobs: -DCMAKE_CUDA_ARCHITECTURES=${{ env.cuda_arch }} -DTEST_ALL_DEPLOYMENTS=${{ env.test_all_deploys }} -DTEST_NUM_MIXED_DEPLOYMENT_REPETITIONS=${{ env.test_repetitions }} - -DPERMIT_NODES_TO_SHARE_GPU=${{ env.mpi_share_gpu }} -DCMAKE_CXX_FLAGS=${{ matrix.mpi == 'ON' && matrix.cuda == 'ON' && '-fno-lto' || '' }} - name: Compile run: cmake --build ${{ env.build_dir }} --parallel + # permit use of single GPU by multiple MPI processes (detriments performance) + - name: Set env-var to permit GPU sharing + run: echo "PERMIT_NODES_TO_SHARE_GPU=${{ env.mpi_share_gpu }}" >> $GITHUB_ENV + # cannot use ctests when distributed, grr! - name: Run GPU + distributed v4 mixed tests (4 nodes sharing 1 GPU) run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 1080c55fc..8418b5ba5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -231,15 +231,6 @@ if ((ENABLE_CUDA OR ENABLE_HIP) AND FLOAT_PRECISION STREQUAL 4) message(FATAL_ERROR "Quad precision is not supported on GPU. Please disable GPU acceleration or lower precision.") endif() -option( - PERMIT_NODES_TO_SHARE_GPU - "Whether to permit multiple distributed nodes to share a single GPU at the detriment of performance. Turned OFF by default." - OFF -) -if (ENABLE_DISTRIBUTION AND (ENABLE_CUDA OR ENABLE_HIP)) - message(STATUS "Permitting nodes to share GPUs is turned ${PERMIT_NODES_TO_SHARE_GPU}. Set PERMIT_NODES_TO_SHARE_GPU to modify.") -endif() - # Deprecated API option( ENABLE_DEPRECATED_API @@ -318,7 +309,7 @@ if (ENABLE_MULTITHREADING) if (NOT OpenMP_FOUND) set(ErrorMsg "Could not find OpenMP, necessary for enabling multithreading.") if (APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") - string(APPEND ErrorMsg " Try first calling `brew install libomp` then `export OpenMP_ROOT=$(brew --prefix)/opt/libomp`") + string(APPEND ErrorMsg " Try first calling \n\tbrew install libomp\nthen\n\texport OpenMP_ROOT=$(brew --prefix)/opt/libomp") endif() message(FATAL_ERROR ${ErrorMsg}) endif() @@ -434,14 +425,6 @@ else() endif() -if (ENABLE_DISTRIBUTION AND (ENABLE_CUDA OR ENABLE_HIP)) - target_compile_definitions( - QuEST PRIVATE - PERMIT_NODES_TO_SHARE_GPU=$,1,0> - ) -endif() - - # add math library if (NOT MSVC) target_link_libraries(QuEST PRIVATE ${MATH_LIBRARY}) diff --git a/docs/launch.md b/docs/launch.md index aadd02ca7..44a0f7fd7 100644 --- a/docs/launch.md +++ b/docs/launch.md @@ -22,6 +22,7 @@ Launching your [compiled](compile.md) QuEST application can be as straightforwar > - Tests > * v4 > * v3 +> - Configuring > - Multithreading > * Choosing threads > * Monitoring utilisation @@ -29,11 +30,11 @@ Launching your [compiled](compile.md) QuEST application can be as straightforwar > - GPU-acceleration > * Launching > * Monitoring -> * Configuring +> * Configuring > * Benchmarking > - Distribution > * Launching -> * Configuring +> * Configuring > * Benchmarking > - Multi-GPU > - Supercomputers @@ -243,6 +244,21 @@ ctest +--------------------- + + + + +## Configuring + +QuEST execution can be configured prior to runtime using the below [environment variables](https://en.wikipedia.org/wiki/Environment_variable). + +- [`PERMIT_NODES_TO_SHARE_GPU`](https://quest-kit.github.io/QuEST/group__modes.html#ga7e12922138caa68ddaa6221e40f62dda) +- [`DEFAULT_VALIDATION_EPSILON`](https://quest-kit.github.io/QuEST/group__modes.html#ga55810d6f3d23de810cd9b12a2bbb8cc2) + + + + --------------------- @@ -429,7 +445,7 @@ Usage of GPU-acceleration can be (inadvisably) forced using [`createForcedQureg( - + ### Configuring @@ -514,7 +530,7 @@ mpirun -np 1024 --oversubscribe ./mytests - + ### Configuring diff --git a/quest/include/environment.h b/quest/include/environment.h index a71454f0e..04f24bfe2 100644 --- a/quest/include/environment.h +++ b/quest/include/environment.h @@ -40,6 +40,9 @@ typedef struct { // deployment modes which cannot be directly changed after compilation int isCuQuantumEnabled; + // deployment configurations which can be changed via environment variables + int isGpuSharingEnabled; + // distributed configuration int rank; int numNodes; diff --git a/quest/include/modes.h b/quest/include/modes.h index b90797acd..7633d2bca 100644 --- a/quest/include/modes.h +++ b/quest/include/modes.h @@ -75,10 +75,6 @@ // define optional-macro defaults (mostly to list them) -#ifndef PERMIT_NODES_TO_SHARE_GPU -#define PERMIT_NODES_TO_SHARE_GPU 0 -#endif - #ifndef INCLUDE_DEPRECATED_FUNCTIONS #define INCLUDE_DEPRECATED_FUNCTIONS 0 #endif @@ -93,11 +89,6 @@ #if 0 - /// @notyetdoced - /// @macrodoc - const int PERMIT_NODES_TO_SHARE_GPU = 0; - - /// @notyetdoced /// @macrodoc const int INCLUDE_DEPRECATED_FUNCTIONS = 0; @@ -112,6 +103,73 @@ +// document environment variables + +// spoof env-vars as consts to doc (hackily and hopefully temporarily) +#if 0 + + + /** @envvardoc + * + * Specifies whether to permit multiple MPI processes to deploy to the same GPU. + * + * @attention + * This environment variable has no effect when either (or both) of distribution or + * GPU-acceleration are disabled. + * + * In multi-GPU execution, which combines distribution with GPU-acceleration, it is + * prudent to assign each GPU to at most one MPI process in order to avoid superfluous + * slowdown. Hence by default, initQuESTEnv() will forbid assigning multiple MPI processes + * to the same GPU. This environment variable can be set to `1` to disable this validation, + * permitting sharing of a single GPU, as is often useful for debugging or unit testing + * (for example, testing multi-GPU execution when only a single GPU is available). + * + * @warning + * Permitting GPU sharing may cause unintended behaviour when additionally using cuQuantum. + * + * @envvarvalues + * - forbid sharing: @p 0, @p '0', @p '', @p , (unspecified) + * - permit sharing: @p 1, @p '1' + * + * @author Tyson Jones + */ + const int PERMIT_NODES_TO_SHARE_GPU = 0; + + + /** @envvardoc + * + * Specifies the default validation epsilon. + * + * Specifying `DEFAULT_VALIDATION_EPSILON` to a positive, real number overrides the + * precision-specific default (`1E-5`, `1E-12`, `1E-13` for single, double and quadruple + * precision respectively). The specified epsilon is used by QuEST for numerical validation + * unless overriden at runtime via setValidationEpsilon(), in which case it can be + * restored to that specified by this environment variable using setValidationEpsilonToDefault(). + * + * @envvarvalues + * - setting @p DEFAULT_VALIDATION_EPSILON=0 disables numerical validation, as if the value + * were instead infinity. + * - setting @p DEFAULT_VALIDATION_EPSILON='' is equivalent to _not_ specifying the variable, + * adopting instead the precision-specific default above. + * - setting @p DEFAULT_VALIDATION_EPSILON=x where `x` is a positive, valid `qreal` in any + * format accepted by `C` or `C++` (e.g. `0.01`, `1E-2`, `+1e-2`) will use `x` as the + * default validation epsilon. + * + * @constraints + * The function initQuESTEnv() will throw a validation error if: + * - The specified epsilon must be `0` or positive. + * - The specified epsilon must not exceed that maximum or minimum value which can be stored + * in a `qreal`, which is specific to its precision. + * + * @author Tyson Jones + */ + const qreal DEFAULT_VALIDATION_EPSILON = 0; + + +#endif + + + // user flags for choosing automatic deployment; only accessible by C++ // backend and C++ users; C users must hardcode -1 diff --git a/quest/include/precision.h b/quest/include/precision.h index cfd150855..f7a18e416 100644 --- a/quest/include/precision.h +++ b/quest/include/precision.h @@ -121,34 +121,19 @@ /* - * RE-CONFIGURABLE DEFAULT VALIDATION PRECISION + * DEFAULT VALIDATION PRECISION * - * which is compile-time overridable by pre-defining DEFAULT_VALIDATION_EPSILON (e.g. - * in user code before importing QuEST, or passed as a preprocessor constant by the - * compiler using argument -D), and runtime overridable using setValidationEpsilon() + * which is pre-run-time overridable by specifying the corresponding environment variable. */ -#ifndef DEFAULT_VALIDATION_EPSILON - - #if FLOAT_PRECISION == 1 - #define DEFAULT_VALIDATION_EPSILON 1E-5 - - #elif FLOAT_PRECISION == 2 - #define DEFAULT_VALIDATION_EPSILON 1E-12 - - #elif FLOAT_PRECISION == 4 - #define DEFAULT_VALIDATION_EPSILON 1E-13 - - #endif - -#endif +#if FLOAT_PRECISION == 1 + #define UNSPECIFIED_DEFAULT_VALIDATION_EPSILON 1E-5 -// spoofing above macros as typedefs and consts to doc -#if 0 +#elif FLOAT_PRECISION == 2 + #define UNSPECIFIED_DEFAULT_VALIDATION_EPSILON 1E-12 - /// @notyetdoced - /// @macrodoc - const qreal DEFAULT_VALIDATION_EPSILON = 1E-12; +#elif FLOAT_PRECISION == 4 + #define UNSPECIFIED_DEFAULT_VALIDATION_EPSILON 1E-13 #endif diff --git a/quest/src/api/environment.cpp b/quest/src/api/environment.cpp index 63b6f41ef..6eef515c4 100644 --- a/quest/src/api/environment.cpp +++ b/quest/src/api/environment.cpp @@ -11,7 +11,9 @@ #include "quest/src/core/errors.hpp" #include "quest/src/core/memory.hpp" +#include "quest/src/core/parser.hpp" #include "quest/src/core/printer.hpp" +#include "quest/src/core/envvars.hpp" #include "quest/src/core/autodeployer.hpp" #include "quest/src/core/validation.hpp" #include "quest/src/core/randomiser.hpp" @@ -75,6 +77,9 @@ void validateAndInitCustomQuESTEnv(int useDistrib, int useGpuAccel, int useMulti // this leads to undefined behaviour in distributed mode, as per the MPI validate_envNeverInit(globalEnvPtr != nullptr, hasEnvBeenFinalized, caller); + envvars_validateAndLoadEnvVars(caller); + validateconfig_setEpsilonToDefault(); + // ensure the chosen deployment is compiled and supported by hardware. // note that these error messages will be printed by every node because // validation occurs before comm_init() below, so all processes spawned @@ -102,12 +107,17 @@ void validateAndInitCustomQuESTEnv(int useDistrib, int useGpuAccel, int useMulti if (useGpuAccel) gpu_bindLocalGPUsToNodes(); - // each MPI process must use a unique GPU. This is critical when - // initializing cuQuantum, so we don't re-init cuStateVec on any - // paticular GPU (causing runtime error), but still ensures we - // keep good performance in our custom backend GPU code; there is - // no reason to use multi-nodes-per-GPU except for dev/debugging. - if (useGpuAccel && useDistrib && ! PERMIT_NODES_TO_SHARE_GPU) + // consult environment variable to decide whether to allow GPU sharing + // (default = false) which informs whether below validation is triggered + bool permitGpuSharing = envvars_getWhetherGpuSharingIsPermitted(); + + // each MPI process should ordinarily use a unique GPU. This is + // critical when initializing cuQuantum so that we don't re-init + // cuStateVec on any paticular GPU (which can apparently cause a + // so-far-unwitnessed runtime error), but is otherwise essential + // for good performance. GPU sharing is useful for unit testing + // however permitting a single GPU to test CUDA+MPI deployment + if (useGpuAccel && useDistrib && ! permitGpuSharing) validate_newEnvNodesEachHaveUniqueGpu(caller); /// @todo @@ -132,10 +142,11 @@ void validateAndInitCustomQuESTEnv(int useDistrib, int useGpuAccel, int useMulti error_allocOfQuESTEnvFailed(); // bind deployment info to global instance - globalEnvPtr->isMultithreaded = useMultithread; - globalEnvPtr->isGpuAccelerated = useGpuAccel; - globalEnvPtr->isDistributed = useDistrib; - globalEnvPtr->isCuQuantumEnabled = useCuQuantum; + globalEnvPtr->isMultithreaded = useMultithread; + globalEnvPtr->isGpuAccelerated = useGpuAccel; + globalEnvPtr->isDistributed = useDistrib; + globalEnvPtr->isCuQuantumEnabled = useCuQuantum; + globalEnvPtr->isGpuSharingEnabled = permitGpuSharing; // bind distributed info globalEnvPtr->rank = (useDistrib)? comm_getRank() : 0; @@ -188,10 +199,11 @@ void printDeploymentInfo() { print_table( "deployment", { - {"isMpiEnabled", globalEnvPtr->isDistributed}, - {"isGpuEnabled", globalEnvPtr->isGpuAccelerated}, - {"isOmpEnabled", globalEnvPtr->isMultithreaded}, - {"isCuQuantumEnabled", globalEnvPtr->isCuQuantumEnabled}, + {"isMpiEnabled", globalEnvPtr->isDistributed}, + {"isGpuEnabled", globalEnvPtr->isGpuAccelerated}, + {"isOmpEnabled", globalEnvPtr->isMultithreaded}, + {"isCuQuantumEnabled", globalEnvPtr->isCuQuantumEnabled}, + {"isGpuSharingEnabled", globalEnvPtr->isGpuSharingEnabled}, }); } diff --git a/quest/src/comm/comm_routines.cpp b/quest/src/comm/comm_routines.cpp index 3c03d23f6..6e161db18 100644 --- a/quest/src/comm/comm_routines.cpp +++ b/quest/src/comm/comm_routines.cpp @@ -76,6 +76,12 @@ using std::vector; * * - look into UCX CUDA multi-rail: * https://docs.nvidia.com/networking/display/hpcxv215/unified+communication+-+x+framework+library#src-119764120_UnifiedCommunicationXFrameworkLibrary-Multi-RailMulti-Rail + * + * - by default, we validate to prevent sharing a GPU between multiple MPI processes since it is + * easy to do unintentionally yet is rarely necessary (outside of unit testing) and can severely + * degrade performance. If we motivated a strong non-testing use-case for this however, we could + * improve performance through use of CUDA's Multi-Process Service (MPS) which will prevent + * serialisation of memcpy to distinct memory partitions and improve kernel scheduling. */ diff --git a/quest/src/core/CMakeLists.txt b/quest/src/core/CMakeLists.txt index e498d4569..9d11d16d7 100644 --- a/quest/src/core/CMakeLists.txt +++ b/quest/src/core/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(QuEST PRIVATE accelerator.cpp autodeployer.cpp + envvars.cpp errors.cpp localiser.cpp memory.cpp diff --git a/quest/src/core/envvars.cpp b/quest/src/core/envvars.cpp new file mode 100644 index 000000000..c88647e0e --- /dev/null +++ b/quest/src/core/envvars.cpp @@ -0,0 +1,158 @@ +/** @file + * Functions for loading environment variables, useful for + * configuring QuEST ahead of calling initQuESTEnv(), after + * compilation. + * + * @author Tyson Jones + */ + +#include "quest/include/precision.h" +#include "quest/include/types.h" + +#include "quest/src/core/errors.hpp" +#include "quest/src/core/parser.hpp" +#include "quest/src/core/validation.hpp" + +#include +#include + +using std::string; + + + +/* + * FIXED ENV-VAR NAMES + */ + + +namespace envvar_names { + string PERMIT_NODES_TO_SHARE_GPU = "PERMIT_NODES_TO_SHARE_GPU"; + string DEFAULT_VALIDATION_EPSILON = "DEFAULT_VALIDATION_EPSILON"; +} + + + +/* + * USER-OVERRIDABLE DEFAULT ENV-VAR VALUES + */ + + +namespace envvar_values { + + // by default, do not permit GPU sharing since it sabotages performance + // and should only ever be carefully, deliberately enabled + bool PERMIT_NODES_TO_SHARE_GPU = false; + + // by default, the initial validation epsilon (before being overriden + // by users at runtime) should depend on qreal (i.e. FLOAT_PRECISION) + qreal DEFAULT_VALIDATION_EPSILON = UNSPECIFIED_DEFAULT_VALIDATION_EPSILON; +} + + +// indicates whether envvars_validateAndLoadEnvVars() has been called +bool global_areEnvVarsLoaded = false; + + + +/* + * PRIVATE UTILITIES + */ + + +bool isEnvVarSpecified(string name) { + + // note var="" is considered unspecified, but var=" " is specified + const char* ptr = std::getenv(name.c_str()); + return (ptr != nullptr) && (ptr[0] != '\0'); +} + + +string getSpecifiedEnvVarValue(string name) { + + // assumes isEnvVarSpecified returned true + // (calling getenv() a second time is fine) + return std::string(std::getenv(name.c_str())); +} + + +void assertEnvVarsAreLoaded() { + + if (!global_areEnvVarsLoaded) + error_envVarsNotYetLoaded(); +} + + + +/* + * PRIVATE BESPOKE ENV-VAR LOADERS + * + * which we have opted to not-yet make generic + * (e.g. for each type) since YAGNI + */ + + +void validateAndSetWhetherGpuSharingIsPermitted(const char* caller) { + + // permit unspecified, falling back to default value + string name = envvar_names::PERMIT_NODES_TO_SHARE_GPU; + if (!isEnvVarSpecified(name)) + return; + + // otherwise ensure value == '0' or '1' precisely (no whitespace) + string value = getSpecifiedEnvVarValue(name); + validate_envVarPermitNodesToShareGpu(value, caller); + + // overwrite default env-var value + envvar_values::PERMIT_NODES_TO_SHARE_GPU = (value[0] == '1'); +} + + +void validateAndSetDefaultValidationEpsilon(const char* caller) { + + // permit unspecified, falling back to the hardcoded precision-specific default + string name = envvar_names::DEFAULT_VALIDATION_EPSILON; + if (!isEnvVarSpecified(name)) + return; + + // otherwise, validate user passed a positive real integer (or zero) + string value = getSpecifiedEnvVarValue(name); + validate_envVarDefaultValidationEpsilon(value, caller); + + // overwrite default env-var value + envvar_values::DEFAULT_VALIDATION_EPSILON = parser_parseReal(value); +} + + + +/* + * PUBLIC + */ + + +void envvars_validateAndLoadEnvVars(const char* caller) { + + // error if loaded twice since this indicates spaghetti + if (global_areEnvVarsLoaded) + error_envVarsAlreadyLoaded(); + + // load all env-vars + validateAndSetWhetherGpuSharingIsPermitted(caller); + validateAndSetDefaultValidationEpsilon(caller); + + // ensure no re-loading + global_areEnvVarsLoaded = true; +} + + +bool envvars_getWhetherGpuSharingIsPermitted() { + assertEnvVarsAreLoaded(); + + return envvar_values::PERMIT_NODES_TO_SHARE_GPU; +} + + +qreal envvars_getDefaultValidationEpsilon() { + assertEnvVarsAreLoaded(); + + return envvar_values::DEFAULT_VALIDATION_EPSILON; +} diff --git a/quest/src/core/envvars.hpp b/quest/src/core/envvars.hpp new file mode 100644 index 000000000..828d5605e --- /dev/null +++ b/quest/src/core/envvars.hpp @@ -0,0 +1,37 @@ +/** @file + * Functions for loading environment variables, useful for + * configuring QuEST ahead of calling initQuESTEnv(), after + * compilation. + * + * @author Tyson Jones + */ + +#ifndef ENVVARS_HPP +#define ENVVARS_HPP + +#include + + +namespace envvar_names { + extern std::string PERMIT_NODES_TO_SHARE_GPU; + extern std::string DEFAULT_VALIDATION_EPSILON; +} + + +/* + * LOAD VARS + */ + +void envvars_validateAndLoadEnvVars(const char* caller); + + +/* + * GET VAR + */ + +bool envvars_getWhetherGpuSharingIsPermitted(); + +qreal envvars_getDefaultValidationEpsilon(); + + +#endif // ENVVARS_HPP diff --git a/quest/src/core/errors.cpp b/quest/src/core/errors.cpp index a2c2649ca..2f44127c8 100644 --- a/quest/src/core/errors.cpp +++ b/quest/src/core/errors.cpp @@ -818,3 +818,19 @@ void assert_printerGivenPositiveNumNewlines() { if (printer_getNumTrailingNewlines() < min) raiseInternalError("A printer utility attempted to print one fewer than the user-set number of trailing newlines; but that number was zero! This violates prior validation."); } + + + +/* + * ENVIRONMENT VARIABLE ERRORS + */ + +void error_envVarsNotYetLoaded() { + + raiseInternalError("An environment variable was queried but all environment variables have not yet been loaded."); +} + +void error_envVarsAlreadyLoaded() { + + raiseInternalError("All environment variables were already loaded and validated yet re-loading was attempted."); +} diff --git a/quest/src/core/errors.hpp b/quest/src/core/errors.hpp index 8c39ee756..ce8f7e68c 100644 --- a/quest/src/core/errors.hpp +++ b/quest/src/core/errors.hpp @@ -339,4 +339,14 @@ void assert_printerGivenPositiveNumNewlines(); +/* + * ENVIRONMENT VARIABLE ERRORS + */ + +void error_envVarsNotYetLoaded(); + +void error_envVarsAlreadyLoaded(); + + + #endif // ERRORS_HPP \ No newline at end of file diff --git a/quest/src/core/parser.cpp b/quest/src/core/parser.cpp index 31dad4e5f..8884acc4c 100644 --- a/quest/src/core/parser.cpp +++ b/quest/src/core/parser.cpp @@ -10,6 +10,7 @@ * @author Tyson Jones */ +#include "quest/include/precision.h" #include "quest/include/types.h" #include "quest/include/paulis.h" @@ -25,7 +26,6 @@ #include #include -using std::stold; using std::regex; using std::vector; using std::string; @@ -82,9 +82,9 @@ namespace patterns { string num = group(comp) + "|" + group(imag) + "|" + group(real); // no capturing because 'num' pollutes captured groups, and pauli syntax overlaps real integers - string pauli = "[" + parser_RECOGNISED_PAULI_CHARS + "]"; + string pauli = "[" + parser_RECOGNISED_PAULI_CHARS + "]"; string paulis = group(optSpace + pauli + optSpace) + "+"; - string line = "^" + group(num) + space + optSpace + paulis + "$"; + string weightedPaulis = "^" + group(num) + space + optSpace + paulis + "$"; } @@ -95,8 +95,8 @@ namespace regexes { regex imag(patterns::imag); regex comp(patterns::comp); regex num(patterns::num); - regex line(patterns::line); regex paulis(patterns::paulis); + regex weightedPaulis(patterns::weightedPaulis); } @@ -172,6 +172,165 @@ int getNumPaulisInLine(string line) { +/* + * REAL NUMBER PARSING + */ + + +qreal precisionAgnosticStringToFloat(string str) { + + // remove whitespace which stold() et al cannot handle after the sign. + // beware this means that e.g. "1 0" (invalid number) would become "10" + // (valid) so this function cannot be used for duck-typing, though that + // is anyway the case since stold() et al permit "10abc" + removeWhiteSpace(str); + + // below throws exception when the (prefix) of str cannot be/fit into a qreal + if (FLOAT_PRECISION == 1) return static_cast(std::stof (str)); + if (FLOAT_PRECISION == 2) return static_cast(std::stod (str)); + if (FLOAT_PRECISION == 4) return static_cast(std::stold(str)); + + // unreachable + return -1; +} + + +bool parser_isAnySizedReal(string str) { + + // we assume that all strings which match the regex can be parsed by + // precisionAgnosticStringToFloat() above (once whitespace is removed) + // EXCEPT strings which contain a number too large to store in the qreal + // type (as is separately checked below). Note it is insufficient to merely + // duck-type using stold() et al because such functions permit non-numerical + // characters to follow the contained number which are silently removed (grr!) + smatch match; + return regex_match(str, match, regexes::real); +} + + +bool parser_isValidReal(string str) { + + // reject str if it doesn't match regex + if (!parser_isAnySizedReal(str)) + return false; + + // check number is in-range of qreal via duck-typing + try { + precisionAgnosticStringToFloat(str); + } catch (const out_of_range&) { + return false; + + // error if our regex permitted an unparsable string + } catch (const invalid_argument&) { + error_attemptedToParseRealFromInvalidString(); + } + + return true; +} + + +qreal parser_parseReal(string str) { + + try { + return precisionAgnosticStringToFloat(str); + } catch (const invalid_argument&) { + error_attemptedToParseRealFromInvalidString(); + } catch (const out_of_range&) { + error_attemptedToParseOutOfRangeReal(); + } + + // unreachable + return -1; +} + + + +/* + * COMPLEX NUMBER PARSING + */ + + +bool parser_isAnySizedComplex(string str) { + + // we assume that all strings which match the regex can be parsed to + // a qcomp (once whitespace is removed) EXCEPT strings which contain a + // number too large to store in the qcomp type (as is separately checked + // below). Note it is insufficient to merely duck-type each component using + // using stold() et al because such functions permit non-numerical chars to + // follow the contained number (grr!) + smatch match; + + // must match real, imaginary or complex number regex + if (regex_match(str, match, regexes::real)) return true; + if (regex_match(str, match, regexes::imag)) return true; + if (regex_match(str, match, regexes::comp)) return true; + + return false; +} + + +bool parser_isValidComplex(string str) { + + // reject str if it doesn't match complex regex + if (!parser_isAnySizedComplex(str)) + return false; + + // we've so far gauranteed str has a valid form, but we must now check + // each included complex component (which we enumerate) is in range of a qreal + sregex_iterator it(str.begin(), str.end(), regexes::real); + sregex_iterator end; + + // valid coeffs contain 1 or 2 reals, never 0, which regex should have caught + if (it == end) + error_attemptedToParseComplexFromInvalidString(); + + // for each of the 1 or 2 components... + for (; it != end; it++) { + + // check component is in-range of qreal via duck-typing + try { + precisionAgnosticStringToFloat(it->str(0)); + } catch (const out_of_range&) { + return false; + + // error if our regex permitted an unparsable component + } catch (const invalid_argument&) { + error_attemptedToParseComplexFromInvalidString(); + } + } + + // report that each/all detected components of str can form a valid qcomp + return true; +} + + +qcomp parser_parseComplex(string str) { + + if (!parser_isValidComplex(str)) + error_attemptedToParseComplexFromInvalidString(); + + // we are gauranteed to fully match real, imag or comp after prior validation + smatch match; + + // extract and parse components and their signs (excluding imaginary symbol) + if (regex_match(str, match, regexes::real)) + return qcomp(parser_parseReal(match.str(1)), 0); + + if (regex_match(str, match, regexes::imag)) + return qcomp(0, parser_parseReal(match.str(1))); + + if (regex_match(str, match, regexes::comp)) + return qcomp( + parser_parseReal(match.str(1)), + parser_parseReal(match.str(2))); + + // should be unreachable + error_attemptedToParseComplexFromInvalidString(); + return qcomp(0,0); +} + + + /* * VALIDATION * @@ -188,15 +347,14 @@ bool isInterpretablePauliStrSumLine(string line) { // notation) followed by 1 or more space characters, then one or // more pauli codes/chars. It does NOT determine whether the coeff // can actually be instantiated as a qcomp - return regex_match(line, regexes::line); + return regex_match(line, regexes::weightedPaulis); } -bool isCoeffValidInPauliStrSumLine(string line) { +bool isPauliStrSumCoeffWithinQcompRange(string line) { // it is gauranteed that line is interpretable and contains a regex-matching - // coefficient, but we must additionally verify it is within range of stold, - // and isn't unexpectedly incompatible with stold in a way uncaptured by regex. + // coefficient, but we must additionally verify it is within range of qreal. // So we duck type each of the 1 or 2 matches with the real regex (i.e. one or // both of the real and imaginary components of a complex coeff). @@ -215,17 +373,17 @@ bool isCoeffValidInPauliStrSumLine(string line) { // enumerate all matches of 'real' regex in line for (; it != end; it++) { - // removed whitespace (stold cannot handle space between sign and number) + // remove whitespace (stold cannot handle space between sign and number) string match = it->str(0); removeWhiteSpace(match); - // return false if stold cannot parse the real as a long double + // return false if number cannot become a qreal try { - stold(match); - } catch (const invalid_argument&) { - return false; + precisionAgnosticStringToFloat(match); } catch (const out_of_range&) { return false; + } catch (const invalid_argument&) { // should be impossible (indicates bad regex) + return false; } } @@ -256,8 +414,8 @@ void assertStringIsValidPauliStrSum(string lines, const char* caller) { validate_parsedPauliStrSumLineIsInterpretable(validLine, line, lineIndex, caller); // assert the coeff is parsable (e.g. doesn't exceed valid number range) - bool validCoeff = isCoeffValidInPauliStrSumLine(line); - validate_parsedPauliStrSumCoeffIsValid(validCoeff, line, lineIndex, caller); + bool validCoeff = isPauliStrSumCoeffWithinQcompRange(line); + validate_parsedPauliStrSumCoeffWithinQcompRange(validCoeff, line, lineIndex, caller); // assert the line has a consistent number of Paulis as previous int numLinePaulis = getNumPaulisInLine(line); @@ -299,52 +457,6 @@ int parser_getPauliIntFromChar(char ch) { */ -qreal parseReal(string real) { - - // attempt to parse at max precision (long double) then cast down if necessary - try { - return static_cast(stold(real)); - - // should be impossible if regex and validation works correctly - } catch (const invalid_argument&) { - error_attemptedToParseRealFromInvalidString(); - - // should be prior caught by validation - } catch (const out_of_range&) { - error_attemptedToParseOutOfRangeReal(); - } - - // unreachable - return -1; -} - - -qcomp parseCoeff(string coeff) { - - // remove all superfluous spaces in coeff so stold is happy (it cannot tolerate spaces after +-) - removeWhiteSpace(coeff); - - // we are gauranteed to fully match real, imag or comp after prior validation - smatch match; - - // extract and parse components and their signs (excluding imaginary symbol) - if (regex_match(coeff, match, regexes::real)) - return qcomp(parseReal(match.str(1)), 0); - - if (regex_match(coeff, match, regexes::imag)) - return qcomp(0, parseReal(match.str(1))); - - if (regex_match(coeff, match, regexes::comp)) - return qcomp( - parseReal(match.str(1)), - parseReal(match.str(2))); - - // should be unreachable - error_attemptedToParseComplexFromInvalidString(); - return qcomp(0,0); -} - - PauliStr parsePaulis(string paulis, bool rightIsLeastSignificant) { // remove whitespace to make string compatible with getPauliStr() @@ -363,14 +475,14 @@ PauliStr parsePaulis(string paulis, bool rightIsLeastSignificant) { } -void parseLine(string line, qcomp &coeff, PauliStr &pauli, bool rightIsLeastSignificant) { +void parseWeightedPaulis(string line, qcomp &coeff, PauliStr &pauli, bool rightIsLeastSignificant) { // separate line into substrings string coeffStr, pauliStr; separateStringIntoCoeffAndPaulis(line, coeffStr, pauliStr); // parse each, overwriting calller primitives - coeff = parseCoeff(coeffStr); + coeff = parser_parseComplex(coeffStr); pauli = parsePaulis(pauliStr, rightIsLeastSignificant); } @@ -402,7 +514,7 @@ PauliStrSum parser_validateAndParsePauliStrSum(string lines, bool rightIsLeastSi qcomp coeff; PauliStr string; - parseLine(line, coeff, string, rightIsLeastSignificant); // validates + parseWeightedPaulis(line, coeff, string, rightIsLeastSignificant); // validates coeffs.push_back(coeff); strings.push_back(string); diff --git a/quest/src/core/parser.hpp b/quest/src/core/parser.hpp index 3e9d18c11..4a9df2d02 100644 --- a/quest/src/core/parser.hpp +++ b/quest/src/core/parser.hpp @@ -7,6 +7,7 @@ #ifndef PARSER_HPP #define PARSER_HPP +#include "quest/include/types.h" #include "quest/include/paulis.h" #include @@ -15,6 +16,21 @@ using std::string; +/* + * PARSING NUMBERS + */ + +bool parser_isAnySizedReal(string str); +bool parser_isAnySizedComplex(string str); + +bool parser_isValidReal(string str); +bool parser_isValidComplex(string str); + +qreal parser_parseReal(string str); +qcomp parser_parseComplex(string str); + + + /* * PARSING INDIVIDUAL PAULIS */ diff --git a/quest/src/core/validation.cpp b/quest/src/core/validation.cpp index 4b3406975..3f242e6df 100644 --- a/quest/src/core/validation.cpp +++ b/quest/src/core/validation.cpp @@ -23,6 +23,7 @@ #include "quest/src/core/utilities.hpp" #include "quest/src/core/parser.hpp" #include "quest/src/core/printer.hpp" +#include "quest/src/core/envvars.hpp" #include "quest/src/comm/comm_config.hpp" #include "quest/src/comm/comm_routines.hpp" #include "quest/src/cpu/cpu_config.hpp" @@ -31,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -708,8 +710,8 @@ namespace report { string PARSED_PAULI_STR_SUM_INCONSISTENT_NUM_PAULIS_IN_LINE = "Line ${LINE_NUMBER} specified ${NUM_LINE_PAULIS} Pauli operators which is inconsistent with the number of Paulis of the previous lines (${NUM_PAULIS})."; - string PARSED_PAULI_STR_SUM_COEFF_IS_INVALID = - "The coefficient of line ${LINE_NUMBER} could not be converted to a qcomp, possibly due to it exceeding the valid numerical range."; + string PARSED_PAULI_STR_SUM_COEFF_EXCEEDS_QCOMP_RANGE = + "The coefficient of line ${LINE_NUMBER} is a valid floating-point number but exceeds the range which can be stored in a qcomp. Consider increasing FLOAT_PRECISION."; string PARSED_STRING_IS_EMPTY = "The given string was empty (contained only whitespace characters) and could not be parsed."; @@ -1066,6 +1068,22 @@ namespace report { string TEMP_ALLOC_FAILED = "A temporary allocation of ${NUM_ELEMS} elements (each of ${NUM_BYTES_PER_ELEM} bytes) failed, possibly because of insufficient memory."; + + /* + * ENVIRONMENT VARIABLES + */ + + string INVALID_PERMIT_NODES_TO_SHARE_GPU_ENV_VAR = + "The optional, boolean '" + envvar_names::PERMIT_NODES_TO_SHARE_GPU + "' environment variable was specified to an invalid value. The variable can be unspecified, or set to '', '0' or '1'."; + + string DEFAULT_EPSILON_ENV_VAR_NOT_A_REAL = + "The optional '" + envvar_names::DEFAULT_VALIDATION_EPSILON + "' environment variable was not a recognisable real number."; + + string DEFAULT_EPSILON_ENV_VAR_EXCEEDS_QREAL_RANGE = + "The optional '" + envvar_names::DEFAULT_VALIDATION_EPSILON + "' environment variable was larger (in magnitude) than the maximum value which can be stored in a qreal."; + + string DEFAULT_EPSILON_ENV_VAR_IS_NEGATIVE = + "The optional '" + envvar_names::DEFAULT_VALIDATION_EPSILON + "' environment variable was negative. The value must be zero or positive."; } @@ -1153,13 +1171,18 @@ qreal REDUCTION_EPSILON_FACTOR = 100; * overwritten (so will stay validate_STRUCT_PROPERTY_UNKNOWN_FLAG) */ -static qreal global_validationEpsilon = DEFAULT_VALIDATION_EPSILON; +// the default epsilon is not known until runtime since the macro +// UNSPECIFIED_DEFAULT_VALIDATION_EPSILON may be overriden by the +// DEFAULT_VALIDATION_EPSILON environment variable. We do not read +// the env-var immediately since it may malformed; we must wait for +// initQuESTEnv() to validate and potentially throw an error +static qreal global_validationEpsilon = -1; // must be overriden void validateconfig_setEpsilon(qreal eps) { global_validationEpsilon = eps; } void validateconfig_setEpsilonToDefault() { - global_validationEpsilon = DEFAULT_VALIDATION_EPSILON; + global_validationEpsilon = envvars_getDefaultValidationEpsilon(); } qreal validateconfig_getEpsilon() { return global_validationEpsilon; @@ -1364,13 +1387,8 @@ void validate_newEnvDistributedBetweenPower2Nodes(const char* caller) { void validate_newEnvNodesEachHaveUniqueGpu(const char* caller) { - // this validation can be disabled for debugging/dev purposes - // (caller should explicitly check this preprocessor too for clarity) - if (PERMIT_NODES_TO_SHARE_GPU) - return; - - bool uniqueGpus = ! gpu_areAnyNodesBoundToSameGpu(); - assertAllNodesAgreeThat(uniqueGpus, report::MULTIPLE_NODES_BOUND_TO_SAME_GPU, caller); + bool sharedGpus = gpu_areAnyNodesBoundToSameGpu(); + assertAllNodesAgreeThat(!sharedGpus, report::MULTIPLE_NODES_BOUND_TO_SAME_GPU, caller); } void validate_gpuIsCuQuantumCompatible(const char* caller) { @@ -3234,12 +3252,12 @@ void validate_parsedPauliStrSumLineHasConsistentNumPaulis(int numPaulis, int num assertThat(numPaulis == numLinePaulis, report::PARSED_PAULI_STR_SUM_INCONSISTENT_NUM_PAULIS_IN_LINE, vars, caller); } -void validate_parsedPauliStrSumCoeffIsValid(bool isCoeffValid, string line, qindex lineIndex, const char* caller) { +void validate_parsedPauliStrSumCoeffWithinQcompRange(bool isCoeffValid, string line, qindex lineIndex, const char* caller) { /// @todo we cannot yet report 'line' because tokenSubs so far only accepts integers :( tokenSubs vars = {{"${LINE_NUMBER}", lineIndex + 1}}; // lines begin at 1 - assertThat(isCoeffValid, report::PARSED_PAULI_STR_SUM_COEFF_IS_INVALID, vars, caller); + assertThat(isCoeffValid, report::PARSED_PAULI_STR_SUM_COEFF_EXCEEDS_QCOMP_RANGE, vars, caller); } void validate_parsedStringIsNotEmpty(bool stringIsNotEmpty, const char* caller) { @@ -4165,3 +4183,26 @@ void validate_tempAllocSucceeded(bool succeeded, qindex numElems, qindex numByte assertThat(succeeded, report::TEMP_ALLOC_FAILED, vars, caller); } + + + +/* + * ENVIRONMENT VARIABLES + */ + +void validate_envVarPermitNodesToShareGpu(string varValue, const char* caller) { + + // though caller should gaurantee varValue contains at least one character, + // we'll still check to avoid a segfault if this gaurantee is broken + bool isValid = (varValue.size() == 1) && (varValue[0] == '0' || varValue[0] == '1'); + assertThat(isValid, report::INVALID_PERMIT_NODES_TO_SHARE_GPU_ENV_VAR, caller); +} + +void validate_envVarDefaultValidationEpsilon(string varValue, const char* caller) { + + assertThat(parser_isAnySizedReal(varValue), report::DEFAULT_EPSILON_ENV_VAR_NOT_A_REAL, caller); + assertThat(parser_isValidReal(varValue), report::DEFAULT_EPSILON_ENV_VAR_EXCEEDS_QREAL_RANGE, caller); + + qreal eps = parser_parseReal(varValue); + assertThat(eps >= 0, report::DEFAULT_EPSILON_ENV_VAR_IS_NEGATIVE, caller); +} diff --git a/quest/src/core/validation.hpp b/quest/src/core/validation.hpp index 0bf48b409..92baac843 100644 --- a/quest/src/core/validation.hpp +++ b/quest/src/core/validation.hpp @@ -312,7 +312,7 @@ void validate_newPauliStrSumAllocs(PauliStrSum sum, qindex numBytesStrings, qind void validate_parsedPauliStrSumLineIsInterpretable(bool isInterpretable, string line, qindex lineIndex, const char* caller); -void validate_parsedPauliStrSumCoeffIsValid(bool isCoeffValid, string line, qindex lineIndex, const char* caller); +void validate_parsedPauliStrSumCoeffWithinQcompRange(bool isCoeffValid, string line, qindex lineIndex, const char* caller); void validate_parsedPauliStrSumLineHasConsistentNumPaulis(int numPaulis, int numLinePaulis, string line, qindex lineIndex, const char* caller); @@ -488,7 +488,6 @@ void validate_densMatrExpecDiagMatrValueIsReal(qcomp value, qcomp exponent, cons * PARTIAL TRACE */ - void validate_quregCanBeReduced(Qureg qureg, int numTraceQubits, const char* caller); void validate_quregCanBeSetToReducedDensMatr(Qureg out, Qureg in, int numTraceQubits, const char* caller); @@ -511,4 +510,14 @@ void validate_tempAllocSucceeded(bool succeeded, qindex numElems, qindex numByte +/* + * ENVIRONMENT VARIABLES + */ + +void validate_envVarPermitNodesToShareGpu(string varValue, const char* caller); + +void validate_envVarDefaultValidationEpsilon(string varValue, const char* caller); + + + #endif // VALIDATION_HPP \ No newline at end of file diff --git a/quest/src/gpu/gpu_cuquantum.cuh b/quest/src/gpu/gpu_cuquantum.cuh index 9f80881a1..3b1c55fdb 100644 --- a/quest/src/gpu/gpu_cuquantum.cuh +++ b/quest/src/gpu/gpu_cuquantum.cuh @@ -134,9 +134,10 @@ int deallocMemInPool(void* ctx, void* ptr, size_t size, cudaStream_t stream) { void gpu_initCuQuantum() { // the cuStateVec docs say custatevecCreate() should be called - // once per physical GPU, though oversubscribing MPI processes - // while setting PERMIT_NODES_TO_SHARE_GPU=1 worked fine in our - // testing - we will treat it as tolerable but undefined behaviour + // once per physical GPU, though assigning multiple MPI processes + // to each GPU with each calling custatevecCreate() below worked + // fine in our testing. We here tolerate oversubscription, letting + // prior validation prevent it (disabled by an environment variable) // create new stream and cuQuantum handle, binding to global config CUDA_CHECK( custatevecCreate(&config.handle) ); diff --git a/tests/main.cpp b/tests/main.cpp index 03294857a..29647753e 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -88,13 +88,14 @@ class startListener : public Catch::EventListenerBase { QuESTEnv env = getQuESTEnv(); std::cout << std::endl; std::cout << "QuEST execution environment:" << std::endl; - std::cout << " precision: " << FLOAT_PRECISION << std::endl; - std::cout << " multithreaded: " << env.isMultithreaded << std::endl; - std::cout << " distributed: " << env.isDistributed << std::endl; - std::cout << " GPU-accelerated: " << env.isGpuAccelerated << std::endl; - std::cout << " cuQuantum: " << env.isCuQuantumEnabled << std::endl; - std::cout << " num nodes: " << env.numNodes << std::endl; - std::cout << " num qubits: " << getNumCachedQubits() << std::endl; + std::cout << " precision: " << FLOAT_PRECISION << std::endl; + std::cout << " multithreaded: " << env.isMultithreaded << std::endl; + std::cout << " distributed: " << env.isDistributed << std::endl; + std::cout << " GPU-accelerated: " << env.isGpuAccelerated << std::endl; + std::cout << " GPU-sharing ok: " << env.isGpuSharingEnabled << std::endl; + std::cout << " cuQuantum: " << env.isCuQuantumEnabled << std::endl; + std::cout << " num nodes: " << env.numNodes << std::endl; + std::cout << " num qubits: " << getNumCachedQubits() << std::endl; std::cout << " num qubit perms: " << TEST_MAX_NUM_QUBIT_PERMUTATIONS << std::endl; std::cout << std::endl; diff --git a/tests/unit/environment.cpp b/tests/unit/environment.cpp index db9a1516c..6d4efb80d 100644 --- a/tests/unit/environment.cpp +++ b/tests/unit/environment.cpp @@ -54,6 +54,13 @@ TEST_CASE( "initQuESTEnv", TEST_CATEGORY ) { SECTION( LABEL_VALIDATION ) { REQUIRE_THROWS_WITH( initQuESTEnv(), ContainsSubstring( "already been initialised") ); + + // cannot automatically check other validations, such as: + // - has env been previously initialised then finalised? + // - is env distributed over power-of-2 nodes? + // - are environment-variables valid? + // - is max 1 MPI process bound to each GPU? + // - is GPU compatible with cuQuantum (if enabled)? } } @@ -133,10 +140,11 @@ TEST_CASE( "getQuESTEnv", TEST_CATEGORY ) { QuESTEnv env = getQuESTEnv(); - REQUIRE( (env.isMultithreaded == 0 || env.isMultithreaded == 1) ); - REQUIRE( (env.isGpuAccelerated == 0 || env.isGpuAccelerated == 1) ); - REQUIRE( (env.isDistributed == 0 || env.isDistributed == 1) ); - REQUIRE( (env.isCuQuantumEnabled == 0 || env.isCuQuantumEnabled == 1) ); + REQUIRE( (env.isMultithreaded == 0 || env.isMultithreaded == 1) ); + REQUIRE( (env.isGpuAccelerated == 0 || env.isGpuAccelerated == 1) ); + REQUIRE( (env.isDistributed == 0 || env.isDistributed == 1) ); + REQUIRE( (env.isCuQuantumEnabled == 0 || env.isCuQuantumEnabled == 1) ); + REQUIRE( (env.isGpuSharingEnabled == 0 || env.isGpuSharingEnabled == 1) ); REQUIRE( env.rank >= 0 ); REQUIRE( env.numNodes >= 0 ); diff --git a/tests/unit/paulis.cpp b/tests/unit/paulis.cpp index f18b57228..255d30c4b 100644 --- a/tests/unit/paulis.cpp +++ b/tests/unit/paulis.cpp @@ -362,8 +362,9 @@ TEST_CASE( "createInlinePauliStrSum", TEST_CATEGORY ) { SECTION( "coefficient parsing" ) { - vector strs = {"1 X", "0 X", "0.1 X", "5E2-1i X", "-1E-50i X", "1 - 6E-5i X", "-1.5E-15 - 5.123E-30i 0"}; - vector coeffs = { 1, 0, 0.1, 5E2-1_i, -(1E-50)*1_i, 1 -(6E-5)*1_i, qcomp(-1.5E-15, -5.123E-30) }; + // beware that when FLOAT_PRECISION=1, qcomp cannot store smaller than 1E-37 (triggering a validation error) + vector strs = {"1 X", "0 X", "0.1 X", "5E2-1i X", "-1E-25i X", "1 - 6E-5i X", "-1.5E-15 - 5.123E-30i 0"}; + vector coeffs = { 1, 0, 0.1, 5E2-1_i, -(1E-25)*1_i, 1 -(6E-5)*1_i, qcomp(-1.5E-15, -5.123E-30) }; size_t i = GENERATE_REF( range(0, (int) strs.size()) ); CAPTURE( strs[i], coeffs[i] ); @@ -377,7 +378,7 @@ TEST_CASE( "createInlinePauliStrSum", TEST_CATEGORY ) { PauliStrSum sum = createInlinePauliStrSum(R"( + 5E2-1i XYZ - - 1E-50i IXY + - 1E-20i IXY + 1 - 6E-5i IIX 0 III 5. XXX @@ -416,6 +417,12 @@ TEST_CASE( "createInlinePauliStrSum", TEST_CATEGORY ) { REQUIRE_NOTHROW( createInlinePauliStrSum("1 2 3") ); // = 1 * YZ and is legal } + SECTION( "out of range" ) { + + // the max/min qcomp depend upon FLOAT_PRECISION but we'll lazily use something even quad-prec cannot store + REQUIRE_THROWS_WITH( createInlinePauliStrSum("-1E-9999 XYZ"), ContainsSubstring("exceeds the range which can be stored in a qcomp") ); + } + SECTION( "inconsistent number of qubits" ) { REQUIRE_THROWS_WITH( createInlinePauliStrSum("3 XYZ \n 2 YX"), ContainsSubstring("inconsistent") ); @@ -444,7 +451,7 @@ TEST_CASE( "createPauliStrSumFromFile", TEST_CATEGORY ) { file.open(fn); file << R"( + 5E2-1i XYZ - - 1E-50i IXY + - 1E-20i IXY + 1 - 6E-5i IIX 0 III 5. IXX @@ -497,7 +504,7 @@ TEST_CASE( "createPauliStrSumFromReversedFile", TEST_CATEGORY ) { file.open(fn); file << R"( + 5E2-1i XYZ - - 1E-50i IXY + - 1E-20i IXY + 1 - 6E-5i IIX 0 III 5. IXX diff --git a/utils/docs/Doxyfile b/utils/docs/Doxyfile index 4eb40b524..daa1f0143 100644 --- a/utils/docs/Doxyfile +++ b/utils/docs/Doxyfile @@ -301,6 +301,8 @@ ALIASES += "notyetdoced=@note Documentation for this function or struct is under ALIASES += "cpponly=@remark This function is only available in C++." ALIASES += "conly=@remark This function is only available in C." ALIASES += "macrodoc=@note This entity is actually a macro." +ALIASES += "envvardoc=@note This entity is actually an environment variable." +ALIASES += "envvarvalues=@par Values" ALIASES += "neverdoced=@warning This entity is a macro, undocumented directly due to a Doxygen limitation. If you see this doc rendered, contact the devs!" ALIASES += "myexample=@par Example" ALIASES += "equivalences=@par Equivalences"