diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 03091481201..68a6e692a46 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,7 +13,7 @@ Fixes # (issue) # Checklist - [ ] I have performed a self-review of my own code -- [ ] I have run [clang-format](https://docs.openmc.org/en/latest/devguide/styleguide.html#automatic-formatting) on any C++ source files (if applicable) +- [ ] I have run [clang-format](https://docs.openmc.org/en/latest/devguide/styleguide.html#automatic-formatting) (version 15) on any C++ source files (if applicable) - [ ] I have followed the [style guidelines](https://docs.openmc.org/en/latest/devguide/styleguide.html#python) for Python source files (if applicable) - [ ] I have made corresponding changes to the documentation (if applicable) - [ ] I have added tests that prove my fix is effective or that my feature works (if applicable) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab2121b9c96..7aaaf1da4b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,9 +35,6 @@ jobs: vectfit: [n] include: - - python-version: "3.7" - omp: n - mpi: n - python-version: "3.8" omp: n mpi: n @@ -47,6 +44,9 @@ jobs: - python-version: "3.11" omp: n mpi: n + - python-version: "3.12" + omp: n + mpi: n - dagmc: y python-version: "3.10" mpi: y @@ -93,10 +93,10 @@ jobs: RDMAV_FORK_SAFE: 1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -111,16 +111,26 @@ jobs: run: | sudo apt -y update sudo apt install -y libpng-dev \ - libmpich-dev \ libnetcdf-dev \ libpnetcdf-dev \ libhdf5-serial-dev \ - libhdf5-mpich-dev \ libeigen3-dev + + - name: Optional apt dependencies for MPI + shell: bash + if: ${{ matrix.mpi == 'y' }} + run: | + sudo apt install -y libhdf5-mpich-dev \ + libmpich-dev sudo update-alternatives --set mpi /usr/bin/mpicc.mpich sudo update-alternatives --set mpirun /usr/bin/mpirun.mpich sudo update-alternatives --set mpi-x86_64-linux-gnu /usr/include/x86_64-linux-gnu/mpich + - name: Optional apt dependencies for vectfit + shell: bash + if: ${{ matrix.vectfit == 'y' }} + run: sudo apt install -y libblas-dev liblapack-dev + - name: install shell: bash run: | @@ -128,7 +138,7 @@ jobs: $GITHUB_WORKSPACE/tools/ci/gha-install.sh - name: cache-xs - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/nndc_hdf5 @@ -156,7 +166,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true diff --git a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml index 3f0b5ab9be0..813596953b7 100644 --- a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:latest-dagmc-libmesh diff --git a/.github/workflows/dockerhub-publish-dagmc.yml b/.github/workflows/dockerhub-publish-dagmc.yml index b6ba2f075ce..6757f772713 100644 --- a/.github/workflows/dockerhub-publish-dagmc.yml +++ b/.github/workflows/dockerhub-publish-dagmc.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:latest-dagmc diff --git a/.github/workflows/dockerhub-publish-dev.yml b/.github/workflows/dockerhub-publish-dev.yml index d2603ead4a0..7a81363a78a 100644 --- a/.github/workflows/dockerhub-publish-dev.yml +++ b/.github/workflows/dockerhub-publish-dev.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:develop diff --git a/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml index 354f0a020e0..a219f2a91d9 100644 --- a/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:develop-dagmc-libmesh diff --git a/.github/workflows/dockerhub-publish-develop-dagmc.yml b/.github/workflows/dockerhub-publish-develop-dagmc.yml index 36ec7a337b8..a901b8d3f0b 100644 --- a/.github/workflows/dockerhub-publish-develop-dagmc.yml +++ b/.github/workflows/dockerhub-publish-develop-dagmc.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:develop-dagmc diff --git a/.github/workflows/dockerhub-publish-develop-libmesh.yml b/.github/workflows/dockerhub-publish-develop-libmesh.yml index a89417316b4..22e9aa68fba 100644 --- a/.github/workflows/dockerhub-publish-develop-libmesh.yml +++ b/.github/workflows/dockerhub-publish-develop-libmesh.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:develop-libmesh diff --git a/.github/workflows/dockerhub-publish-libmesh.yml b/.github/workflows/dockerhub-publish-libmesh.yml index e592ccb8ef7..843ce0f6fdb 100644 --- a/.github/workflows/dockerhub-publish-libmesh.yml +++ b/.github/workflows/dockerhub-publish-libmesh.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:latest-libmesh diff --git a/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml index c90302af440..db62bb53e69 100644 --- a/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml @@ -8,25 +8,25 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:${{ env.RELEASE_VERSION }}-dagmc-libmesh diff --git a/.github/workflows/dockerhub-publish-release-dagmc.yml b/.github/workflows/dockerhub-publish-release-dagmc.yml index ef66f6eaddd..de959378289 100644 --- a/.github/workflows/dockerhub-publish-release-dagmc.yml +++ b/.github/workflows/dockerhub-publish-release-dagmc.yml @@ -8,25 +8,25 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:${{ env.RELEASE_VERSION }}-dagmc diff --git a/.github/workflows/dockerhub-publish-release-libmesh.yml b/.github/workflows/dockerhub-publish-release-libmesh.yml index 72edfbc68ed..e8ea98aebd5 100644 --- a/.github/workflows/dockerhub-publish-release-libmesh.yml +++ b/.github/workflows/dockerhub-publish-release-libmesh.yml @@ -8,25 +8,25 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:${{ env.RELEASE_VERSION }}-libmesh diff --git a/.github/workflows/dockerhub-publish-release.yml b/.github/workflows/dockerhub-publish-release.yml index 22d21b4c960..fab030192b7 100644 --- a/.github/workflows/dockerhub-publish-release.yml +++ b/.github/workflows/dockerhub-publish-release.yml @@ -8,25 +8,25 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/dockerhub-publish.yml b/.github/workflows/dockerhub-publish.yml index 00a3ba3164d..fd51a9fa733 100644 --- a/.github/workflows/dockerhub-publish.yml +++ b/.github/workflows/dockerhub-publish.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:latest diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 4914d5e5ea9..cef14ca2c8c 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -1,12 +1,19 @@ name: C++ Format Check -on: pull_request +on: + # allow workflow to be run manually + workflow_dispatch: + + pull_request: + branches: + - develop + - master jobs: cpp-linter: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: cpp-linter/cpp-linter-action@v2 id: linter env: diff --git a/CMakeLists.txt b/CMakeLists.txt index a51db311141..1841251ff5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,8 +3,8 @@ project(openmc C CXX) # Set version numbers set(OPENMC_VERSION_MAJOR 0) -set(OPENMC_VERSION_MINOR 13) -set(OPENMC_VERSION_RELEASE 4) +set(OPENMC_VERSION_MINOR 14) +set(OPENMC_VERSION_RELEASE 1) set(OPENMC_VERSION ${OPENMC_VERSION_MAJOR}.${OPENMC_VERSION_MINOR}.${OPENMC_VERSION_RELEASE}) configure_file(include/openmc/version.h.in "${CMAKE_BINARY_DIR}/include/openmc/version.h" @ONLY) @@ -39,6 +39,7 @@ option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tall option(OPENMC_USE_MPI "Enable MPI" OFF) option(OPENMC_USE_MCPL "Enable MCPL" OFF) option(OPENMC_USE_NCRYSTAL "Enable support for NCrystal scattering" OFF) +option(OPENMC_USE_UWUW "Enable UWUW" OFF) # Warnings for deprecated options foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh") @@ -80,6 +81,14 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build" FORCE) endif() +#=============================================================================== +# OpenMP for shared-memory parallelism (and GPU support some day!) +#=============================================================================== + +if(OPENMC_USE_OPENMP) + find_package(OpenMP REQUIRED) +endif() + #=============================================================================== # MPI for distributed-memory parallelism #=============================================================================== @@ -117,8 +126,14 @@ endif() if(OPENMC_USE_DAGMC) find_package(DAGMC REQUIRED PATH_SUFFIXES lib/cmake) if (${DAGMC_VERSION} VERSION_LESS 3.2.0) - message(FATAL_ERROR "Discovered DAGMC Version: ${DAGMC_VERSION}. \ - Please update DAGMC to version 3.2.0 or greater.") + message(FATAL_ERROR "Discovered DAGMC Version: ${DAGMC_VERSION}." + "Please update DAGMC to version 3.2.0 or greater.") + endif() + message(STATUS "Found DAGMC: ${DAGMC_DIR} (version ${DAGMC_VERSION})") + + # Check if UWUW is needed and available + if(OPENMC_USE_UWUW AND NOT DAGMC_BUILD_UWUW) + message(FATAL_ERROR "UWUW is enabled but DAGMC was not configured with UWUW.") endif() endif() @@ -154,6 +169,11 @@ if(NOT DEFINED HDF5_PREFER_PARALLEL) endif() find_package(HDF5 REQUIRED COMPONENTS C HL) + +# Remove HDF5 transitive dependencies that are system libraries +list(FILTER HDF5_LIBRARIES EXCLUDE REGEX ".*lib(pthread|dl|m).*") +message(STATUS "HDF5 Libraries: ${HDF5_LIBRARIES}") + if(HDF5_IS_PARALLEL) if(NOT OPENMC_USE_MPI) message(FATAL_ERROR "Parallel HDF5 was detected, but MPI was not enabled.\ @@ -186,15 +206,6 @@ endif() # Skip for Visual Studio which has its own configurations through GUI if(NOT MSVC) -if(OPENMC_USE_OPENMP) - find_package(OpenMP) - if(OPENMP_FOUND) - # In CMake 3.9+, can use the OpenMP::OpenMP_CXX imported target - list(APPEND cxxflags ${OpenMP_CXX_FLAGS}) - list(APPEND ldflags ${OpenMP_CXX_FLAGS}) - endif() -endif() - set(CMAKE_POSITION_INDEPENDENT_CODE ON) if(OPENMC_ENABLE_PROFILE) @@ -376,6 +387,9 @@ list(APPEND libopenmc_SOURCES src/progress_bar.cpp src/random_dist.cpp src/random_lcg.cpp + src/random_ray/random_ray_simulation.cpp + src/random_ray/random_ray.cpp + src/random_ray/flat_source_domain.cpp src/reaction.cpp src/reaction_product.cpp src/scattdata.cpp @@ -405,7 +419,9 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_energyfunc.cpp src/tallies/filter_legendre.cpp src/tallies/filter_material.cpp + src/tallies/filter_materialfrom.cpp src/tallies/filter_mesh.cpp + src/tallies/filter_meshborn.cpp src/tallies/filter_meshsurface.cpp src/tallies/filter_mu.cpp src/tallies/filter_particle.cpp @@ -490,7 +506,7 @@ endif() # target_link_libraries treats any arguments starting with - but not -l as # linker flags. Thus, we can pass both linker flags and libraries together. target_link_libraries(libopenmc ${ldflags} ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES} - xtensor gsl::gsl-lite-v1 fmt::fmt) + xtensor gsl::gsl-lite-v1 fmt::fmt ${CMAKE_DL_LIBS}) if(TARGET pugixml::pugixml) target_link_libraries(libopenmc pugixml::pugixml) @@ -500,7 +516,7 @@ endif() if(OPENMC_USE_DAGMC) target_compile_definitions(libopenmc PRIVATE DAGMC) - target_link_libraries(libopenmc dagmc-shared uwuw-shared) + target_link_libraries(libopenmc dagmc-shared) endif() if(OPENMC_USE_LIBMESH) @@ -513,6 +529,10 @@ if (PNG_FOUND) target_link_libraries(libopenmc PNG::PNG) endif() +if (OPENMC_USE_OPENMP) + target_link_libraries(libopenmc OpenMP::OpenMP_CXX) +endif() + if (OPENMC_USE_MPI) target_link_libraries(libopenmc MPI::MPI_CXX) endif() @@ -533,6 +553,11 @@ if(OPENMC_USE_NCRYSTAL) target_link_libraries(libopenmc NCrystal::NCrystal) endif() +if (OPENMC_USE_UWUW) + target_compile_definitions(libopenmc PRIVATE UWUW) + target_link_libraries(libopenmc uwuw-shared) +endif() + #=============================================================================== # Log build info that this executable can report later #=============================================================================== diff --git a/CODEOWNERS b/CODEOWNERS index 1b5130e0b6d..6d452e36653 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,9 +5,9 @@ openmc/data/ @paulromano openmc/lib/ @paulromano # Depletion -openmc/deplete/ @drewejohnson -tests/regression_tests/deplete/ @drewejohnson -tests/unit_tests/test_deplete_*.py @drewejohnson +openmc/deplete/ @paulromano +tests/regression_tests/deplete/ @paulromano +tests/unit_tests/test_deplete_*.py @paulromano # MG-related functionality openmc/mgxs_library.py @nelsonag @@ -26,6 +26,12 @@ src/dagmc.cpp @pshriwise tests/regression_tests/dagmc/ @pshriwise tests/unit_tests/dagmc/ @pshriwise +# Weight windows +openmc/weight_windows.py @pshriwise +openmc/lib/weight_windows.py @pshriwise +src/weight_windows.py @pshriwise +tests/unit_tests/weightwindows/ @pshriwise + # Photon transport openmc/data/BREMX.DAT @amandalund openmc/data/compton_profiles.h5 @amandalund @@ -49,3 +55,12 @@ openmc/data/resonance_covariance.py @icmeyer # Docker Dockerfile @shimwell + +# Random ray +src/random_ray/ @jtramm + +# NCrystal interface +src/ncrystal_interface.cpp @marquezj + +# MCPL interface +src/mcpl_interface.cpp @ebknudsen diff --git a/Dockerfile b/Dockerfile index fa223bd2284..107df4afe76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG compile_cores=1 ARG build_dagmc=off ARG build_libmesh=off -FROM debian:bullseye-slim AS dependencies +FROM debian:bookworm-slim AS dependencies ARG compile_cores ARG build_dagmc @@ -34,21 +34,21 @@ ARG build_libmesh ENV HOME=/root # Embree variables -ENV EMBREE_TAG='v3.12.2' +ENV EMBREE_TAG='v4.3.1' ENV EMBREE_REPO='https://github.com/embree/embree' ENV EMBREE_INSTALL_DIR=$HOME/EMBREE/ # MOAB variables -ENV MOAB_TAG='5.3.0' +ENV MOAB_TAG='5.5.1' ENV MOAB_REPO='https://bitbucket.org/fathomteam/moab/' # Double-Down variables -ENV DD_TAG='v1.0.0' +ENV DD_TAG='v1.1.0' ENV DD_REPO='https://github.com/pshriwise/double-down' ENV DD_INSTALL_DIR=$HOME/Double_down # DAGMC variables -ENV DAGMC_BRANCH='v3.2.1' +ENV DAGMC_BRANCH='v3.2.3' ENV DAGMC_REPO='https://github.com/svalinn/DAGMC' ENV DAGMC_INSTALL_DIR=$HOME/DAGMC/ @@ -71,9 +71,13 @@ RUN apt-get update -y && \ apt-get install -y \ python3-pip python-is-python3 wget git build-essential cmake \ mpich libmpich-dev libhdf5-serial-dev libhdf5-mpich-dev \ - libpng-dev && \ + libpng-dev python3-venv && \ apt-get autoremove +# create virtual enviroment to avoid externally managed environment error +RUN python3 -m venv openmc_venv +ENV PATH=/openmc_venv/bin:$PATH + # Update system-provided pip RUN pip install --upgrade pip diff --git a/LICENSE b/LICENSE index 81c1f5f7b11..1f9198c1e3b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2023 Massachusetts Institute of Technology, UChicago Argonne +Copyright (c) 2011-2024 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 756fe26dc00..44a5e0d5a3f 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -31,6 +31,14 @@ if(@OPENMC_USE_MPI@) find_package(MPI REQUIRED) endif() +if(@OPENMC_USE_OPENMP@) + find_package(OpenMP REQUIRED) +endif() + if(@OPENMC_USE_MCPL@) find_package(MCPL REQUIRED) endif() + +if(@OPENMC_USE_UWUW@) + find_package(UWUW REQUIRED) +endif() diff --git a/docs/source/_images/2x2_fsrs.jpeg b/docs/source/_images/2x2_fsrs.jpeg new file mode 100644 index 00000000000..1c8e474d0e8 Binary files /dev/null and b/docs/source/_images/2x2_fsrs.jpeg differ diff --git a/docs/source/_images/2x2_materials.jpeg b/docs/source/_images/2x2_materials.jpeg new file mode 100644 index 00000000000..b76607eca66 Binary files /dev/null and b/docs/source/_images/2x2_materials.jpeg differ diff --git a/docs/source/capi/index.rst b/docs/source/capi/index.rst index 52c0b2406aa..d9ac0d1e019 100644 --- a/docs/source/capi/index.rst +++ b/docs/source/capi/index.rst @@ -420,6 +420,16 @@ Functions :return: Return status (negative if an error occurred) :rtype: int +.. c:function:: int openmc_mesh_filter_get_mesh(int32_t index, int32_t* index_mesh) + + Get the mesh for a mesh filter + + :param int32_t index: Index in the filters array + :param index_mesh: Index in the meshes array + :type index_mesh: int32_t* + :return: Return status (negative if an error occurred) + :rtype: int + .. c:function:: int openmc_mesh_filter_set_mesh(int32_t index, int32_t index_mesh) Set the mesh for a mesh filter @@ -429,6 +439,98 @@ Functions :return: Return status (negative if an error occurred) :rtype: int +.. c:function:: int openmc_mesh_filter_get_translation(int32_t index, double translation[3]) + + Get the 3-D translation coordinates for a mesh filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_mesh_filter_set_translation(int32_t index, double translation[3]) + + Set the 3-D translation coordinates for a mesh filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshborn_filter_get_mesh(int32_t index, int32_t* index_mesh) + + Get the mesh for a meshborn filter + + :param int32_t index: Index in the filters array + :param index_mesh: Index in the meshes array + :type index_mesh: int32_t* + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshborn_filter_set_mesh(int32_t index, int32_t index_mesh) + + Set the mesh for a meshborn filter + + :param int32_t index: Index in the filters array + :param int32_t index_mesh: Index in the meshes array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshborn_filter_get_translation(int32_t index, double translation[3]) + + Get the 3-D translation coordinates for a meshborn filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshborn_filter_set_translation(int32_t index, double translation[3]) + + Set the 3-D translation coordinates for a meshborn filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh) + + Get the mesh for a mesh surface filter + + :param int32_t index: Index in the filters array + :param index_mesh: Index in the meshes array + :type index_mesh: int32_t* + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh) + + Set the mesh for a mesh surface filter + + :param int32_t index: Index in the filters array + :param int32_t index_mesh: Index in the meshes array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshsurface_filter_get_translation(int32_t index, double translation[3]) + + Get the 3-D translation coordinates for a mesh surface filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshsurface_filter_set_translation(int32_t index, double translation[3]) + + Set the 3-D translation coordinates for a mesh surface filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + .. c:function:: int openmc_next_batch() Simulate next batch of particles. Must be called after openmc_simulation_init(). diff --git a/docs/source/conf.py b/docs/source/conf.py index dc5acd5f62d..53d529f3dc0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,16 +62,16 @@ # General information about the project. project = 'OpenMC' -copyright = '2011-2023, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' +copyright = '2011-2024, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = "0.13" +version = "0.14" # The full version, including alpha/beta/rc tags. -release = "0.13.4" +release = "0.14.1-dev" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index 2a744b819ce..3c71d14ad92 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -29,6 +29,10 @@ whenever a file is saved. For example, `Visual Studio Code `_ includes support for running clang-format. +.. note:: + OpenMC's CI uses `clang-format` version 15. A different version of `clang-format` + may produce different line changes and as a result fail the CI test. + Miscellaneous ------------- @@ -142,7 +146,7 @@ Style for Python code should follow PEP8_. Docstrings for functions and methods should follow numpydoc_ style. -Python code should work with Python 3.7+. +Python code should work with Python 3.8+. Use of third-party Python packages should be limited to numpy_, scipy_, matplotlib_, pandas_, and h5py_. Use of other third-party packages must be diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index 9a102d2dc29..d8fc53b3ad0 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -134,6 +134,12 @@ following files to your new test directory: compiler options during openmc configuration and build (e.g., no MPI, no debug/optimization). +For tests using the Python API, both the **inputs_true.dat** and +**results_true.dat** files can be generated automatically in the correct format +via:: + + pytest --update + In addition to this description, please see the various types of tests that are already included in the test suite to see how to create them. If all is implemented correctly, the new test will automatically be discovered by pytest. diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 03abe9a3c4f..6318a991601 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -252,9 +252,9 @@ to false. *Default*: true ----------------------------------------- +------------------------------------- ```` Element ----------------------------------------- +------------------------------------- This element indicates the number of neutrons to run in flight concurrently when using event-based parallelism. A higher value uses more memory, but @@ -262,9 +262,17 @@ may be more efficient computationally. *Default*: 100000 ---------------------------- +--------------------------------- +```` Element +--------------------------------- + +This element indicates the maximum number of events a particle can undergo. + + *Default*: 1000000 + +----------------------- ```` Element ---------------------------- +----------------------- The ```` element allows the user to set a maximum scattering order to apply to every nuclide/material in the problem. That is, if the data @@ -276,6 +284,13 @@ then, OpenMC will only use up to the :math:`P_1` data. .. note:: This element is not used in the continuous-energy :ref:`energy_mode`. +------------------------ +```` Element +------------------------ + +The ```` element indicates the number of times a particle can split during a history. + + *Default*: 1000 -------------------------------------- ```` Element @@ -398,6 +413,32 @@ or sub-elements and can be set to either "false" or "true". .. note:: This element is not used in the multi-group :ref:`energy_mode`. +------------------------ +```` Element +------------------------ + +The ```` element enables random ray mode and contains a number of +settings relevant to the solver. Tips for selecting these parameters can be +found in the :ref:`random ray user guide `. + + :distance_inactive: + The inactive ray length (dead zone length) in [cm]. + + *Default*: None + + :distance_active: + The active ray length in [cm]. + + *Default*: None + + :source: + Specifies the starting ray distribution, and follows the format for + :ref:`source_element`. It must be uniform in space and angle and cover the + full domain. It does not represent a physical neutron or photon source -- it + is only used to sample integrating ray starting locations and directions. + + *Default*: None + ---------------------------------- ```` Element ---------------------------------- @@ -473,6 +514,8 @@ pseudo-random number generator. *Default*: 1 +.. _source_element: + -------------------- ```` Element -------------------- @@ -491,7 +534,8 @@ attributes/sub-elements: *Default*: 1.0 :type: - Indicator of source type. One of ``independent``, ``file``, or ``compiled``. + Indicator of source type. One of ``independent``, ``file``, ``compiled``, or ``mesh``. + The type of the source will be determined by this attribute if it is present. :particle: The source particle type, either ``neutron`` or ``photon``. @@ -664,6 +708,14 @@ attributes/sub-elements: *Default*: false + :mesh: + For mesh sources, this indicates the ID of the corresponding mesh. + + :source: + For mesh sources, this sub-element specifies the source for an individual + mesh element and follows the format for :ref:`source_element`. The number of + ```` sub-elements should correspond to the number of mesh elements. + .. _univariate: Univariate Probability Distributions diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index edff686b1bd..f61e967ca8a 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -96,6 +96,8 @@ The current version of the statepoint file format is 18.1. - **library** (*char[]*) -- Mesh library used to represent the mesh ("moab" or "libmesh"). - **length_multiplier** (*double*) Scaling factor applied to the mesh. + - **options** (*char[]*) -- Special options that control spatial + search data structures used. - **volumes** (*double[]*) -- Volume of each mesh cell. - **vertices** (*double[]*) -- x, y, z values of the mesh vertices. - **connectivity** (*int[]*) -- Connectivity array for the mesh diff --git a/docs/source/io_formats/summary.rst b/docs/source/io_formats/summary.rst index c140d98ae3a..7d3ab94d9f4 100644 --- a/docs/source/io_formats/summary.rst +++ b/docs/source/io_formats/summary.rst @@ -60,9 +60,11 @@ The current version of the summary file format is 6.0. - **coefficients** (*double[]*) -- Array of coefficients that define the surface. See :ref:`surface_element` for what coefficients are defined for each surface type. - - **boundary_condition** (*char[]*) -- Boundary condition applied to - the surface. Can be 'transmission', 'vacuum', 'reflective', or - 'periodic'. + - **boundary_type** (*char[]*) -- Boundary condition applied to + the surface. Can be 'transmission', 'vacuum', 'reflective', + 'periodic', or 'white'. + - **albedo** (*double*) -- Boundary albedo as a positive multiplier + of particle weight. If absent, it is assumed to be 1.0. - **geom_type** (*char[]*) -- Type of geometry used to create the cell. Either 'csg' or 'dagmc'. diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index b88877b8388..78101ab668d 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -100,6 +100,18 @@ The ```` element accepts the following sub-elements: *Default*: None + :ignore_zeros: + Whether to allow zero tally bins to be ignored when assessing the + convergece of the precision trigger. If True, only nonzero tally scores + will be compared to the trigger's threshold. + + .. note:: The ``ignore_zeros`` option can cause the tally trigger to fire + prematurely if there are no hits in any bins at the first + evalulation. It is the user's responsibility to specify enough + particles per batch to get a nonzero score in at least one bin. + + *Default*: False + :scores: The score(s) in this tally to which the trigger should be applied. @@ -364,6 +376,10 @@ attributes/sub-elements: The mesh library used to represent an unstructured mesh. This can be either "moab" or "libmesh". (For unstructured mesh only.) + :options: + Special options that control spatial search data structures used. (For + unstructured mesh using MOAB only) + :filename: The name of the mesh file to be loaded at runtime. (For unstructured mesh only.) diff --git a/docs/source/license.rst b/docs/source/license.rst index fb29afbc047..23315e8fe0b 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,7 @@ License Agreement ================= -Copyright © 2011-2023 Massachusetts Institute of Technology, UChicago Argonne +Copyright © 2011-2024 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/docs/source/methods/index.rst b/docs/source/methods/index.rst index 59892ac273f..08aa7d05341 100644 --- a/docs/source/methods/index.rst +++ b/docs/source/methods/index.rst @@ -20,3 +20,4 @@ Theory and Methodology energy_deposition parallelization cmfd + random_ray \ No newline at end of file diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst new file mode 100644 index 00000000000..aa436784766 --- /dev/null +++ b/docs/source/methods/random_ray.rst @@ -0,0 +1,804 @@ +.. _methods_random_ray: + +========== +Random Ray +========== + +.. _methods_random_ray_intro: + +------------------- +What is Random Ray? +------------------- + +`Random ray `_ is a stochastic transport method, closely related to +the deterministic Method of Characteristics (MOC) [Askew-1972]_. Rather than +each ray representing a single neutron as in Monte Carlo, it represents a +characteristic line through the simulation geometry upon which the transport +equation can be written as an ordinary differential equation that can be solved +analytically (although with discretization required in energy, making it a +multigroup method). The behavior of the governing transport equation can be +approximated by solving along many characteristic tracks (rays) through the +system. Unlike particles in Monte Carlo, rays in random ray or MOC are not +affected by the material characteristics of the simulated problem---rays are +selected so as to explore the full simulation problem with a statistically equal +distribution in space and angle. + +.. raw:: html + + + +The above animation is an example of the random ray integration process at work, +showing a series of random rays being sampled and transported through the +geometry. In the following sections, we will discuss how the random ray solver +works. + +---------------------------------------------- +Why is a Random Ray Solver Included in OpenMC? +---------------------------------------------- + +* One area that Monte Carlo struggles with is maintaining numerical efficiency + in regions of low physical particle flux. Random ray, on the other hand, has + approximately even variance throughout the entire global simulation domain, + such that areas with low neutron flux are no less well known that areas of + high neutron flux. Absent weight windows in MC, random ray can be several + orders of magnitude faster than multigroup Monte Carlo in classes of problems + where areas with low physical neutron flux need to be resolved. While MC + uncertainty can be greatly improved with variance reduction techniques, they + add some user complexity, and weight windows can often be expensive to + generate via MC transport alone (e.g., via the `MAGIC method + `_). The random ray solver + may be used in future versions of OpenMC as a fast way to generate weight + windows for subsequent usage by the MC solver in OpenMC. + +* In practical implementation terms, random ray is mechanically very similar to + how Monte Carlo works, in terms of the process of ray tracing on constructive + solid geometry (CSG) and handling stochastic convergence, etc. In the original + 1972 paper by Askew that introduces MOC (which random ray is a variant of), he + stated: + + .. epigraph:: + + "One of the features of the method proposed [MoC] is that ... the + tracking process needed to perform this operation is common to the + proposed method ... and to Monte Carlo methods. Thus a single tracking + routine capable of recognizing a geometric arrangement could be utilized + to service all types of solution, choice being made depending which was + more appropriate to the problem size and required accuracy." + + -- Askew [Askew-1972]_ + + This prediction holds up---the additional requirements needed in OpenMC to + handle random ray transport turned out to be fairly small. + +* It amortizes the code complexity in OpenMC for representing multigroup cross + sections. There is a significant amount of interface code, documentation, and + complexity in allowing OpenMC to generate and use multigroup XS data in its + MGMC mode. Random ray allows the same multigroup data to be used, making full + reuse of these existing capabilities. + +------------------------------- +Random Ray Numerical Derivation +------------------------------- + +In this section, we will derive the numerical basis for the random ray solver +mode in OpenMC. The derivation of random ray is also discussed in several papers +(`1 `_, `2 `_, `3 `_), and some of those +derivations are reproduced here verbatim. Several extensions are also made to +add clarity, particularly on the topic of OpenMC's treatment of cell volumes in +the random ray solver. + +~~~~~~~~~~~~~~~~~~~~~~~~~ +Method of Characteristics +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Boltzmann neutron transport equation is a partial differential equation +(PDE) that describes the angular flux within a system. It is a balance equation, +with the streaming and absorption terms typically appearing on the left hand +side, which are balanced by the scattering source and fission source terms on +the right hand side. + +.. math:: + :label: transport + + \begin{align*} + \mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r},\mathbf{\Omega},E) & + \Sigma_t(\mathbf{r},E) \psi(\mathbf{r},\mathbf{\Omega},E) = \\ + & \int_0^\infty d E^\prime \int_{4\pi} d \Omega^{\prime} \Sigma_s(\mathbf{r},\mathbf{\Omega}^\prime \rightarrow \mathbf{\Omega}, E^\prime \rightarrow E) \psi(\mathbf{r},\mathbf{\Omega}^\prime, E^\prime) \\ + & + \frac{\chi(\mathbf{r}, E)}{4\pi k_{eff}} \int_0^\infty dE^\prime \nu \Sigma_f(\mathbf{r},E^\prime) \int_{4\pi}d \Omega^\prime \psi(\mathbf{r},\mathbf{\Omega}^\prime,E^\prime) + \end{align*} + +In Equation :eq:`transport`, :math:`\psi` is the angular neutron flux. This +parameter represents the total distance traveled by all neutrons in a particular +direction inside of a control volume per second, and is often given in units of +:math:`1/(\text{cm}^{2} \text{s})`. As OpenMC does not support time dependence +in the random ray solver mode, we consider the steady state equation, where the +units of flux become :math:`1/\text{cm}^{2}`. The angular direction unit vector, +:math:`\mathbf{\Omega}`, represents the direction of travel for the neutron. The +spatial position vector, :math:`\mathbf{r}`, represents the location within the +simulation. The neutron energy, :math:`E`, or speed in continuous space, is +often given in units of electron volts. The total macroscopic neutron cross +section is :math:`\Sigma_t`. This value represents the total probability of +interaction between a neutron traveling at a certain speed (i.e., neutron energy +:math:`E`) and a target nucleus (i.e., the material through which the neutron is +traveling) per unit path length, typically given in units of +:math:`1/\text{cm}`. Macroscopic cross section data is a combination of +empirical data and quantum mechanical modeling employed in order to generate an +evaluation represented either in pointwise form or resonance parameters for each +target isotope of interest in a material, as well as the density of the +material, and is provided as input to a simulation. The scattering neutron cross +section, :math:`\Sigma_s`, is similar to the total cross section but only +measures scattering interactions between the neutron and the target nucleus, and +depends on the change in angle and energy the neutron experiences as a result of +the interaction. Several additional reactions like (n,2n) and (n,3n) are +included in the scattering transfer cross section. The fission neutron cross +section, :math:`\Sigma_f`, is also similar to the total cross section but only +measures the fission interaction between a neutron and a target nucleus. The +energy spectrum for neutrons born from fission, :math:`\chi`, represents a known +distribution of outgoing neutron energies based on the material that fissioned, +which is taken as input data to a computation. The average number of neutrons +born per fission is :math:`\nu`. The eigenvalue of the equation, +:math:`k_{eff}`, represents the effective neutron multiplication factor. If the +right hand side of Equation :eq:`transport` is condensed into a single term, +represented by the total neutron source term :math:`Q(\mathbf{r}, \mathbf{\Omega},E)`, +the form given in Equation :eq:`transport_simple` is reached. + +.. math:: + :label: transport_simple + + \overbrace{\mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r},\mathbf{\Omega},E)}^{\text{streaming term}} + \overbrace{\Sigma_t(\mathbf{r},E) \psi(\mathbf{r},\mathbf{\Omega},E)}^{\text{absorption term}} = \overbrace{Q(\mathbf{r}, \mathbf{\Omega},E)}^{\text{total neutron source term}} + +Fundamentally, MOC works by solving Equation :eq:`transport_simple` along a +single characteristic line, thus altering the full spatial and angular scope of +the transport equation into something that holds true only for a particular +linear path (or track) through the reactor. These tracks are linear for neutral +particles that are not subject to field effects. With our transport equation in +hand, we will now derive the solution along a track. To accomplish this, we +parameterize :math:`\mathbf{r}` with respect to some reference location +:math:`\mathbf{r}_0` such that :math:`\mathbf{r} = \mathbf{r}_0 + s\mathbf{\Omega}`. In this +manner, Equation :eq:`transport_simple` can be rewritten for a specific segment +length :math:`s` at a specific angle :math:`\mathbf{\Omega}` through a constant +cross section region of the reactor geometry as in Equation :eq:`char_long`. + +.. math:: + :label: char_long + + \mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r}_0 + s\mathbf{\Omega},\mathbf{\Omega},E) + \Sigma_t(\mathbf{r}_0 + s\mathbf{\Omega},E) \psi(\mathbf{r}_0 + s\mathbf{\Omega},\mathbf{\Omega},E) = Q(\mathbf{r}_0 + s\mathbf{\Omega}, \mathbf{\Omega},E) + +As this equation holds along a one dimensional path, we can assume the +dependence of :math:`s` on :math:`\mathbf{r}_0` and :math:`\mathbf{\Omega}` such that +:math:`\mathbf{r}_0 + s\mathbf{\Omega}` simplifies to :math:`s`. When the differential +operator is also applied to the angular flux :math:`\psi`, we arrive at the +characteristic form of the Boltzmann Neutron Transport Equation given in +Equation :eq:`char`. + +.. math:: + :label: char + + \frac{d}{ds} \psi(s,\mathbf{\Omega},E) + \Sigma_t(s,E) \psi(s,\mathbf{\Omega},E) = Q(s, \mathbf{\Omega},E) + +An analytical solution to this characteristic equation can be achieved with the +use of an integrating factor: + +.. math:: + :label: int_factor + + e^{ \int_0^s ds' \Sigma_t (s', E)} + +to arrive at the final form of the characteristic equation shown in Equation +:eq:`full_char`. + +.. math:: + :label: full_char + + \psi(s,\mathbf{\Omega},E) = \psi(\mathbf{r}_0,\mathbf{\Omega},E) e^{-\int_0^s ds^\prime \Sigma_t(s^\prime,E)} + \int_0^s ds^{\prime\prime} Q(s^{\prime\prime},\mathbf{\Omega}, E) e^{-\int_{s^{\prime\prime}}^s ds^\prime \Sigma_t(s^\prime,E)} + +With this characteristic form of the transport equation, we now have an +analytical solution along a linear path through any constant cross section +region of a system. While the solution only holds along a linear track, no +discretizations have yet been made. + +Similar to many other solution approaches to the Boltzmann neutron transport +equation, the MOC approach also uses a "multigroup" approximation in order to +discretize the continuous energy spectrum of neutrons traveling through the +system into fixed set of energy groups :math:`G`, where each group :math:`g \in +G` has its own specific cross section parameters. This makes the difficult +non-linear continuous energy dependence much more manageable as group wise cross +section data can be precomputed and fed into a simulation as input data. The +computation of multigroup cross section data is not a trivial task and can +introduce errors in the simulation. However, this is an active field of research +common to all multigroup methods, and there are numerous generation methods +available that are capable of reducing the biases introduced by the multigroup +approximation. Commonly used methods include the subgroup self-shielding method +and use of fast (unconverged) Monte Carlo simulations to produce cross section +estimates. It is important to note that Monte Carlo methods are capable of +treating the energy variable of the neutron continuously, meaning that they do +not need to make this approximation and are therefore not subject to any +multigroup errors. + +Following the multigroup discretization, another assumption made is that a large +and complex problem can be broken up into small constant cross section regions, +and that these regions have group dependent, flat, isotropic sources (fission +and scattering), :math:`Q_g`. Anisotropic as well as higher order sources are +also possible with MOC-based methods but are not used yet in OpenMC for +simplicity. With these key assumptions, the multigroup MOC form of the neutron +transport equation can be written as in Equation :eq:`moc_final`. + +.. math:: + :label: moc_final + + \psi_g(s, \mathbf{\Omega}) = \psi_g(\mathbf{r_0}, \mathbf{\Omega}) e^{-\int_0^s ds^\prime \Sigma_{t_g}(s^\prime)} + \int_0^s ds^{\prime\prime} Q_g(s^{\prime\prime},\mathbf{\Omega}) e^{-\int_{s^{\prime\prime}}^s ds^\prime \Sigma_{t_g}(s^\prime)} + +The CSG definition of the system is used to create spatially defined source +regions (each region being denoted as :math:`i`). These neutron source regions +are often approximated as being constant +(flat) in source intensity but can also be defined using a higher order source +(linear, quadratic, etc.) that allows for fewer source regions to be required to +achieve a specified solution fidelity. In OpenMC, the approximation of a +spatially constant isotropic fission and scattering source :math:`Q_{i,g}` in +cell :math:`i` leads +to simple exponential attenuation along an individual characteristic of length +:math:`s` given by Equation :eq:`fsr_attenuation`. + +.. math:: + :label: fsr_attenuation + + \psi_g(s) = \psi_g(0) e^{-\Sigma_{t,i,g} s} + \frac{Q_{i,g}}{\Sigma_{t,i,g}} \left( 1 - e^{-\Sigma_{t,i,g} s} \right) + +For convenience, we can also write this equation in terms of the incoming and +outgoing angular flux (:math:`\psi_g^{in}` and :math:`\psi_g^{out}`), and +consider a specific tracklength for a particular ray :math:`r` crossing cell +:math:`i` as :math:`\ell_r`, as in: + +.. math:: + :label: fsr_attenuation_in_out + + \psi_g^{out} = \psi_g^{in} e^{-\Sigma_{t,i,g} \ell_r} + \frac{Q_{i,g}}{\Sigma_{t,i,g}} \left( 1 - e^{-\Sigma_{t,i,g} \ell_r} \right) . + +We can then define the average angular flux of a single ray passing through the +cell as: + +.. math:: + :label: average + + \overline{\psi}_{r,i,g} = \frac{1}{\ell_r} \int_0^{\ell_r} \psi_{g}(s)ds . + +We can then substitute in Equation :eq:`fsr_attenuation` and solve, resulting +in: + +.. math:: + :label: average_solved + + \overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} - \frac{\psi_{r,g}^{out} - \psi_{r,g}^{in}}{\ell_r \Sigma_{t,i,g}} . + +By rearranging Equation :eq:`fsr_attenuation_in_out`, we can then define +:math:`\Delta \psi_{r,g}` as the change in angular flux for ray :math:`r` +passing through region :math:`i` as: + +.. math:: + :label: delta_psi + + \Delta \psi_{r,g} = \psi_{r,g}^{in} - \psi_{r,g}^{out} = \left(\psi_{r,g}^{in} - \frac{Q_{i,g}}{\Sigma_{t,i,g}} \right) \left( 1 - e^{-\Sigma_{t,i,g} \ell_r} \right) . + +Equation :eq:`delta_psi` is a useful expression as it is easily computed with +the known inputs for a ray crossing through the region. + +By substituting :eq:`delta_psi` into :eq:`average_solved`, we can arrive at a +final expression for the average angular flux for a ray crossing a region as: + +.. math:: + :label: average_psi_final + + \overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}} + +~~~~~~~~~~~ +Random Rays +~~~~~~~~~~~ + +In the previous subsection, the governing characteristic equation along a 1D +line through the system was written, such that an analytical solution for the +ODE can be computed. If enough characteristic tracks (ODEs) are solved, then the +behavior of the governing PDE can be numerically approximated. In traditional +deterministic MOC, the selection of tracks is chosen deterministically, where +azimuthal and polar quadratures are defined along with even track spacing in +three dimensions. This is the point at which random ray diverges from +deterministic MOC numerically. In the random ray method, rays are randomly +sampled from a uniform distribution in space and angle and tracked along a +predefined distance through the geometry before terminating. **Importantly, +different rays are sampled each power iteration, leading to a fully stochastic +convergence process.** This results in a need to utilize both inactive and +active batches as in the Monte Carlo method. + +While Monte Carlo implicitly converges the scattering source fully within each +iteration, random ray (and MOC) solvers are not typically written to fully +converge the scattering source within a single iteration. Rather, both the +fission and scattering sources are updated each power iteration, thus requiring +enough outer iterations to reach a stationary distribution in both the fission +source and scattering source. So, even in a low dominance ratio problem like a +2D pincell, several hundred inactive batches may still be required with random +ray to allow the scattering source to fully develop, as neutrons undergoing +hundreds of scatters may constitute a non-trivial contribution to the fission +source. We note that use of a two-level second iteration scheme is sometimes +used by some MOC or random ray solvers so as to fully converge the scattering +source with many inner iterations before updating the fission source in the +outer iteration. It is typically more efficient to use the single level +iteration scheme, as there is little reason to spend so much work converging the +scattering source if the fission source is not yet converged. + +Overall, the difference in how random ray and Monte Carlo converge the +scattering source means that in practice, random ray typically requires more +inactive iterations than are required in Monte Carlo. While a Monte Carlo +simulation may need 100 inactive iterations to reach a stationary source +distribution for many problems, a random ray solve will likely require 1,000 +iterations or more. Source convergence metrics (e.g., Shannon entropy) are thus +recommended when performing random ray simulations to ascertain when the source +has fully developed. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Converting Angular Flux to Scalar Flux +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Thus far in our derivation, we have been able to write analytical equations that +solve for the change in angular flux of a ray crossing a flat source region +(Equation :eq:`delta_psi`) as well as the ray's average angular flux through +that region (Equation :eq:`average_psi_final`). To determine the source for the +next power iteration, we need to assemble our estimates of angular fluxes from +all the sampled rays into scalar fluxes within each FSR. + +We can define the scalar flux in region :math:`i` as: + +.. math:: + :label: integral + + \phi_i = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} . + +The integral in the numerator: + +.. math:: + :label: numerator + + \int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r} . + +is not known analytically, but with random ray, we are going the numerically +approximate it by discretizing over a finite number of tracks (with a finite +number of locations and angles) crossing the domain. We can then use the +characteristic method to determine the total angular flux along that line. + +Conceptually, this can be thought of as taking a volume-weighted sum of angular +fluxes for all :math:`N_i` rays that happen to pass through cell :math:`i` that +iteration. When written in discretized form (with the discretization happening +in terms of individual ray segments :math:`r` that pass through region +:math:`i`), we arrive at: + +.. math:: + :label: discretized + + \phi_{i,g} = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} = \overline{\overline{\psi}}_{i,g} \approx \frac{\sum\limits_{r=1}^{N_i} \ell_r w_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r w_r} . + +Here we introduce the term :math:`w_r`, which represents the "weight" of the ray +(its 2D area), such that the volume that a ray is responsible for can be +determined by multiplying its length :math:`\ell` by its weight :math:`w`. As +the scalar flux vector is a shape function only, we are actually free to +multiply all ray weights :math:`w` by any constant such that the overall shape +is still maintained, even if the magnitude of the shape function changes. Thus, +we can simply set :math:`w_r` to be unity for all rays, such that: + +.. math:: + :label: weights + + \text{Volume of cell } i = V_i \approx \sum\limits_{r=1}^{N_i} \ell_r w_r = \sum\limits_{r=1}^{N_i} \ell_r . + +We can then rewrite our discretized equation as: + +.. math:: + :label: discretized_2 + + \phi_{i,g} \approx \frac{\sum\limits_{r=1}^{N_i} \ell_r w_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r w_r} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r} . + +Thus, the scalar flux can be inferred if we know the volume weighted sum of the +average angular fluxes that pass through the cell. Substituting +:eq:`average_psi_final` into :eq:`discretized_2`, we arrive at: + +.. math:: + :label: scalar_full + + \phi_{i,g} = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} = \overline{\overline{\psi}}_{i,g} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}}}{\sum\limits_{r=1}^{N_i} \ell_r}, + +which when partially simplified becomes: + +.. math:: + :label: scalar_four_vols + + \phi = \frac{Q_{i,g} \sum\limits_{r=1}^{N_i} \ell_r}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} + \frac{\sum\limits_{r=1}^{N_i} \ell_r \frac{\Delta \psi_i}{\ell_r}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} . + +Note that there are now four (seemingly identical) volume terms in this equation. + +~~~~~~~~~~~~~~ +Volume Dilemma +~~~~~~~~~~~~~~ + +At first glance, Equation :eq:`scalar_four_vols` appears ripe for cancellation +of terms. Mathematically, such cancellation allows us to arrive at the following +"naive" estimator for the scalar flux: + +.. math:: + :label: phi_naive + + \phi_{i,g}^{naive} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} . + +This derivation appears mathematically sound at first glance but unfortunately +raises a serious issue as discussed in more depth by `Tramm et al. +`_ and `Cosgrove and Tramm `_. Namely, the second +term: + +.. math:: + :label: ratio_estimator + + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} + +features stochastic variables (the sums over random ray lengths and angular +fluxes) in both the numerator and denominator, making it a stochastic ratio +estimator, which is inherently biased. In practice, usage of the naive estimator +does result in a biased, but "consistent" estimator (i.e., it is biased, but +the bias tends towards zero as the sample size increases). Experimentally, the +right answer can be obtained with this estimator, though a very fine ray density +is required to eliminate the bias. + +How might we solve the biased ratio estimator problem? While there is no obvious +way to alter the numerator term (which arises from the characteristic +integration approach itself), there is potentially more flexibility in how we +treat the stochastic term in the denominator, :math:`\sum\limits_{r=1}^{N_i} +\ell_r` . From Equation :eq:`weights` we know that this term can be directly +inferred from the volume of the problem, which does not actually change between +iterations. Thus, an alternative treatment for this "volume" term in the +denominator is to replace the actual stochastically sampled total track length +with the expected value of the total track length. For instance, if the true +volume of the FSR is known (as is the total volume of the full simulation domain +and the total tracklength used for integration that iteration), then we know the +true expected value of the tracklength in that FSR. That is, if a FSR accounts +for 2% of the overall volume of a simulation domain, then we know that the +expected value of tracklength in that FSR will be 2% of the total tracklength +for all rays that iteration. This is a key insight, as it allows us to the +replace the actual tracklength that was accumulated inside that FSR each +iteration with the expected value. + +If we know the analytical volumes, then those can be used to directly compute +the expected value of the tracklength in each cell. However, as the analytical +volumes are not typically known in OpenMC due to the usage of user-defined +constructive solid geometry, we need to source this quantity from elsewhere. An +obvious choice is to simply accumulate the total tracklength through each FSR +across all iterations (batches) and to use that sum to compute the expected +average length per iteration, as: + +.. math:: + :label: sim_estimator + + \sum\limits^{}_{i} \ell_i \approx \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} + +where :math:`b` is a single batch in :math:`B` total batches simulated so far. + +In this manner, the expected value of the tracklength will become more refined +as iterations continue, until after many iterations the variance of the +denominator term becomes trivial compared to the numerator term, essentially +eliminating the presence of the stochastic ratio estimator. A "simulation +averaged" estimator is therefore: + +.. math:: + :label: phi_sim + + \phi_{i,g}^{simulation} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B}} + +In practical terms, the "simulation averaged" estimator is virtually +indistinguishable numerically from use of the true analytical volume to estimate +this term. Note also that the term "simulation averaged" refers only to the +volume/length treatment, the scalar flux estimate itself is computed fully again +each iteration. + +There are some drawbacks to this method. Recall, this denominator volume term +originally stemmed from taking a volume weighted integral of the angular flux, +in which case the denominator served as a normalization term for the numerator +integral in Equation :eq:`integral`. Essentially, we have now used a different +term for the volume in the numerator as compared to the normalizing volume in +the denominator. The inevitable mismatch (due to noise) between these two +quantities results in a significant increase in variance. Notably, the same +problem occurs if using a tracklength estimate based on the analytical volume, +as again the numerator integral and the normalizing denominator integral no +longer match on a per-iteration basis. + +In practice, the simulation averaged method does completely remove the bias, +though at the cost of a notable increase in variance. Empirical testing reveals +that on most problems, the simulation averaged estimator does win out overall in +numerical performance, as a much coarser quadrature can be used resulting in +faster runtimes overall. Thus, OpenMC uses the simulation averaged estimator in +its random ray mode. + +~~~~~~~~~~~~~~~ +Power Iteration +~~~~~~~~~~~~~~~ + +Given a starting source term, we now have a way of computing an estimate of the +scalar flux in each cell by way of transporting rays randomly through the +domain, recording the change in angular flux for the rays into each cell as they +make their traversals, and summing these contributions up as in Equation +:eq:`phi_sim`. How then do we turn this into an iterative process such that we +improve the estimate of the source and scalar flux over many iterations, given +that our initial starting source will just be a guess? + +The source :math:`Q^{n}` for iteration :math:`n` can be inferred +from the scalar flux from the previous iteration :math:`n-1` as: + +.. math:: + :label: source_update + + Q^{n}(i, g) = \frac{\chi}{k^{n-1}_{eff}} \nu \Sigma_f(i, g) \phi^{n-1}(g) + \sum\limits^{G}_{g'} \Sigma_{s}(i,g,g') \phi^{n-1}(g') + +where :math:`Q^{n}(i, g)` is the total source (fission + scattering) in region +:math:`i` and energy group :math:`g`. Notably, the in-scattering source in group +:math:`g` must be computed by summing over the contributions from all groups +:math:`g' \in G`. + +In a similar manner, the eigenvalue for iteration :math:`n` can be computed as: + +.. math:: + :label: eigenvalue_update + + k^{n}_{eff} = k^{n-1}_{eff} \frac{F^n}{F^{n-1}}, + +where the total spatial- and energy-integrated fission rate :math:`F^n` in +iteration :math:`n` can be computed as: + +.. math:: + :label: fission_source + + F^n = \sum\limits^{M}_{i} \left( V_i \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right) + +where :math:`M` is the total number of FSRs in the simulation. Similarly, the +total spatial- and energy-integrated fission rate :math:`F^{n-1}` in iteration +:math:`n-1` can be computed as: + +.. math:: + :label: fission_source_prev + + F^{n-1} = \sum\limits^{M}_{i} \left( V_i \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n-1}(g) \right) + +Notably, the volume term :math:`V_i` appears in the eigenvalue update equation. +The same logic applies to the treatment of this term as was discussed earlier. +In OpenMC, we use the "simulation averaged" volume derived from summing over all +ray tracklength contributions to a FSR over all iterations and dividing by the +total integration tracklength to date. Thus, Equation :eq:`fission_source` +becomes: + +.. math:: + :label: fission_source_volumed + + F^n = \sum\limits^{M}_{i} \left( \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right) + +and a similar substitution can be made to update Equation +:eq:`fission_source_prev` . In OpenMC, the most up-to-date version of the volume +estimate is used, such that the total fission source from the previous iteration +(:math:`n-1`) is also recomputed each iteration. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Ray Starting Conditions and Inactive Length +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another key area of divergence between deterministic MOC and random ray is the +starting conditions for rays. In deterministic MOC, the angular flux spectrum +for rays are stored at any reflective or periodic boundaries so as to provide a +starting condition for the next iteration. As there are many tracks, storage of +angular fluxes can become costly in terms of memory consumption unless there are +only vacuum boundaries present. + +In random ray, as the starting locations of rays are sampled anew each +iteration, the initial angular flux spectrum for the ray is unknown. While a +guess can be made by taking the isotropic source from the FSR the ray was +sampled in, direct usage of this quantity would result in significant bias and +error being imparted on the simulation. + +Thus, an `on-the-fly approximation method `_ was developed (known +as the "dead zone"), where the first several mean free paths of a ray are +considered to be "inactive" or "read only". In this sense, the angular flux is +solved for using the MOC equation, but the ray does not "tally" any scalar flux +back to the FSRs that it travels through. After several mean free paths have +been traversed, the ray's angular flux spectrum typically becomes dominated by +the accumulated source terms from the cells it has traveled through, while the +(incorrect) starting conditions have been attenuated away. In the animation in +the :ref:`introductory section on this page `, the +yellow portion of the ray lengths is the dead zone. As can be seen in this +animation, the tallied :math:`\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}` term +that is plotted is not affected by the ray when the ray is within its inactive +length. Only when the ray enters its active mode does the ray contribute to the +:math:`\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}` sum for the iteration. + +~~~~~~~~~~~~~~~~~~~~~ +Ray Ending Conditions +~~~~~~~~~~~~~~~~~~~~~ + +To ensure that a uniform density of rays is integrated in space and angle +throughout the simulation domain, after exiting the initial inactive "dead zone" +portion of the ray, the rays are run for a user-specified distance. Typically, a +choice of at least several times the length of the inactive "dead zone" is made +so as to amortize the cost of the dead zone. For example, if a dead zone of 30 +cm is selected, then an active length of 300 cm might be selected so that the +cost of the dead zone is at most 10% of the overall runtime. + +-------------------- +Simplified Algorithm +-------------------- + +A simplified set of functions that execute a single random ray power iteration +are given below. Not all global variables are defined in this illustrative +example, but the high level components of the algorithm are shown. A number of +significant simplifications are made for clarity---for example, no inactive +"dead zone" length is shown, geometry operations are abstracted, no parallelism +(or thread safety) is expressed, a naive exponential treatment is used, and rays +are not halted at their exact termination distances, among other subtleties. +Nonetheless, the below algorithms may be useful for gaining intuition on the +basic components of the random ray process. Rather than expressing the algorithm +in abstract pseudocode, C++ is used to make the control flow easier to +understand. + +The first block below shows the logic for a single power iteration (batch): + +.. code-block:: C++ + + double power_iteration(double k_eff) { + + // Update source term (scattering + fission) + update_neutron_source(k_eff); + + // Reset scalar fluxes to zero + fill(global::scalar_flux_new, 0.0f); + + // Transport sweep over all random rays for the iteration + for (int i = 0; i < nrays; i++) { + RandomRay ray; + initialize_ray(ray); + transport_single_ray(ray); + } + + // Normalize scalar flux and update volumes + normalize_scalar_flux_and_volumes(); + + // Add source to scalar flux, compute number of FSR hits + add_source_to_scalar_flux(); + + // Compute k-eff using updated scalar flux + k_eff = compute_k_eff(k_eff); + + // Set phi_old = phi_new + global::scalar_flux_old.swap(global::scalar_flux_new); + + return k_eff; + } + +The second function shows the logic for transporting a single ray within the +transport loop: + +.. code-block:: C++ + + void transport_single_ray(RandomRay& ray) { + + // Reset distance to zero + double distance = 0.0; + + // Continue transport of ray until active length is reached + while (distance < user_setting::active_length) { + // Ray trace to find distance to next surface (i.e., segment length) + double s = distance_to_nearest_boundary(ray); + + // Attenuate flux (and accumulate source/attenuate) on segment + attenuate_flux(ray, s); + + // Advance particle to next surface + ray.location = ray.location + s * ray.direction; + + // Move ray across the surface + cross_surface(ray); + + // Add segment length "s" to total distance traveled + distance += s; + } + } + +The final function below shows the logic for solving for the characteristic MOC +equation (and accumulating the scalar flux contribution of the ray into the +scalar flux value for the FSR). + +.. code-block:: C++ + + void attenuate_flux(RandomRay& ray, double s) { + + // Determine which flat source region (FSR) the ray is currently in + int fsr = get_fsr_id(ray.location); + + // Determine material type + int material = get_material_type(fsr); + + // MOC incoming flux attenuation + source contribution/attenuation equation + for (int e = 0; e < global::n_energy_groups; e++) { + float sigma_t = global::macro_xs[material].total; + float tau = sigma_t * s; + float delta_psi = (ray.angular_flux[e] - global::source[fsr][e] / sigma_t) * (1 - exp(-tau)); + ray.angular_flux_[e] -= delta_psi; + global::scalar_flux_new[fsr][e] += delta_psi; + } + + // Record total tracklength in this FSR (to compute volume) + global::volume[fsr] += s; + } + +------------------------ +How are Tallies Handled? +------------------------ + +Most tallies, filters, and scores that you would expect to work with a +multigroup solver like random ray should work. For example, you can define 3D +mesh tallies with energy filters and flux, fission, and nu-fission scores, etc. +There are some restrictions though. For starters, it is assumed that all filter +mesh boundaries will conform to physical surface boundaries (or lattice +boundaries) in the simulation geometry. It is acceptable for multiple cells +(FSRs) to be contained within a filter mesh cell (e.g., pincell-level or +assembly-level tallies should work), but it is currently left as undefined +behavior if a single simulation cell is able to score to multiple filter mesh +cells. In the future, the capability to fully support mesh tallies may be added +to OpenMC, but for now this restriction needs to be respected. + +--------------------------- +Fundamental Sources of Bias +--------------------------- + +Compared to continuous energy Monte Carlo simulations, the known sources of bias +in random ray particle transport are: + + - **Multigroup Energy Discretization:** The multigroup treatment of flux and + cross sections incurs a significant bias, as a reaction rate (:math:`R_g = + V \phi_g \Sigma_g`) for an energy group :math:`g` can only be conserved + for a given choice of multigroup cross section :math:`\Sigma_g` if the + flux (:math:`\phi_g`) is known a priori. If the flux was already known, + then there would be no point to the simulation, resulting in a fundamental + need for approximating this quantity. There are numerous methods for + generating relatively accurate multigroup cross section libraries that can + each be applied to a narrow design area reliably, although there are + always limitations and/or complexities that arise with a multigroup energy + treatment. This is by far the most significant source of simulation bias + between Monte Carlo and random ray for most problems. While the other + areas typically have solutions that are highly effective at mitigating + bias, error stemming from multigroup energy discretization is much harder + to remedy. + - **Flat Source Approximation:**. In OpenMC, a "flat" (0th order) source + approximation is made, wherein the scattering and fission sources within a + cell are assumed to be spatially uniform. As the source in reality is a + continuous function, this leads to bias, although the bias can be reduced + to acceptable levels if the flat source regions are sufficiently small. + The bias can also be mitigated by assuming a higher-order source (e.g., + linear or quadratic), although OpenMC does not yet have this capability. + In practical terms, this source of bias can become very large if cells are + large (with dimensions beyond that of a typical particle mean free path), + but the subdivision of cells can often reduce this bias to trivial levels. + - **Anisotropic Source Approximation:** In OpenMC, the source is not only + assumed to be flat but also isotropic, leading to bias. It is possible for + MOC (and likely random ray) to treat anisotropy explicitly, but this is + not currently supported in OpenMC. This source of bias is not significant + for some problems, but becomes more problematic for others. Even in the + absence of explicit treatment of anistropy, use of transport-corrected + multigroup cross sections can often mitigate this bias, particularly for + light water reactor simulation problems. + - **Angular Flux Initial Conditions:** Each time a ray is sampled, its + starting angular flux is unknown, so a guess must be made (typically the + source term for the cell it starts in). Usage of an adequate inactive ray + length (dead zone) mitigates this error. As the starting guess is + attenuated at a rate of :math:`\exp(-\Sigma_t \ell)`, this bias can driven + below machine precision in a low cost manner on many problems. + +.. _Tramm-2017a: https://doi.org/10.1016/j.jcp.2017.04.038 +.. _Tramm-2017b: https://doi.org/10.1016/j.anucene.2017.10.015 +.. _Tramm-2018: https://dspace.mit.edu/handle/1721.1/119038 +.. _Tramm-2020: https://doi.org/10.1051/EPJCONF/202124703021 +.. _Cosgrove-2023: https://doi.org/10.1080/00295639.2023.2270618 + +.. only:: html + + .. rubric:: References + +.. [Askew-1972] Askew, “A Characteristics Formulation of the Neutron Transport + Equation in Complicated Geometries.” Technical Report AAEW-M 1108, UK Atomic + Energy Establishment (1972). diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index afecaff5333..5ae9f20edf4 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -25,6 +25,7 @@ Simulation Settings openmc.IndependentSource openmc.FileSource openmc.CompiledSource + openmc.MeshSource openmc.SourceParticle openmc.VolumeCalculation openmc.Settings @@ -34,6 +35,7 @@ Simulation Settings :nosignatures: :template: myfunction.rst + openmc.read_source_file openmc.write_source_file openmc.wwinp_to_wws @@ -118,6 +120,7 @@ Constructing Tallies openmc.Filter openmc.UniverseFilter openmc.MaterialFilter + openmc.MaterialFromFilter openmc.CellFilter openmc.CellFromFilter openmc.CellBornFilter @@ -125,6 +128,7 @@ Constructing Tallies openmc.CollisionFilter openmc.SurfaceFilter openmc.MeshFilter + openmc.MeshBornFilter openmc.MeshSurfaceFilter openmc.EnergyFilter openmc.EnergyoutFilter @@ -160,6 +164,7 @@ Geometry Plotting :template: myclass.rst openmc.Plot + openmc.ProjectionPlot openmc.Plots Running OpenMC diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index bce647ccb12..995ad97fa74 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -16,7 +16,6 @@ Functions current_batch export_properties export_weight_windows - import_weight_windows finalize find_cell find_material @@ -25,6 +24,7 @@ Functions hard_reset id_map import_properties + import_weight_windows init is_statepoint_batch iter_batches @@ -40,9 +40,10 @@ Functions run run_in_memory sample_external_source - simulation_init simulation_finalize + simulation_init source_bank + statepoint_load statepoint_write Classes @@ -53,12 +54,86 @@ Classes :nosignatures: :template: myclass.rst + AzimuthalFilter Cell + CellFilter + CellInstanceFilter + CellbornFilter + CellfromFilter + CollisionFilter + CylindricalMesh + DelayedGroupFilter + DistribcellFilter EnergyFilter - MaterialFilter + EnergyFunctionFilter + EnergyoutFilter + Filter + LegendreFilter Material + MaterialFilter + MaterialFromFilter + Mesh MeshFilter + MeshBornFilter MeshSurfaceFilter + MuFilter Nuclide + ParticleFilter + PolarFilter + RectilinearMesh RegularMesh + SpatialLegendreFilter + SphericalHarmonicsFilter + SphericalMesh + SurfaceFilter Tally + UniverseFilter + UnstructuredMesh + WeightWindows + ZernikeFilter + ZernikeRadialFilter + +Data +---- + +.. data:: cells + + Mapping of cell ID to :class:`openmc.lib.Cell` instances. + + :type: dict + +.. data:: filters + + Mapping of filter ID to :class:`openmc.lib.Filter` instances. + + :type: dict + +.. data:: materials + + Mapping of material ID to :class:`openmc.lib.Material` instances. + + :type: dict + +.. data:: meshes + + Mapping of mesh ID to :class:`openmc.lib.Mesh` instances. + + :type: dict + +.. data:: nuclides + + Mapping of nuclide name to :class:`openmc.lib.Nuclide` instances. + + :type: dict + +.. data:: tallies + + Mapping of tally ID to :class:`openmc.lib.Tally` instances. + + :type: dict + +.. data:: weight_windows + + Mapping of weight window ID to :class:`openmc.lib.WeightWindows` instances. + + :type: dict diff --git a/docs/source/pythonapi/model.rst b/docs/source/pythonapi/model.rst index c9c6e117a95..99459f64d09 100644 --- a/docs/source/pythonapi/model.rst +++ b/docs/source/pythonapi/model.rst @@ -11,8 +11,6 @@ Convenience Functions :template: myfunction.rst openmc.model.borated_water - openmc.model.hexagonal_prism - openmc.model.rectangular_prism openmc.model.subdivide openmc.model.pin @@ -26,9 +24,11 @@ Composite Surfaces openmc.model.CruciformPrism openmc.model.CylinderSector + openmc.model.HexagonalPrism openmc.model.IsogonalOctagon openmc.model.Polygon openmc.model.RectangularParallelepiped + openmc.model.RectangularPrism openmc.model.RightCircularCylinder openmc.model.XConeOneSided openmc.model.YConeOneSided diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index b25b02fb868..7f222f77cb8 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -107,31 +107,54 @@ can be used to access the installed packages. .. _Spack: https://spack.readthedocs.io/en/latest/ .. _setup guide: https://spack.readthedocs.io/en/latest/getting_started.html --------------------------------- -Installing from Source on Ubuntu --------------------------------- +------------------------------- +Manually Installing from Source +------------------------------- -To build OpenMC from source, several :ref:`prerequisites ` are -needed. If you are using Ubuntu or higher, all prerequisites can be installed -directly from the package manager: +Obtaining prerequisites on Ubuntu +--------------------------------- + +When building OpenMC from source, all :ref:`prerequisites ` can +be installed using the package manager: .. code-block:: sh sudo apt install g++ cmake libhdf5-dev libpng-dev -After the packages have been installed, follow the instructions below for -building and installing OpenMC from source. +After the packages have been installed, follow the instructions to build from +source below. -------------------------------------------- -Installing from Source on Linux or Mac OS X -------------------------------------------- +Obtaining prerequisites on macOS +-------------------------------- + +For an OpenMC build with multithreading enabled, a package manager like +`Homebrew `_ should first be installed. Then, the following +packages should be installed, for example in Homebrew via: + +.. code-block:: sh + + brew install llvm cmake xtensor hdf5 python libomp libpng + +The compiler provided by the above LLVM package should be used in place of the +one provisioned by XCode, which does not support the multithreading library used +by OpenMC. Consequently, the C++ compiler should explicitly be set before +proceeding: + +.. code-block:: sh + + export CXX=/opt/homebrew/opt/llvm/bin/clang++ + +After the packages have been installed, follow the instructions to build from +source below. + +Building Source on Linux or macOS +--------------------------------- All OpenMC source code is hosted on `GitHub `_. If you have `git -`_, the `gcc `_ compiler suite, -`CMake `_, and `HDF5 -`_ installed, you can download and -install OpenMC be entering the following commands in a terminal: +`_, a modern C++ compiler, `CMake `_, +and `HDF5 `_ installed, you can +download and install OpenMC by entering the following commands in a terminal: .. code-block:: sh @@ -151,14 +174,14 @@ should specify an installation directory where you have write access, e.g. cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local .. The :mod:`openmc` Python package must be installed separately. The easiest way -to install it is using `pip `_, which is -included by default in Python 3.4+. From the root directory of the OpenMC -distribution/repository, run: +to install it is using `pip `_. +From the root directory of the OpenMC repository, run: .. code-block:: sh python -m pip install . -If you want to build a parallel version of OpenMC (using OpenMP or MPI), -directions can be found in the :ref:`detailed installation instructions +By default, OpenMC will be built with multithreading support. To build +distributed-memory parallel versions of OpenMC using MPI or to configure other +options, directions can be found in the :ref:`detailed installation instructions `. diff --git a/docs/source/releasenotes/0.14.0.rst b/docs/source/releasenotes/0.14.0.rst new file mode 100644 index 00000000000..445edfa077a --- /dev/null +++ b/docs/source/releasenotes/0.14.0.rst @@ -0,0 +1,284 @@ +==================== +What's New in 0.14.0 +==================== + +.. currentmodule:: openmc + +------- +Summary +------- + +This release of OpenMC includes many bug fixes, performance improvements, and +several notable new features. Some of the highlights include projection plots, +pulse height tallies for photons, weight window generation, and an ability to +specify continuous removal or feed of nuclides/elements during depletion. +Additionally, one of the longstanding annoyances of depletion calculations, +namely the need to include initial "dilute" nuclides, has been eliminated. There +are also a wide array of general improvements in the Python API. + +------------------------------------ +Compatibility Notes and Deprecations +------------------------------------ + +- The :class:`openmc.deplete.MicroXS` has been completely redesigned and + improved. See further comments below under "New Features". (`#2572 + `_, `#2579 + `_, `#2595 + `_, `#2700 + `_) +- The ``rectangular_prism`` function has been replaced by the + :class:`openmc.model.RectangularPrism` class and the ``hexagonal_prism`` + function has been replaced by the :class:`openmc.model.HexagonalPrism` class. + Note that whereas the ``rectangular_prism`` and ``hexagonal_prism`` functions + returned a region representing the interior of the prism, the new + :class:`~openmc.model.RectangularPrism` and + :class:`~openmc.model.HexagonalPrism` classes return composite surfaces, so + you need to use the unary ``-`` or ``+`` operators to obtain a region that can + be assigned to a cell. (`#2739 + `_) +- The ``Source`` class has been refactored and split up into three separate + classes: :class:`~openmc.IndependentSource`, :class:`~openmc.FileSource`, and + :class:`~openmc.CompiledSource`. (`#2524 + `_) +- The ``vertices`` and ``centroids`` attributes on mesh classes now always + return Cartesian coordinates and the shape of the returned arrays has changed + to allow `ijk` indexing using a tuple (i.e., `xyz = vertices[i, j, k]`). + (`#2711 `_) +- The :attr:`openmc.Material.decay_photon_energy` attribute has been replaced by + the :meth:`openmc.Material.get_decay_photon_energy` method. (`#2715 + `_) + +------------ +New Features +------------ + +- A new :class:`openmc.ProjectionPlot` class enables the generation of orthographic or + perspective projection plots. (`#1926 + `_) +- The :class:`openmc.model.RightCircularCylinder` class now supports optional + filleted edges. (`#2309 `_) +- Continuous removal or feed of nuclides/elements between materials can now be + modeled during depletion via the + :meth:`openmc.deplete.abc.Integrator.add_transfer_rate` method. (`#2358 + `_, `#2564 + `_, `#2626 + `_) +- The MAGIC method for global weight window generation has been implemented as + part of the C++ API. (`#2359 + `_) +- A new capability for pulse height tallies (currently limited to photons) has + been added and can be used via the "pulse-height" tally score. (`#2452 + `_) +- A :class:`openmc.model.CruciformPrism` class has been added that provides a + generalized cruciform prism composite surface. (`#2457 + `_) +- Type hints have been added in various places throughout the Python API. + (`#2462 `_, `#2467 + `_, `#2468 + `_, `#2470 + `_, `#2471 + `_, `#2601 + `_) +- Voxel plots can now be generated through the :meth:`openmc.Plot.to_vtk` + method. (`#2464 `_) +- The :class:`openmc.mgxs.EnergyGroups` class now allows you to alternatively + pass a string of the group structure name (e.g., "CCFE-709") instead of the + energy group boundaries. (`#2466 + `_) +- Several enhancements have been made to the :meth:`openmc.Universe.plot` method + (addition of axis labels with units, ability to show legend and/or outlines, automatic + determination of origin/width, ability to pass total number of pixels). + (`#2472 `_, `#2482 + `_, `#2483 + `_, `#2492 + `_, `#2513 + `_, `#2575 + `_) +- Functionality in the Python dealing with bounding boxes now relies on a new + :class:`openmc.BoundingBox` class. (`#2475 + `_) +- Users now have more flexibility in specifying nuclides and reactions in the + :func:`openmc.plot_xs` function. (`#2478 + `_) +- The import time of the :mod:`openmc` Python module has been improved by + deferring the import of matplotlib. (`#2488 + `_) +- Mesh clases in the Python API now support a ``bounding_box`` property. (`#2507 + `_, `#2620 + `_, `#2621 + `_) +- The ``Source`` class has been refactored and split up into three separate + classes: :class:`~openmc.IndependentSource`, :class:`~openmc.FileSource`, and + :class:`~openmc.CompiledSource`. (`#2524 + `_) +- Support was added for curvilinear elements when exporting cylindrical and + spherical meshes to VTK. (`#2533 + `_) +- The :class:`openmc.Tally` class now has a + :attr:`~openmc.Tally.multiply_density` attribute that indicates whether + reaction rate tallies should include the number density of the nuclide of + interest. (`#2539 `_) +- The :func:`~openmc.wwinp_to_wws` function now supports ``wwinp`` files with + cylindrical or spherical meshes. (`#2556 + `_) +- Depletion no longer relies on adding initial "dilute" nuclides to each + depletable material in order to compute reaction rates. (`#2559 + `_, `#2568 + `_) +- The :class:`openmc.deplete.Results` class now has + :meth:`~openmc.deplete.Results.get_mass` (`#2565 + `_), + :meth:`~openmc.deplete.Results.get_activity` (`#2617 + `_), and + :meth:`~openmc.deplete.Results.get_decay_heat` (`#2625 + `_) methods. +- The :meth:`openmc.deplete.StepResult.save` method now supports a ``path`` + argument. (`#2567 `_) +- The :class:`openmc.deplete.MicroXS` has been completely redesigned and + improved. First, it no longer relies on the :mod:`openmc.mgxs` module, no + longer subclasses :class:`pandas.DataFrame`, and doesn't require adding + initial "dilute" nuclides into material compositions. It now enables users to + specify an energy group structure to collect multigroup cross sections, + specify nuclides/reactions, and works with mesh domains in addition to the + existing domains. A new :func:`openmc.deplete.get_microxs_and_flux` function + was added that improves the workflow for calculating microscopic cross + sections along with fluxes. Altogether, these changes make it straightforward + to switch between coupled and independent operators for depletion/activation + calculations. (`#2572 `_, + `#2579 `_, `#2595 + `_, `#2700 + `_) +- The :class:`openmc.Geometry` class now has ``merge_surfaces`` and + ``surface_precision`` arguments. (`#2602 + `_) +- Several predefined energy group structures have been added ("MPACT-51", + "MPACT-60", "MPACT-69", "SCALE-252"). (`#2614 + `_) +- When running a depletion calculation, you are now allowed to include nuclides + in the initial material compositions that do not have neutron cross sections + (decay-only nuclides). (`#2616 + `_) +- The :class:`~openmc.CylindricalMesh` and :class:`~openmc.SphericalMesh` + classes can now be fully formed using the constructor. (`#2619 + `_) +- A time cutoff can now be specified in the :attr:`openmc.Settings.cutoff` + attribute. (`#2631 `_) +- The :meth:`openmc.Material.add_element` method now supports a + ``cross_sections`` argument that allows a cross section data source to be + specified. (`#2633 `_) +- The :class:`~openmc.Cell` class now has a :meth:`~openmc.Cell.plot` method. + (`#2648 `_) +- The :class:`~openmc.Geometry` class now has a :meth:`~openmc.Geometry.plot` + method. (`#2661 `_) +- When weight window checks are performed can now be explicitly specified with + the :attr:`openmc.Settings.weight_window_checkpoints` attribute. (`#2670 + `_) +- The :class:`~openmc.Settings` class now has a + :attr:`~openmc.Settings.max_write_lost_particles` attribute that can limit the + number of lost particle files written. (`#2688 + `_) +- The :class:`~openmc.deplete.CoupledOperator` class now has a + ``diff_volume_method`` argument that specifies how the volume of new materials + should be determined. (`#2691 + `_) +- The :meth:`openmc.DAGMCUniverse.bounding_region` method now has a + ``padding_distance`` argument. (`#2701 + `_) +- A new :meth:`openmc.Material.get_decay_photon_energy` method replaces the + :attr:`decay_photon_energy` attribute and includes an ability to eliminate + low-importance points. This is facilitated by a new + :meth:`openmc.stats.Discrete.clip` method. (`#2715 + `_) +- The :meth:`openmc.model.Model.differentiate_depletable_mats` method allows + depletable materials to be differentiated independent of the depletion + calculation itself. (`#2718 + `_) +- Albedos can now be specified on surface boundary conditions. (`#2724 + `_) + +--------- +Bug Fixes +--------- + +- Enable use of NCrystal materials in plot_xs (`#2435 `_) +- Avoid segfault from extern "C" std::string (`#2455 `_) +- Fix several issues with the Model class (`#2465 `_) +- Provide alternative batch estimation message (`#2479 `_) +- Correct index check for remove_tally (`#2494 `_) +- Support for NCrystal material in from_xml_element (`#2496 `_) +- Fix compilation with gcc 5 (`#2498 `_) +- Fixed in the Tally::add_filter method (`#2501 `_) +- Fix meaning of "masking" for plots (`#2510 `_) +- Fix description of statepoint.batches in Settings class (`#2514 `_) +- Reorder list initialization of Plot constructor (`#2519 `_) +- Added mkdir to cwd argument in Model.run (`#2523 `_) +- Fix export of spherical coordinates in SphericalMesh (`#2538 `_) +- Add virtual destructor on PlottableInterface (`#2541 `_) +- Ensure parent directory is created during depletion (`#2543 `_) +- Fix potential out-of-bounds access in TimeFilter (`#2532 `_) +- Remove use of sscanf for reading surface coefficients (`#2574 `_) +- Fix torus intersection bug (`#2589 `_) +- Multigroup per-thread cache fixes (`#2591 `_) +- Bank surface source particles in all active cycles (`#2592 `_) +- Fix for muir standard deviation (`#2598 `_) +- Check for zero fission cross section (`#2600 `_) +- XML read fixes in Plot classes (`#2623 `_) +- Added infinity check in VolumeCalculation (`#2634 `_) +- Fix sampling issue in Mixture distributions (`#2658 `_) +- Prevent segfault in distance to boundary calculation (`#2659 `_) +- Several CylindricalMesh fixes (`#2676 + `_, `#2680 + `_, `#2684 + `_, `#2710 + `_) +- Add type checks on Intersection, Union, Complement (`#2685 `_) +- Fixed typo in CF4Integrator docstring (`#2704 `_) +- Ensure property setters are used in CylindricalMesh and SphericalMesh (`#2709 `_) +- Fix sample_external_source bug (`#2713 `_) +- Fix localization issue affecting openmc-plotter (`#2723 `_) +- Correct openmc.lib wrapper for evaluate_legendre (`#2729 `_) +- Bug fix in Region.from_expression during tokenization (`#2733 `_) +- Fix bug in temperature interpolation (`#2734 `_) +- Check for invalid domain IDs in volume calculations (`#2742 `_) +- Skip boundary condition check for volume calculations (`#2743 `_) +- Fix loop over coordinates for source domain rejection (`#2751 `_) + +------------ +Contributors +------------ + +- `April Novak `_ +- `Baptiste Mouginot `_ +- `Ben Collins `_ +- `Chritopher Billingham `_ +- `Christopher Fichtlscherer `_ +- `Christina Cai `_ +- `Lorenzo Chierici `_ +- `Huw Rhys Jones `_ +- `Emilio Castro `_ +- `Erik Knudsen `_ +- `Ethan Peterson `_ +- `Egor Afanasenko `_ +- `Paul Wilson `_ +- `Gavin Ridley `_ +- `Hunter Belanger `_ +- `Jack Fletcher `_ +- `John Vincent Cauilan `_ +- `Josh May `_ +- `John Tramm `_ +- `Kevin McLaughlin `_ +- `Yue Jin `_ +- `Lewis Gross `_ +- `Luke Labrie-Cleary `_ +- `Patrick Myers `_ +- `Nicola Rizzi `_ +- `Yuvraj Jain `_ +- `Paul Romano `_ +- `Patrick Shriwise `_ +- `Rosie Barker `_ +- `Jonathan Shimwell `_ +- `John Tchakerian `_ +- `Travis Labossiere-Hickman `_ +- `Xinyan Wang `_ +- `Olek Yardas `_ +- `Zoe Prieto `_ diff --git a/docs/source/releasenotes/index.rst b/docs/source/releasenotes/index.rst index 30b6d51de2f..6923101cc43 100644 --- a/docs/source/releasenotes/index.rst +++ b/docs/source/releasenotes/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 1 + 0.14.0 0.13.3 0.13.2 0.13.1 diff --git a/docs/source/usersguide/data.rst b/docs/source/usersguide/data.rst index 48003ecb70b..6af2973388b 100644 --- a/docs/source/usersguide/data.rst +++ b/docs/source/usersguide/data.rst @@ -279,6 +279,8 @@ The `official ENDF/B-VII.1 HDF5 library multipole library, so if you are using this library, the windowed multipole data will already be available to you. +.. _create_mgxs: + ------------------------- Multigroup Cross Sections ------------------------- diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 2382609a48c..3a3d02231ad 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -147,12 +147,13 @@ For many regions, a bounding-box can be determined automatically:: While a bounding box can be determined for regions involving half-spaces of spheres, cylinders, and axis-aligned planes, it generally cannot be determined if the region involves cones, non-axis-aligned planes, or other exotic -second-order surfaces. For example, the :func:`openmc.model.hexagonal_prism` -function returns the interior region of a hexagonal prism; because it is bounded -by a :class:`openmc.Plane`, trying to get its bounding box won't work:: +second-order surfaces. For example, the :class:`openmc.model.HexagonalPrism` +class returns a hexagonal prism surface; because it utilizes a +:class:`openmc.Plane`, trying to get the bounding box of its interior won't +work:: - >>> hex = openmc.model.hexagonal_prism() - >>> hex.bounding_box + >>> hex = openmc.model.HexagonalPrism() + >>> (-hex).bounding_box (array([-0.8660254, -inf, -inf]), array([ 0.8660254, inf, inf])) @@ -172,13 +173,17 @@ surface. To specify a vacuum boundary condition, simply change the outer_surface = openmc.Sphere(r=100.0) outer_surface.boundary_type = 'vacuum' -Reflective and periodic boundary conditions can be set with the strings -'reflective' and 'periodic'. Vacuum and reflective boundary conditions can be -applied to any type of surface. Periodic boundary conditions can be applied to -pairs of planar surfaces. If there are only two periodic surfaces they will be -matched automatically. Otherwise it is necessary to specify pairs explicitly -using the :attr:`Surface.periodic_surface` attribute as in the following -example:: +Reflective, periodic, and white boundary conditions can be set with the +strings 'reflective', 'periodic', and 'white' respectively. +Vacuum, reflective and white boundary conditions can be applied to any +type of surface. The 'white' boundary condition supports diffuse particle +reflection in contrast to specular reflection provided by the 'reflective' +boundary condition. + +Periodic boundary conditions can be applied to pairs of planar surfaces. +If there are only two periodic surfaces they will be matched automatically. +Otherwise it is necessary to specify pairs explicitly using the +:attr:`Surface.periodic_surface` attribute as in the following example:: p1 = openmc.Plane(a=0.3, b=5.0, d=1.0, boundary_type='periodic') p2 = openmc.Plane(a=0.3, b=5.0, d=-1.0, boundary_type='periodic') @@ -196,6 +201,20 @@ lies in the first quadrant of the Cartesian grid. If the geometry instead lies in the fourth quadrant, the :class:`YPlane` must be replaced by a :class:`Plane` with the normal vector pointing in the :math:`-y` direction. +Additionally, 'reflective', 'periodic', and 'white' boundary conditions have +an albedo parameter that can be used to modify the importance of particles +that encounter the boundary. The albedo value specifies the ratio between +the particle's importance after interaction with the boundary to its initial +importance. The following example creates a reflective planar surface which +reduces the reflected particles' importance by 33.3%:: + + x1 = openmc.XPlane(1.0, boundary_type='reflective', albedo=0.667) + + # This is equivalent + x1 = openmc.XPlane(1.0) + x1.boundary_type = 'reflective' + x1.albedo = 0.667 + .. _usersguide_cells: ----- @@ -410,7 +429,7 @@ code would work:: hexlat.universes = [outer_ring, middle_ring, inner_ring] If you need to create a hexagonal boundary (composed of six planar surfaces) for -a hexagonal lattice, :func:`openmc.model.hexagonal_prism` can be used. +a hexagonal lattice, :class:`openmc.model.HexagonalPrism` can be used. .. _usersguide_geom_export: diff --git a/docs/source/usersguide/index.rst b/docs/source/usersguide/index.rst index 511bdc6cef0..03a77e87063 100644 --- a/docs/source/usersguide/index.rst +++ b/docs/source/usersguide/index.rst @@ -25,4 +25,6 @@ essential aspects of using OpenMC to perform simulations. processing parallel volume + random_ray troubleshoot + \ No newline at end of file diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index ab2cbe64546..130e96c0ae6 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -464,11 +464,11 @@ can typically be set for a single command, i.e. .. _compile_linux: -Compiling on Linux and Mac OS X -------------------------------- +Compiling on Linux and macOS +---------------------------- -To compile OpenMC on Linux or Max OS X, run the following commands from within -the root directory of the source code: +To compile OpenMC on Linux or macOS, run the following commands from within the +root directory of the source code: .. code-block:: sh @@ -540,7 +540,7 @@ to install the Python package in :ref:`"editable" mode `. Prerequisites ------------- -The Python API works with Python 3.7+. In addition to Python itself, the API +The Python API works with Python 3.8+. In addition to Python itself, the API relies on a number of third-party packages. All prerequisites can be installed using Conda_ (recommended), pip_, or through the package manager in most Linux distributions. diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index 35a2aff61ef..2d588519c55 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -126,27 +126,29 @@ will depend on the 3D viewer, but should be straightforward. Projection Plots ---------------- -.. image:: ../_images/hexlat_anim.gif - :width: 200px - -The :class:`openmc.ProjectionPlot` class presents an alternative method -of producing 3D visualizations of OpenMC geometries. It was developed to -overcome the primary shortcoming of voxel plots, that an enormous number -of voxels must be employed to capture detailed geometric features. -Projection plots perform volume rendering on material or -cell volumes, with colors specified in the same manner as slice plots. -This is done using the native ray tracing capabilities within OpenMC, -so any geometry in which particles successfully run without overlaps -or leaks will work with projection plots. - -One drawback of projection plots is that particle tracks cannot be overlaid -on them at present. Moreover, checking for overlap regions is not currently possible with projection plots. The image heading this section can -be created by adding the following code to the hexagonal lattice example packaged -with OpenMC, before exporting to plots.xml. +.. only:: html + + .. image:: ../_images/hexlat_anim.gif + :width: 200px + +The :class:`openmc.ProjectionPlot` class presents an alternative method of +producing 3D visualizations of OpenMC geometries. It was developed to overcome +the primary shortcoming of voxel plots, that an enormous number of voxels must +be employed to capture detailed geometric features. Projection plots perform +volume rendering on material or cell volumes, with colors specified in the same +manner as slice plots. This is done using the native ray tracing capabilities +within OpenMC, so any geometry in which particles successfully run without +overlaps or leaks will work with projection plots. + +One drawback of projection plots is that particle tracks cannot be overlaid on +them at present. Moreover, checking for overlap regions is not currently +possible with projection plots. The image heading this section can be created by +adding the following code to the hexagonal lattice example packaged with OpenMC, +before exporting to plots.xml. :: - r = 5 + r = 5 import numpy as np for i in range(100): phi = 2 * np.pi * i/100 @@ -162,48 +164,46 @@ with OpenMC, before exporting to plots.xml. thisp.xs[iron] = 1.0 thisp.wireframe_domains = [fuel] thisp.wireframe_thickness = 2 - + plot_file.append(thisp) -This generates a sequence of png files which can be joined to form a gif. -Each image specifies a different camera position using some simple periodic -functions to create a perfectly looped gif. :attr:`ProjectionPlot.look_at` -defines where the camera's centerline should point at. -:attr:`ProjectionPlot.camera_position` similarly defines where the camera -is situated in the universe level we seek to plot. The other settings -resemble those employed by :class:`openmc.Plot`, with the exception of -the :class:`ProjectionPlot.set_transparent` method and :attr:`ProjectionPlot.xs` -dictionary. These are used to control volume rendering of material -volumes. "xs" here stands for cross section, and it defines material -opacities in units of inverse centimeters. Setting this value to a -large number would make a material or cell opaque, and setting it to -zero makes a material transparent. Thus, the :class:`ProjectionPlot.set_transparent` -can be used to make all materials in the geometry transparent. From there, -individual material or cell opacities can be tuned to produce the -desired result. - -Two camera projections are available when using these plots, perspective -and orthographic. The default, perspective projection, -is a cone of rays passing through each pixel which radiate from the camera -position and span the field of view in the x and y positions. The horizontal -field of view can be set with the :attr: `ProjectionPlot.horizontal_field_of_view` attribute, -which is to be specified in units of degrees. The field of view only influences -behavior in perspective projection mode. +This generates a sequence of png files which can be joined to form a gif. Each +image specifies a different camera position using some simple periodic functions +to create a perfectly looped gif. :attr:`ProjectionPlot.look_at` defines where +the camera's centerline should point at. :attr:`ProjectionPlot.camera_position` +similarly defines where the camera is situated in the universe level we seek to +plot. The other settings resemble those employed by :class:`openmc.Plot`, with +the exception of the :class:`ProjectionPlot.set_transparent` method and +:attr:`ProjectionPlot.xs` dictionary. These are used to control volume rendering +of material volumes. "xs" here stands for cross section, and it defines material +opacities in units of inverse centimeters. Setting this value to a large number +would make a material or cell opaque, and setting it to zero makes a material +transparent. Thus, the :class:`ProjectionPlot.set_transparent` can be used to +make all materials in the geometry transparent. From there, individual material +or cell opacities can be tuned to produce the desired result. + +Two camera projections are available when using these plots, perspective and +orthographic. The default, perspective projection, is a cone of rays passing +through each pixel which radiate from the camera position and span the field of +view in the x and y positions. The horizontal field of view can be set with the +:attr: `ProjectionPlot.horizontal_field_of_view` attribute, which is to be +specified in units of degrees. The field of view only influences behavior in +perspective projection mode. In the orthographic projection, rays follow the same angle but originate from -different points. The horizontal width of this plane of ray starting points -may be set with the :attr: `ProjectionPlot.orthographic_width` element. If this element -is nonzero, the orthographic projection is employed. Left to its default value -of zero, the perspective projection is employed. - -Lastly, projection plots come packaged with wireframe generation that -can target either all surface/cell/material boundaries in the geometry, -or only wireframing around specific regions. In the above example, we -have set only the fuel region from the hexagonal lattice example to have -a wireframe drawn around it. This is accomplished by setting the -:attr: `ProjectionPlot.wireframe_domains`, which may be set to either material -IDs or cell IDs. The :attr:`ProjectionPlot.wireframe_thickness` -attribute sets the wireframe thickness in units of pixels. +different points. The horizontal width of this plane of ray starting points may +be set with the :attr: `ProjectionPlot.orthographic_width` element. If this +element is nonzero, the orthographic projection is employed. Left to its default +value of zero, the perspective projection is employed. + +Lastly, projection plots come packaged with wireframe generation that can target +either all surface/cell/material boundaries in the geometry, or only wireframing +around specific regions. In the above example, we have set only the fuel region +from the hexagonal lattice example to have a wireframe drawn around it. This is +accomplished by setting the :attr: `ProjectionPlot.wireframe_domains`, which may +be set to either material IDs or cell IDs. The +:attr:`ProjectionPlot.wireframe_thickness` attribute sets the wireframe +thickness in units of pixels. .. note:: When setting specific material or cell regions to have wireframes drawn around them, the plot must be colored by materials if wireframing diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst new file mode 100644 index 00000000000..dad86c826e0 --- /dev/null +++ b/docs/source/usersguide/random_ray.rst @@ -0,0 +1,480 @@ +.. _random_ray: + +================= +Random Ray Solver +================= + +In general, the random ray solver mode uses most of the same settings and +:ref:`run strategies ` as the standard Monte Carlo solver +mode. For instance, random ray solves are also split up into :ref:`inactive and +active batches `. However, there are a couple of settings +that are unique to the random ray solver and a few areas that the random ray +run strategy differs, both of which will be described in this section. + +------------------------ +Enabling Random Ray Mode +------------------------ + +To utilize the random ray solver, the :attr:`~openmc.Settings.random_ray` +dictionary must be present in the :class:`openmc.Settings` Python class. There +are a number of additional settings that must be specified within this +dictionary that will be discussed below. Additionally, the "multi-group" energy +mode must be specified. + +------- +Batches +------- + +In Monte Carlo simulations, inactive batches are used to let the fission source +develop into a stationary distribution before active batches are performed that +actually accumulate statistics. While this is true of random ray as well, in the +random ray mode the inactive batches are also used to let the scattering source +develop. Monte Carlo fully represents the scattering source within each +iteration (by its nature of fully simulating particles from birth to death +through any number of physical scattering events), whereas the scattering source +in random ray can only represent as many scattering events as batches have been +completed. For example, by iteration 10 in random ray, the scattering source +only captures the behavior of neutrons through their 10th scattering event. +Thus, while inactive batches are only required in an eigenvalue solve in Monte +Carlo, **inactive batches are required for both eigenvalue and fixed source +solves in random ray mode** due to this additional need to converge the +scattering source. + +The additional burden of converging the scattering source generally results in a +higher requirement for the number of inactive batches---often by an order of +magnitude or more. For instance, it may be reasonable to only use 50 inactive +batches for a light water reactor simulation with Monte Carlo, but random ray +might require 500 or more inactive batches. Similar to Monte Carlo, +:ref:`Shannon entropy ` can be used to gauge whether the +combined scattering and fission source has fully developed. + +Similar to Monte Carlo, active batches are used in the random ray solver mode to +accumulate and converge statistics on unknown quantities (i.e., the random ray +sources, scalar fluxes, as well as any user-specified tallies). + +The batch parameters are set in the same manner as with the regular Monte Carlo +solver:: + + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 1200 + settings.inactive = 600 + +------------------------------- +Inactive Ray Length (Dead Zone) +------------------------------- + +A major issue with random ray is that the starting angular flux distribution for +each sampled ray is unknown. Thus, an on-the-fly method is used to build a high +quality approximation of the angular flux of the ray each iteration. This is +accomplished by running the ray through an inactive length (also known as a dead +zone length), where the ray is moved through the geometry and its angular flux +is solved for via the normal :ref:`MOC ` equation, but +no information is written back to the system. Thus, the ray is run in a "read +only" mode for the set inactive length. This parameter can be adjusted, in units +of cm, as:: + + settings.random_ray['distance_inactive'] = 40.0 + +After several mean free paths are traversed, the angular flux spectrum of the +ray becomes dominated by the in-scattering and fission source components that it +picked up when travelling through the geometry, while its original (incorrect) +starting angular flux is attenuated toward zero. Thus, longer selections of +inactive ray length will asymptotically approach the true angular flux. + +In practice, 10 mean free paths are sufficient (with light water reactors often +requiring only about 10--50 cm of inactive ray length for the error to become +undetectable). However, we caution that certain models with large quantities of +void regions (even if just limited to a few streaming channels) may require +significantly longer inactive ray lengths to ensure that the angular flux is +accurate before the conclusion of the inactive ray length. Additionally, +problems where a sensitive estimate of the uncollided flux is required (e.g., +the detector response to fast neutrons is required, and the detected is located +far away from the source in a moderator region) may require the user to specify +an inactive length that is derived from the pyhsical geometry of the simulation +problem rather than its material properties. For instance, consider a detector +placed 30 cm outside of a reactor core, with a moderator region separating the +detector from the core. In this case, rays sampled in the moderator region and +heading toward the detector will begin life with a highly scattered thermal +spectrum and will have an inaccurate fast spectrum. If the dead zone length is +only 20 cm, we might imagine such rays writing to the detector tally within +their active lengths, despite their innaccurate estimate of the uncollided fast +angular flux. Thus, an inactive length of 100--200 cm would ensure that any such +rays would still be within their inactive regions, and only rays that have +actually traversed through the core (and thus have an accurate representation of +the core's emitted fast flux) will score to the detector region while in their +active phase. + + +------------------------------------ +Active Ray Length and Number of Rays +------------------------------------ + +Once the inactive length of the ray has completed, the active region of the ray +begins. The ray is now run in regular mode, where changes in angular flux as it +traverses through each flat source region are written back to the system, so as +to contribute to the estimate for the iteration scalar flux (which is used to +compute the source for the next iteration). The active ray length can be +adjusted, in units of [cm], as:: + + settings.random_ray['distance_active'] = 400.0 + +Assuming that a sufficient inactive ray length is used so that the starting +angular flux is highly accurate, any selection of active length greater than +zero is theoretically acceptable. However, in order to adequately sample the +full integration domain, a selection of a very short track length would require +a very high number of rays to be selected. Due to the static costs per ray of +computing the starting angular flux in the dead zone, typically very short ray +lengths are undesireable. Thus, to amortize the per-ray cost of the inactive +region of the ray, it is desirable to select a very long inactive ray length. +For example, if the inactive length is set to 20 cm, a 200 cm active ray length +ensures that only about 10% of the overall simulation runtime is spent in the +inactive ray phase integration, making the dead zone a relatively inexpensive +way of estimating the angular flux. + +Thus, to fully amortize the cost of the dead zone integration, one might ask why +not simply run a single ray per iteration with an extremely long active length? +While this is also theoretically possible, this results in two issues. The first +problem is that each ray only represents a single angular sample. As we want to +sample the angular phase space of the simulation with similar fidelity to the +spatial phase space, we naturally want a lot of angles. This means in practice, +we want to balance the need to amortize the cost of the inactive region of the +ray with the need to sample lots of angles. The second problem is that +parallelism in OpenMC is expressed in terms of rays, with each being processed +by an independent MPI rank and/or OpenMP thread, thus we want to ensure each +thread has many rays to process. + +In practical terms, the best strategy is typically to set an active ray length +that is about 10 times that of the inactive ray length. This is often the right +balance between ensuring not too much time is spent in the dead zone, while +still adequately sampling the angular phase space. However, as discussed in the +previous section, some types of simulation may demand that additional thought be +applied to this parameter. For instance, in the same example where we have a +detector region far outside a reactor core, we want to make sure that there is +enough active ray length that rays exiting the core can reach the detector +region. For example, if the detector were to be 30 cm outside of the core, then +we would need to ensure that at least a few hundred cm of active length were +used so as to ensure even rays with indirect angles will be able to reach the +target region. + +The number of rays each iteration can be set by reusing the normal Monte Carlo +particle count selection parameter, as:: + + settings.particles = 2000 + +----------- +Ray Density +----------- + +In the preceding sections, it was argued that for most use cases, the inactive +length for a ray can be determined by taking a multiple of the mean free path +for the limiting energy group. The active ray length could then be set by taking +a multiple of the inactive length. With these parameters set, how many rays per +iteration should be run? + +There are three basic settings that control the density of the stochastic +quadrature being used to integrate the domain each iteration. These three +variables are: + +- The number of rays (in OpenMC settings parlance, "particles") +- The inactive distance per ray +- The active distance per ray + +While the inactive and active ray lengths can usually be chosen by simply +examining the geometry, tallies, and cross section data, one has much more +flexibility in the choice of the number of rays to run. Consider a few +scenarios: + +- If a choice of zero rays is made, then no information is gained by the system + after each batch. +- If a choice of rays close to zero is made, then some information is gained + after each batch, but many source regions may not have been visited that + iteration, which is not ideal numerically and can result in instability. + Empirically, we have found that the simulation can remain stable and produce + accurate results even when on average 20% or more of the cells have zero rays + passing through them each iteration. However, besides the cost of transporting + rays, a new neutron source must be computed based on the scalar flux at each + iteration. This cost is dictated only by the number of source regions and + energy groups---it is independent of the number of rays. Thus, in practical + terms, if too few rays are run, then the simulation runtime becomes dominated + by the fixed cost of source updates, making it inefficient overall given that + a huge number of active batches will likely be required to converge statistics + to acceptable levels. Additionally, if many cells are missed each iteration, + then the fission and scattering sources may not develop very quickly, + resulting in a need for far more inactive batches than might otherwise be + required. +- If a choice of running a very large number of rays is made such that you + guarantee that all cells are hit each iteration, this avoids any issues with + numerical instability. As even more rays are run, this reduces the number of + active batches that must be used to converge statistics and therefore + minimizes the fixed per-iteration source update costs. While this seems + advantageous, it has the same practical downside as with Monte Carlo---namely, + that the inactive batches tend to be overly well integrated, resulting in a + lot of wasted time. This issue is actually much more serious than in Monte + Carlo (where typically only tens of inactive batches are needed), as random + ray often requires hundreds or even thousands of inactive batches. Thus, + minimizing the cost of the source updates in the active phase needs to be + balanced against the increased cost of the inactive phase of the simulation. +- If a choice of rays is made such that relatively few (e.g., around 0.1%) of + cells are missed each iteration, the cost of the inactive batches of the + simulation is minimized. In this "goldilocks" regime, there is very little + chance of numerical instability, and enough information is gained by each cell + to progress the fission and scattering sources forward at their maximum rate. + However, the inactive batches can proceed with minimal cost. While this will + result in the active phase of the simulation requiring more batches (and + correspondingly higher source update costs), the added cost is typically far + less than the savings by making the inactive phase much cheaper. + +To help you set this parameter, OpenMC will report the average flat source +region miss rate at the end of the simulation. Additionally, OpenMC will alert +you if very high miss rates are detected, indicating that more rays and/or a +longer active ray length might improve numerical performance. Thus, a "guess and +check" approach to this parameter is recommended, where a very low guess is +made, a few iterations are performed, and then the simulation is restarted with +a larger value until the "low ray density" messages go away. + +.. note:: + In summary, the user should select an inactive length corresponding to many + times the mean free path of a particle, generally O(10--100) cm, to ensure accuracy of + the starting angular flux. The active length should be 10× the inactive + length to amortize its cost. The number of rays should be enough so that + nearly all :ref:`FSRs ` are hit at least once each power iteration (the hit fraction + is reported by OpenMC for empirical user adjustment). + +.. warning:: + For simulations where long range uncollided flux estimates need to be + accurately resolved (e.g., shielding, detector response, and problems with + significant void areas), make sure that selections for inactive and active + ray lengths are sufficiently long to allow for transport to occur between + source and target regions of interest. + +---------- +Ray Source +---------- + +Random ray requires that the ray source be uniform in space and isotropic in +angle. To facilitate sampling, the user must specify a single random ray source +for sampling rays in both eigenvalue and fixed source solver modes. The random +ray integration source should be of type :class:`openmc.IndependentSource`, and +is specified as part of the :attr:`openmc.Settings.random_ray` dictionary. Note +that the source must not be limited to only fissionable regions. Additionally, +the source box must cover the entire simulation domain. In the case of a +simulation domain that is not box shaped, a box source should still be used to +bound the domain but with the source limited to rejection sampling the actual +simulation universe (which can be specified via the ``domains`` field of the +:class:`openmc.IndependentSource` Python class). Similar to Monte Carlo sources, +for two-dimensional problems (e.g., a 2D pincell) it is desirable to make the +source bounded near the origin of the infinite dimension. An example of an +acceptable ray source for a two-dimensional 2x2 lattice would look like: + +:: + + pitch = 1.26 + lower_left = (-pitch, -pitch, -pitch) + upper_right = ( pitch, pitch, pitch) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) + +.. note:: + The random ray source is not related to the underlying particle flux or + source distribution of the simulation problem. It is akin to the selection + of an integration quadrature. Thus, in fixed source mode, the ray source + still needs to be provided and still needs to be uniform in space and angle + throughout the simulation domain. In fixed source mode, the user will + provide physical particle fixed sources in addition to the random ray + source. + +.. _subdivision_fsr: + +---------------------------------- +Subdivision of Flat Source Regions +---------------------------------- + +While the scattering and fission sources in Monte Carlo +are treated continuously, they are assumed to be invariant (flat) within a +MOC or random ray flat source region (FSR). This introduces bias into the +simulation, which can be remedied by reducing the physical size of the FSR +to dimensions below that of typical mean free paths of particles. + +In OpenMC, this subdivision currently must be done manually. The level of +subdivision needed will be dependent on the fidelity the user requires. For +typical light water reactor analysis, consider the following example subdivision +of a two-dimensional 2x2 reflective pincell lattice: + +.. figure:: ../_images/2x2_materials.jpeg + :class: with-border + :width: 400 + + Material definition for an asymmetrical 2x2 lattice (1.26 cm pitch) + +.. figure:: ../_images/2x2_fsrs.jpeg + :class: with-border + :width: 400 + + FSR decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch) + +In the future, automated subdivision of FSRs via mesh overlay may be supported. + +------- +Tallies +------- + +Most tallies, filters, and scores that you would expect to work with a +multigroup solver like random ray are supported. For example, you can define 3D +mesh tallies with energy filters and flux, fission, and nu-fission scores, etc. +There are some restrictions though. For starters, it is assumed that all filter +mesh boundaries will conform to physical surface boundaries (or lattice +boundaries) in the simulation geometry. It is acceptable for multiple cells +(FSRs) to be contained within a mesh element (e.g., pincell-level or +assembly-level tallies should work), but it is currently left as undefined +behavior if a single simulation cell is contained in multiple mesh elements. + +Supported scores: + - flux + - total + - fission + - nu-fission + - events + +Supported Estimators: + - tracklength + +Supported Filters: + - cell + - cell instance + - distribcell + - energy + - material + - mesh + - universe + +Note that there is no difference between the analog, tracklength, and collision +estimators in random ray mode as individual particles are not being simulated. +Tracklength-style tally estimation is inherent to the random ray method. + +-------- +Plotting +-------- + +Visualization of geometry is handled in the same way as normal with OpenMC (see +:ref:`plotting guide ` for more details). That is, ``openmc +--plot`` is handled without any modifications, as the random ray solver uses the +same geometry definition as in Monte Carlo. + +In addition to OpenMC's standard geometry plotting mode, the random ray solver +also features an additional method of data visualization. If a ``plots.xml`` +file is present, any voxel plots that are defined will be output at the end of a +random ray simulation. Rather than being stored in HDF5 file format, the random +ray plotting will generate ``.vtk`` files that can be directly read and plotted +with `Paraview `_. + +In fixed source Monte Carlo (MC) simulations, by default the only thing global +tally provided is the leakage fraction. In a k-eigenvalue MC simulation, by +default global tallies are collected for the eigenvalue and leakage fraction. +Spatial flux information must be manually requested, and often fine-grained +spatial meshes are considered costly/unnecessary, so it is impractical in MC +mode to plot spatial flux or power info by default. Conversely, in random ray, +the solver functions by estimating the multigroup source and flux spectrums in +every fine-grained FSR each iteration. Thus, for random ray, in both fixed +source and eigenvalue simulations, the simulation always finishes with a well +converged flux estimate for all areas. As such, it is much more common in random +ray, MOC, and other deterministic codes to provide spatial flux information by +default. In the future, all FSR data will be made available in the statepoint +file, which facilitates plotting and manipulation through the Python API; at +present, statepoint support is not available. + +Only voxel plots will be used to generate output; other plot types present in +the ``plots.xml`` file will be ignored. The following fields will be written to +the VTK structured grid file: + + - material + - FSR index + - flux spectrum (for each energy group) + - total fission source (integrated across all energy groups) + +------------------------------------------ +Inputting Multigroup Cross Sections (MGXS) +------------------------------------------ + +Multigroup cross sections for use with OpenMC's random ray solver are input the +same way as with OpenMC's traditional multigroup Monte Carlo mode. There is more +information on generating multigroup cross sections via OpenMC in the +:ref:`multigroup materials ` user guide. You may also wish to +use an existing multigroup library. An example of using OpenMC's Python +interface to generate a correctly formatted ``mgxs.h5`` input file is given +in the `OpenMC Jupyter notebook collection +`_. + +.. note:: + Currently only isotropic and isothermal multigroup cross sections are + supported in random ray mode. To represent multiple material temperatures, + separate materials can be defined each with a separate multigroup dataset + corresponding to a given temperature. + +--------------------------------------- +Putting it All Together: Example Inputs +--------------------------------------- + +An example of a settings definition for random ray is given below:: + + # Geometry and MGXS material definition of 2x2 lattice (not shown) + pitch = 1.26 + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + ... + + # Instantiate a settings object for a random ray solve + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 1200 + settings.inactive = 600 + settings.particles = 2000 + + settings.random_ray['distance_inactive'] = 40.0 + settings.random_ray['distance_active'] = 400.0 + + # Create an initial uniform spatial source distribution for sampling rays + lower_left = (-pitch, -pitch, -pitch) + upper_right = ( pitch, pitch, pitch) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) + + settings.export_to_xml() + + # Define tallies + + # Create a mesh filter + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch/2, -pitch/2) + mesh.upper_right = (pitch/2, pitch/2) + mesh_filter = openmc.MeshFilter(mesh) + + # Create a multigroup energy filter + energy_filter = openmc.EnergyFilter(group_edges) + + # Create tally using our two filters and add scores + tally = openmc.Tally() + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux', 'fission', 'nu-fission'] + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + tallies.export_to_xml() + + # Create voxel plot + plot = openmc.Plot() + plot.origin = [0, 0, 0] + plot.width = [2*pitch, 2*pitch, 1] + plot.pixels = [1000, 1000, 1] + plot.type = 'voxel' + + # Instantiate a Plots collection and export to XML + plots = openmc.Plots([plot]) + plots.export_to_xml() + +All other inputs (e.g., geometry, materials) will be unchanged from a typical +Monte Carlo run (see the :ref:`geometry ` and +:ref:`multigroup materials ` user guides for more information). + +There is also a complete example of a pincell available in the +``openmc/examples/pincell_random_ray`` folder. diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 81fa78991a7..e966423f0e3 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -628,3 +628,37 @@ instance, whereas the :meth:`openmc.Track.filter` method returns a new .. code-block:: sh openmc-track-combine tracks_p*.h5 --out tracks.h5 + +----------------------- +Restarting a Simulation +----------------------- + +OpenMC can be run in a mode where it reads in a statepoint file and continues a +simulation from the ending point of the statepoint file. A restart simulation +can be performed by passing the path to the statepoint file to the OpenMC +executable: + +.. code-block:: sh + + openmc -r statepoint.100.h5 + +From the Python API, the `restart_file` argument provides the same behavior: + +.. code-block:: python + + openmc.run(restart_file='statepoint.100.h5') + +or if using the :class:`~openmc.Model` class: + +.. code-block:: python + + model.run(restart_file='statepoint.100.h5') + +The restart simulation will execute until the number of batches specified in the +:class:`~openmc.Settings` object on a model (or in the :ref:`settings XML file +`) is satisfied. Note that if the number of batches in the +statepoint file is the same as that specified in the settings object (i.e., if +the inputs were not modified before the restart run), no particles will be +transported and OpenMC will exit immediately. + +.. note:: A statepoint file must match the input model to be successfully used in a restart simulation. diff --git a/examples/assembly/assembly.py b/examples/assembly/assembly.py index 1355374f128..a370a361144 100644 --- a/examples/assembly/assembly.py +++ b/examples/assembly/assembly.py @@ -99,11 +99,11 @@ def assembly_model(): assembly.universes[gt_pos[:, 0], gt_pos[:, 1]] = guide_tube_pin() # Create outer boundary of the geometry to surround the lattice - outer_boundary = openmc.model.rectangular_prism( + outer_boundary = openmc.model.RectangularPrism( pitch, pitch, boundary_type='reflective') # Create a cell filled with the lattice - main_cell = openmc.Cell(fill=assembly, region=outer_boundary) + main_cell = openmc.Cell(fill=assembly, region=-outer_boundary) # Finally, create geometry by providing a list of cells that fill the root # universe diff --git a/examples/custom_source/CMakeLists.txt b/examples/custom_source/CMakeLists.txt index 9498176944a..21463ed51ed 100644 --- a/examples/custom_source/CMakeLists.txt +++ b/examples/custom_source/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.3 FATAL_ERROR) +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_sources CXX) add_library(source SHARED source_ring.cpp) find_package(OpenMC REQUIRED) diff --git a/examples/custom_source/build_xml.py b/examples/custom_source/build_xml.py index 6db136216d7..ff6dae2bb65 100644 --- a/examples/custom_source/build_xml.py +++ b/examples/custom_source/build_xml.py @@ -8,8 +8,8 @@ mats.export_to_xml() # Create a 5 cm x 5 cm box filled with iron -box = openmc.model.rectangular_prism(10.0, 10.0, boundary_type='vacuum') -cell = openmc.Cell(fill=iron, region=box) +box = openmc.model.RectangularPrism(10.0, 10.0, boundary_type='vacuum') +cell = openmc.Cell(fill=iron, region=-box) geometry = openmc.Geometry([cell]) geometry.export_to_xml() diff --git a/examples/parameterized_custom_source/CMakeLists.txt b/examples/parameterized_custom_source/CMakeLists.txt index 3024e90cffa..8232f3b546a 100644 --- a/examples/parameterized_custom_source/CMakeLists.txt +++ b/examples/parameterized_custom_source/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.3 FATAL_ERROR) +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_sources CXX) add_library(parameterized_source SHARED parameterized_source_ring.cpp) find_package(OpenMC REQUIRED) diff --git a/examples/parameterized_custom_source/build_xml.py b/examples/parameterized_custom_source/build_xml.py index 23d9930c0f2..5ac6bf92870 100644 --- a/examples/parameterized_custom_source/build_xml.py +++ b/examples/parameterized_custom_source/build_xml.py @@ -8,8 +8,8 @@ mats.export_to_xml() # Create a 5 cm x 5 cm box filled with iron -box = openmc.model.rectangular_prism(10.0, 10.0, boundary_type='vacuum') -cell = openmc.Cell(fill=iron, region=box) +box = openmc.model.RectangularPrism(10.0, 10.0, boundary_type='vacuum') +cell = openmc.Cell(fill=iron, region=-box) geometry = openmc.Geometry([cell]) geometry.export_to_xml() diff --git a/examples/pincell/build_xml.py b/examples/pincell/build_xml.py index cd8923fbea1..d008c882237 100644 --- a/examples/pincell/build_xml.py +++ b/examples/pincell/build_xml.py @@ -43,13 +43,13 @@ # Create a region represented as the inside of a rectangular prism pitch = 1.25984 -box = openmc.rectangular_prism(pitch, pitch, boundary_type='reflective') +box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective') # Create cells, mapping materials to regions fuel = openmc.Cell(fill=uo2, region=-fuel_or) gap = openmc.Cell(fill=helium, region=+fuel_or & -clad_ir) clad = openmc.Cell(fill=zircaloy, region=+clad_ir & -clad_or) -water = openmc.Cell(fill=borated_water, region=+clad_or & box) +water = openmc.Cell(fill=borated_water, region=+clad_or & -box) # Create a geometry and export to XML geometry = openmc.Geometry([fuel, gap, clad, water]) diff --git a/examples/pincell_depletion/run_depletion.py b/examples/pincell_depletion/run_depletion.py index 493e3117837..e1959938f05 100644 --- a/examples/pincell_depletion/run_depletion.py +++ b/examples/pincell_depletion/run_depletion.py @@ -41,13 +41,13 @@ fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') clad_ir = openmc.ZCylinder(r=0.40005, name='Clad IR') clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') -box = openmc.model.rectangular_prism(pitch, pitch, boundary_type='reflective') +box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective') # Define cells fuel = openmc.Cell(fill=uo2, region=-fuel_or) gap = openmc.Cell(fill=helium, region=+fuel_or & -clad_ir) clad = openmc.Cell(fill=zircaloy, region=+clad_ir & -clad_or) -water = openmc.Cell(fill=borated_water, region=+clad_or & box) +water = openmc.Cell(fill=borated_water, region=+clad_or & -box) # Define overall geometry geometry = openmc.Geometry([fuel, gap, clad, water]) diff --git a/examples/pincell_multigroup/build_xml.py b/examples/pincell_multigroup/build_xml.py index 48671698d0a..019ae6a113f 100644 --- a/examples/pincell_multigroup/build_xml.py +++ b/examples/pincell_multigroup/build_xml.py @@ -90,11 +90,11 @@ # Create a region represented as the inside of a rectangular prism pitch = 1.26 -box = openmc.rectangular_prism(pitch, pitch, boundary_type='reflective') +box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective') # Instantiate Cells fuel = openmc.Cell(fill=uo2, region=-fuel_or, name='fuel') -moderator = openmc.Cell(fill=water, region=+fuel_or & box, name='moderator') +moderator = openmc.Cell(fill=water, region=+fuel_or & -box, name='moderator') # Create a geometry with the two cells and export to XML geometry = openmc.Geometry([fuel, moderator]) diff --git a/examples/pincell_random_ray/build_xml.py b/examples/pincell_random_ray/build_xml.py new file mode 100644 index 00000000000..b3dd8020a51 --- /dev/null +++ b/examples/pincell_random_ray/build_xml.py @@ -0,0 +1,203 @@ +import numpy as np +import openmc +import openmc.mgxs + +############################################################################### +# Create multigroup data + +# Instantiate the energy group data +group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] +groups = openmc.mgxs.EnergyGroups(group_edges) + +# Instantiate the 7-group (C5G7) cross section data +uo2_xsdata = openmc.XSdata('UO2', groups) +uo2_xsdata.order = 0 +uo2_xsdata.set_total( + [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) +uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, + 3.0020e-02, 1.1126e-01, 2.8278e-01]) +scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) +scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) +uo2_xsdata.set_scatter_matrix(scatter_matrix) +uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, + 1.85648e-02, 1.78084e-02, 8.30348e-02, + 2.16004e-01]) +uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) +uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + +h2o_xsdata = openmc.XSdata('LWTR', groups) +h2o_xsdata.order = 0 +h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, + 0.718, 1.2544497, 2.650379]) +h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, + 1.9406e-03, 5.7416e-03, 1.5001e-02, + 3.7239e-02]) +scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) +scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) +h2o_xsdata.set_scatter_matrix(scatter_matrix) + +mg_cross_sections_file = openmc.MGXSLibrary(groups) +mg_cross_sections_file.add_xsdatas([uo2_xsdata, h2o_xsdata]) +mg_cross_sections_file.export_to_hdf5() + +############################################################################### +# Create materials for the problem + +# Instantiate some Materials and register the appropriate macroscopic data +uo2 = openmc.Material(name='UO2 fuel') +uo2.set_density('macro', 1.0) +uo2.add_macroscopic('UO2') + +water = openmc.Material(name='Water') +water.set_density('macro', 1.0) +water.add_macroscopic('LWTR') + +# Instantiate a Materials collection and export to XML +materials_file = openmc.Materials([uo2, water]) +materials_file.cross_sections = "mgxs.h5" +materials_file.export_to_xml() + +############################################################################### +# Define problem geometry + +# The geometry we will define a simplified pincell with fuel radius 0.54 cm +# surrounded by moderator (same as in the multigroup example). +# In random ray, we typically want several radial regions and azimuthal +# sectors in both the fuel and moderator areas of the pincell. This is +# due to the flat source approximation requiring that source regions are +# small compared to the typical mean free path of a neutron. Below we +# sudivide the basic pincell into 8 aziumthal sectors (pizza slices) and +# 5 concentric rings in both the fuel and moderator. + +# TODO: When available in OpenMC, use cylindrical lattice instead to +# simplify definition and improve runtime performance. + +pincell_base = openmc.Universe() + +# These are the subdivided radii (creating 5 concentric regions in the +# fuel and moderator) +ring_radii = [0.241, 0.341, 0.418, 0.482, 0.54, 0.572, 0.612, 0.694, 0.786] +fills = [uo2, uo2, uo2, uo2, uo2, water, water, water, water, water] + +# We then create cells representing the bounded rings, with special +# treatment for both the innermost and outermost cells +cells = [] +for r in range(10): + cell = [] + if r == 0: + outer_bound = openmc.ZCylinder(r=ring_radii[r]) + cell = openmc.Cell(fill=fills[r], region=-outer_bound) + elif r == 9: + inner_bound = openmc.ZCylinder(r=ring_radii[r-1]) + cell = openmc.Cell(fill=fills[r], region=+inner_bound) + else: + inner_bound = openmc.ZCylinder(r=ring_radii[r-1]) + outer_bound = openmc.ZCylinder(r=ring_radii[r]) + cell = openmc.Cell(fill=fills[r], region=+inner_bound & -outer_bound) + pincell_base.add_cell(cell) + +# We then generate 8 planes to bound 8 azimuthal sectors +azimuthal_planes = [] +for i in range(8): + angle = 2 * i * openmc.pi / 8 + normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) + azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) + +# Create a cell for each azimuthal sector using the pincell base class +azimuthal_cells = [] +for i in range(8): + azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') + azimuthal_cell.fill = pincell_base + azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cells.append(azimuthal_cell) + +# Create the (subdivided) geometry with the azimuthal universes +pincell = openmc.Universe(cells=azimuthal_cells) + +# Create a region represented as the inside of a rectangular prism +pitch = 1.26 +box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective') +pincell_bounded = openmc.Cell(fill=pincell, region=-box, name='pincell') + +# Create a geometry (specifying merge surfaces option to remove +# all the redundant cylinder/plane surfaces) and export to XML +geometry = openmc.Geometry([pincell_bounded], merge_surfaces=True) +geometry.export_to_xml() + +############################################################################### +# Define problem settings + +# Instantiate a Settings object, set all runtime parameters, and export to XML +settings = openmc.Settings() +settings.energy_mode = "multi-group" +settings.batches = 600 +settings.inactive = 300 +settings.particles = 50 + +# Create an initial uniform spatial source distribution for sampling rays. +# Note that this must be uniform in space and angle. +lower_left = (-pitch/2, -pitch/2, -1) +upper_right = (pitch/2, pitch/2, 1) +uniform_dist = openmc.stats.Box(lower_left, upper_right) +settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) +settings.random_ray['distance_inactive'] = 40.0 +settings.random_ray['distance_active'] = 400.0 + +settings.export_to_xml() + +############################################################################### +# Define tallies + +# Create a mesh that will be used for tallying +mesh = openmc.RegularMesh() +mesh.dimension = (2, 2) +mesh.lower_left = (-pitch/2, -pitch/2) +mesh.upper_right = (pitch/2, pitch/2) + +# Create a mesh filter that can be used in a tally +mesh_filter = openmc.MeshFilter(mesh) + +# Let's also create a filter to measure each group +# indepdendently +energy_filter = openmc.EnergyFilter(group_edges) + +# Now use the mesh filter in a tally and indicate what scores are desired +tally = openmc.Tally(name="Mesh and Energy tally") +tally.filters = [mesh_filter, energy_filter] +tally.scores = ['flux', 'fission', 'nu-fission'] + +# Instantiate a Tallies collection and export to XML +tallies = openmc.Tallies([tally]) +tallies.export_to_xml() + +############################################################################### +# Exporting to OpenMC plots.xml file +############################################################################### + +plot = openmc.Plot() +plot.origin = [0, 0, 0] +plot.width = [pitch, pitch, pitch] +plot.pixels = [1000, 1000, 1] +plot.type = 'voxel' + +# Instantiate a Plots collection and export to XML +plots = openmc.Plots([plot]) +plots.export_to_xml() diff --git a/include/openmc/boundary_condition.h b/include/openmc/boundary_condition.h index 0f16fa3b31c..cd8988162f2 100644 --- a/include/openmc/boundary_condition.h +++ b/include/openmc/boundary_condition.h @@ -1,12 +1,16 @@ #ifndef OPENMC_BOUNDARY_CONDITION_H #define OPENMC_BOUNDARY_CONDITION_H +#include "openmc/hdf5_interface.h" +#include "openmc/particle.h" #include "openmc/position.h" +#include namespace openmc { // Forward declare some types used in function arguments. class Particle; +class RandomRay; class Surface; //============================================================================== @@ -15,6 +19,8 @@ class Surface; class BoundaryCondition { public: + virtual ~BoundaryCondition() = default; + //! Perform tracking operations for a particle that strikes the boundary. //! \param p The particle that struck the boundary. This class is not meant //! to directly modify anything about the particle, but it will do so @@ -22,8 +28,44 @@ class BoundaryCondition { //! \param surf The specific surface on the boundary the particle struck. virtual void handle_particle(Particle& p, const Surface& surf) const = 0; + //! Modify the incident particle's weight according to the boundary's albedo. + //! \param p The particle that struck the boundary. This function calculates + //! the reduction in the incident particle's weight as it interacts + //! with a boundary. The lost weight is tallied before the remaining weight + //! is reassigned to the incident particle. Implementations of the + //! handle_particle function typically call this method in its body. + //! \param surf The specific surface on the boundary the particle struck. + void handle_albedo(Particle& p, const Surface& surf) const + { + if (!has_albedo()) + return; + double initial_wgt = p.wgt(); + // Treat the lost weight fraction as leakage, similar to VacuumBC. + // This ensures the lost weight is tallied properly. + p.wgt() *= (1.0 - albedo_); + p.cross_vacuum_bc(surf); + p.wgt() = initial_wgt * albedo_; + }; + //! Return a string classification of this BC. virtual std::string type() const = 0; + + //! Write albedo data of this BC to hdf5. + void to_hdf5(hid_t surf_group) const + { + if (has_albedo()) { + write_string(surf_group, "albedo", fmt::format("{}", albedo_), false); + } + }; + + //! Set albedo of this BC. + void set_albedo(double albedo) { albedo_ = albedo; } + + //! Return if this BC has an albedo. + bool has_albedo() const { return (albedo_ > 0.0); } + +private: + double albedo_ = -1.0; }; //============================================================================== diff --git a/include/openmc/capi.h b/include/openmc/capi.h index a444814f585..9401156a64f 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -93,6 +93,8 @@ int openmc_material_set_id(int32_t index, int32_t id); int openmc_material_get_name(int32_t index, const char** name); int openmc_material_set_name(int32_t index, const char* name); int openmc_material_set_volume(int32_t index, double volume); +int openmc_material_get_depletable(int32_t index, bool* depletable); +int openmc_material_set_depletable(int32_t index, bool depletable); int openmc_material_filter_get_bins( int32_t index, const int32_t** bins, size_t* n); int openmc_material_filter_set_bins( @@ -103,6 +105,10 @@ int openmc_mesh_filter_get_translation(int32_t index, double translation[3]); int openmc_mesh_filter_set_translation(int32_t index, double translation[3]); int openmc_mesh_get_id(int32_t index, int32_t* id); int openmc_mesh_set_id(int32_t index, int32_t id); +int openmc_mesh_get_n_elements(int32_t index, size_t* n); +int openmc_mesh_get_volumes(int32_t index, double* volumes); +int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin, + int result_size, void* result, int* hits, uint64_t* seed); int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh); int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh); int openmc_new_filter(const char* type, int32_t* index); @@ -144,6 +150,7 @@ int openmc_sphharm_filter_get_cosine(int32_t index, char cosine[]); int openmc_sphharm_filter_set_order(int32_t index, int order); int openmc_sphharm_filter_set_cosine(int32_t index, const char cosine[]); int openmc_statepoint_write(const char* filename, bool* write_source); +int openmc_statepoint_load(const char* filename); int openmc_tally_allocate(int32_t index, const char* type); int openmc_tally_get_active(int32_t index, bool* active); int openmc_tally_get_estimator(int32_t index, int* estimator); diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 39acaf06740..e68443a32fd 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -40,6 +40,7 @@ constexpr int32_t OP_UNION {std::numeric_limits::max() - 4}; //============================================================================== class Cell; +class GeometryState; class ParentCell; class CellInstance; class Universe; @@ -82,7 +83,7 @@ class Region { //! Find the oncoming boundary of this cell. std::pair distance( - Position r, Direction u, int32_t on_surface, Particle* p) const; + Position r, Direction u, int32_t on_surface) const; //! Get the BoundingBox for this cell. BoundingBox bounding_box(int32_t cell_id) const; @@ -183,7 +184,7 @@ class Cell { //! Find the oncoming boundary of this cell. virtual std::pair distance( - Position r, Direction u, int32_t on_surface, Particle* p) const = 0; + Position r, Direction u, int32_t on_surface, GeometryState* p) const = 0; //! Write all information needed to reconstruct the cell to an HDF5 group. //! \param group_id An HDF5 group id. @@ -248,6 +249,42 @@ class Cell { std::unordered_map> get_contained_cells( int32_t instance = 0, Position* hint = nullptr) const; + //! Determine the material index corresponding to a specific cell instance, + //! taking into account presence of distribcell material + //! \param[in] instance of the cell + //! \return material index + int32_t material(int32_t instance) const + { + // If distributed materials are used, then each instance has its own + // material definition. If distributed materials are not used, then + // all instances used the same material stored at material_[0]. The + // presence of distributed materials is inferred from the size of + // the material_ vector being greater than one. + if (material_.size() > 1) { + return material_[instance]; + } else { + return material_[0]; + } + } + + //! Determine the temperature index corresponding to a specific cell instance, + //! taking into account presence of distribcell temperature + //! \param[in] instance of the cell + //! \return temperature index + double sqrtkT(int32_t instance) const + { + // If distributed materials are used, then each instance has its own + // temperature definition. If distributed materials are not used, then + // all instances used the same temperature stored at sqrtkT_[0]. The + // presence of distributed materials is inferred from the size of + // the sqrtkT_ vector being greater than one. + if (sqrtkT_.size() > 1) { + return sqrtkT_[instance]; + } else { + return sqrtkT_[0]; + } + } + protected: //! Determine the path to this cell instance in the geometry hierarchy //! \param[in] instance of the cell to find parent cells for @@ -260,7 +297,8 @@ class Cell { //! \param[in] instance of the cell to find parent cells for //! \param[in] p particle used to do a fast search for parent cells //! \return parent cells - vector find_parent_cells(int32_t instance, Particle& p) const; + vector find_parent_cells( + int32_t instance, GeometryState& p) const; //! Determine the path to this cell instance in the geometry hierarchy //! \param[in] instance of the cell to find parent cells for @@ -332,10 +370,10 @@ class CSGCell : public Cell { // Methods vector surfaces() const override { return region_.surfaces(); } - std::pair distance( - Position r, Direction u, int32_t on_surface, Particle* p) const override + std::pair distance(Position r, Direction u, + int32_t on_surface, GeometryState* p) const override { - return region_.distance(r, u, on_surface, p); + return region_.distance(r, u, on_surface); } bool contains(Position r, Direction u, int32_t on_surface) const override diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 2b9eaca2989..1c683e5f50c 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -317,7 +317,6 @@ enum class GlobalTally { K_COLLISION, K_ABSORPTION, K_TRACKLENGTH, LEAKAGE }; // Miscellaneous constexpr int C_NONE {-1}; -constexpr int F90_NONE {0}; // TODO: replace usage of this with C_NONE // Interpolation rules enum class Interpolation { @@ -341,6 +340,8 @@ enum class RunMode { VOLUME }; +enum class SolverType { MONTE_CARLO, RANDOM_RAY }; + //============================================================================== // Geometry Constants diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 5ed426e5afb..2facf4fc05e 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -3,7 +3,8 @@ namespace openmc { extern "C" const bool DAGMC_ENABLED; -} +extern "C" const bool UWUW_ENABLED; +} // namespace openmc // always include the XML interface header #include "openmc/xml_interface.h" @@ -42,7 +43,7 @@ class DAGSurface : public Surface { double evaluate(Position r) const override; double distance(Position r, Direction u, bool coincident) const override; Direction normal(Position r) const override; - Direction reflect(Position r, Direction u, Particle* p) const override; + Direction reflect(Position r, Direction u, GeometryState* p) const override; inline void to_hdf5_inner(hid_t group_id) const override {}; @@ -63,8 +64,8 @@ class DAGCell : public Cell { bool contains(Position r, Direction u, int32_t on_surface) const override; - std::pair distance( - Position r, Direction u, int32_t on_surface, Particle* p) const override; + std::pair distance(Position r, Direction u, + int32_t on_surface, GeometryState* p) const override; BoundingBox bounding_box() const override; @@ -120,6 +121,12 @@ class DAGUniverse : public Universe { void write_uwuw_materials_xml( const std::string& outfile = "uwuw_materials.xml") const; + //! Assign a material to a cell from uwuw material library + //! \param[in] vol_handle The DAGMC material assignment string + //! \param[in] c The OpenMC cell to which the material is assigned + void uwuw_assign_material( + moab::EntityHandle vol_handle, std::unique_ptr& c) const; + //! Assign a material to a cell based //! \param[in] mat_string The DAGMC material assignment string //! \param[in] c The OpenMC cell to which the material is assigned @@ -143,7 +150,7 @@ class DAGUniverse : public Universe { //! string of the ID ranges for entities of dimension \p dim std::string dagmc_ids_for_dim(int dim) const; - bool find_cell(Particle& p) const override; + bool find_cell(GeometryState& p) const override; void to_hdf5(hid_t universes_group) const override; diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index 65c68ad6457..e70c803de2f 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -7,6 +7,7 @@ #include // for size_t #include "pugixml.hpp" +#include #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr @@ -43,9 +44,9 @@ class DiscreteIndex { public: DiscreteIndex() {}; DiscreteIndex(pugi::xml_node node); - DiscreteIndex(const double* p, int n); + DiscreteIndex(gsl::span p); - void assign(const double* p, int n); + void assign(gsl::span p); //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer @@ -77,7 +78,7 @@ class DiscreteIndex { class Discrete : public Distribution { public: explicit Discrete(pugi::xml_node node); - Discrete(const double* x, const double* p, int n); + Discrete(const double* x, const double* p, size_t n); //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer diff --git a/include/openmc/distribution_multi.h b/include/openmc/distribution_multi.h index e171d58ab61..9e84d03d57f 100644 --- a/include/openmc/distribution_multi.h +++ b/include/openmc/distribution_multi.h @@ -22,6 +22,8 @@ class UnitSphereDistribution { explicit UnitSphereDistribution(pugi::xml_node node); virtual ~UnitSphereDistribution() = default; + static unique_ptr create(pugi::xml_node node); + //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer //! \return Direction sampled diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 9fba9506025..bd10a299605 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -19,6 +19,8 @@ class SpatialDistribution { //! Sample a position from the distribution virtual Position sample(uint64_t* seed) const = 0; + + static unique_ptr create(pugi::xml_node node); }; //============================================================================== @@ -102,16 +104,27 @@ class SphericalIndependent : public SpatialDistribution { class MeshSpatial : public SpatialDistribution { public: explicit MeshSpatial(pugi::xml_node node); + explicit MeshSpatial(int32_t mesh_id, gsl::span strengths); //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer //! \return Sampled position Position sample(uint64_t* seed) const override; - const Mesh* mesh() const { return model::meshes.at(mesh_idx_).get(); } + //! Sample the mesh for an element and position within that element + //! \param seed Pseudorandom number seed pointer + //! \return Sampled element index and position within that element + std::pair sample_mesh(uint64_t* seed) const; + //! For unstructured meshes, ensure that elements are all linear tetrahedra + void check_element_types() const; + + // Accessors + const Mesh* mesh() const { return model::meshes.at(mesh_idx_).get(); } int32_t n_sources() const { return this->mesh()->n_bins(); } + double total_strength() { return this->elem_idx_dist_.integral(); } + private: int32_t mesh_idx_ {C_NONE}; DiscreteIndex elem_idx_dist_; //!< Distribution of diff --git a/include/openmc/file_utils.h b/include/openmc/file_utils.h index 11b20b4aabc..6e9812d5e83 100644 --- a/include/openmc/file_utils.h +++ b/include/openmc/file_utils.h @@ -16,6 +16,11 @@ bool dir_exists(const std::string& path); //! \return Whether file exists bool file_exists(const std::string& filename); +//! Determine directory containing given file +//! \param[in] filename Path to file +//! \return Name of directory containing file +std::string dir_name(const std::string& filename); + // Gets the file extension of whatever string is passed in. This is defined as // a sequence of strictly alphanumeric characters which follow the last period, // i.e. at least one alphabet character is present, and zero or more numbers. diff --git a/include/openmc/geometry.h b/include/openmc/geometry.h index 001e58c4cc3..107cc7d1f3e 100644 --- a/include/openmc/geometry.h +++ b/include/openmc/geometry.h @@ -11,7 +11,7 @@ namespace openmc { class BoundaryInfo; -class Particle; +class GeometryState; //============================================================================== // Global variables @@ -39,7 +39,7 @@ inline bool coincident(double d1, double d2) //! Check for overlapping cells at a particle's position. //============================================================================== -bool check_cell_overlap(Particle& p, bool error = true); +bool check_cell_overlap(GeometryState& p, bool error = true); //============================================================================== //! Get the cell instance for a particle at the specified universe level @@ -50,7 +50,7 @@ bool check_cell_overlap(Particle& p, bool error = true); //! should be computed. \return The instance of the cell at the specified level. //============================================================================== -int cell_instance_at_level(const Particle& p, int level); +int cell_instance_at_level(const GeometryState& p, int level); //============================================================================== //! Locate a particle in the geometry tree and set its geometry data fields. @@ -60,20 +60,22 @@ int cell_instance_at_level(const Particle& p, int level); //! \return True if the particle's location could be found and ascribed to a //! valid geometry coordinate stack. //============================================================================== -bool exhaustive_find_cell(Particle& p); -bool neighbor_list_find_cell(Particle& p); // Only usable on surface crossings +bool exhaustive_find_cell(GeometryState& p, bool verbose = false); +bool neighbor_list_find_cell( + GeometryState& p, bool verbose = false); // Only usable on surface crossings //============================================================================== //! Move a particle into a new lattice tile. //============================================================================== -void cross_lattice(Particle& p, const BoundaryInfo& boundary); +void cross_lattice( + GeometryState& p, const BoundaryInfo& boundary, bool verbose = false); //============================================================================== //! Find the next boundary a particle will intersect. //============================================================================== -BoundaryInfo distance_to_boundary(Particle& p); +BoundaryInfo distance_to_boundary(GeometryState& p); } // namespace openmc diff --git a/include/openmc/interpolate.h b/include/openmc/interpolate.h index 05353f1306b..db501f71b66 100644 --- a/include/openmc/interpolate.h +++ b/include/openmc/interpolate.h @@ -4,6 +4,9 @@ #include #include +#include + +#include "openmc/error.h" #include "openmc/search.h" namespace openmc { @@ -47,14 +50,15 @@ inline double interpolate_lagrangian(gsl::span xs, numerator *= (x - xs[idx + j]); denominator *= (xs[idx + i] - xs[idx + j]); } - output += (numerator / denominator) * ys[i]; + output += (numerator / denominator) * ys[idx + i]; } return output; } -double interpolate(gsl::span xs, gsl::span ys, - double x, Interpolation i = Interpolation::lin_lin) +inline double interpolate(gsl::span xs, + gsl::span ys, double x, + Interpolation i = Interpolation::lin_lin) { int idx = lower_bound_index(xs.begin(), xs.end(), x); @@ -92,4 +96,4 @@ double interpolate(gsl::span xs, gsl::span ys, } // namespace openmc -#endif \ No newline at end of file +#endif diff --git a/include/openmc/material.h b/include/openmc/material.h index deaee2469be..9235be356f6 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -94,7 +94,7 @@ class Material { // which will get auto-assigned to the next available ID. After creating // the new material, it is added to openmc::model::materials. //! \return reference to the cloned material - Material & clone(); + Material& clone(); //---------------------------------------------------------------------------- // Accessors @@ -149,6 +149,7 @@ class Material { //! Get whether material is fissionable //! \return Whether material is fissionable bool fissionable() const { return fissionable_; } + bool& fissionable() { return fissionable_; } //! Get volume of material //! \return Volume in [cm^3] @@ -158,6 +159,10 @@ class Material { //! \return Temperature in [K] double temperature() const; + //! Whether or not the material is depletable + bool depletable() const { return depletable_; } + bool& depletable() { return depletable_; } + //! Get pointer to NCrystal material object //! \return Pointer to NCrystal material object const NCrystalMat& ncrystal_mat() const { return ncrystal_mat_; }; @@ -173,11 +178,8 @@ class Material { double density_; //!< Total atom density in [atom/b-cm] double density_gpcc_; //!< Total atom density in [g/cm^3] double volume_ {-1.0}; //!< Volume in [cm^3] - bool fissionable_ { - false}; //!< Does this material contain fissionable nuclides - bool depletable_ {false}; //!< Is the material depletable? - vector p0_; //!< Indicate which nuclides are to be treated with - //!< iso-in-lab scattering + vector p0_; //!< Indicate which nuclides are to be treated with + //!< iso-in-lab scattering // To improve performance of tallying, we store an array (direct address // table) that indicates for each nuclide in data::nuclides the index of the @@ -210,6 +212,9 @@ class Material { // Private data members gsl::index index_; + bool depletable_ {false}; //!< Is the material depletable? + bool fissionable_ { + false}; //!< Does this material contain fissionable nuclides //! \brief Default temperature for cells containing this material. //! //! A negative value indicates no default temperature was specified. diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 47f54c023fc..3917e6368a4 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -9,7 +9,9 @@ #include "hdf5.h" #include "pugixml.hpp" #include "xtensor/xtensor.hpp" +#include +#include "openmc/error.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" #include "openmc/position.h" @@ -68,12 +70,20 @@ extern const libMesh::Parallel::Communicator* libmesh_comm; class Mesh { public: + // Types, aliases + struct MaterialVolume { + int32_t material; //!< material index + double volume; //!< volume in [cm^3] + }; + // Constructors and destructor Mesh() = default; Mesh(pugi::xml_node node); virtual ~Mesh() = default; // Methods + //! Perform any preparation needed to support use in mesh filters + virtual void prepare_for_tallies() {}; //! Update a position to the local coordinates of the mesh virtual void local_coords(Position& r) const {}; @@ -81,12 +91,12 @@ class Mesh { //! Return a position in the local coordinates of the mesh virtual Position local_coords(const Position& r) const { return r; }; - //! Sample a mesh volume using a certain seed + //! Sample a position within a mesh element // - //! \param[in] seed Seed to use for random sampling - //! \param[in] bin Bin value of the tet sampled - //! \return sampled position within tet - virtual Position sample(uint64_t* seed, int32_t bin) const = 0; + //! \param[in] bin Bin value of the mesh element sampled + //! \param[inout] seed Seed to use for random sampling + //! \return sampled position within mesh element + virtual Position sample_element(int32_t bin, uint64_t* seed) const = 0; //! Determine which bins were crossed by a particle // @@ -156,9 +166,28 @@ class Mesh { virtual std::string get_mesh_type() const = 0; + //! Determine volume of materials within a single mesh elemenet + // + //! \param[in] n_sample Number of samples within each element + //! \param[in] bin Index of mesh element + //! \param[out] Array of (material index, volume) for desired element + //! \param[inout] seed Pseudorandom number seed + //! \return Number of materials within element + int material_volumes(int n_sample, int bin, gsl::span volumes, + uint64_t* seed) const; + + //! Determine volume of materials within a single mesh elemenet + // + //! \param[in] n_sample Number of samples within each element + //! \param[in] bin Index of mesh element + //! \param[inout] seed Pseudorandom number seed + //! \return Vector of (material index, volume) for desired element + vector material_volumes( + int n_sample, int bin, uint64_t* seed) const; + // Data members - int id_ {-1}; //!< User-specified ID - int n_dimension_; //!< Number of dimensions + int id_ {-1}; //!< User-specified ID + int n_dimension_ {-1}; //!< Number of dimensions }; class StructuredMesh : public Mesh { @@ -183,7 +212,12 @@ class StructuredMesh : public Mesh { } }; - Position sample(uint64_t* seed, int32_t bin) const override; + Position sample_element(int32_t bin, uint64_t* seed) const override + { + return sample_element(get_indices_from_bin(bin), seed); + }; + + virtual Position sample_element(const MeshIndex& ijk, uint64_t* seed) const; int get_bin(Position r) const override; @@ -240,6 +274,30 @@ class StructuredMesh : public Mesh { //! \param[in] i Direction index virtual int get_index_in_direction(double r, int i) const = 0; + //! Get the coordinate for the mesh grid boundary in the positive direction + //! + //! \param[in] ijk Array of mesh indices + //! \param[in] i Direction index + virtual double positive_grid_boundary(const MeshIndex& ijk, int i) const + { + auto msg = + fmt::format("Attempting to call positive_grid_boundary on a {} mesh.", + get_mesh_type()); + fatal_error(msg); + }; + + //! Get the coordinate for the mesh grid boundary in the negative direction + //! + //! \param[in] ijk Array of mesh indices + //! \param[in] i Direction index + virtual double negative_grid_boundary(const MeshIndex& ijk, int i) const + { + auto msg = + fmt::format("Attempting to call negative_grid_boundary on a {} mesh.", + get_mesh_type()); + fatal_error(msg); + }; + //! Get the closest distance from the coordinate r to the grid surface //! in i direction that bounds mesh cell ijk and that is larger than l //! The coordinate r does not have to be inside the mesh cell ijk. In @@ -322,18 +380,17 @@ class RegularMesh : public StructuredMesh { void to_hdf5(hid_t group) const override; - // New methods //! Get the coordinate for the mesh grid boundary in the positive direction //! //! \param[in] ijk Array of mesh indices //! \param[in] i Direction index - double positive_grid_boundary(const MeshIndex& ijk, int i) const; + double positive_grid_boundary(const MeshIndex& ijk, int i) const override; //! Get the coordinate for the mesh grid boundary in the negative direction //! //! \param[in] ijk Array of mesh indices //! \param[in] i Direction index - double negative_grid_boundary(const MeshIndex& ijk, int i) const; + double negative_grid_boundary(const MeshIndex& ijk, int i) const override; //! Count number of bank sites in each mesh bin / energy bin // @@ -373,18 +430,17 @@ class RectilinearMesh : public StructuredMesh { void to_hdf5(hid_t group) const override; - // New methods //! Get the coordinate for the mesh grid boundary in the positive direction //! //! \param[in] ijk Array of mesh indices //! \param[in] i Direction index - double positive_grid_boundary(const MeshIndex& ijk, int i) const; + double positive_grid_boundary(const MeshIndex& ijk, int i) const override; //! Get the coordinate for the mesh grid boundary in the negative direction //! //! \param[in] ijk Array of mesh indices //! \param[in] i Direction index - double negative_grid_boundary(const MeshIndex& ijk, int i) const; + double negative_grid_boundary(const MeshIndex& ijk, int i) const override; //! Return the volume for a given mesh index double volume(const MeshIndex& ijk) const override; @@ -410,6 +466,8 @@ class CylindricalMesh : public PeriodicStructuredMesh { static const std::string mesh_type; + Position sample_element(const MeshIndex& ijk, uint64_t* seed) const override; + MeshDistance distance_to_grid_boundary(const MeshIndex& ijk, int i, const Position& r0, const Direction& u, double l) const override; @@ -420,10 +478,16 @@ class CylindricalMesh : public PeriodicStructuredMesh { double volume(const MeshIndex& ijk) const override; - array, 3> grid_; + // grid accessors + double r(int i) const { return grid_[0][i]; } + double phi(int i) const { return grid_[1][i]; } + double z(int i) const { return grid_[2][i]; } int set_grid(); + // Data members + array, 3> grid_; + private: double find_r_crossing( const Position& r, const Direction& u, double l, int shell) const; @@ -466,6 +530,8 @@ class SphericalMesh : public PeriodicStructuredMesh { static const std::string mesh_type; + Position sample_element(const MeshIndex& ijk, uint64_t* seed) const override; + MeshDistance distance_to_grid_boundary(const MeshIndex& ijk, int i, const Position& r0, const Direction& u, double l) const override; @@ -474,10 +540,15 @@ class SphericalMesh : public PeriodicStructuredMesh { void to_hdf5(hid_t group) const override; - array, 3> grid_; + double r(int i) const { return grid_[0][i]; } + double theta(int i) const { return grid_[1][i]; } + double phi(int i) const { return grid_[2][i]; } int set_grid(); + // Data members + array, 3> grid_; + private: double find_r_crossing( const Position& r, const Direction& u, double l, int shell) const; @@ -588,11 +659,6 @@ class UnstructuredMesh : public Mesh { //! Set the length multiplier to apply to each point in the mesh void set_length_multiplier(const double length_multiplier); - // Data members - double length_multiplier_ { - 1.0}; //!< Constant multiplication factor to apply to mesh coordinates - bool specified_length_multiplier_ {false}; - //! Sample barycentric coordinates given a seed and the vertex positions and //! return the sampled position // @@ -601,6 +667,11 @@ class UnstructuredMesh : public Mesh { //! \return Sampled position within the tetrahedron Position sample_tet(std::array coords, uint64_t* seed) const; + // Data members + double length_multiplier_ { + -1.0}; //!< Multiplicative factor applied to mesh coordinates + std::string options_; //!< Options for search data structures + private: //! Setup method for the mesh. Builds data structures, //! sets up element mapping, creates bounding boxes, etc. @@ -621,7 +692,10 @@ class MOABMesh : public UnstructuredMesh { // Overridden Methods - Position sample(uint64_t* seed, int32_t bin) const override; + //! Perform any preparation needed to support use in mesh filters + void prepare_for_tallies() override; + + Position sample_element(int32_t bin, uint64_t* seed) const override; void bins_crossed(Position r0, Position r1, const Direction& u, vector& bins, vector& lengths) const override; @@ -789,7 +863,7 @@ class LibMesh : public UnstructuredMesh { void bins_crossed(Position r0, Position r1, const Direction& u, vector& bins, vector& lengths) const override; - Position sample(uint64_t* seed, int32_t bin) const override; + Position sample_element(int32_t bin, uint64_t* seed) const override; int get_bin(Position r) const override; diff --git a/include/openmc/mgxs.h b/include/openmc/mgxs.h index d77c34bd5f5..3fb0608104f 100644 --- a/include/openmc/mgxs.h +++ b/include/openmc/mgxs.h @@ -29,7 +29,6 @@ class Mgxs { int num_delayed_groups; // number of delayed neutron groups vector xs; // Cross section data // MGXS Incoming Flux Angular grid information - bool is_isotropic; // used to skip search for angle indices if isotropic int n_pol; int n_azi; vector polar; @@ -85,6 +84,8 @@ class Mgxs { std::string name; // name of dataset, e.g., UO2 double awr; // atomic weight ratio bool fissionable; // Is this fissionable + bool is_isotropic { + true}; // used to skip search for angle indices if isotropic Mgxs() = default; diff --git a/include/openmc/openmp_interface.h b/include/openmc/openmp_interface.h index dac03ac5916..fe517415baa 100644 --- a/include/openmc/openmp_interface.h +++ b/include/openmc/openmp_interface.h @@ -7,6 +7,27 @@ namespace openmc { +//============================================================================== +//! Accessor functions related to number of threads and thread number +//============================================================================== +inline int num_threads() +{ +#ifdef _OPENMP + return omp_get_max_threads(); +#else + return 1; +#endif +} + +inline int thread_num() +{ +#ifdef _OPENMP + return omp_get_thread_num(); +#else + return 0; +#endif +} + //============================================================================== //! An object used to prevent concurrent access to a piece of data. // @@ -29,11 +50,26 @@ class OpenMPMutex { #endif } - // Mutexes cannot be copied. We need to explicitly delete the copy - // constructor and copy assignment operator to ensure the compiler doesn't - // "help" us by implicitly trying to copy the underlying mutexes. - OpenMPMutex(const OpenMPMutex&) = delete; - OpenMPMutex& operator=(const OpenMPMutex&) = delete; + // omp_lock_t objects cannot be deep copied, they can only be shallow + // copied. Thus, while shallow copying of an omp_lock_t object is + // completely valid (provided no race conditions exist), true copying + // of an OpenMPMutex object is not valid due to the action of the + // destructor. However, since locks are fungible, we can simply replace + // copying operations with default construction. This allows storage of + // OpenMPMutex objects within containers that may need to move/copy them + // (e.g., std::vector). It is left to the caller to understand that + // copying of OpenMPMutex does not produce two handles to the same mutex, + // rather, it produces two different mutexes. + + // Copy constructor + OpenMPMutex(const OpenMPMutex& other) { OpenMPMutex(); } + + // Copy assignment operator + OpenMPMutex& operator=(const OpenMPMutex& other) + { + OpenMPMutex(); + return *this; + } //! Lock the mutex. // diff --git a/include/openmc/output.h b/include/openmc/output.h index 1ef95a88792..1ece3de960c 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -57,6 +57,8 @@ void print_results(); void write_tallies(); +void show_time(const char* label, double secs, int indent_level = 0); + } // namespace openmc #endif // OPENMC_OUTPUT_H @@ -80,4 +82,4 @@ struct formatter> { } }; -} // namespace fmt \ No newline at end of file +} // namespace fmt diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 80a561f4d05..e423880f87c 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -5,7 +5,6 @@ //! \brief Particle type #include -#include #include #include "openmc/constants.h" @@ -103,17 +102,8 @@ class Particle : public ParticleData { //! mark a particle as lost and create a particle restart file //! \param message A warning message to display - void mark_as_lost(const char* message); - - void mark_as_lost(const std::string& message) - { - mark_as_lost(message.c_str()); - } - - void mark_as_lost(const std::stringstream& message) - { - mark_as_lost(message.str()); - } + virtual void mark_as_lost(const char* message) override; + using GeometryState::mark_as_lost; //! create a particle restart HDF5 file void write_restart() const; diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index d0813f9c5df..5c765e2e605 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -27,9 +27,6 @@ constexpr int MAX_DELAYED_GROUPS {8}; constexpr double CACHE_INVALID {-1.0}; -// Maximum number of collisions/crossings -constexpr int MAX_EVENTS {1000000}; - //========================================================================== // Aliases and type definitions @@ -194,6 +191,162 @@ struct BoundaryInfo { lattice_translation {}; //!< which way lattice indices will change }; +/* + * Contains all geometry state information for a particle. + */ +class GeometryState { +public: + GeometryState(); + + /* + * GeometryState does not store any ID info, so give some reasonable behavior + * here. The Particle class redefines this. This is only here for the error + * reporting behavior that occurs in geometry.cpp. The explanation for + * mark_as_lost is the same. + */ + virtual void mark_as_lost(const char* message); + void mark_as_lost(const std::string& message); + void mark_as_lost(const std::stringstream& message); + + // resets all coordinate levels for the particle + void clear() + { + for (auto& level : coord_) + level.reset(); + n_coord_ = 1; + } + + // Initialize all internal state from position and direction + void init_from_r_u(Position r_a, Direction u_a) + { + clear(); + surface() = 0; + material() = C_NONE; + r() = r_a; + u() = u_a; + r_last_current() = r_a; + r_last() = r_a; + u_last() = u_a; + } + + // Unique ID. This is not geometric info, but the + // error reporting in geometry.cpp requires this. + // We could save this to implement it in Particle, + // but that would require virtuals. + int64_t& id() { return id_; } + const int64_t& id() const { return id_; } + + // Number of current coordinate levels + int& n_coord() { return n_coord_; } + const int& n_coord() const { return n_coord_; } + + // Offset for distributed properties + int& cell_instance() { return cell_instance_; } + const int& cell_instance() const { return cell_instance_; } + + // Coordinates for all nesting levels + LocalCoord& coord(int i) { return coord_[i]; } + const LocalCoord& coord(int i) const { return coord_[i]; } + const vector& coord() const { return coord_; } + + // Innermost universe nesting coordinates + LocalCoord& lowest_coord() { return coord_[n_coord_ - 1]; } + const LocalCoord& lowest_coord() const { return coord_[n_coord_ - 1]; } + + // Last coordinates on all nesting levels, before crossing a surface + int& n_coord_last() { return n_coord_last_; } + const int& n_coord_last() const { return n_coord_last_; } + int& cell_last(int i) { return cell_last_[i]; } + const int& cell_last(int i) const { return cell_last_[i]; } + + // Coordinates at birth + Position& r_born() { return r_born_; } + const Position& r_born() const { return r_born_; } + + // Coordinates of last collision or reflective/periodic surface + // crossing for current tallies + Position& r_last_current() { return r_last_current_; } + const Position& r_last_current() const { return r_last_current_; } + + // Previous direction and spatial coordinates before a collision + Position& r_last() { return r_last_; } + const Position& r_last() const { return r_last_; } + Position& u_last() { return u_last_; } + const Position& u_last() const { return u_last_; } + + // Accessors for position in global coordinates + Position& r() { return coord_[0].r; } + const Position& r() const { return coord_[0].r; } + + // Accessors for position in local coordinates + Position& r_local() { return coord_[n_coord_ - 1].r; } + const Position& r_local() const { return coord_[n_coord_ - 1].r; } + + // Accessors for direction in global coordinates + Direction& u() { return coord_[0].u; } + const Direction& u() const { return coord_[0].u; } + + // Accessors for direction in local coordinates + Direction& u_local() { return coord_[n_coord_ - 1].u; } + const Direction& u_local() const { return coord_[n_coord_ - 1].u; } + + // Surface that the particle is on + int& surface() { return surface_; } + const int& surface() const { return surface_; } + + // Boundary information + BoundaryInfo& boundary() { return boundary_; } + +#ifdef DAGMC + // DagMC state variables + moab::DagMC::RayHistory& history() { return history_; } + Direction& last_dir() { return last_dir_; } +#endif + + // material of current and last cell + int& material() { return material_; } + const int& material() const { return material_; } + int& material_last() { return material_last_; } + const int& material_last() const { return material_last_; } + + // temperature of current and last cell + double& sqrtkT() { return sqrtkT_; } + const double& sqrtkT() const { return sqrtkT_; } + double& sqrtkT_last() { return sqrtkT_last_; } + +private: + int64_t id_ {-1}; //!< Unique ID + + int n_coord_ {1}; //!< number of current coordinate levels + int cell_instance_; //!< offset for distributed properties + vector coord_; //!< coordinates for all levels + + int n_coord_last_ {1}; //!< number of current coordinates + vector cell_last_; //!< coordinates for all levels + + Position r_born_; //!< coordinates at birth + Position r_last_current_; //!< coordinates of the last collision or + //!< reflective/periodic surface crossing for + //!< current tallies + Position r_last_; //!< previous coordinates + Direction u_last_; //!< previous direction coordinates + + int surface_ {0}; //!< index for surface particle is on + + BoundaryInfo boundary_; //!< Info about the next intersection + + int material_ {-1}; //!< index for current material + int material_last_ {-1}; //!< index for last material + + double sqrtkT_ {-1.0}; //!< sqrt(k_Boltzmann * temperature) in eV + double sqrtkT_last_ {0.0}; //!< last temperature + +#ifdef DAGMC + moab::DagMC::RayHistory history_; + Direction last_dir_; +#endif +}; + //============================================================================ //! Defines how particle data is laid out in memory //============================================================================ @@ -229,163 +382,112 @@ struct BoundaryInfo { * Algorithms.” Annals of Nuclear Energy 113 (March 2018): 506–18. * https://doi.org/10.1016/j.anucene.2017.11.032. */ -class ParticleData { -public: - //---------------------------------------------------------------------------- - // Constructors - ParticleData(); - +class ParticleData : public GeometryState { private: //========================================================================== - // Data members (accessor methods are below) + // Data members -- see public: below for descriptions - // Cross section caches - vector neutron_xs_; //!< Microscopic neutron cross sections - vector photon_xs_; //!< Microscopic photon cross sections - MacroXS macro_xs_; //!< Macroscopic cross sections - CacheDataMG mg_xs_cache_; //!< Multigroup XS cache + vector neutron_xs_; + vector photon_xs_; + MacroXS macro_xs_; + CacheDataMG mg_xs_cache_; - int64_t id_; //!< Unique ID - ParticleType type_ {ParticleType::neutron}; //!< Particle type (n, p, e, etc.) + ParticleType type_ {ParticleType::neutron}; - int n_coord_ {1}; //!< number of current coordinate levels - int cell_instance_; //!< offset for distributed properties - vector coord_; //!< coordinates for all levels + double E_; + double E_last_; + int g_ {0}; + int g_last_; - // Particle coordinates before crossing a surface - int n_coord_last_ {1}; //!< number of current coordinates - vector cell_last_; //!< coordinates for all levels + double wgt_ {1.0}; + double mu_; + double time_ {0.0}; + double time_last_ {0.0}; + double wgt_last_ {1.0}; - // Energy data - double E_; //!< post-collision energy in eV - double E_last_; //!< pre-collision energy in eV - int g_ {0}; //!< post-collision energy group (MG only) - int g_last_; //!< pre-collision energy group (MG only) + bool fission_ {false}; + TallyEvent event_; + int event_nuclide_; + int event_mt_; + int delayed_group_ {0}; - // Other physical data - double wgt_ {1.0}; //!< particle weight - double mu_; //!< angle of scatter - double time_ {0.0}; //!< time in [s] - double time_last_ {0.0}; //!< previous time in [s] + int n_bank_ {0}; + int n_bank_second_ {0}; + double wgt_bank_ {0.0}; + int n_delayed_bank_[MAX_DELAYED_GROUPS]; - // Other physical data - Position r_last_current_; //!< coordinates of the last collision or - //!< reflective/periodic surface crossing for - //!< current tallies - Position r_last_; //!< previous coordinates - Direction u_last_; //!< previous direction coordinates - double wgt_last_ {1.0}; //!< pre-collision particle weight - - // What event took place - bool fission_ {false}; //!< did particle cause implicit fission - TallyEvent event_; //!< scatter, absorption - int event_nuclide_; //!< index in nuclides array - int event_mt_; //!< reaction MT - int delayed_group_ {0}; //!< delayed group - - // Post-collision physical data - int n_bank_ {0}; //!< number of fission sites banked - int n_bank_second_ {0}; //!< number of secondary particles banked - double wgt_bank_ {0.0}; //!< weight of fission sites banked - int n_delayed_bank_[MAX_DELAYED_GROUPS]; //!< number of delayed fission - //!< sites banked - - // Indices for various arrays - int surface_ {0}; //!< index for surface particle is on - int cell_born_ {-1}; //!< index for cell particle was born in - int material_ {-1}; //!< index for current material - int material_last_ {-1}; //!< index for last material - - // Boundary information - BoundaryInfo boundary_; - - // Temperature of current cell - double sqrtkT_ {-1.0}; //!< sqrt(k_Boltzmann * temperature) in eV - double sqrtkT_last_ {0.0}; //!< last temperature + int cell_born_ {-1}; - // Statistical data - int n_collision_ {0}; //!< number of collisions + int n_collision_ {0}; - // Track output bool write_track_ {false}; - // Current PRNG state - uint64_t seeds_[N_STREAMS]; // current seeds - int stream_; // current RNG stream + uint64_t seeds_[N_STREAMS]; + int stream_; - // Secondary particle bank vector secondary_bank_; - int64_t current_work_; // current work index + int64_t current_work_; - vector flux_derivs_; // for derivatives for this particle + vector flux_derivs_; - vector filter_matches_; // tally filter matches + vector filter_matches_; - vector tracks_; // tracks for outputting to file + vector tracks_; - vector nu_bank_; // bank of most recently fissioned particles + vector nu_bank_; - vector pht_storage_; // interim pulse-height results + vector pht_storage_; - // Global tally accumulators double keff_tally_absorption_ {0.0}; double keff_tally_collision_ {0.0}; double keff_tally_tracklength_ {0.0}; double keff_tally_leakage_ {0.0}; - bool trace_ {false}; //!< flag to show debug information + bool trace_ {false}; - double collision_distance_; // distance to particle's next closest collision + double collision_distance_; - int n_event_ {0}; // number of events executed in this particle's history + int n_event_ {0}; - // Weight window information - int n_split_ {0}; // Number of times this particle has been split - double ww_factor_ { - 0.0}; // Particle-specific factor for on-the-fly weight window adjustment + int n_split_ {0}; + double ww_factor_ {0.0}; -// DagMC state variables -#ifdef DAGMC - moab::DagMC::RayHistory history_; - Direction last_dir_; -#endif - - int64_t n_progeny_ {0}; // Number of progeny produced by this particle + int64_t n_progeny_ {0}; public: + //---------------------------------------------------------------------------- + // Constructors + ParticleData(); + //========================================================================== // Methods and accessors - NuclideMicroXS& neutron_xs(int i) { return neutron_xs_[i]; } + // Cross section caches + NuclideMicroXS& neutron_xs(int i) + { + return neutron_xs_[i]; + } // Microscopic neutron cross sections const NuclideMicroXS& neutron_xs(int i) const { return neutron_xs_[i]; } + + // Microscopic photon cross sections ElementMicroXS& photon_xs(int i) { return photon_xs_[i]; } + + // Macroscopic cross sections MacroXS& macro_xs() { return macro_xs_; } const MacroXS& macro_xs() const { return macro_xs_; } + + // Multigroup macroscopic cross sections CacheDataMG& mg_xs_cache() { return mg_xs_cache_; } const CacheDataMG& mg_xs_cache() const { return mg_xs_cache_; } - int64_t& id() { return id_; } - const int64_t& id() const { return id_; } + // Particle type (n, p, e, gamma, etc) ParticleType& type() { return type_; } const ParticleType& type() const { return type_; } - int& n_coord() { return n_coord_; } - const int& n_coord() const { return n_coord_; } - int& cell_instance() { return cell_instance_; } - const int& cell_instance() const { return cell_instance_; } - LocalCoord& coord(int i) { return coord_[i]; } - const LocalCoord& coord(int i) const { return coord_[i]; } - const vector& coord() const { return coord_; } - - LocalCoord& lowest_coord() { return coord_[n_coord_ - 1]; } - const LocalCoord& lowest_coord() const { return coord_[n_coord_ - 1]; } - - int& n_coord_last() { return n_coord_last_; } - const int& n_coord_last() const { return n_coord_last_; } - int& cell_last(int i) { return cell_last_[i]; } - const int& cell_last(int i) const { return cell_last_[i]; } - + // Current particle energy, energy before collision, + // and corresponding multigroup group indices. Energy + // units are eV. double& E() { return E_; } const double& E() const { return E_; } double& E_last() { return E_last_; } @@ -395,112 +497,119 @@ class ParticleData { int& g_last() { return g_last_; } const int& g_last() const { return g_last_; } + // Statistic weight of particle. Setting to zero + // indicates that the particle is dead. double& wgt() { return wgt_; } double wgt() const { return wgt_; } + double& wgt_last() { return wgt_last_; } + const double& wgt_last() const { return wgt_last_; } + bool alive() const { return wgt_ != 0.0; } + + // Polar scattering angle after a collision double& mu() { return mu_; } const double& mu() const { return mu_; } + + // Tracks the time of a particle as it traverses the problem. + // Units are seconds. double& time() { return time_; } const double& time() const { return time_; } double& time_last() { return time_last_; } const double& time_last() const { return time_last_; } - bool alive() const { return wgt_ != 0.0; } - Position& r_last_current() { return r_last_current_; } - const Position& r_last_current() const { return r_last_current_; } - Position& r_last() { return r_last_; } - const Position& r_last() const { return r_last_; } - Position& u_last() { return u_last_; } - const Position& u_last() const { return u_last_; } - double& wgt_last() { return wgt_last_; } - const double& wgt_last() const { return wgt_last_; } - - bool& fission() { return fission_; } + // What event took place, described in greater detail below TallyEvent& event() { return event_; } const TallyEvent& event() const { return event_; } - int& event_nuclide() { return event_nuclide_; } + bool& fission() { return fission_; } // true if implicit fission + int& event_nuclide() { return event_nuclide_; } // index of collision nuclide const int& event_nuclide() const { return event_nuclide_; } - int& event_mt() { return event_mt_; } - int& delayed_group() { return delayed_group_; } + int& event_mt() { return event_mt_; } // MT number of collision + int& delayed_group() { return delayed_group_; } // delayed group - int& n_bank() { return n_bank_; } - int& n_bank_second() { return n_bank_second_; } - double& wgt_bank() { return wgt_bank_; } - int* n_delayed_bank() { return n_delayed_bank_; } - int& n_delayed_bank(int i) { return n_delayed_bank_[i]; } + // Post-collision data + int& n_bank() { return n_bank_; } // number of banked fission sites + int& n_bank_second() + { + return n_bank_second_; + } // number of secondaries banked + double& wgt_bank() { return wgt_bank_; } // weight of banked fission sites + int* n_delayed_bank() + { + return n_delayed_bank_; + } // number of delayed fission sites + int& n_delayed_bank(int i) + { + return n_delayed_bank_[i]; + } // number of delayed fission sites - int& surface() { return surface_; } - const int& surface() const { return surface_; } + // Index of cell particle is born in int& cell_born() { return cell_born_; } const int& cell_born() const { return cell_born_; } - int& material() { return material_; } - const int& material() const { return material_; } - int& material_last() { return material_last_; } - - BoundaryInfo& boundary() { return boundary_; } - - double& sqrtkT() { return sqrtkT_; } - const double& sqrtkT() const { return sqrtkT_; } - double& sqrtkT_last() { return sqrtkT_last_; } + // index of the current and last material + // Total number of collisions suffered by particle int& n_collision() { return n_collision_; } const int& n_collision() const { return n_collision_; } + // whether this track is to be written bool& write_track() { return write_track_; } + + // RNG state uint64_t& seeds(int i) { return seeds_[i]; } uint64_t* seeds() { return seeds_; } int& stream() { return stream_; } + // secondary particle bank SourceSite& secondary_bank(int i) { return secondary_bank_[i]; } decltype(secondary_bank_)& secondary_bank() { return secondary_bank_; } + + // Current simulation work index int64_t& current_work() { return current_work_; } const int64_t& current_work() const { return current_work_; } + + // Used in tally derivatives double& flux_derivs(int i) { return flux_derivs_[i]; } const double& flux_derivs(int i) const { return flux_derivs_[i]; } + + // Matches of tallies decltype(filter_matches_)& filter_matches() { return filter_matches_; } FilterMatch& filter_matches(int i) { return filter_matches_[i]; } + + // Tracks to output to file decltype(tracks_)& tracks() { return tracks_; } + + // Bank of recently fissioned particles decltype(nu_bank_)& nu_bank() { return nu_bank_; } NuBank& nu_bank(int i) { return nu_bank_[i]; } + + // Interim pulse height tally storage vector& pht_storage() { return pht_storage_; } + // Global tally accumulators double& keff_tally_absorption() { return keff_tally_absorption_; } double& keff_tally_collision() { return keff_tally_collision_; } double& keff_tally_tracklength() { return keff_tally_tracklength_; } double& keff_tally_leakage() { return keff_tally_leakage_; } + // Shows debug info bool& trace() { return trace_; } + + // Distance to the next collision double& collision_distance() { return collision_distance_; } + + // Number of events particle has undergone int& n_event() { return n_event_; } + // Number of times variance reduction has caused a particle split int n_split() const { return n_split_; } int& n_split() { return n_split_; } + // Particle-specific factor for on-the-fly weight window adjustment double ww_factor() const { return ww_factor_; } double& ww_factor() { return ww_factor_; } -#ifdef DAGMC - moab::DagMC::RayHistory& history() { return history_; } - Direction& last_dir() { return last_dir_; } -#endif - + // Number of progeny produced by this particle int64_t& n_progeny() { return n_progeny_; } - // Accessors for position in global coordinates - Position& r() { return coord_[0].r; } - const Position& r() const { return coord_[0].r; } - - // Accessors for position in local coordinates - Position& r_local() { return coord_[n_coord_ - 1].r; } - const Position& r_local() const { return coord_[n_coord_ - 1].r; } - - // Accessors for direction in global coordinates - Direction& u() { return coord_[0].u; } - const Direction& u() const { return coord_[0].u; } - - // Accessors for direction in local coordinates - Direction& u_local() { return coord_[n_coord_ - 1].u; } - const Direction& u_local() const { return coord_[n_coord_ - 1].u; } - //! Gets the pointer to the particle's current PRN seed uint64_t* current_seed() { return seeds_ + stream_; } const uint64_t* current_seed() const { return seeds_ + stream_; } @@ -512,14 +621,6 @@ class ParticleData { micro.last_E = 0.0; } - //! resets all coordinate levels for the particle - void clear() - { - for (auto& level : coord_) - level.reset(); - n_coord_ = 1; - } - //! Get track information based on particle's current state TrackState get_track_state() const; diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 3e97e2544c0..ee6c3fbec58 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -1,6 +1,7 @@ #ifndef OPENMC_PLOT_H #define OPENMC_PLOT_H +#include #include #include @@ -99,6 +100,7 @@ class PlottableInterface { virtual void print_info() const = 0; const std::string& path_plot() const { return path_plot_; } + std::string& path_plot() { return path_plot_; } int id() const { return id_; } int level() const { return level_; } @@ -120,7 +122,7 @@ struct IdData { IdData(size_t h_res, size_t v_res); // Methods - void set_value(size_t y, size_t x, const Particle& p, int level); + void set_value(size_t y, size_t x, const GeometryState& p, int level); void set_overlap(size_t y, size_t x); // Members @@ -132,7 +134,7 @@ struct PropertyData { PropertyData(size_t h_res, size_t v_res); // Methods - void set_value(size_t y, size_t x, const Particle& p, int level); + void set_value(size_t y, size_t x, const GeometryState& p, int level); void set_overlap(size_t y, size_t x); // Members @@ -200,11 +202,11 @@ T SlicePlotBase::get_map() const xyz[out_i] = origin_[out_i] + width_[1] / 2. - out_pixel / 2.; // arbitrary direction - Direction dir = {0.7071, 0.7071, 0.0}; + Direction dir = {1. / std::sqrt(2.), 1. / std::sqrt(2.), 0.0}; #pragma omp parallel { - Particle p; + GeometryState p; p.r() = xyz; p.u() = dir; p.coord(0).universe = model::root_universe; @@ -290,7 +292,7 @@ class ProjectionPlot : public PlottableInterface { * find a distance to the boundary in a non-standard surface intersection * check. It's an exhaustive search over surfaces in the top-level universe. */ - static int advance_to_boundary_from_void(Particle& p); + static int advance_to_boundary_from_void(GeometryState& p); /* Checks if a vector of two TrackSegments is equivalent. We define this * to mean not having matching intersection lengths, but rather having diff --git a/include/openmc/random_dist.h b/include/openmc/random_dist.h index 4c9ab3c677a..0fb186edca0 100644 --- a/include/openmc/random_dist.h +++ b/include/openmc/random_dist.h @@ -48,9 +48,9 @@ extern "C" double maxwell_spectrum(double T, uint64_t* seed); extern "C" double watt_spectrum(double a, double b, uint64_t* seed); //============================================================================== -//! Samples an energy from the Gaussian energy-dependent fission distribution. +//! Samples an energy from the Gaussian distribution. //! -//! Samples from a Normal distribution with a given mean and standard deviation +//! Samples from a normal distribution with a given mean and standard deviation //! The PDF is defined as s(x) = (1/2*sigma*sqrt(2) * e-((mu-x)/2*sigma)^2 //! Its sampled according to //! http://www-pdg.lbl.gov/2009/reviews/rpp2009-rev-monte-carlo-techniques.pdf diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h new file mode 100644 index 00000000000..5f64914edb9 --- /dev/null +++ b/include/openmc/random_ray/flat_source_domain.h @@ -0,0 +1,141 @@ +#ifndef OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H +#define OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H + +#include "openmc/openmp_interface.h" +#include "openmc/position.h" + +namespace openmc { + +/* + * The FlatSourceDomain class encompasses data and methods for storing + * scalar flux and source region for all flat source regions in a + * random ray simulation domain. + */ + +class FlatSourceDomain { +public: + //---------------------------------------------------------------------------- + // Helper Structs + + // A mapping object that is used to map between a specific random ray + // source region and an OpenMC native tally bin that it should score to + // every iteration. + struct TallyTask { + int tally_idx; + int filter_idx; + int score_idx; + int score_type; + TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), + score_type(score_type) + {} + }; + + //---------------------------------------------------------------------------- + // Constructors + FlatSourceDomain(); + + //---------------------------------------------------------------------------- + // Methods + void update_neutron_source(double k_eff); + double compute_k_eff(double k_eff_old) const; + void normalize_scalar_flux_and_volumes( + double total_active_distance_per_iteration); + int64_t add_source_to_scalar_flux(); + void batch_reset(); + void convert_source_regions_to_tallies(); + void random_ray_tally() const; + void accumulate_iteration_flux(); + void output_to_vtk() const; + void all_reduce_replicated_source_regions(); + + //---------------------------------------------------------------------------- + // Public Data members + + bool mapped_all_tallies_ {false}; // If all source regions have been visited + + int64_t n_source_regions_ {0}; // Total number of source regions in the model + + // 1D array representing source region starting offset for each OpenMC Cell + // in model::cells + vector source_region_offsets_; + + // 1D arrays representing values for all source regions + vector lock_; + vector was_hit_; + vector volume_; + vector position_recorded_; + vector position_; + + // 2D arrays stored in 1D representing values for all source regions x energy + // groups + vector scalar_flux_old_; + vector scalar_flux_new_; + vector source_; + + //---------------------------------------------------------------------------- + // Private data members +private: + int negroups_; // Number of energy groups in simulation + int64_t n_source_elements_ {0}; // Total number of source regions in the model + // times the number of energy groups + + // 2D array representing values for all source regions x energy groups x tally + // tasks + vector> tally_task_; + + // 1D arrays representing values for all source regions + vector material_; + vector volume_t_; + + // 2D arrays stored in 1D representing values for all source regions x energy + // groups + vector scalar_flux_final_; + +}; // class FlatSourceDomain + +//============================================================================ +//! Non-member functions +//============================================================================ + +// Returns the inputted value in big endian byte ordering. If the system is +// little endian, the byte ordering is flipped. If the system is big endian, +// the inputted value is returned as is. This function is necessary as +// .vtk binary files use big endian byte ordering. +template +T convert_to_big_endian(T in) +{ + // 4 byte integer + uint32_t test = 1; + + // 1 byte pointer to first byte of test integer + uint8_t* ptr = reinterpret_cast(&test); + + // If the first byte of test is 0, then the system is big endian. In this + // case, we don't have to do anything as .vtk files are big endian + if (*ptr == 0) + return in; + + // Otherwise, the system is in little endian, so we need to flip the + // endianness + uint8_t* orig = reinterpret_cast(&in); + uint8_t swapper[sizeof(T)]; + for (int i = 0; i < sizeof(T); i++) { + swapper[i] = orig[sizeof(T) - i - 1]; + } + T out = *reinterpret_cast(&swapper); + return out; +} + +template +void parallel_fill(vector& arr, T value) +{ +#pragma omp parallel for schedule(static) + for (int i = 0; i < arr.size(); i++) { + arr[i] = value; + } +} + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h new file mode 100644 index 00000000000..5ee64574ec1 --- /dev/null +++ b/include/openmc/random_ray/random_ray.h @@ -0,0 +1,55 @@ +#ifndef OPENMC_RANDOM_RAY_H +#define OPENMC_RANDOM_RAY_H + +#include "openmc/memory.h" +#include "openmc/particle.h" +#include "openmc/random_ray/flat_source_domain.h" +#include "openmc/source.h" + +namespace openmc { + +/* + * The RandomRay class encompasses data and methods for transporting random rays + * through the model. It is a small extension of the Particle class. + */ + +// TODO: Inherit from GeometryState instead of Particle +class RandomRay : public Particle { +public: + //---------------------------------------------------------------------------- + // Constructors + RandomRay(); + RandomRay(uint64_t ray_id, FlatSourceDomain* domain); + + //---------------------------------------------------------------------------- + // Methods + void event_advance_ray(); + void attenuate_flux(double distance, bool is_active); + void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); + uint64_t transport_history_based_single_ray(); + + //---------------------------------------------------------------------------- + // Static data members + static double distance_inactive_; // Inactive (dead zone) ray length + static double distance_active_; // Active ray length + static unique_ptr ray_source_; // Starting source for ray sampling + + //---------------------------------------------------------------------------- + // Public data members + vector angular_flux_; + +private: + //---------------------------------------------------------------------------- + // Private data members + vector delta_psi_; + int negroups_; + FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source + // data needed for ray transport + double distance_travelled_ {0}; + bool is_active_ {false}; + bool is_alive_ {true}; +}; // class RandomRay + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_H diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h new file mode 100644 index 00000000000..cde0d27dff2 --- /dev/null +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -0,0 +1,59 @@ +#ifndef OPENMC_RANDOM_RAY_SIMULATION_H +#define OPENMC_RANDOM_RAY_SIMULATION_H + +#include "openmc/random_ray/flat_source_domain.h" + +namespace openmc { + +/* + * The RandomRaySimulation class encompasses data and methods for running a + * random ray simulation. + */ + +class RandomRaySimulation { +public: + //---------------------------------------------------------------------------- + // Constructors + RandomRaySimulation(); + + //---------------------------------------------------------------------------- + // Methods + void simulate(); + void reduce_simulation_statistics(); + void output_simulation_results() const; + void instability_check( + int64_t n_hits, double k_eff, double& avg_miss_rate) const; + void print_results_random_ray(uint64_t total_geometric_intersections, + double avg_miss_rate, int negroups, int64_t n_source_regions) const; + + //---------------------------------------------------------------------------- + // Data members +private: + // Contains all flat source region data + FlatSourceDomain domain_; + + // Random ray eigenvalue + double k_eff_ {1.0}; + + // Tracks the average FSR miss rate for analysis and reporting + double avg_miss_rate_ {0.0}; + + // Tracks the total number of geometric intersections by all rays for + // reporting + uint64_t total_geometric_intersections_ {0}; + + // Number of energy groups + int negroups_; + +}; // class RandomRaySimulation + +//============================================================================ +//! Non-member functions +//============================================================================ + +void openmc_run_random_ray(); +void validate_random_ray_inputs(); + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_SIMULATION_H diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 69a8d7d13be..b71ec364931 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -93,8 +93,8 @@ extern "C" int32_t gen_per_batch; //!< number of generations per batch extern "C" int64_t n_particles; //!< number of particles per generation extern int64_t - max_particles_in_flight; //!< Max num. event-based particles in flight - + max_particles_in_flight; //!< Max num. event-based particles in flight +extern int max_particle_events; //!< Maximum number of particle events extern ElectronTreatment electron_treatment; //!< how to treat secondary electrons extern array @@ -112,8 +112,9 @@ extern ResScatMethod res_scat_method; //!< resonance upscattering method extern double res_scat_energy_min; //!< Min energy in [eV] for res. upscattering extern double res_scat_energy_max; //!< Max energy in [eV] for res. upscattering extern vector - res_scat_nuclides; //!< Nuclides using res. upscattering treatment -extern RunMode run_mode; //!< Run mode (eigenvalue, fixed src, etc.) + res_scat_nuclides; //!< Nuclides using res. upscattering treatment +extern RunMode run_mode; //!< Run mode (eigenvalue, fixed src, etc.) +extern SolverType solver_type; //!< Solver Type (Monte Carlo or Random Ray) extern std::unordered_set sourcepoint_batch; //!< Batches when source should be written extern std::unordered_set @@ -139,6 +140,7 @@ extern int trigger_batch_interval; //!< Batch interval for triggers extern "C" int verbosity; //!< How verbose to make output extern double weight_cutoff; //!< Weight cutoff for Russian roulette extern double weight_survive; //!< Survival weight after Russian roulette + } // namespace settings //============================================================================== diff --git a/include/openmc/source.h b/include/openmc/source.h index ad2aae562bb..d5ea9bd1d56 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -50,6 +50,8 @@ class Source { // Methods that can be overridden virtual double strength() const { return 1.0; } + + static unique_ptr create(pugi::xml_node node); }; //============================================================================== @@ -101,12 +103,13 @@ class IndependentSource : public Source { class FileSource : public Source { public: // Constructors - explicit FileSource(std::string path); - explicit FileSource(const vector& sites) : sites_ {sites} {} + explicit FileSource(pugi::xml_node node); + explicit FileSource(const std::string& path); // Methods SourceSite sample(uint64_t* seed) const override; - + void load_sites_from_file( + const std::string& path); //!< Load source sites from file private: vector sites_; //!< Source sites from a file }; @@ -118,7 +121,7 @@ class FileSource : public Source { class CompiledSourceWrapper : public Source { public: // Constructors, destructors - CompiledSourceWrapper(std::string path, std::string parameters); + CompiledSourceWrapper(pugi::xml_node node); ~CompiledSourceWrapper(); // Defer implementation to custom source library @@ -129,6 +132,8 @@ class CompiledSourceWrapper : public Source { double strength() const override { return compiled_source_->strength(); } + void setup(const std::string& path, const std::string& parameters); + private: void* shared_library_; //!< library from dlopen unique_ptr compiled_source_; @@ -136,6 +141,35 @@ class CompiledSourceWrapper : public Source { typedef unique_ptr create_compiled_source_t(std::string parameters); +//============================================================================== +//! Mesh-based source with different distributions for each element +//============================================================================== + +class MeshSource : public Source { +public: + // Constructors + explicit MeshSource(pugi::xml_node node); + + //! Sample from the external source distribution + //! \param[inout] seed Pseudorandom seed pointer + //! \return Sampled site + SourceSite sample(uint64_t* seed) const override; + + // Properties + double strength() const override { return space_->total_strength(); } + + // Accessors + const std::unique_ptr& source(int32_t i) const + { + return sources_.size() == 1 ? sources_[0] : sources_[i]; + } + +private: + // Data members + unique_ptr space_; //!< Mesh spatial + vector> sources_; //!< Source distributions +}; + //============================================================================== // Functions //============================================================================== diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 4c97a051452..350775123c1 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -83,10 +83,10 @@ struct BoundingBox { class Surface { public: - int id_; //!< Unique ID - std::string name_; //!< User-defined name - std::shared_ptr bc_ {nullptr}; //!< Boundary condition - GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) + int id_; //!< Unique ID + std::string name_; //!< User-defined name + unique_ptr bc_; //!< Boundary condition + GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) bool surf_source_ {false}; //!< Activate source banking for the surface? explicit Surface(pugi::xml_node surf_node); @@ -105,12 +105,13 @@ class Surface { //! Determine the direction of a ray reflected from the surface. //! \param[in] r The point at which the ray is incident. //! \param[in] u Incident direction of the ray - //! \param[inout] p Pointer to the particle + //! \param[inout] p Pointer to the particle. Only DAGMC uses this. //! \return Outgoing direction of the ray - virtual Direction reflect(Position r, Direction u, Particle* p) const; + virtual Direction reflect( + Position r, Direction u, GeometryState* p = nullptr) const; virtual Direction diffuse_reflect( - Position r, Direction u, uint64_t* seed) const; + Position r, Direction u, uint64_t* seed, GeometryState* p = nullptr) const; //! Evaluate the equation describing the surface. //! diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 8a17ee7ea2a..1166c0eee53 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -31,7 +31,9 @@ enum class FilterType { ENERGY_OUT, LEGENDRE, MATERIAL, + MATERIALFROM, MESH, + MESHBORN, MESH_SURFACE, MU, PARTICLE, diff --git a/include/openmc/tallies/filter_material.h b/include/openmc/tallies/filter_material.h index 5da556d5e93..aa5df5b3ab2 100644 --- a/include/openmc/tallies/filter_material.h +++ b/include/openmc/tallies/filter_material.h @@ -46,7 +46,7 @@ class MaterialFilter : public Filter { void set_materials(gsl::span materials); -private: +protected: //---------------------------------------------------------------------------- // Data members diff --git a/include/openmc/tallies/filter_materialfrom.h b/include/openmc/tallies/filter_materialfrom.h new file mode 100644 index 00000000000..52039852a5d --- /dev/null +++ b/include/openmc/tallies/filter_materialfrom.h @@ -0,0 +1,29 @@ +#ifndef OPENMC_TALLIES_FILTER_MATERIALFROM_H +#define OPENMC_TALLIES_FILTER_MATERIALFROM_H + +#include + +#include "openmc/tallies/filter_material.h" + +namespace openmc { + +//============================================================================== +//! Specifies which material particles exit when crossing a surface. +//============================================================================== + +class MaterialFromFilter : public MaterialFilter { +public: + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "materialfrom"; } + FilterType type() const override { return FilterType::MATERIALFROM; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + std::string text_label(int bin) const override; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_MATERIALFROM_H diff --git a/include/openmc/tallies/filter_meshborn.h b/include/openmc/tallies/filter_meshborn.h new file mode 100644 index 00000000000..8ab7a8c766b --- /dev/null +++ b/include/openmc/tallies/filter_meshborn.h @@ -0,0 +1,26 @@ +#ifndef OPENMC_TALLIES_FILTER_MESHBORN_H +#define OPENMC_TALLIES_FILTER_MESHBORN_H + +#include + +#include "openmc/position.h" +#include "openmc/tallies/filter_mesh.h" + +namespace openmc { + +class MeshBornFilter : public MeshFilter { +public: + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "meshborn"; } + FilterType type() const override { return FilterType::MESHBORN; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + std::string text_label(int bin) const override; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_MESHBORN_H diff --git a/include/openmc/tallies/trigger.h b/include/openmc/tallies/trigger.h index 9fe159b9d9b..7feed5e8ad2 100644 --- a/include/openmc/tallies/trigger.h +++ b/include/openmc/tallies/trigger.h @@ -23,6 +23,7 @@ enum class TriggerMetric { struct Trigger { TriggerMetric metric; //!< The type of uncertainty (e.g. std dev) measured double threshold; //!< Uncertainty value below which trigger is satisfied + bool ignore_zeros; //!< Whether to allow zero tally bins to be ignored int score_index; //!< Index of the relevant score in the tally's arrays }; diff --git a/include/openmc/timer.h b/include/openmc/timer.h index 62b97883f40..d928aad4560 100644 --- a/include/openmc/timer.h +++ b/include/openmc/timer.h @@ -31,6 +31,7 @@ extern Timer time_event_advance_particle; extern Timer time_event_surface_crossing; extern Timer time_event_collision; extern Timer time_event_death; +extern Timer time_update_src; } // namespace simulation diff --git a/include/openmc/universe.h b/include/openmc/universe.h index b98d2d57f64..26f33cb383d 100644 --- a/include/openmc/universe.h +++ b/include/openmc/universe.h @@ -9,6 +9,7 @@ namespace openmc { class DAGUniverse; #endif +class GeometryState; class Universe; class UniversePartitioner; @@ -32,7 +33,7 @@ class Universe { //! \param group_id An HDF5 group id. virtual void to_hdf5(hid_t group_id) const; - virtual bool find_cell(Particle& p) const; + virtual bool find_cell(GeometryState& p) const; BoundingBox bounding_box() const; diff --git a/include/openmc/volume_calc.h b/include/openmc/volume_calc.h index 2773a39fa69..376b8c707dd 100644 --- a/include/openmc/volume_calc.h +++ b/include/openmc/volume_calc.h @@ -1,18 +1,23 @@ #ifndef OPENMC_VOLUME_CALC_H #define OPENMC_VOLUME_CALC_H +#include // for find #include #include +#include #include "openmc/array.h" +#include "openmc/openmp_interface.h" #include "openmc/position.h" #include "openmc/tallies/trigger.h" #include "openmc/vector.h" #include "pugixml.hpp" #include "xtensor/xtensor.hpp" - #include +#ifdef _OPENMP +#include +#endif namespace openmc { @@ -89,6 +94,35 @@ extern vector volume_calcs; // Non-member functions //============================================================================== +//! Reduce vector of indices and hits from each thread to a single copy +// +//! \param[in] local_indices Indices specific to each thread +//! \param[in] local_hits Hit count specific to each thread +//! \param[out] indices Reduced vector of indices +//! \param[out] hits Reduced vector of hits +template +void reduce_indices_hits(const vector& local_indices, + const vector& local_hits, vector& indices, vector& hits) +{ + const int n_threads = num_threads(); + +#pragma omp for ordered schedule(static, 1) + for (int i = 0; i < n_threads; ++i) { +#pragma omp ordered + for (int j = 0; j < local_indices.size(); ++j) { + // Check if this material has been added to the master list and if + // so, accumulate the number of hits + auto it = std::find(indices.begin(), indices.end(), local_indices[j]); + if (it == indices.end()) { + indices.push_back(local_indices[j]); + hits.push_back(local_hits[j]); + } else { + hits[it - indices.begin()] += local_hits[j]; + } + } + } +} + void free_memory_volume(); } // namespace openmc diff --git a/man/man1/openmc.1 b/man/man1/openmc.1 index 331e42ea00c..d121a8956d9 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -63,7 +63,7 @@ Indicates the default path to an HDF5 file that contains multi-group cross section libraries if the user has not specified the tag in .I materials.xml\fP. .SH LICENSE -Copyright \(co 2011-2023 Massachusetts Institute of Technology, UChicago +Copyright \(co 2011-2024 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors. .PP Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/openmc/__init__.py b/openmc/__init__.py index 5bb150fb072..17b0abe2f69 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -36,7 +36,7 @@ from .config import * # Import a few names from the model module -from openmc.model import rectangular_prism, hexagonal_prism, Model +from openmc.model import Model -__version__ = '0.13.4-dev' +__version__ = '0.14.1-dev' diff --git a/openmc/arithmetic.py b/openmc/arithmetic.py index 5ca7cc66682..4520553dafc 100644 --- a/openmc/arithmetic.py +++ b/openmc/arithmetic.py @@ -54,8 +54,7 @@ def __eq__(self, other): return str(other) == str(self) def __repr__(self): - return '({} {} {})'.format(self.left_score, self.binary_op, - self.right_score) + return f'({self.left_score} {self.binary_op} {self.right_score})' @property def left_score(self): @@ -188,9 +187,9 @@ class CrossFilter: Parameters ---------- - left_filter : Filter or CrossFilter + left_filter : openmc.Filter or CrossFilter The left filter in the outer product - right_filter : Filter or CrossFilter + right_filter : openmc.Filter or CrossFilter The right filter in the outer product binary_op : str The tally arithmetic binary operator (e.g., '+', '-', etc.) used to @@ -200,9 +199,9 @@ class CrossFilter: ---------- type : str The type of the crossfilter (e.g., 'energy / energy') - left_filter : Filter or CrossFilter + left_filter : openmc.Filter or CrossFilter The left filter in the outer product - right_filter : Filter or CrossFilter + right_filter : openmc.Filter or CrossFilter The right filter in the outer product binary_op : str The tally arithmetic binary operator (e.g., '+', '-', etc.) used to @@ -271,7 +270,7 @@ def binary_op(self, binary_op): def type(self): left_type = self.left_filter.type right_type = self.right_filter.type - return '({} {} {})'.format(left_type, self.binary_op, right_type) + return f'({left_type} {self.binary_op} {right_type})' @property def bins(self): @@ -517,7 +516,7 @@ class AggregateFilter: Parameters ---------- - aggregate_filter : Filter or CrossFilter + aggregate_filter : openmc.Filter or CrossFilter The filter included in the aggregation bins : Iterable of tuple The filter bins included in the aggregation @@ -529,7 +528,7 @@ class AggregateFilter: ---------- type : str The type of the aggregatefilter (e.g., 'sum(energy)', 'sum(cell)') - aggregate_filter : filter + aggregate_filter : openmc.Filter The filter included in the aggregation aggregate_op : str The tally aggregation operator (e.g., 'sum', 'avg', etc.) used diff --git a/openmc/bounding_box.py b/openmc/bounding_box.py index daa6a318e13..6e58ca8ba02 100644 --- a/openmc/bounding_box.py +++ b/openmc/bounding_box.py @@ -9,7 +9,7 @@ class BoundingBox: """Axis-aligned bounding box. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -95,6 +95,24 @@ def __or__(self, other: BoundingBox) -> BoundingBox: new |= other return new + def __contains__(self, other): + """Check whether or not a point or another bounding box is in the bounding box. + + For another bounding box to be in the parent it must lie fully inside of it. + """ + # test for a single point + if isinstance(other, (tuple, list, np.ndarray)): + point = other + check_length("Point", point, 3, 3) + return all(point > self.lower_left) and all(point < self.upper_right) + elif isinstance(other, BoundingBox): + return all([p in self for p in [other.lower_left, other.upper_right]]) + else: + raise TypeError( + f"Unable to determine if {other} is in the bounding box." + f" Expected a tuple or a bounding box, but {type(other)} given" + ) + @property def center(self) -> np.ndarray: return (self[0] + self[1]) / 2 diff --git a/openmc/cell.py b/openmc/cell.py index 9a744910c80..94fac841358 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -1,8 +1,8 @@ from collections.abc import Iterable from math import cos, sin, pi from numbers import Real -import lxml.etree as ET +import lxml.etree as ET import numpy as np from uncertainties import UFloat @@ -343,8 +343,7 @@ def bounding_box(self): if self.region is not None: return self.region.bounding_box else: - return BoundingBox(np.array([-np.inf, -np.inf, -np.inf]), - np.array([np.inf, np.inf, np.inf])) + return BoundingBox.infinite() @property def num_instances(self): @@ -562,7 +561,7 @@ def clone(self, clone_materials=True, clone_regions=True, memo=None): def plot(self, *args, **kwargs): """Display a slice plot of the cell. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -594,7 +593,7 @@ def plot(self, *args, **kwargs): # Make water blue water = openmc.Cell(fill=h2o) - universe.plot(..., colors={water: (0., 0., 1.)) + water.plot(colors={water: (0., 0., 1.)}) seed : int Seed for the random number generator openmc_exec : str @@ -619,8 +618,10 @@ def plot(self, *args, **kwargs): """ # Create dummy universe but preserve used_ids - u = openmc.Universe(cells=[self], universe_id=openmc.Universe.next_id + 1) + next_id = openmc.Universe.next_id + u = openmc.Universe(cells=[self]) openmc.Universe.used_ids.remove(u.id) + openmc.Universe.next_id = next_id return u.plot(*args, **kwargs) def create_xml_subelement(self, xml_element, memo=None): @@ -722,7 +723,7 @@ def from_xml_element(cls, elem, surfaces, materials, get_universe): Dictionary mapping surface IDs to :class:`openmc.Surface` instances materials : dict Dictionary mapping material ID strings to :class:`openmc.Material` - instances (defined in :math:`openmc.Geometry.from_xml`) + instances (defined in :meth:`openmc.Geometry.from_xml`) get_universe : function Function returning universe (defined in :meth:`openmc.Geometry.from_xml`) diff --git a/openmc/cmfd.py b/openmc/cmfd.py index 1b4bfa5e27c..eff6a151fbb 100644 --- a/openmc/cmfd.py +++ b/openmc/cmfd.py @@ -135,7 +135,7 @@ def __repr__(self): return outstr def _get_repr(self, list_var, label): - outstr = "\t{:<11} = ".format(label) + outstr = f"\t{label:<11} = " if list(list_var): outstr += ", ".join(str(i) for i in list_var) return outstr @@ -242,9 +242,9 @@ def grid(self, grid): check_length('CMFD mesh grid', grid, grid_length) for i in range(grid_length): - check_type('CMFD mesh {}-grid'.format(dims[i]), grid[i], Iterable, + check_type(f'CMFD mesh {dims[i]}-grid', grid[i], Iterable, Real) - check_greater_than('CMFD mesh {}-grid length'.format(dims[i]), + check_greater_than(f'CMFD mesh {dims[i]}-grid length', len(grid[i]), 1) self._grid = [np.array(g) for g in grid] self._display_mesh_warning('rectilinear', 'CMFD mesh grid') @@ -612,7 +612,7 @@ def display(self, display): for key, value in display.items(): check_value('display key', key, ('balance', 'entropy', 'dominance', 'source')) - check_type("display['{}']".format(key), value, bool) + check_type(f"display['{key}']", value, bool) self._display[key] = value @downscatter.setter @@ -928,7 +928,7 @@ def _write_cmfd_statepoint(self, filename): with h5py.File(filename, 'a') as f: if 'cmfd' not in f: if openmc.lib.settings.verbosity >= 5: - print(' Writing CMFD data to {}...'.format(filename)) + print(f' Writing CMFD data to {filename}...') sys.stdout.flush() cmfd_group = f.create_group("cmfd") cmfd_group.attrs['cmfd_on'] = self._cmfd_on @@ -982,14 +982,16 @@ def _initialize_linsolver(self): temp_data = np.ones(len(loss_row)) temp_loss = sparse.csr_matrix((temp_data, (loss_row, loss_col)), shape=(n, n)) + temp_loss.sort_indices() # Pass coremap as 1-d array of 32-bit integers coremap = np.swapaxes(self._coremap, 0, 2).flatten().astype(np.int32) - args = temp_loss.indptr, len(temp_loss.indptr), \ - temp_loss.indices, len(temp_loss.indices), n, \ + return openmc.lib._dll.openmc_initialize_linsolver( + temp_loss.indptr.astype(np.int32), len(temp_loss.indptr), + temp_loss.indices.astype(np.int32), len(temp_loss.indices), n, self._spectral, coremap, self._use_all_threads - return openmc.lib._dll.openmc_initialize_linsolver(*args) + ) def _write_cmfd_output(self): """Write CMFD output to buffer at the end of each batch""" @@ -1132,12 +1134,12 @@ def _reset_cmfd(self, filename): with h5py.File(filename, 'r') as f: if 'cmfd' not in f: raise OpenMCError('Could not find CMFD parameters in ', - 'file {}'.format(filename)) + f'file {filename}') else: # Overwrite CMFD values from statepoint if (openmc.lib.master() and openmc.lib.settings.verbosity >= 5): - print(' Loading CMFD data from {}...'.format(filename)) + print(f' Loading CMFD data from {filename}...') sys.stdout.flush() cmfd_group = f['cmfd'] @@ -1407,8 +1409,7 @@ def _write_matrix(self, matrix, base_filename): # Get all data entries for particular row in matrix data = matrix.data[matrix.indptr[row]:matrix.indptr[row+1]] for i in range(len(cols)): - fh.write('{:3d}, {:3d}, {:0.8f}\n'.format( - row, cols[i], data[i])) + fh.write(f'{row:3d}, {cols[i]:3d}, {data[i]:0.8f}\n') # Save matrix in scipy format sparse.save_npz(base_filename, matrix) @@ -1585,6 +1586,7 @@ def _build_loss_matrix(self, adjoint): loss_row = self._loss_row loss_col = self._loss_col loss = sparse.csr_matrix((data, (loss_row, loss_col)), shape=(n, n)) + loss.sort_indices() return loss def _build_prod_matrix(self, adjoint): @@ -1611,6 +1613,7 @@ def _build_prod_matrix(self, adjoint): prod_row = self._prod_row prod_col = self._prod_col prod = sparse.csr_matrix((data, (prod_row, prod_col)), shape=(n, n)) + prod.sort_indices() return prod def _execute_power_iter(self, loss, prod): @@ -2307,8 +2310,8 @@ def _precompute_matrix_indices(self): constant_values=_CMFD_NOACCEL)[:,:,1:] # Create empty row and column vectors to store for loss matrix - row = np.array([]) - col = np.array([]) + row = np.array([], dtype=int) + col = np.array([], dtype=int) # Store all indices used to populate production and loss matrix is_accel = self._coremap != _CMFD_NOACCEL diff --git a/openmc/config.py b/openmc/config.py index 3b89925360c..b823d6b06b2 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -4,7 +4,7 @@ import warnings from openmc.data import DataLibrary -from openmc.data.decay import _DECAY_PHOTON_ENERGY +from openmc.data.decay import _DECAY_ENERGY, _DECAY_PHOTON_ENERGY __all__ = ["config"] @@ -41,6 +41,7 @@ def __setitem__(self, key, value): os.environ['OPENMC_CHAIN_FILE'] = str(value) # Reset photon source data since it relies on chain file _DECAY_PHOTON_ENERGY.clear() + _DECAY_ENERGY.clear() else: raise KeyError(f'Unrecognized config key: {key}. Acceptable keys ' 'are "cross_sections", "mg_cross_sections" and ' @@ -76,7 +77,7 @@ def _default_config(): if (chain_file is None and config.get('cross_sections') is not None and config['cross_sections'].exists() - ): + ): # Check for depletion chain in cross_sections.xml data = DataLibrary.from_xml(config['cross_sections']) for lib in reversed(data.libraries): diff --git a/openmc/data/ace.py b/openmc/data/ace.py index 7c348bd176c..1247593a806 100644 --- a/openmc/data/ace.py +++ b/openmc/data/ace.py @@ -143,7 +143,7 @@ def ascii_to_binary(ascii_file, binary_file): # that XSS will start at the second record nxs = [int(x) for x in ' '.join(lines[idx + 6:idx + 8]).split()] jxs = [int(x) for x in ' '.join(lines[idx + 8:idx + 12]).split()] - binary_file.write(struct.pack(str('=16i32i{}x'.format(record_length - 500)), + binary_file.write(struct.pack(str(f'=16i32i{record_length - 500}x'), *(nxs + jxs))) # Read/write XSS array. Null bytes are added to form a complete record @@ -152,8 +152,7 @@ def ascii_to_binary(ascii_file, binary_file): start = idx + _ACE_HEADER_SIZE xss = np.fromstring(' '.join(lines[start:start + n_lines]), sep=' ') extra_bytes = record_length - ((len(xss)*8 - 1) % record_length + 1) - binary_file.write(struct.pack(str('={}d{}x'.format( - nxs[0], extra_bytes)), *xss)) + binary_file.write(struct.pack(str(f'={nxs[0]}d{extra_bytes}x'), *xss)) # Advance to next table in file idx += _ACE_HEADER_SIZE + n_lines @@ -184,8 +183,7 @@ def get_table(filename, name=None): if lib.tables: return lib.tables[0] else: - raise ValueError('Could not find ACE table with name: {}' - .format(name)) + raise ValueError(f'Could not find ACE table with name: {name}') # The beginning of an ASCII ACE file consists of 12 lines that include the name, @@ -295,14 +293,14 @@ def _read_binary(self, ace_file, table_names, verbose=False, if verbose: kelvin = round(temperature * EV_PER_MEV / K_BOLTZMANN) - print("Loading nuclide {} at {} K".format(name, kelvin)) + print(f"Loading nuclide {name} at {kelvin} K") # Read JXS jxs = list(struct.unpack(str('=32i'), ace_file.read(128))) # Read XSS ace_file.seek(start_position + recl_length) - xss = list(struct.unpack(str('={}d'.format(length)), + xss = list(struct.unpack(str(f'={length}d'), ace_file.read(length*8))) # Insert zeros at beginning of NXS, JXS, and XSS arrays so that the @@ -393,7 +391,7 @@ def _read_ascii(self, ace_file, table_names, verbose=False): if verbose: kelvin = round(temperature * EV_PER_MEV / K_BOLTZMANN) - print("Loading nuclide {} at {} K".format(name, kelvin)) + print(f"Loading nuclide {name} at {kelvin} K") # Insert zeros at beginning of NXS, JXS, and XSS arrays so that the # indexing will be the same as Fortran. This makes it easier to @@ -455,8 +453,7 @@ def from_suffix(cls, suffix): for member in cls: if suffix.endswith(member.value): return member - raise ValueError("Suffix '{}' has no corresponding ACE table type." - .format(suffix)) + raise ValueError(f"Suffix '{suffix}' has no corresponding ACE table type.") class Table(EqualityMixin): @@ -507,7 +504,7 @@ def data_type(self): return TableType.from_suffix(xs[-1]) def __repr__(self): - return "".format(self.name) + return f"" def get_libraries_from_xsdir(path): diff --git a/openmc/data/angle_energy.py b/openmc/data/angle_energy.py index b8fde547871..71ca47587d7 100644 --- a/openmc/data/angle_energy.py +++ b/openmc/data/angle_energy.py @@ -112,7 +112,6 @@ def from_ace(ace, location_dist, location_start, rx=None): distribution = openmc.data.NBodyPhaseSpace.from_ace( ace, idx, rx.q_value) else: - raise ValueError("Unsupported ACE secondary energy " - "distribution law {}".format(law)) + raise ValueError(f"Unsupported ACE secondary energy distribution law {law}") return distribution diff --git a/openmc/data/correlated.py b/openmc/data/correlated.py index f131ce30d56..2ff095a5c4e 100644 --- a/openmc/data/correlated.py +++ b/openmc/data/correlated.py @@ -113,7 +113,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_(self._name) + group.attrs['type'] = np.bytes_(self._name) dset = group.create_dataset('energy', data=self.energy) dset.attrs['interpolation'] = np.vstack((self.breakpoints, diff --git a/openmc/data/data.py b/openmc/data/data.py index 609a901d9b4..7caf31e26ee 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -179,6 +179,93 @@ 118: 'Og'} ATOMIC_NUMBER = {value: key for key, value in ATOMIC_SYMBOL.items()} +DADZ = { + '(n,2nd)': (-3, -1), + '(n,2n)': (-1, 0), + '(n,3n)': (-2, 0), + '(n,na)': (-4, -2), + '(n,n3a)': (-12, -6), + '(n,2na)': (-5, -2), + '(n,3na)': (-6, -2), + '(n,np)': (-1, -1), + '(n,n2a)': (-8, -4), + '(n,2n2a)': (-9, -4), + '(n,nd)': (-2, -1), + '(n,nt)': (-3, -1), + '(n,n3He)': (-3, -2), + '(n,nd2a)': (-10, -5), + '(n,nt2a)': (-11, -5), + '(n,4n)': (-3, 0), + '(n,2np)': (-2, -1), + '(n,3np)': (-3, -1), + '(n,n2p)': (-2, -2), + '(n,npa)': (-5, -3), + '(n,gamma)': (1, 0), + '(n,p)': (0, -1), + '(n,d)': (-1, -1), + '(n,t)': (-2, -1), + '(n,3He)': (-2, -2), + '(n,a)': (-3, -2), + '(n,2a)': (-7, -4), + '(n,3a)': (-11, -6), + '(n,2p)': (-1, -2), + '(n,pa)': (-4, -3), + '(n,t2a)': (-10, -5), + '(n,d2a)': (-9, -5), + '(n,pd)': (-2, -2), + '(n,pt)': (-3, -2), + '(n,da)': (-5, -3), + '(n,5n)': (-4, 0), + '(n,6n)': (-5, 0), + '(n,2nt)': (-4, -1), + '(n,ta)': (-6, -3), + '(n,4np)': (-4, -1), + '(n,3nd)': (-4, -1), + '(n,nda)': (-6, -3), + '(n,2npa)': (-6, -3), + '(n,7n)': (-6, 0), + '(n,8n)': (-7, 0), + '(n,5np)': (-5, -1), + '(n,6np)': (-6, -1), + '(n,7np)': (-7, -1), + '(n,4na)': (-7, -2), + '(n,5na)': (-8, -2), + '(n,6na)': (-9, -2), + '(n,7na)': (-10, -2), + '(n,4nd)': (-5, -1), + '(n,5nd)': (-6, -1), + '(n,6nd)': (-7, -1), + '(n,3nt)': (-5, -1), + '(n,4nt)': (-6, -1), + '(n,5nt)': (-7, -1), + '(n,6nt)': (-8, -1), + '(n,2n3He)': (-4, -2), + '(n,3n3He)': (-5, -2), + '(n,4n3He)': (-6, -2), + '(n,3n2p)': (-4, -2), + '(n,3n2a)': (-10, -4), + '(n,3npa)': (-7, -3), + '(n,dt)': (-4, -2), + '(n,npd)': (-3, -2), + '(n,npt)': (-4, -2), + '(n,ndt)': (-5, -2), + '(n,np3He)': (-4, -3), + '(n,nd3He)': (-5, -3), + '(n,nt3He)': (-6, -3), + '(n,nta)': (-7, -3), + '(n,2n2p)': (-3, -2), + '(n,p3He)': (-4, -3), + '(n,d3He)': (-5, -3), + '(n,3Hea)': (-6, -4), + '(n,4n2p)': (-5, -2), + '(n,4n2a)': (-11, -4), + '(n,4npa)': (-8, -3), + '(n,3p)': (-2, -3), + '(n,n3p)': (-3, -3), + '(n,3n2pa)': (-8, -4), + '(n,5n2p)': (-6, -2), +} + # Values here are from the Committee on Data for Science and Technology # (CODATA) 2018 recommendation (https://physics.nist.gov/cuu/Constants/). diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 7b8993a2891..be3dab77abf 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -128,7 +128,7 @@ def get_yields(file_obj): isomeric_state = int(values[4*j + 1]) name = ATOMIC_SYMBOL[Z] + str(A) if isomeric_state > 0: - name += '_m{}'.format(isomeric_state) + name += f'_m{isomeric_state}' yield_j = ufloat(values[4*j + 2], values[4*j + 3]) yields[name] = yield_j @@ -257,9 +257,9 @@ def daughter(self): Z += delta_Z if self._daughter_state > 0: - return '{}{}_m{}'.format(ATOMIC_SYMBOL[Z], A, self._daughter_state) + return f'{ATOMIC_SYMBOL[Z]}{A}_m{self._daughter_state}' else: - return '{}{}'.format(ATOMIC_SYMBOL[Z], A) + return f'{ATOMIC_SYMBOL[Z]}{A}' @property def parent(self): @@ -350,10 +350,9 @@ def __init__(self, ev_or_filename): self.nuclide['mass_number'] = A self.nuclide['isomeric_state'] = metastable if metastable > 0: - self.nuclide['name'] = '{}{}_m{}'.format(ATOMIC_SYMBOL[Z], A, - metastable) + self.nuclide['name'] = f'{ATOMIC_SYMBOL[Z]}{A}_m{metastable}' else: - self.nuclide['name'] = '{}{}'.format(ATOMIC_SYMBOL[Z], A) + self.nuclide['name'] = f'{ATOMIC_SYMBOL[Z]}{A}' self.nuclide['mass'] = items[1] # AWR self.nuclide['excited_state'] = items[2] # State of the original nuclide self.nuclide['stable'] = (items[4] == 1) # Nucleus stability flag diff --git a/openmc/data/endf.c b/openmc/data/endf.c index 936fd3bbbe2..dd5eee54184 100644 --- a/openmc/data/endf.c +++ b/openmc/data/endf.c @@ -14,7 +14,7 @@ double cfloat_endf(const char* buffer, int n) { - char arr[12]; // 11 characters plus a null terminator + char arr[13]; // 11 characters plus e and a null terminator int j = 0; // current position in arr int found_significand = 0; int found_exponent = 0; diff --git a/openmc/data/endf.py b/openmc/data/endf.py index d526bc53f55..73299723a8b 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -449,8 +449,7 @@ def __init__(self, filename_or_obj): def __repr__(self): name = self.target['zsymam'].replace(' ', '') - return '<{} for {} {}>'.format(self.info['sublibrary'], name, - self.info['library']) + return f"<{self.info['sublibrary']} for {name} {self.info['library']}>" def _read_header(self): file_obj = io.StringIO(self.section[1, 451]) diff --git a/openmc/data/energy_distribution.py b/openmc/data/energy_distribution.py index a13893a68f1..069ab1b9beb 100644 --- a/openmc/data/energy_distribution.py +++ b/openmc/data/energy_distribution.py @@ -53,8 +53,7 @@ def from_hdf5(group): elif energy_type == 'continuous': return ContinuousTabular.from_hdf5(group) else: - raise ValueError("Unknown energy distribution type: {}" - .format(energy_type)) + raise ValueError(f"Unknown energy distribution type: {energy_type}") @staticmethod def from_endf(file_obj, params): @@ -277,7 +276,7 @@ def to_hdf5(self, group): """ - group.attrs['type'] = np.string_('maxwell') + group.attrs['type'] = np.bytes_('maxwell') group.attrs['u'] = self.u self.theta.to_hdf5(group, 'theta') @@ -410,7 +409,7 @@ def to_hdf5(self, group): """ - group.attrs['type'] = np.string_('evaporation') + group.attrs['type'] = np.bytes_('evaporation') group.attrs['u'] = self.u self.theta.to_hdf5(group, 'theta') @@ -556,7 +555,7 @@ def to_hdf5(self, group): """ - group.attrs['type'] = np.string_('watt') + group.attrs['type'] = np.bytes_('watt') group.attrs['u'] = self.u self.a.to_hdf5(group, 'a') self.b.to_hdf5(group, 'b') @@ -728,7 +727,7 @@ def to_hdf5(self, group): """ - group.attrs['type'] = np.string_('madland-nix') + group.attrs['type'] = np.bytes_('madland-nix') group.attrs['efl'] = self.efl group.attrs['efh'] = self.efh self.tm.to_hdf5(group) @@ -846,7 +845,7 @@ def to_hdf5(self, group): """ - group.attrs['type'] = np.string_('discrete_photon') + group.attrs['type'] = np.bytes_('discrete_photon') group.attrs['primary_flag'] = self.primary_flag group.attrs['energy'] = self.energy group.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio @@ -945,7 +944,7 @@ def to_hdf5(self, group): """ - group.attrs['type'] = np.string_('level') + group.attrs['type'] = np.bytes_('level') group.attrs['threshold'] = self.threshold group.attrs['mass_ratio'] = self.mass_ratio @@ -1074,7 +1073,7 @@ def to_hdf5(self, group): """ - group.attrs['type'] = np.string_('continuous') + group.attrs['type'] = np.bytes_('continuous') dset = group.create_dataset('energy', data=self.energy) dset.attrs['interpolation'] = np.vstack((self.breakpoints, diff --git a/openmc/data/function.py b/openmc/data/function.py index 299924b37cc..23fd5e9d4fa 100644 --- a/openmc/data/function.py +++ b/openmc/data/function.py @@ -364,7 +364,7 @@ def to_hdf5(self, group, name='xy'): """ dataset = group.create_dataset(name, data=np.vstack( [self.x, self.y])) - dataset.attrs['type'] = np.string_(type(self).__name__) + dataset.attrs['type'] = np.bytes_(type(self).__name__) dataset.attrs['breakpoints'] = self.breakpoints dataset.attrs['interpolation'] = self.interpolation @@ -460,7 +460,7 @@ def to_hdf5(self, group, name='xy'): """ dataset = group.create_dataset(name, data=self.coef) - dataset.attrs['type'] = np.string_(type(self).__name__) + dataset.attrs['type'] = np.bytes_(type(self).__name__) @classmethod def from_hdf5(cls, dataset): @@ -592,7 +592,7 @@ def to_hdf5(self, group, name='xy'): """ sum_group = group.create_group(name) - sum_group.attrs['type'] = np.string_(type(self).__name__) + sum_group.attrs['type'] = np.bytes_(type(self).__name__) sum_group.attrs['n'] = len(self.functions) for i, f in enumerate(self.functions): f.to_hdf5(sum_group, f'func_{i+1}') diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index b49399139d5..d92bf9c213a 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -366,7 +366,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_('kalbach-mann') + group.attrs['type'] = np.bytes_('kalbach-mann') dset = group.create_dataset('energy', data=self.energy) dset.attrs['interpolation'] = np.vstack((self.breakpoints, diff --git a/openmc/data/library.py b/openmc/data/library.py index 210270749ac..a6ce1bbd32c 100644 --- a/openmc/data/library.py +++ b/openmc/data/library.py @@ -1,8 +1,8 @@ import os -import lxml.etree as ET import pathlib import h5py +import lxml.etree as ET import openmc from openmc._xml import clean_indentation, reorder_attributes @@ -15,7 +15,7 @@ class DataLibrary(list): cross section data from a single file. The dictionary has keys 'path', 'type', and 'materials'. - .. versionchanged:: 0.13.4 + .. versionchanged:: 0.14.0 This class now behaves like a list rather than requiring you to access the list of libraries through a special attribute. @@ -93,8 +93,7 @@ def register_file(self, filename): materials = list(h5file) else: raise ValueError( - "File type {} not supported by {}" - .format(path.name, self.__class__.__name__)) + f"File type {path.name} not supported by {self.__class__.__name__}") library = {'path': str(path), 'type': filetype, 'materials': materials} self.append(library) diff --git a/openmc/data/multipole.py b/openmc/data/multipole.py index 9fd6c9a9bea..dd14e0d1945 100644 --- a/openmc/data/multipole.py +++ b/openmc/data/multipole.py @@ -194,9 +194,8 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, test_xs_ref[i] = np.interp(test_energy, energy, ce_xs[i]) if log: - print(" energy: {:.3e} to {:.3e} eV ({} points)".format( - energy[0], energy[-1], ne)) - print(" error tolerance: rtol={}, atol={}".format(rtol, atol)) + print(f" energy: {energy[0]:.3e} to {energy[-1]:.3e} eV ({ne} points)") + print(f" error tolerance: rtol={rtol}, atol={atol}") # transform xs (sigma) and energy (E) to f (sigma*E) and s (sqrt(E)) to be # compatible with the multipole representation @@ -230,8 +229,8 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, orders = list(range(lowest_order, highest_order + 1, 2)) if log: - print("Found {} peaks".format(n_peaks)) - print("Fitting orders from {} to {}".format(orders[0], orders[-1])) + print(f"Found {n_peaks} peaks") + print(f"Fitting orders from {orders[0]} to {orders[-1]}") # perform VF with increasing orders found_ideal = False @@ -239,7 +238,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, best_quality = best_ratio = -np.inf for i, order in enumerate(orders): if log: - print("Order={}({}/{})".format(order, i, len(orders))) + print(f"Order={order}({i}/{len(orders)})") # initial guessed poles poles_r = np.linspace(s[0], s[-1], order//2) poles = poles_r + poles_r*0.01j @@ -249,7 +248,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, # fitting iteration for i_vf in range(n_vf_iter): if log >= DETAILED_LOGGING: - print("VF iteration {}/{}".format(i_vf + 1, n_vf_iter)) + print(f"VF iteration {i_vf + 1}/{n_vf_iter}") # call vf poles, residues, cf, f_fit, rms = vf.vectfit(f, s, poles, weight) @@ -268,7 +267,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, # re-calculate residues if poles changed if n_real_poles > 0: if log >= DETAILED_LOGGING: - print(" # real poles: {}".format(n_real_poles)) + print(f" # real poles: {n_real_poles}") new_poles, residues, cf, f_fit, rms = \ vf.vectfit(f, s, new_poles, weight, skip_pole=True) @@ -296,10 +295,10 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, quality = -np.inf if log >= DETAILED_LOGGING: - print(" # poles: {}".format(new_poles.size)) - print(" Max relative error: {:.3f}%".format(maxre*100)) - print(" Satisfaction: {:.1f}%, {:.1f}%".format(ratio*100, ratio2*100)) - print(" Quality: {:.2f}".format(quality)) + print(f" # poles: {new_poles.size}") + print(f" Max relative error: {maxre * 100:.3f}%") + print(f" Satisfaction: {ratio * 100:.1f}%, {ratio2 * 100:.1f}%") + print(f" Quality: {quality:.2f}") if quality > best_quality: if log >= DETAILED_LOGGING: @@ -354,7 +353,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, mp_residues = np.concatenate((best_residues[:, real_idx], best_residues[:, conj_idx]*2), axis=1)/1j if log: - print("Final number of poles: {}".format(mp_poles.size)) + print(f"Final number of poles: {mp_poles.size}") if path_out: if not os.path.exists(path_out): @@ -378,14 +377,14 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, ax2.set_ylabel('relative error', color='r') ax2.tick_params('y', colors='r') - plt.title("MT {} vector fitted with {} poles".format(mt, mp_poles.size)) + plt.title(f"MT {mt} vector fitted with {mp_poles.size} poles") fig.tight_layout() fig_file = os.path.join(path_out, "{:.0f}-{:.0f}_MT{}.png".format( energy[0], energy[-1], mt)) plt.savefig(fig_file) plt.close() if log: - print("Saved figure: {}".format(fig_file)) + print(f"Saved figure: {fig_file}") return (mp_poles, mp_residues) @@ -423,7 +422,7 @@ def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, # make 0K ACE data using njoy if log: - print("Running NJOY to get 0K point-wise data (error={})...".format(njoy_error)) + print(f"Running NJOY to get 0K point-wise data (error={njoy_error})...") nuc_ce = IncidentNeutron.from_njoy(endf_file, temperatures=[0.0], error=njoy_error, broadr=False, heatr=False, purr=False) @@ -477,9 +476,8 @@ def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, mts = [2, 27] if log: - print(" MTs: {}".format(mts)) - print(" Energy range: {:.3e} to {:.3e} eV ({} points)".format( - E_min, E_max, n_points)) + print(f" MTs: {mts}") + print(f" Energy range: {E_min:.3e} to {E_max:.3e} eV ({n_points} points)") # ====================================================================== # PERFORM VECTOR FITTING @@ -500,7 +498,7 @@ def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, # VF piece by piece for i_piece in range(vf_pieces): if log: - print("Vector fitting piece {}/{}...".format(i_piece + 1, vf_pieces)) + print(f"Vector fitting piece {i_piece + 1}/{vf_pieces}...") # start E of this piece e_bound = (sqrt(E_min) + piece_width*(i_piece-0.5))**2 if i_piece == 0 or sqrt(alpha*e_bound) < 4.0: @@ -534,12 +532,12 @@ def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, if not os.path.exists(path_out): os.makedirs(path_out) if not mp_filename: - mp_filename = "{}_mp.pickle".format(nuc_ce.name) + mp_filename = f"{nuc_ce.name}_mp.pickle" mp_filename = os.path.join(path_out, mp_filename) with open(mp_filename, 'wb') as f: pickle.dump(mp_data, f) if log: - print("Dumped multipole data to file: {}".format(mp_filename)) + print(f"Dumped multipole data to file: {mp_filename}") return mp_data @@ -605,9 +603,8 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, if log: print("Windowing:") - print(" config: # windows={}, spacing={}, CF order={}".format( - n_win, spacing, n_cf)) - print(" error tolerance: rtol={}, atol={}".format(rtol, atol)) + print(f" config: # windows={n_win}, spacing={spacing}, CF order={n_cf}") + print(f" error tolerance: rtol={rtol}, atol={atol}") # sort poles (and residues) by the real component of the pole for ip in range(n_pieces): @@ -623,7 +620,7 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, win_data = [] for iw in range(n_win): if log >= DETAILED_LOGGING: - print("Processing window {}/{}...".format(iw + 1, n_win)) + print(f"Processing window {iw + 1}/{n_win}...") # inner window boundaries inbegin = sqrt(E_min) + spacing * iw @@ -658,7 +655,7 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, lp = rp = center_pole_ind while True: if log >= DETAILED_LOGGING: - print("Trying poles {} to {}".format(lp, rp)) + print(f"Trying poles {lp} to {rp}") # calculate the cross sections contributed by the windowed poles if rp > lp: @@ -1108,7 +1105,7 @@ def from_multipole(cls, mp_data, search=None, log=False, **kwargs): for n_w in np.unique(np.linspace(n_win_min, n_win_max, 20, dtype=int)): for n_cf in range(10, 1, -1): if log: - print("Testing N_win={} N_cf={}".format(n_w, n_cf)) + print(f"Testing N_win={n_w} N_cf={n_cf}") # update arguments dictionary kwargs.update(n_win=n_w, n_cf=n_cf) @@ -1169,10 +1166,11 @@ def _evaluate(self, E, T): sqrtE = sqrt(E) invE = 1.0 / E - # Locate us. The i_window calc omits a + 1 present in F90 because of - # the 1-based vs. 0-based indexing. Similarly startw needs to be - # decreased by 1. endw does not need to be decreased because - # range(startw, endw) does not include endw. + # Locate us. The i_window calc omits a + 1 present from the legacy + # Fortran version of OpenMC because of the 1-based vs. 0-based + # indexing. Similarly startw needs to be decreased by 1. endw does + # not need to be decreased because range(startw, endw) does not include + # endw. i_window = min(self.n_windows - 1, int(np.floor((sqrtE - sqrt(self.E_min)) / self.spacing))) startw = self.windows[i_window, 0] - 1 @@ -1273,7 +1271,7 @@ def export_to_hdf5(self, path, mode='a', libver='earliest'): # Open file and write version. with h5py.File(str(path), mode, libver=libver) as f: - f.attrs['filetype'] = np.string_('data_wmp') + f.attrs['filetype'] = np.bytes_('data_wmp') f.attrs['version'] = np.array(WMP_VERSION) g = f.create_group(self.name) diff --git a/openmc/data/nbody.py b/openmc/data/nbody.py index 1f2ff5b4d1c..ec1ac25c0fd 100644 --- a/openmc/data/nbody.py +++ b/openmc/data/nbody.py @@ -91,7 +91,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_('nbody') + group.attrs['type'] = np.bytes_('nbody') group.attrs['total_mass'] = self.total_mass group.attrs['n_particles'] = self.n_particles group.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index e0c0032f072..894be187178 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -122,10 +122,10 @@ def __getitem__(self, mt): if len(mts) > 0: return self._get_redundant_reaction(mt, mts) else: - raise KeyError('No reaction with MT={}.'.format(mt)) + raise KeyError(f'No reaction with MT={mt}.') def __repr__(self): - return "".format(self.name) + return f"" def __iter__(self): return iter(self.reactions.values()) @@ -231,7 +231,7 @@ def urr(self, urr): @property def temperatures(self): - return ["{}K".format(int(round(kT / K_BOLTZMANN))) for kT in self.kTs] + return [f"{int(round(kT / K_BOLTZMANN))}K" for kT in self.kTs] @property def atomic_symbol(self): @@ -261,7 +261,7 @@ def add_temperature_from_ace(self, ace_or_filename, metastable_scheme='nndc'): # Check if temprature already exists strT = data.temperatures[0] if strT in self.temperatures: - warn('Cross sections at T={} already exist.'.format(strT)) + warn(f'Cross sections at T={strT} already exist.') return # Check that name matches @@ -425,7 +425,7 @@ def export_to_hdf5(self, path, mode='a', libver='earliest'): # Open file and write version with h5py.File(str(path), mode, libver=libver) as f: - f.attrs['filetype'] = np.string_('data_neutron') + f.attrs['filetype'] = np.bytes_('data_neutron') f.attrs['version'] = np.array(HDF5_VERSION) # Write basic data @@ -461,7 +461,7 @@ def export_to_hdf5(self, path, mode='a', libver='earliest'): if not (photon_rx or rx.mt in keep_mts): continue - rx_group = rxs_group.create_group('reaction_{:03}'.format(rx.mt)) + rx_group = rxs_group.create_group(f'reaction_{rx.mt:03}') rx.to_hdf5(rx_group) # Write total nu data if available @@ -593,7 +593,7 @@ def from_ace(cls, ace_or_filename, metastable_scheme='nndc'): zaid, xs = ace.name.split('.') if not xs.endswith('c'): raise TypeError( - "{} is not a continuous-energy neutron ACE table.".format(ace)) + f"{ace} is not a continuous-energy neutron ACE table.") name, element, Z, mass_number, metastable = \ get_metadata(int(zaid), metastable_scheme) @@ -732,9 +732,9 @@ def from_endf(cls, ev_or_filename, covariance=False): # Determine name element = ATOMIC_SYMBOL[atomic_number] if metastable > 0: - name = '{}{}_m{}'.format(element, mass_number, metastable) + name = f'{element}{mass_number}_m{metastable}' else: - name = '{}{}'.format(element, mass_number) + name = f'{element}{mass_number}' # Instantiate incident neutron data data = cls(name, atomic_number, mass_number, metastable, diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 9c2055cc48b..c1dcbab17f5 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -188,7 +188,7 @@ def run(commands, tapein, tapeout, input_filename=None, stdout=False, with tempfile.TemporaryDirectory() as tmpdir: # Copy evaluations to appropriates 'tapes' for tape_num, filename in tapein.items(): - tmpfilename = os.path.join(tmpdir, 'tape{}'.format(tape_num)) + tmpfilename = os.path.join(tmpdir, f'tape{tape_num}') shutil.copy(str(filename), tmpfilename) # Start up NJOY process @@ -216,7 +216,7 @@ def run(commands, tapein, tapeout, input_filename=None, stdout=False, # Copy output files back to original directory for tape_num, filename in tapeout.items(): - tmpfilename = os.path.join(tmpdir, 'tape{}'.format(tape_num)) + tmpfilename = os.path.join(tmpdir, f'tape{tape_num}') if os.path.isfile(tmpfilename): shutil.move(tmpfilename, str(filename)) @@ -317,7 +317,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, else: output_dir = Path(output_dir) if not output_dir.is_dir(): - raise IOError("{} is not a directory".format(output_dir)) + raise IOError(f"{output_dir} is not a directory") ev = evaluation if evaluation is not None else endf.Evaluation(filename) mat = ev.material @@ -389,12 +389,12 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, # Extend input with an ACER run for each temperature nace = nacer_in + 1 + 2*i ndir = nace + 1 - ext = '{:02}'.format(i + 1) + ext = f'{i + 1:02}' commands += _TEMPLATE_ACER.format(**locals()) # Indicate tapes to save for each ACER run - tapeout[nace] = output_dir / "ace_{:.1f}".format(temperature) - tapeout[ndir] = output_dir / "xsdir_{:.1f}".format(temperature) + tapeout[nace] = output_dir / f"ace_{temperature:.1f}" + tapeout[ndir] = output_dir / f"xsdir_{temperature:.1f}" commands += 'stop\n' run(commands, tapein, tapeout, **kwargs) @@ -404,7 +404,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, with ace.open('w') as ace_file, xsdir.open('w') as xsdir_file: for temperature in temperatures: # Get contents of ACE file - text = (output_dir / "ace_{:.1f}".format(temperature)).read_text() + text = (output_dir / f"ace_{temperature:.1f}").read_text() # If the target is metastable, make sure that ZAID in the ACE # file reflects this by adding 400 @@ -417,13 +417,13 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, ace_file.write(text) # Concatenate into destination xsdir file - xsdir_in = output_dir / "xsdir_{:.1f}".format(temperature) + xsdir_in = output_dir / f"xsdir_{temperature:.1f}" xsdir_file.write(xsdir_in.read_text()) # Remove ACE/xsdir files for each temperature for temperature in temperatures: - (output_dir / "ace_{:.1f}".format(temperature)).unlink() - (output_dir / "xsdir_{:.1f}".format(temperature)).unlink() + (output_dir / f"ace_{temperature:.1f}").unlink() + (output_dir / f"xsdir_{temperature:.1f}").unlink() def make_ace_thermal(filename, filename_thermal, temperatures=None, @@ -480,7 +480,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, else: output_dir = Path(output_dir) if not output_dir.is_dir(): - raise IOError("{} is not a directory".format(output_dir)) + raise IOError(f"{output_dir} is not a directory") ev = evaluation if evaluation is not None else endf.Evaluation(filename) mat = ev.material @@ -581,12 +581,12 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, # Extend input with an ACER run for each temperature nace = nthermal_acer_in + 1 + 2*i ndir = nace + 1 - ext = '{:02}'.format(i + 1) + ext = f'{i + 1:02}' commands += _THERMAL_TEMPLATE_ACER.format(**locals()) # Indicate tapes to save for each ACER run - tapeout[nace] = output_dir / "ace_{:.1f}".format(temperature) - tapeout[ndir] = output_dir / "xsdir_{:.1f}".format(temperature) + tapeout[nace] = output_dir / f"ace_{temperature:.1f}" + tapeout[ndir] = output_dir / f"xsdir_{temperature:.1f}" commands += 'stop\n' run(commands, tapein, tapeout, **kwargs) @@ -595,13 +595,13 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, with ace.open('w') as ace_file, xsdir.open('w') as xsdir_file: # Concatenate ACE and xsdir files together for temperature in temperatures: - ace_in = output_dir / "ace_{:.1f}".format(temperature) + ace_in = output_dir / f"ace_{temperature:.1f}" ace_file.write(ace_in.read_text()) - xsdir_in = output_dir / "xsdir_{:.1f}".format(temperature) + xsdir_in = output_dir / f"xsdir_{temperature:.1f}" xsdir_file.write(xsdir_in.read_text()) # Remove ACE/xsdir files for each temperature for temperature in temperatures: - (output_dir / "ace_{:.1f}".format(temperature)).unlink() - (output_dir / "xsdir_{:.1f}".format(temperature)).unlink() + (output_dir / f"ace_{temperature:.1f}").unlink() + (output_dir / f"xsdir_{temperature:.1f}").unlink() diff --git a/openmc/data/photon.py b/openmc/data/photon.py index 7c96e9fac0d..c9d51f06238 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -451,10 +451,10 @@ def __getitem__(self, mt): if mt in self.reactions: return self.reactions[mt] else: - raise KeyError('No reaction with MT={}.'.format(mt)) + raise KeyError(f'No reaction with MT={mt}.') def __repr__(self): - return "".format(self.name) + return f"" def __iter__(self): return iter(self.reactions.values()) @@ -508,7 +508,7 @@ def from_ace(cls, ace_or_filename): # Get atomic number based on name of ACE table zaid, xs = ace.name.split('.') if not xs.endswith('p'): - raise TypeError("{} is not a photoatomic transport ACE table.".format(ace)) + raise TypeError(f"{ace} is not a photoatomic transport ACE table.") Z = get_metadata(int(zaid))[2] # Read each reaction @@ -638,7 +638,7 @@ def from_endf(cls, photoatomic, relaxation=None): with h5py.File(filename, 'r') as f: _COMPTON_PROFILES['pz'] = f['pz'][()] for i in range(1, 101): - group = f['{:03}'.format(i)] + group = f[f'{i:03}'] num_electrons = group['num_electrons'][()] binding_energy = group['binding_energy'][()]*EV_PER_MEV J = group['J'][()] @@ -713,7 +713,7 @@ def from_hdf5(cls, group_or_filename): # Check for necessary reactions for mt in (502, 504, 522): - assert mt in data, "Reaction {} not found".format(mt) + assert mt in data, f"Reaction {mt} not found" # Read atomic relaxation data.atomic_relaxation = AtomicRelaxation.from_hdf5(group['subshells']) @@ -764,7 +764,7 @@ def export_to_hdf5(self, path, mode='a', libver='earliest'): """ with h5py.File(str(path), mode, libver=libver) as f: # Write filetype and version - f.attrs['filetype'] = np.string_('data_photon') + f.attrs['filetype'] = np.bytes_('data_photon') if 'version' not in f.attrs: f.attrs['version'] = np.array(HDF5_VERSION) @@ -836,7 +836,7 @@ def _add_bremsstrahlung(self): filename = os.path.join(os.path.dirname(__file__), 'density_effect.h5') with h5py.File(filename, 'r') as f: for i in range(1, 101): - group = f['{:03}'.format(i)] + group = f[f'{i:03}'] _BREMSSTRAHLUNG[i] = { 'I': group.attrs['I'], 'num_electrons': group['num_electrons'][()], @@ -924,10 +924,9 @@ def __init__(self, mt): def __repr__(self): if self.mt in _REACTION_NAME: - return "".format( - self.mt, _REACTION_NAME[self.mt][0]) + return f"" else: - return "".format(self.mt) + return f"" @property def anomalous_real(self): diff --git a/openmc/data/product.py b/openmc/data/product.py index 93697a9e7b7..88c83b81fd4 100644 --- a/openmc/data/product.py +++ b/openmc/data/product.py @@ -124,8 +124,8 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['particle'] = np.string_(self.particle) - group.attrs['emission_mode'] = np.string_(self.emission_mode) + group.attrs['particle'] = np.bytes_(self.particle) + group.attrs['emission_mode'] = np.bytes_(self.emission_mode) if self.decay_rate > 0.0: group.attrs['decay_rate'] = self.decay_rate @@ -135,7 +135,7 @@ def to_hdf5(self, group): # Write applicability/distribution group.attrs['n_distribution'] = len(self.distribution) for i, d in enumerate(self.distribution): - dgroup = group.create_group('distribution_{}'.format(i)) + dgroup = group.create_group(f'distribution_{i}') if self.applicability: self.applicability[i].to_hdf5(dgroup, 'applicability') d.to_hdf5(dgroup) @@ -170,7 +170,7 @@ def from_hdf5(cls, group): distribution = [] applicability = [] for i in range(n_distribution): - dgroup = group['distribution_{}'.format(i)] + dgroup = group[f'distribution_{i}'] if 'applicability' in dgroup: applicability.append(Tabulated1D.from_hdf5( dgroup['applicability'])) diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 04a68d7399e..8f90d2de471 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -56,13 +56,13 @@ 301: 'heating', 444: 'damage-energy', 649: '(n,pc)', 699: '(n,dc)', 749: '(n,tc)', 799: '(n,3Hec)', 849: '(n,ac)', 891: '(n,2nc)', 901: 'heating-local'} -REACTION_NAME.update({i: '(n,n{})'.format(i - 50) for i in range(51, 91)}) -REACTION_NAME.update({i: '(n,p{})'.format(i - 600) for i in range(600, 649)}) -REACTION_NAME.update({i: '(n,d{})'.format(i - 650) for i in range(650, 699)}) -REACTION_NAME.update({i: '(n,t{})'.format(i - 700) for i in range(700, 749)}) -REACTION_NAME.update({i: '(n,3He{})'.format(i - 750) for i in range(750, 799)}) -REACTION_NAME.update({i: '(n,a{})'.format(i - 800) for i in range(800, 849)}) -REACTION_NAME.update({i: '(n,2n{})'.format(i - 875) for i in range(875, 891)}) +REACTION_NAME.update({i: f'(n,n{i - 50})' for i in range(51, 91)}) +REACTION_NAME.update({i: f'(n,p{i - 600})' for i in range(600, 649)}) +REACTION_NAME.update({i: f'(n,d{i - 650})' for i in range(650, 699)}) +REACTION_NAME.update({i: f'(n,t{i - 700})' for i in range(700, 749)}) +REACTION_NAME.update({i: f'(n,3He{i - 750})' for i in range(750, 799)}) +REACTION_NAME.update({i: f'(n,a{i - 800})' for i in range(800, 849)}) +REACTION_NAME.update({i: f'(n,2n{i - 875})' for i in range(875, 891)}) REACTION_MT = {name: mt for mt, name in REACTION_NAME.items()} REACTION_MT['fission'] = 18 @@ -119,7 +119,7 @@ def _get_products(ev, mt): p = Product('electron') else: Z, A = divmod(za, 1000) - p = Product('{}{}'.format(ATOMIC_SYMBOL[Z], A)) + p = Product(f'{ATOMIC_SYMBOL[Z]}{A}') p.yield_ = yield_ @@ -557,9 +557,9 @@ def _get_activation_products(ev, rx): # Get GNDS name for product symbol = ATOMIC_SYMBOL[Z] if excited_state > 0: - name = '{}{}_e{}'.format(symbol, A, excited_state) + name = f'{symbol}{A}_e{excited_state}' else: - name = '{}{}'.format(symbol, A) + name = f'{symbol}{A}' p = Product(name) if mf == 9: @@ -656,8 +656,7 @@ def _get_photon_products_ace(ace, rx): photon.yield_ = Tabulated1D(energy, yield_) else: - raise ValueError("MFTYPE must be 12, 13, 16. Got {0}".format( - mftype)) + raise ValueError(f"MFTYPE must be 12, 13, 16. Got {mftype}") # ================================================================== # Photon energy distribution @@ -846,9 +845,9 @@ def __init__(self, mt): def __repr__(self): if self.mt in REACTION_NAME: - return "".format(self.mt, REACTION_NAME[self.mt]) + return f"" else: - return "".format(self.mt) + return f"" @property def center_of_mass(self): @@ -920,9 +919,9 @@ def to_hdf5(self, group): group.attrs['mt'] = self.mt if self.mt in REACTION_NAME: - group.attrs['label'] = np.string_(REACTION_NAME[self.mt]) + group.attrs['label'] = np.bytes_(REACTION_NAME[self.mt]) else: - group.attrs['label'] = np.string_(self.mt) + group.attrs['label'] = np.bytes_(self.mt) group.attrs['Q_value'] = self.q_value group.attrs['center_of_mass'] = 1 if self.center_of_mass else 0 group.attrs['redundant'] = 1 if self.redundant else 0 @@ -933,7 +932,7 @@ def to_hdf5(self, group): threshold_idx = getattr(self.xs[T], '_threshold_idx', 0) dset.attrs['threshold_idx'] = threshold_idx for i, p in enumerate(self.products): - pgroup = group.create_group('product_{}'.format(i)) + pgroup = group.create_group(f'product_{i}') p.to_hdf5(pgroup) @classmethod @@ -985,7 +984,7 @@ def from_hdf5(cls, group, energy): # Read reaction products for i in range(n_product): - pgroup = group['product_{}'.format(i)] + pgroup = group[f'product_{i}'] rx.products.append(Product.from_hdf5(pgroup)) return rx diff --git a/openmc/data/resonance.py b/openmc/data/resonance.py index 340d73a186e..31e230df588 100644 --- a/openmc/data/resonance.py +++ b/openmc/data/resonance.py @@ -830,7 +830,7 @@ def from_endf(cls, ev, file_obj, items): elif mt == 102: columns.append('captureWidth') else: - columns.append('width (MT={})'.format(mt)) + columns.append(f'width (MT={mt})') # Create Pandas dataframe with resonance parameters parameters = pd.DataFrame.from_records(records, columns=columns) @@ -896,7 +896,7 @@ def __init__(self, spin, parity, channels, parameters): self.parameters = parameters def __repr__(self): - return ''.format(self.spin, self.parity) + return f'' class Unresolved(ResonanceRange): diff --git a/openmc/data/resonance_covariance.py b/openmc/data/resonance_covariance.py index 9ba429cb486..7096570449c 100644 --- a/openmc/data/resonance_covariance.py +++ b/openmc/data/resonance_covariance.py @@ -239,14 +239,14 @@ def sample(self, n_samples): samples = [] # Handling MLBW/SLBW sampling + rng = np.random.default_rng() if formalism == 'mlbw' or formalism == 'slbw': params = ['energy', 'neutronWidth', 'captureWidth', 'fissionWidth', 'competitiveWidth'] param_list = params[:mpar] mean_array = parameters[param_list].values mean = mean_array.flatten() - par_samples = np.random.multivariate_normal(mean, cov, - size=n_samples) + par_samples = rng.multivariate_normal(mean, cov, size=n_samples) spin = parameters['J'].values l_value = parameters['L'].values for sample in par_samples: @@ -277,8 +277,7 @@ def sample(self, n_samples): param_list = params[:mpar] mean_array = parameters[param_list].values mean = mean_array.flatten() - par_samples = np.random.multivariate_normal(mean, cov, - size=n_samples) + par_samples = rng.multivariate_normal(mean, cov, size=n_samples) spin = parameters['J'].values l_value = parameters['L'].values for sample in par_samples: diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 994a8ed2ae8..f5841d9c90b 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -90,7 +90,7 @@ def _temperature_str(T): # round() normally returns an int when called with a single argument, but # numpy floats overload rounding to return another float - return "{}K".format(int(round(T))) + return f"{int(round(T))}K" def get_thermal_name(name): @@ -221,7 +221,7 @@ def to_hdf5(self, group, name): """ dataset = group.create_dataset(name, data=np.vstack( [self.bragg_edges, self.factors])) - dataset.attrs['type'] = np.string_(type(self).__name__) + dataset.attrs['type'] = np.bytes_(type(self).__name__) @classmethod def from_hdf5(cls, dataset): @@ -294,7 +294,7 @@ def to_hdf5(self, group, name): """ data = np.array([self.bound_xs, self.debye_waller]) dataset = group.create_dataset(name, data=data) - dataset.attrs['type'] = np.string_(type(self).__name__) + dataset.attrs['type'] = np.bytes_(type(self).__name__) @classmethod def from_hdf5(cls, dataset): @@ -439,7 +439,7 @@ def __init__(self, name, atomic_weight_ratio, energy_max, kTs): def __repr__(self): if hasattr(self, 'name'): - return "".format(self.name) + return f"" else: return "" @@ -464,7 +464,7 @@ def export_to_hdf5(self, path, mode='a', libver='earliest'): """ # Open file and write version with h5py.File(str(path), mode, libver=libver) as f: - f.attrs['filetype'] = np.string_('data_thermal') + f.attrs['filetype'] = np.bytes_('data_thermal') f.attrs['version'] = np.array(HDF5_VERSION) # Write basic data @@ -506,7 +506,7 @@ def add_temperature_from_ace(self, ace_or_filename, name=None): # Check if temprature already exists strT = data.temperatures[0] if strT in self.temperatures: - warn('S(a,b) data at T={} already exists.'.format(strT)) + warn(f'S(a,b) data at T={strT} already exists.') return # Check that name matches @@ -614,7 +614,7 @@ def from_ace(cls, ace_or_filename, name=None): # Get new name that is GND-consistent ace_name, xs = ace.name.split('.') if not xs.endswith('t'): - raise TypeError("{} is not a thermal scattering ACE table.".format(ace)) + raise TypeError(f"{ace} is not a thermal scattering ACE table.") if name is None: name = get_thermal_name(ace_name) diff --git a/openmc/data/thermal_angle_energy.py b/openmc/data/thermal_angle_energy.py index 17a560092d1..0dd2a0b7a51 100644 --- a/openmc/data/thermal_angle_energy.py +++ b/openmc/data/thermal_angle_energy.py @@ -43,7 +43,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_('coherent_elastic') + group.attrs['type'] = np.bytes_('coherent_elastic') self.coherent_xs.to_hdf5(group, 'coherent_xs') @classmethod @@ -104,7 +104,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_('incoherent_elastic') + group.attrs['type'] = np.bytes_('incoherent_elastic') group.create_dataset('debye_waller', data=self.debye_waller) @classmethod @@ -146,7 +146,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_('incoherent_elastic_discrete') + group.attrs['type'] = np.bytes_('incoherent_elastic_discrete') group.create_dataset('mu_out', data=self.mu_out) @classmethod @@ -203,7 +203,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_('incoherent_inelastic_discrete') + group.attrs['type'] = np.bytes_('incoherent_inelastic_discrete') group.create_dataset('energy_out', data=self.energy_out) group.create_dataset('mu_out', data=self.mu_out) group.create_dataset('skewed', data=self.skewed) @@ -266,7 +266,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_('mixed_elastic') + group.attrs['type'] = np.bytes_('mixed_elastic') coherent_group = group.create_group('coherent') self.coherent.to_hdf5(coherent_group) incoherent_group = group.create_group('incoherent') diff --git a/openmc/data/uncorrelated.py b/openmc/data/uncorrelated.py index 9dcb18bf616..141007b70a9 100644 --- a/openmc/data/uncorrelated.py +++ b/openmc/data/uncorrelated.py @@ -63,7 +63,7 @@ def to_hdf5(self, group): HDF5 group to write to """ - group.attrs['type'] = np.string_('uncorrelated') + group.attrs['type'] = np.bytes_('uncorrelated') if self.angle is not None: angle_group = group.create_group('angle') self.angle.to_hdf5(angle_group) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 1acfe6b1a80..5d9b8a2403b 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -3,27 +3,30 @@ This module contains Abstract Base Classes for implementing operator, integrator, depletion system solver, and operator helper classes """ +from __future__ import annotations from abc import ABC, abstractmethod from collections import namedtuple, defaultdict from collections.abc import Iterable, Callable from copy import deepcopy from inspect import signature from numbers import Real, Integral -from contextlib import contextmanager -import os from pathlib import Path import time +from typing import Optional, Union, Sequence from warnings import warn -from numpy import nonzero, empty, asarray +import numpy as np from uncertainties import ufloat -from openmc.checkvalue import check_type, check_greater_than +from openmc.checkvalue import check_type, check_greater_than, PathLike from openmc.mpi import comm +from openmc.utility_funcs import change_directory +from openmc import Material from .stepresult import StepResult from .chain import Chain from .results import Results from .pool import deplete +from .reaction_rates import ReactionRates from .transfer_rates import TransferRates @@ -57,24 +60,6 @@ # Can't set __doc__ on properties on Python 3.4 pass -@contextmanager -def change_directory(output_dir): - """ - Helper function for managing the current directory. - - Parameters - ---------- - output_dir : pathlib.Path - Directory to switch to. - """ - orig_dir = os.getcwd() - try: - output_dir.mkdir(parents=True, exist_ok=True) - os.chdir(output_dir) - yield - finally: - os.chdir(orig_dir) - class TransportOperator(ABC): """Abstract class defining a transport operator @@ -175,7 +160,7 @@ def finalize(self): pass @abstractmethod - def write_bos_data(self, step): + def write_bos_data(self, step: int): """Document beginning of step data for a given step Called at the beginning of a depletion step and at @@ -214,7 +199,7 @@ class ReactionRateHelper(ABC): def __init__(self, n_nucs, n_react): self._nuclides = None - self._results_cache = empty((n_nucs, n_react)) + self._results_cache = np.empty((n_nucs, n_react)) @abstractmethod def generate_tallies(self, materials, scores): @@ -231,7 +216,12 @@ def nuclides(self, nuclides): self._nuclides = nuclides @abstractmethod - def get_material_rates(self, mat_id, nuc_index, react_index): + def get_material_rates( + self, + mat_id: int, + nuc_index: Sequence[str], + react_index: Sequence[str] + ): """Return 2D array of [nuclide, reaction] reaction rates Parameters @@ -244,7 +234,7 @@ def get_material_rates(self, mat_id, nuc_index, react_index): Ordering of reactions """ - def divide_by_atoms(self, number): + def divide_by_atoms(self, number: Sequence[float]): """Normalize reaction rates by number of atoms Acts on the current material examined by :meth:`get_material_rates` @@ -261,7 +251,7 @@ def divide_by_atoms(self, number): normalized by the number of nuclides """ - mask = nonzero(number) + mask = np.nonzero(number) results = self._results_cache for col in range(results.shape[1]): results[mask, col] /= number[mask] @@ -293,7 +283,7 @@ def reset(self): """Reset state for normalization""" @abstractmethod - def prepare(self, chain_nucs, rate_index): + def prepare(self, chain_nucs: Sequence[str], rate_index: dict): """Perform work needed to obtain energy produced This method is called prior to calculating the reaction rates @@ -332,7 +322,7 @@ def nuclides(self, nuclides): self._nuclides = nuclides @abstractmethod - def factor(self, source_rate): + def factor(self, source_rate: float): """Return normalization factor Parameters @@ -435,7 +425,7 @@ def generate_tallies(materials, mat_indexes): in parallel mode. """ - def update_tally_nuclides(self, nuclides): + def update_tally_nuclides(self, nuclides: Sequence[str]) -> list: """Return nuclides with non-zero densities and yield data Parameters @@ -543,7 +533,7 @@ class Integrator(ABC): User-supplied functions are expected to have the following signature: ``solver(A, n0, t) -> n1`` where - * ``A`` is a :class:`scipy.sparse.csr_matrix` making up the + * ``A`` is a :class:`scipy.sparse.csc_matrix` making up the depletion matrix * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for a given material in atoms/cm3 @@ -554,12 +544,20 @@ class Integrator(ABC): transfer_rates : openmc.deplete.TransferRates Instance of TransferRates class to perform continuous transfer during depletion - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 """ - def __init__(self, operator, timesteps, power=None, power_density=None, - source_rates=None, timestep_units='s', solver="cram48"): + def __init__( + self, + operator: TransportOperator, + timesteps: Sequence[float], + power: Optional[Union[float, Sequence[float]]] = None, + power_density: Optional[Union[float, Sequence[float]]] = None, + source_rates: Optional[Sequence[float]] = None, + timestep_units: str = 's', + solver: str = "cram48" + ): # Check number of stages previously used if operator.prev_res is not None: res = operator.prev_res[-1] @@ -629,10 +627,10 @@ def __init__(self, operator, timesteps, power=None, power_density=None, days = watt_days_per_kg * kilograms / rate seconds.append(days*_SECONDS_PER_DAY) else: - raise ValueError("Invalid timestep unit '{}'".format(unit)) + raise ValueError(f"Invalid timestep unit '{unit}'") - self.timesteps = asarray(seconds) - self.source_rates = asarray(source_rates) + self.timesteps = np.asarray(seconds) + self.source_rates = np.asarray(source_rates) self.transfer_rates = None @@ -646,8 +644,7 @@ def __init__(self, operator, timesteps, power=None, power_density=None, self._solver = CRAM16 else: raise ValueError( - "Solver {} not understood. Expected 'cram48' or " - "'cram16'".format(solver)) + f"Solver {solver} not understood. Expected 'cram48' or 'cram16'") else: self.solver = solver @@ -659,14 +656,13 @@ def solver(self): def solver(self, func): if not isinstance(func, Callable): raise TypeError( - "Solver must be callable, not {}".format(type(func))) + f"Solver must be callable, not {type(func)}") try: sig = signature(func) except ValueError: # Guard against callables that aren't introspectable, e.g. # fortran functions wrapped by F2PY - warn("Could not determine arguments to {}. Proceeding " - "anyways".format(func)) + warn(f"Could not determine arguments to {func}. Proceeding anyways") self._solver = func return @@ -678,8 +674,7 @@ def solver(self, func): for ix, param in enumerate(sig.parameters.values()): if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD}: raise ValueError( - "Keyword arguments like {} at position {} are not " - "allowed".format(ix, param)) + f"Keyword arguments like {ix} at position {param} are not allowed") self._solver = func @@ -691,7 +686,14 @@ def _timed_deplete(self, n, rates, dt, matrix_func=None): return time.time() - start, results @abstractmethod - def __call__(self, n, rates, dt, source_rate, i): + def __call__( + self, + n: Sequence[np.ndarray], + rates: ReactionRates, + dt: float, + source_rate: float, + i: int + ): """Perform the integration across one time step Parameters @@ -744,7 +746,7 @@ def _get_bos_data_from_operator(self, step_index, source_rate, bos_conc): self.operator.write_bos_data(step_index + self._i_res) return x, res - def _get_bos_data_from_restart(self, step_index, source_rate, bos_conc): + def _get_bos_data_from_restart(self, source_rate, bos_conc): """Get beginning of step concentrations, reaction rates from restart""" res = self.operator.prev_res[-1] # Depletion methods expect list of arrays @@ -762,7 +764,12 @@ def _get_start_data(self): return (self.operator.prev_res[-1].time[-1], len(self.operator.prev_res) - 1) - def integrate(self, final_step=True, output=True): + def integrate( + self, + final_step: bool = True, + output: bool = True, + path: PathLike = 'depletion_results.h5' + ): """Perform the entire depletion process across all steps Parameters @@ -776,6 +783,10 @@ def integrate(self, final_step=True, output=True): Indicate whether to display information about progress .. versionadded:: 0.13.1 + path : PathLike + Path to file to write. Defaults to 'depletion_results.h5'. + + .. versionadded:: 0.14.1 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() @@ -789,7 +800,7 @@ def integrate(self, final_step=True, output=True): if i > 0 or self.operator.prev_res is None: n, res = self._get_bos_data_from_operator(i, source_rate, n) else: - n, res = self._get_bos_data_from_restart(i, source_rate, n) + n, res = self._get_bos_data_from_restart(source_rate, n) # Solve Bateman equations over time interval proc_time, n_list, res_list = self(n, res.rates, dt, source_rate, i) @@ -802,7 +813,7 @@ def integrate(self, final_step=True, output=True): n = n_list.pop() StepResult.save(self.operator, n_list, res_list, [t, t + dt], - source_rate, self._i_res + i, proc_time) + source_rate, self._i_res + i, proc_time, path) t += dt @@ -814,13 +825,19 @@ def integrate(self, final_step=True, output=True): print(f"[openmc.deplete] t={t} (final operator evaluation)") res_list = [self.operator(n, source_rate if final_step else 0.0)] StepResult.save(self.operator, [n], res_list, [t, t], - source_rate, self._i_res + len(self), proc_time) + source_rate, self._i_res + len(self), proc_time, path) self.operator.write_bos_data(len(self) + self._i_res) self.operator.finalize() - def add_transfer_rate(self, material, components, transfer_rate, - transfer_rate_units='1/s', destination_material=None): + def add_transfer_rate( + self, + material: Union[str, int, Material], + components: Sequence[str], + transfer_rate: float, + transfer_rate_units: str = '1/s', + destination_material: Optional[Union[str, int, Material]] = None + ): """Add transfer rates to depletable material. Parameters @@ -921,7 +938,7 @@ class SIIntegrator(Integrator): User-supplied functions are expected to have the following signature: ``solver(A, n0, t) -> n1`` where - * ``A`` is a :class:`scipy.sparse.csr_matrix` making up the + * ``A`` is a :class:`scipy.sparse.csc_matrix` making up the depletion matrix * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for a given material in atoms/cm3 @@ -933,9 +950,17 @@ class SIIntegrator(Integrator): """ - def __init__(self, operator, timesteps, power=None, power_density=None, - source_rates=None, timestep_units='s', n_steps=10, - solver="cram48"): + def __init__( + self, + operator: TransportOperator, + timesteps: Sequence[float], + power: Optional[Union[float, Sequence[float]]] = None, + power_density: Optional[Union[float, Sequence[float]]] = None, + source_rates: Optional[Sequence[float]] = None, + timestep_units: str = 's', + n_steps: int = 10, + solver: str = "cram48" + ): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( @@ -954,15 +979,21 @@ def _get_bos_data_from_operator(self, step_index, step_power, n_bos): self.operator.settings.particles //= self.n_steps return inherited - def integrate(self, output=True): + def integrate( + self, + output: bool = True, + path: PathLike = "depletion_results.h5" + ): """Perform the entire depletion process across all steps Parameters ---------- output : bool, optional Indicate whether to display information about progress + path : PathLike + Path to file to write. Defaults to 'depletion_results.h5'. - .. versionadded:: 0.13.1 + .. versionadded:: 0.14.1 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() @@ -976,7 +1007,7 @@ def integrate(self, output=True): if self.operator.prev_res is None: n, res = self._get_bos_data_from_operator(i, p, n) else: - n, res = self._get_bos_data_from_restart(i, p, n) + n, res = self._get_bos_data_from_restart(p, n) else: # Pull rates, k from previous iteration w/o # re-running transport @@ -992,13 +1023,13 @@ def integrate(self, output=True): n = n_list.pop() StepResult.save(self.operator, n_list, res_list, [t, t + dt], - p, self._i_res + i, proc_time) + p, self._i_res + i, proc_time, path) t += dt # No final simulation for SIE, use last iteration results StepResult.save(self.operator, [n], [res_list[-1]], [t, t], - p, self._i_res + len(self), proc_time) + p, self._i_res + len(self), proc_time, path) self.operator.write_bos_data(self._i_res + len(self)) self.operator.finalize() @@ -1023,7 +1054,7 @@ def __call__(self, A, n0, dt): Parameters ---------- - A : scipy.sparse.csr_matrix + A : scipy.sparse.csc_matrix Sparse transmutation matrix ``A[j, i]`` describing rates at which isotope ``i`` transmutes to isotope ``j`` n0 : numpy.ndarray diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 4076995d88e..1d383498052 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -7,115 +7,109 @@ from io import StringIO from itertools import chain import math -import os import re from collections import defaultdict, namedtuple from collections.abc import Mapping, Iterable from numbers import Real, Integral from warnings import warn -from openmc.checkvalue import check_type, check_greater_than -from openmc.data import gnds_name, zam, DataLibrary -from openmc.exceptions import DataError -from .nuclide import FissionYieldDistribution - import lxml.etree as ET import scipy.sparse as sp +from openmc.checkvalue import check_type, check_greater_than +from openmc.data import gnds_name, zam +from .nuclide import FissionYieldDistribution, Nuclide import openmc.data -from openmc._xml import clean_indentation -from .nuclide import Nuclide, DecayTuple, ReactionTuple -# tuple of (possible MT values, (dA, dZ), secondaries) where dA is the change in -# the mass number and dZ is the change in the atomic number -ReactionInfo = namedtuple('ReactionInfo', ('mts', 'dadz', 'secondaries')) +# tuple of (possible MT values, secondaries) +ReactionInfo = namedtuple('ReactionInfo', ('mts', 'secondaries')) REACTIONS = { - '(n,2nd)': ReactionInfo({11}, (-3, -1), ('H2',)), - '(n,2n)': ReactionInfo(set(chain([16], range(875, 892))), (-1, 0), ()), - '(n,3n)': ReactionInfo({17}, (-2, 0), ()), - '(n,na)': ReactionInfo({22}, (-4, -2), ('He4',)), - '(n,n3a)': ReactionInfo({23}, (-12, -6), ('He4', 'He4', 'He4')), - '(n,2na)': ReactionInfo({24}, (-5, -2), ('He4',)), - '(n,3na)': ReactionInfo({25}, (-6, -2), ('He4',)), - '(n,np)': ReactionInfo({28}, (-1, -1), ('H1',)), - '(n,n2a)': ReactionInfo({29}, (-8, -4), ('He4', 'He4')), - '(n,2n2a)': ReactionInfo({30}, (-9, -4), ('He4', 'He4')), - '(n,nd)': ReactionInfo({32}, (-2, -1), ('H2',)), - '(n,nt)': ReactionInfo({33}, (-3, -1), ('H3',)), - '(n,n3He)': ReactionInfo({34}, (-3, -2), ('He3',)), - '(n,nd2a)': ReactionInfo({35}, (-10, -5), ('H2', 'He4', 'He4')), - '(n,nt2a)': ReactionInfo({36}, (-11, -5), ('H3', 'He4', 'He4')), - '(n,4n)': ReactionInfo({37}, (-3, 0), ()), - '(n,2np)': ReactionInfo({41}, (-2, -1), ('H1',)), - '(n,3np)': ReactionInfo({42}, (-3, -1), ('H1',)), - '(n,n2p)': ReactionInfo({44}, (-2, -2), ('H1', 'H1')), - '(n,npa)': ReactionInfo({45}, (-5, -3), ('H1', 'He4')), - '(n,gamma)': ReactionInfo({102}, (1, 0), ()), - '(n,p)': ReactionInfo(set(chain([103], range(600, 650))), (0, -1), ('H1',)), - '(n,d)': ReactionInfo(set(chain([104], range(650, 700))), (-1, -1), ('H2',)), - '(n,t)': ReactionInfo(set(chain([105], range(700, 750))), (-2, -1), ('H3',)), - '(n,3He)': ReactionInfo(set(chain([106], range(750, 800))), (-2, -2), ('He3',)), - '(n,a)': ReactionInfo(set(chain([107], range(800, 850))), (-3, -2), ('He4',)), - '(n,2a)': ReactionInfo({108}, (-7, -4), ('He4', 'He4')), - '(n,3a)': ReactionInfo({109}, (-11, -6), ('He4', 'He4', 'He4')), - '(n,2p)': ReactionInfo({111}, (-1, -2), ('H1', 'H1')), - '(n,pa)': ReactionInfo({112}, (-4, -3), ('H1', 'He4')), - '(n,t2a)': ReactionInfo({113}, (-10, -5), ('H3', 'He4', 'He4')), - '(n,d2a)': ReactionInfo({114}, (-9, -5), ('H2', 'He4', 'He4')), - '(n,pd)': ReactionInfo({115}, (-2, -2), ('H1', 'H2')), - '(n,pt)': ReactionInfo({116}, (-3, -2), ('H1', 'H3')), - '(n,da)': ReactionInfo({117}, (-5, -3), ('H2', 'He4')), - '(n,5n)': ReactionInfo({152}, (-4, 0), ()), - '(n,6n)': ReactionInfo({153}, (-5, 0), ()), - '(n,2nt)': ReactionInfo({154}, (-4, -1), ('H3',)), - '(n,ta)': ReactionInfo({155}, (-6, -3), ('H3', 'He4')), - '(n,4np)': ReactionInfo({156}, (-4, -1), ('H1',)), - '(n,3nd)': ReactionInfo({157}, (-4, -1), ('H2',)), - '(n,nda)': ReactionInfo({158}, (-6, -3), ('H2', 'He4')), - '(n,2npa)': ReactionInfo({159}, (-6, -3), ('H1', 'He4')), - '(n,7n)': ReactionInfo({160}, (-6, 0), ()), - '(n,8n)': ReactionInfo({161}, (-7, 0), ()), - '(n,5np)': ReactionInfo({162}, (-5, -1), ('H1',)), - '(n,6np)': ReactionInfo({163}, (-6, -1), ('H1',)), - '(n,7np)': ReactionInfo({164}, (-7, -1), ('H1',)), - '(n,4na)': ReactionInfo({165}, (-7, -2), ('He4',)), - '(n,5na)': ReactionInfo({166}, (-8, -2), ('He4',)), - '(n,6na)': ReactionInfo({167}, (-9, -2), ('He4',)), - '(n,7na)': ReactionInfo({168}, (-10, -2), ('He4',)), - '(n,4nd)': ReactionInfo({169}, (-5, -1), ('H2',)), - '(n,5nd)': ReactionInfo({170}, (-6, -1), ('H2',)), - '(n,6nd)': ReactionInfo({171}, (-7, -1), ('H2',)), - '(n,3nt)': ReactionInfo({172}, (-5, -1), ('H3',)), - '(n,4nt)': ReactionInfo({173}, (-6, -1), ('H3',)), - '(n,5nt)': ReactionInfo({174}, (-7, -1), ('H3',)), - '(n,6nt)': ReactionInfo({175}, (-8, -1), ('H3',)), - '(n,2n3He)': ReactionInfo({176}, (-4, -2), ('He3',)), - '(n,3n3He)': ReactionInfo({177}, (-5, -2), ('He3',)), - '(n,4n3He)': ReactionInfo({178}, (-6, -2), ('He3',)), - '(n,3n2p)': ReactionInfo({179}, (-4, -2), ('H1', 'H1')), - '(n,3n2a)': ReactionInfo({180}, (-10, -4), ('He4', 'He4')), - '(n,3npa)': ReactionInfo({181}, (-7, -3), ('H1', 'He4')), - '(n,dt)': ReactionInfo({182}, (-4, -2), ('H2', 'H3')), - '(n,npd)': ReactionInfo({183}, (-3, -2), ('H1', 'H2')), - '(n,npt)': ReactionInfo({184}, (-4, -2), ('H1', 'H3')), - '(n,ndt)': ReactionInfo({185}, (-5, -2), ('H2', 'H3')), - '(n,np3He)': ReactionInfo({186}, (-4, -3), ('H1', 'He3')), - '(n,nd3He)': ReactionInfo({187}, (-5, -3), ('H2', 'He3')), - '(n,nt3He)': ReactionInfo({188}, (-6, -3), ('H3', 'He3')), - '(n,nta)': ReactionInfo({189}, (-7, -3), ('H3', 'He4')), - '(n,2n2p)': ReactionInfo({190}, (-3, -2), ('H1', 'H1')), - '(n,p3He)': ReactionInfo({191}, (-4, -3), ('H1', 'He3')), - '(n,d3He)': ReactionInfo({192}, (-5, -3), ('H2', 'He3')), - '(n,3Hea)': ReactionInfo({193}, (-6, -4), ('He3', 'He4')), - '(n,4n2p)': ReactionInfo({194}, (-5, -2), ('H1', 'H1')), - '(n,4n2a)': ReactionInfo({195}, (-11, -4), ('He4', 'He4')), - '(n,4npa)': ReactionInfo({196}, (-8, -3), ('H1', 'He4')), - '(n,3p)': ReactionInfo({197}, (-2, -3), ('H1', 'H1', 'H1')), - '(n,n3p)': ReactionInfo({198}, (-3, -3), ('H1', 'H1', 'H1')), - '(n,3n2pa)': ReactionInfo({199}, (-8, -4), ('H1', 'H1', 'He4')), - '(n,5n2p)': ReactionInfo({200}, (-6, -2), ('H1', 'H1')), + '(n,2nd)': ReactionInfo({11}, ('H2',)), + '(n,2n)': ReactionInfo(set(chain([16], range(875, 892))), ()), + '(n,3n)': ReactionInfo({17}, ()), + '(n,na)': ReactionInfo({22}, ('He4',)), + '(n,n3a)': ReactionInfo({23}, ('He4', 'He4', 'He4')), + '(n,2na)': ReactionInfo({24}, ('He4',)), + '(n,3na)': ReactionInfo({25}, ('He4',)), + '(n,np)': ReactionInfo({28}, ('H1',)), + '(n,n2a)': ReactionInfo({29}, ('He4', 'He4')), + '(n,2n2a)': ReactionInfo({30}, ('He4', 'He4')), + '(n,nd)': ReactionInfo({32}, ('H2',)), + '(n,nt)': ReactionInfo({33}, ('H3',)), + '(n,n3He)': ReactionInfo({34}, ('He3',)), + '(n,nd2a)': ReactionInfo({35}, ('H2', 'He4', 'He4')), + '(n,nt2a)': ReactionInfo({36}, ('H3', 'He4', 'He4')), + '(n,4n)': ReactionInfo({37}, ()), + '(n,2np)': ReactionInfo({41}, ('H1',)), + '(n,3np)': ReactionInfo({42}, ('H1',)), + '(n,n2p)': ReactionInfo({44}, ('H1', 'H1')), + '(n,npa)': ReactionInfo({45}, ('H1', 'He4')), + '(n,gamma)': ReactionInfo({102}, ()), + '(n,p)': ReactionInfo(set(chain([103], range(600, 650))), ('H1',)), + '(n,d)': ReactionInfo(set(chain([104], range(650, 700))), ('H2',)), + '(n,t)': ReactionInfo(set(chain([105], range(700, 750))), ('H3',)), + '(n,3He)': ReactionInfo(set(chain([106], range(750, 800))), ('He3',)), + '(n,a)': ReactionInfo(set(chain([107], range(800, 850))), ('He4',)), + '(n,2a)': ReactionInfo({108}, ('He4', 'He4')), + '(n,3a)': ReactionInfo({109}, ('He4', 'He4', 'He4')), + '(n,2p)': ReactionInfo({111}, ('H1', 'H1')), + '(n,pa)': ReactionInfo({112}, ('H1', 'He4')), + '(n,t2a)': ReactionInfo({113}, ('H3', 'He4', 'He4')), + '(n,d2a)': ReactionInfo({114}, ('H2', 'He4', 'He4')), + '(n,pd)': ReactionInfo({115}, ('H1', 'H2')), + '(n,pt)': ReactionInfo({116}, ('H1', 'H3')), + '(n,da)': ReactionInfo({117}, ('H2', 'He4')), + '(n,5n)': ReactionInfo({152}, ()), + '(n,6n)': ReactionInfo({153}, ()), + '(n,2nt)': ReactionInfo({154}, ('H3',)), + '(n,ta)': ReactionInfo({155}, ('H3', 'He4')), + '(n,4np)': ReactionInfo({156}, ('H1',)), + '(n,3nd)': ReactionInfo({157}, ('H2',)), + '(n,nda)': ReactionInfo({158}, ('H2', 'He4')), + '(n,2npa)': ReactionInfo({159}, ('H1', 'He4')), + '(n,7n)': ReactionInfo({160}, ()), + '(n,8n)': ReactionInfo({161}, ()), + '(n,5np)': ReactionInfo({162}, ('H1',)), + '(n,6np)': ReactionInfo({163}, ('H1',)), + '(n,7np)': ReactionInfo({164}, ('H1',)), + '(n,4na)': ReactionInfo({165}, ('He4',)), + '(n,5na)': ReactionInfo({166}, ('He4',)), + '(n,6na)': ReactionInfo({167}, ('He4',)), + '(n,7na)': ReactionInfo({168}, ('He4',)), + '(n,4nd)': ReactionInfo({169}, ('H2',)), + '(n,5nd)': ReactionInfo({170}, ('H2',)), + '(n,6nd)': ReactionInfo({171}, ('H2',)), + '(n,3nt)': ReactionInfo({172}, ('H3',)), + '(n,4nt)': ReactionInfo({173}, ('H3',)), + '(n,5nt)': ReactionInfo({174}, ('H3',)), + '(n,6nt)': ReactionInfo({175}, ('H3',)), + '(n,2n3He)': ReactionInfo({176}, ('He3',)), + '(n,3n3He)': ReactionInfo({177}, ('He3',)), + '(n,4n3He)': ReactionInfo({178}, ('He3',)), + '(n,3n2p)': ReactionInfo({179}, ('H1', 'H1')), + '(n,3n2a)': ReactionInfo({180}, ('He4', 'He4')), + '(n,3npa)': ReactionInfo({181}, ('H1', 'He4')), + '(n,dt)': ReactionInfo({182}, ('H2', 'H3')), + '(n,npd)': ReactionInfo({183}, ('H1', 'H2')), + '(n,npt)': ReactionInfo({184}, ('H1', 'H3')), + '(n,ndt)': ReactionInfo({185}, ('H2', 'H3')), + '(n,np3He)': ReactionInfo({186}, ('H1', 'He3')), + '(n,nd3He)': ReactionInfo({187}, ('H2', 'He3')), + '(n,nt3He)': ReactionInfo({188}, ('H3', 'He3')), + '(n,nta)': ReactionInfo({189}, ('H3', 'He4')), + '(n,2n2p)': ReactionInfo({190}, ('H1', 'H1')), + '(n,p3He)': ReactionInfo({191}, ('H1', 'He3')), + '(n,d3He)': ReactionInfo({192}, ('H2', 'He3')), + '(n,3Hea)': ReactionInfo({193}, ('He3', 'He4')), + '(n,4n2p)': ReactionInfo({194}, ('H1', 'H1')), + '(n,4n2a)': ReactionInfo({195}, ('He4', 'He4')), + '(n,4npa)': ReactionInfo({196}, ('H1', 'He4')), + '(n,3p)': ReactionInfo({197}, ('H1', 'H1', 'H1')), + '(n,n3p)': ReactionInfo({198}, ('H1', 'H1', 'H1')), + '(n,3n2pa)': ReactionInfo({199}, ('H1', 'H1', 'He4')), + '(n,5n2p)': ReactionInfo({200}, ('H1', 'H1')), } __all__ = ["Chain", "REACTIONS"] @@ -147,7 +141,7 @@ def replace_missing(product, decay_data): # First check if ground state is available if state: - product = '{}{}'.format(symbol, A) + product = f'{symbol}{A}' # Find isotope with longest half-life half_life = 0.0 @@ -178,7 +172,7 @@ def replace_missing(product, decay_data): Z += 1 else: Z -= 1 - product = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) + product = f'{openmc.data.ATOMIC_SYMBOL[Z]}{A}' return product @@ -418,12 +412,12 @@ def from_endf(cls, decay_files, fpy_files, neutron_files, if parent in reactions: reactions_available = set(reactions[parent].keys()) for name in transmutation_reactions: - mts, changes, _ = REACTIONS[name] + mts = REACTIONS[name].mts + delta_A, delta_Z = openmc.data.DADZ[name] if mts & reactions_available: - delta_A, delta_Z = changes A = data.nuclide['mass_number'] + delta_A Z = data.nuclide['atomic_number'] + delta_Z - daughter = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) + daughter = f'{openmc.data.ATOMIC_SYMBOL[Z]}{A}' if daughter not in decay_data: daughter = replace_missing(daughter, decay_data) @@ -489,7 +483,7 @@ def from_endf(cls, decay_files, fpy_files, neutron_files, if missing_daughter: print('The following decay modes have daughters with no decay data:') for mode in missing_daughter: - print(' {}'.format(mode)) + print(f' {mode}') print('') if missing_rx_product: @@ -501,7 +495,7 @@ def from_endf(cls, decay_files, fpy_files, neutron_files, if missing_fpy: print('The following fissionable nuclides have no fission product yields:') for parent, replacement in missing_fpy: - print(' {}, replaced with {}'.format(parent, replacement)) + print(f' {parent}, replaced with {replacement}') print('') if missing_fp: @@ -596,16 +590,19 @@ def form_matrix(self, rates, fission_yields=None): Returns ------- - scipy.sparse.csr_matrix + scipy.sparse.csc_matrix Sparse matrix representing depletion. See Also -------- :meth:`get_default_fission_yields` """ - matrix = defaultdict(float) reactions = set() + # Use DOK matrix as intermediate representation for matrix + n = len(self) + matrix = sp.dok_matrix((n, n)) + if fission_yields is None: fission_yields = self.get_default_fission_yields() @@ -680,16 +677,13 @@ def form_matrix(self, rates, fission_yields=None): # Clear set of reactions reactions.clear() - # Use DOK matrix as intermediate representation, then convert to CSR and return - n = len(self) - matrix_dok = sp.dok_matrix((n, n)) - dict.update(matrix_dok, matrix) - return matrix_dok.tocsr() + # Return CSC representation instead of DOK + return matrix.tocsc() def form_rr_term(self, tr_rates, mats): """Function to form the transfer rate term matrices. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -713,11 +707,13 @@ def form_rr_term(self, tr_rates, mats): Returns ------- - scipy.sparse.csr_matrix + scipy.sparse.csc_matrix Sparse matrix representing transfer term. """ - matrix = defaultdict(float) + # Use DOK as intermediate representation + n = len(self) + matrix = sp.dok_matrix((n, n)) for i, nuc in enumerate(self.nuclides): elm = re.split(r'\d+', nuc.name)[0] @@ -743,10 +739,9 @@ def form_rr_term(self, tr_rates, mats): else: matrix[i, i] = 0.0 #Nothing else is allowed - n = len(self) - matrix_dok = sp.dok_matrix((n, n)) - dict.update(matrix_dok, matrix) - return matrix_dok.tocsr() + + # Return CSC instead of DOK + return matrix.tocsc() def get_branch_ratios(self, reaction="(n,gamma)"): """Return a dictionary with reaction branching ratios @@ -878,8 +873,7 @@ def set_branch_ratios(self, branch_ratios, reaction="(n,gamma)", if len(indexes) == 0: if strict: raise AttributeError( - "Nuclide {} does not have {} reactions".format( - parent, reaction)) + f"Nuclide {parent} does not have {reaction} reactions") missing_reaction.add(parent) continue @@ -901,8 +895,7 @@ def set_branch_ratios(self, branch_ratios, reaction="(n,gamma)", if len(rxn_ix_map) == 0: raise IndexError( - "No {} reactions found in this {}".format( - reaction, self.__class__.__name__)) + f"No {reaction} reactions found in this {self.__class__.__name__}") if len(missing_parents) > 0: warn("The following nuclides were not found in {}: {}".format( @@ -913,14 +906,14 @@ def set_branch_ratios(self, branch_ratios, reaction="(n,gamma)", "{}".format(reaction, ", ".join(sorted(missing_reaction)))) if len(missing_products) > 0: - tail = ("{} -> {}".format(k, v) + tail = (f"{k} -> {v}" for k, v in sorted(missing_products.items())) warn("The following products were not found in the {} and " "parents were unmodified: \n{}".format( self.__class__.__name__, ", ".join(tail))) if len(bad_sums) > 0: - tail = ("{}: {:5.3f}".format(k, s) + tail = (f"{k}: {s:5.3f}" for k, s in sorted(bad_sums.items())) warn("The following parent nuclides were given {} branch ratios " "with a sum outside tolerance of 1 +/- {:5.3e}:\n{}".format( diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 9e6b5fcd3b5..acdcf467c57 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -10,6 +10,7 @@ import copy from warnings import warn +from typing import Optional import numpy as np from uncertainties import ufloat @@ -33,18 +34,19 @@ __all__ = ["CoupledOperator", "Operator", "OperatorResult"] -def _find_cross_sections(model): +def _find_cross_sections(model: Optional[str] = None): """Determine cross sections to use for depletion Parameters ---------- - model : openmc.model.Model + model : openmc.model.Model, optional Reactor model """ - if model.materials and model.materials.cross_sections is not None: - # Prefer info from Model class if available - return model.materials.cross_sections + if model: + if model.materials and model.materials.cross_sections is not None: + # Prefer info from Model class if available + return model.materials.cross_sections # otherwise fallback to environment variable cross_sections = openmc.config.get("cross_sections") @@ -67,7 +69,7 @@ def _get_nuclides_with_data(cross_sections): Returns ------- nuclides : set of str - Set of nuclide names that have cross secton data + Set of nuclide names that have cross section data """ nuclides = set() @@ -170,16 +172,12 @@ class CoupledOperator(OpenMCOperator): equally between the new materials, 'match cell' sets the volume of the material to volume of the cell they fill. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Attributes ---------- model : openmc.model.Model OpenMC model object - geometry : openmc.Geometry - OpenMC geometry object - settings : openmc.Settings - OpenMC settings object output_dir : pathlib.Path Path to output directory to save results. round_number : bool @@ -240,8 +238,6 @@ def __init__(self, model, chain_file=None, prev_results=None, warn("Fission Q dictionary will not be used") fission_q = None self.model = model - self.settings = model.settings - self.geometry = model.geometry # determine set of materials in the model if not model.materials: @@ -263,6 +259,9 @@ def __init__(self, model, chain_file=None, prev_results=None, 'fission_yield_opts': fission_yield_opts } + # Records how many times the operator has been called + self._n_calls = 0 + super().__init__( materials=model.materials, cross_sections=cross_sections, @@ -292,7 +291,7 @@ def _load_previous_results(self): # on this process if comm.size != 1: prev_results = self.prev_res - self.prev_res = Results() + self.prev_res = Results(filename=None) mat_indexes = _distribute(range(len(self.burnable_mats))) for res_obj in prev_results: new_res = res_obj.distribute(self.local_mats, mat_indexes) @@ -353,7 +352,7 @@ def _get_helper_classes(self, helper_kwargs): if normalization_mode == "fission-q": self._normalization_helper = ChainFissionHelper() elif normalization_mode == "energy-deposition": - score = "heating" if self.settings.photon_transport else "heating-local" + score = "heating" if self.model.settings.photon_transport else "heating-local" self._normalization_helper = EnergyScoreHelper(score) else: self._normalization_helper = SourceRateHelper() @@ -375,8 +374,12 @@ def initial_condition(self): # Create XML files if comm.rank == 0: - self.geometry.export_to_xml() - self.settings.export_to_xml() + self.model.geometry.export_to_xml() + self.model.settings.export_to_xml() + if self.model.plots: + self.model.plots.export_to_xml() + if self.model.tallies: + self.model.tallies.export_to_xml() self._generate_materials_xml() # Initialize OpenMC library @@ -427,6 +430,16 @@ def __call__(self, vec, source_rate): # Reset results in OpenMC openmc.lib.reset() + # The timers are reset only if the operator has been called before. + # This is because we call this method after loading cross sections, and + # no transport has taken place yet. As a result, we only reset the + # timers after the first step so as to correctly report the time spent + # reading cross sections in the first depletion step, and from there + # correctly report all particle tracking rates in multistep depletion + # solvers. + if self._n_calls > 0: + openmc.lib.reset_timers() + self._update_materials_and_nuclides(vec) # If the source rate is zero, return zero reaction rates without running @@ -447,6 +460,8 @@ def __call__(self, vec, source_rate): op_result = OperatorResult(keff, rates) + self._n_calls += 1 + return copy.deepcopy(op_result) def _update_materials(self): @@ -503,16 +518,44 @@ def write_bos_data(step): """ openmc.lib.statepoint_write( - "openmc_simulation_n{}.h5".format(step), + f"openmc_simulation_n{step}.h5", write_source=False) - openmc.lib.reset_timers() - def finalize(self): """Finalize a depletion simulation and release resources.""" if self.cleanup_when_done: openmc.lib.finalize() + # The next few class variables and methods should be removed after one + # release cycle or so. For now, we will provide compatibility to + # accessing CoupledOperator.settings and CoupledOperator.geometry. In + # the future these should stay on the Model class. + + var_warning_msg = "The CoupledOperator.{0} variable should be \ +accessed through CoupledOperator.model.{0}." + geometry_warning_msg = var_warning_msg.format("geometry") + settings_warning_msg = var_warning_msg.format("settings") + + @property + def settings(self): + warn(self.settings_warning_msg, FutureWarning) + return self.model.settings + + @settings.setter + def settings(self, new_settings): + warn(self.settings_warning_msg, FutureWarning) + self.model.settings = new_settings + + @property + def geometry(self): + warn(self.geometry_warning_msg, FutureWarning) + return self.model.geometry + + @geometry.setter + def geometry(self, new_geometry): + warn(self.geometry_warning_msg, FutureWarning) + self.model.geometry = new_geometry + # Retain deprecated name for the time being def Operator(*args, **kwargs): diff --git a/openmc/deplete/cram.py b/openmc/deplete/cram.py index f5f80dfdc48..53de83bb68e 100644 --- a/openmc/deplete/cram.py +++ b/openmc/deplete/cram.py @@ -75,9 +75,9 @@ def __call__(self, A, n0, dt): Final compositions after ``dt`` """ - A = sp.csr_matrix(A * dt, dtype=np.float64) + A = dt * sp.csc_matrix(A, dtype=np.float64) y = n0.copy() - ident = sp.eye(A.shape[0]) + ident = sp.eye(A.shape[0], format='csc') for alpha, theta in zip(self.alpha, self.theta): y += 2*np.real(alpha*sla.spsolve(A - theta*ident, y)) return y * self.alpha0 diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index d089ac373ef..250b94deb42 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -39,7 +39,7 @@ class IndependentOperator(OpenMCOperator): .. versionadded:: 0.13.1 - .. versionchanged:: 0.13.4 + .. versionchanged:: 0.14.0 Arguments updated to include list of fluxes and microscopic cross sections. @@ -130,6 +130,13 @@ def __init__(self, # Validate micro-xs parameters check_type('materials', materials, openmc.Materials) check_type('micros', micros, Iterable, MicroXS) + + if not (len(fluxes) == len(micros) == len(materials)): + msg = (f'The length of fluxes ({len(fluxes)}) should be equal to ' + f'the length of micros ({len(micros)}) and the length of ' + f'materials ({len(materials)}).') + raise ValueError(msg) + if keff is not None: check_type('keff', keff, tuple, float) keff = ufloat(*keff) @@ -233,7 +240,6 @@ def from_nuclides(cls, volume, nuclides, reduce_chain_level=reduce_chain_level, fission_yield_opts=fission_yield_opts) - @staticmethod def _consolidate_nuclides_to_material(nuclides, nuc_units, volume): """Puts nuclide list into an openmc.Materials object. @@ -262,7 +268,6 @@ def _load_previous_results(self): self.prev_res[-1].transfer_volumes(model) self.materials = model.materials - # Store previous results in operator # Distribute reaction rates according to those tracked # on this process @@ -274,7 +279,6 @@ def _load_previous_results(self): new_res = res_obj.distribute(self.local_mats, mat_indexes) self.prev_res.append(new_res) - def _get_nuclides_with_data(self, cross_sections: List[MicroXS]) -> Set[str]: """Finds nuclides with cross section data""" return set(cross_sections[0].nuclides) @@ -404,6 +408,12 @@ def __call__(self, vec, source_rate): self._update_materials_and_nuclides(vec) + # If the source rate is zero, return zero reaction rates + if source_rate == 0.0: + rates = self.reaction_rates.copy() + rates.fill(0.0) + return OperatorResult(ufloat(0.0, 0.0), rates) + rates = self._calculate_reaction_rates(source_rate) keff = self._keff diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 59c7b18e121..497a5284a1d 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -5,21 +5,37 @@ """ from __future__ import annotations -import tempfile -from typing import List, Tuple, Iterable, Optional, Union +from tempfile import TemporaryDirectory +from typing import List, Tuple, Iterable, Optional, Union, Sequence import pandas as pd import numpy as np from openmc.checkvalue import check_type, check_value, check_iterable_type, PathLike from openmc.exceptions import DataError +from openmc.utility_funcs import change_directory from openmc import StatePoint +from openmc.mgxs import GROUP_STRUCTURES +from openmc.data import REACTION_MT import openmc from .chain import Chain, REACTIONS from .coupled_operator import _find_cross_sections, _get_nuclides_with_data +import openmc.lib _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') +_valid_rxns.append('damage-energy') + + +def _resolve_chain_file_path(chain_file: str): + if chain_file is None: + chain_file = openmc.config.get('chain_file') + if 'chain_file' not in openmc.config: + raise DataError( + "No depletion chain specified and could not find depletion " + "chain in openmc.config['chain_file']" + ) + return chain_file def get_microxs_and_flux( @@ -33,7 +49,7 @@ def get_microxs_and_flux( ) -> Tuple[List[np.ndarray], List[MicroXS]]: """Generate a microscopic cross sections and flux from a Model - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -69,13 +85,7 @@ def get_microxs_and_flux( original_tallies = model.tallies # Determine what reactions and nuclides are available in chain - if chain_file is None: - chain_file = openmc.config.get('chain_file') - if chain_file is None: - raise DataError( - "No depletion chain specified and could not find depletion " - "chain in openmc.config['chain_file']" - ) + chain_file = _resolve_chain_file_path(chain_file) chain = Chain.from_xml(chain_file) if reactions is None: reactions = chain.reactions @@ -115,7 +125,7 @@ def get_microxs_and_flux( model.tallies = openmc.Tallies([rr_tally, flux_tally]) # create temporary run - with tempfile.TemporaryDirectory() as temp_dir: + with TemporaryDirectory() as temp_dir: if run_kwargs is None: run_kwargs = {} else: @@ -156,7 +166,7 @@ class MicroXS: .. versionadded:: 0.13.1 - .. versionchanged:: 0.13.4 + .. versionchanged:: 0.14.0 Class was heavily refactored and no longer subclasses pandas.DataFrame. Parameters @@ -192,8 +202,121 @@ def __init__(self, data: np.ndarray, nuclides: List[str], reactions: List[str]): self._index_nuc = {nuc: i for i, nuc in enumerate(nuclides)} self._index_rx = {rx: i for i, rx in enumerate(reactions)} - # TODO: Add a classmethod for generating MicroXS directly from cross section - # data using a known flux spectrum + @classmethod + def from_multigroup_flux( + cls, + energies: Union[Sequence[float], str], + multigroup_flux: Sequence[float], + chain_file: Optional[PathLike] = None, + temperature: float = 293.6, + nuclides: Optional[Sequence[str]] = None, + reactions: Optional[Sequence[str]] = None, + **init_kwargs: dict, + ) -> MicroXS: + """Generated microscopic cross sections from a known flux. + + The size of the MicroXS matrix depends on the chain file and cross + sections available. MicroXS entry will be 0 if the nuclide cross section + is not found. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + energies : iterable of float or str + Energy group boundaries in [eV] or the name of the group structure + multi_group_flux : iterable of float + Energy-dependent multigroup flux values + chain_file : str, optional + Path to the depletion chain XML file that will be used in depletion + simulation. Defaults to ``openmc.config['chain_file']``. + temperature : int, optional + Temperature for cross section evaluation in [K]. + nuclides : list of str, optional + Nuclides to get cross sections for. If not specified, all burnable + nuclides from the depletion chain file are used. + reactions : list of str, optional + Reactions to get cross sections for. If not specified, all neutron + reactions listed in the depletion chain file are used. + **init_kwargs : dict + Keyword arguments passed to :func:`openmc.lib.init` + + Returns + ------- + MicroXS + """ + + check_type("temperature", temperature, (int, float)) + # if energy is string then use group structure of that name + if isinstance(energies, str): + energies = GROUP_STRUCTURES[energies] + else: + # if user inputs energies check they are ascending (low to high) as + # some depletion codes use high energy to low energy. + if not np.all(np.diff(energies) > 0): + raise ValueError('Energy group boundaries must be in ascending order') + + # check dimension consistency + if len(multigroup_flux) != len(energies) - 1: + raise ValueError('Length of flux array should be len(energies)-1') + + chain_file_path = _resolve_chain_file_path(chain_file) + chain = Chain.from_xml(chain_file_path) + + cross_sections = _find_cross_sections(model=None) + nuclides_with_data = _get_nuclides_with_data(cross_sections) + + # If no nuclides were specified, default to all nuclides from the chain + if not nuclides: + nuclides = chain.nuclides + nuclides = [nuc.name for nuc in nuclides] + + # Get reaction MT values. If no reactions specified, default to the + # reactions available in the chain file + if reactions is None: + reactions = chain.reactions + mts = [REACTION_MT[name] for name in reactions] + + # Normalize multigroup flux + multigroup_flux = np.asarray(multigroup_flux) + multigroup_flux /= multigroup_flux.sum() + + # Create 2D array for microscopic cross sections + microxs_arr = np.zeros((len(nuclides), len(mts))) + + # Create a material with all nuclides + mat_all_nucs = openmc.Material() + for nuc in nuclides: + if nuc in nuclides_with_data: + mat_all_nucs.add_nuclide(nuc, 1.0) + mat_all_nucs.set_density("atom/b-cm", 1.0) + + # Create simple model containing the above material + surf1 = openmc.Sphere(boundary_type="vacuum") + surf1_cell = openmc.Cell(fill=mat_all_nucs, region=-surf1) + model = openmc.Model() + model.geometry = openmc.Geometry([surf1_cell]) + model.settings = openmc.Settings( + particles=1, batches=1, output={'summary': False}) + + with change_directory(tmpdir=True): + # Export model within temporary directory + model.export_to_model_xml() + + with openmc.lib.run_in_memory(**init_kwargs): + # For each nuclide and reaction, compute the flux-averaged + # cross section + for nuc_index, nuc in enumerate(nuclides): + if nuc not in nuclides_with_data: + continue + lib_nuc = openmc.lib.nuclides[nuc] + for mt_index, mt in enumerate(mts): + xs = lib_nuc.collapse_rate( + mt, temperature, energies, multigroup_flux + ) + microxs_arr[nuc_index, mt_index] = xs + + return cls(microxs_arr, nuclides, reactions) @classmethod def from_csv(cls, csv_file, **kwargs): diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index e2067a8e359..60e3e5317f1 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -8,8 +8,8 @@ from collections import namedtuple, defaultdict from warnings import warn from numbers import Real -import lxml.etree as ET +import lxml.etree as ET import numpy as np from openmc.checkvalue import check_type @@ -275,7 +275,7 @@ def from_xml(cls, element, root=None, fission_q=None): if parent is not None: assert root is not None fpy_elem = root.find( - './/nuclide[@name="{}"]/neutron_fission_yields'.format(parent) + f'.//nuclide[@name="{parent}"]/neutron_fission_yields' ) if fpy_elem is None: raise ValueError( @@ -413,7 +413,7 @@ def validate(self, strict=True, quiet=False, tolerance=1e-4): continue msg = msg_func( name=self.name, actual=sum_br, expected=1.0, tol=tolerance, - prop="{} reaction branch ratios".format(rxn_type)) + prop=f"{rxn_type} reaction branch ratios") if strict: raise ValueError(msg) elif quiet: @@ -430,7 +430,7 @@ def validate(self, strict=True, quiet=False, tolerance=1e-4): msg = msg_func( name=self.name, actual=sum_yield, expected=2.0, tol=tolerance, - prop="fission yields (E = {:7.4e} eV)".format(energy)) + prop=f"fission yields (E = {energy:7.4e} eV)") if strict: raise ValueError(msg) elif quiet: @@ -695,8 +695,7 @@ def __rmul__(self, scalar): return self * scalar def __repr__(self): - return "<{} containing {} products and yields>".format( - self.__class__.__name__, len(self)) + return f"<{self.__class__.__name__} containing {len(self)} products and yields>" def __deepcopy__(self, memo): result = FissionYield(self.products, self.yields.copy()) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 766fbf133ed..ad7a3924b7e 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -64,7 +64,7 @@ class OpenMCOperator(TransportOperator): equally between the new materials, 'match cell' sets the volume of the material to volume of the cell they fill. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Attributes ---------- @@ -213,7 +213,7 @@ def _get_burnable_mats(self) -> Tuple[List[str], Dict[str, float], List[str]]: msg = (f"Nuclilde {nuclide} in material {mat.id} is not " "present in the depletion chain and has no cross " "section data.") - raise warn(msg) + warn(msg) if mat.depletable: burnable_mats.add(str(mat.id)) if mat.volume is None: diff --git a/openmc/deplete/pool.py b/openmc/deplete/pool.py index 9dc9ea468f4..27ecaa4dd8b 100644 --- a/openmc/deplete/pool.py +++ b/openmc/deplete/pool.py @@ -66,7 +66,7 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None, transfer_rates : openmc.deplete.TransferRates, Optional Object to perform continuous reprocessing. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 matrix_args: Any, optional Additional arguments passed to matrix_func diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index d205bf5718e..2e537a1b735 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -104,6 +104,8 @@ def get_activity( ) -> Tuple[np.ndarray, typing.Union[np.ndarray, List[dict]]]: """Get activity of material over time. + .. versionadded:: 0.14.0 + Parameters ---------- mat : openmc.Material, str @@ -220,6 +222,8 @@ def get_decay_heat( ) -> Tuple[np.ndarray, typing.Union[np.ndarray, List[dict]]]: """Get decay heat of material over time. + .. versionadded:: 0.14.0 + Parameters ---------- mat : openmc.Material, str @@ -273,7 +277,7 @@ def get_mass(self, ) -> Tuple[np.ndarray, np.ndarray]: """Get mass of nuclides over time from a single material - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -314,7 +318,7 @@ def get_mass(self, # Divide by volume to get density mass /= self[0].volume[mat_id] elif mass_units == "kg": - mass *= 1e3 + mass /= 1e3 return times, mass diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index a3aacae41bb..9cf33898f3b 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -291,7 +291,7 @@ def _write_hdf5_metadata(self, handle): # Store concentration mat and nuclide dictionaries (along with volumes) handle.attrs['version'] = np.array(VERSION_RESULTS) - handle.attrs['filetype'] = np.string_('depletion results') + handle.attrs['filetype'] = np.bytes_('depletion results') mat_list = sorted(self.mat_to_hdf5_ind, key=int) nuc_list = sorted(self.index_nuc) @@ -534,7 +534,7 @@ def save(op, x, op_results, t, source_rate, step_ind, proc_time=None, path : PathLike Path to file to write. Defaults to 'depletion_results.h5'. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 """ # Get indexing terms vol_dict, nuc_list, burn_list, full_burn_list = op.get_results_info() diff --git a/openmc/deplete/transfer_rates.py b/openmc/deplete/transfer_rates.py index da0bb338f64..01c9b2e3534 100644 --- a/openmc/deplete/transfer_rates.py +++ b/openmc/deplete/transfer_rates.py @@ -16,7 +16,7 @@ class TransferRates: An instance of this class can be passed directly to an instance of one of the :class:`openmc.deplete.Integrator` classes. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- diff --git a/openmc/element.py b/openmc/element.py index 01651bebb6f..082bee5226d 100644 --- a/openmc/element.py +++ b/openmc/element.py @@ -1,4 +1,6 @@ import re +import warnings + import lxml.etree as ET import openmc.checkvalue as cv @@ -122,6 +124,10 @@ def expand(self, percent, percent_type, enrichment=None, # Get the nuclides present in nature natural_nuclides = {name for name, abundance in natural_isotopes(self)} + # Issue warning if no existing nuclides + if len(natural_nuclides) == 0: + warnings.warn(f"No naturally occurring isotopes found for {self}.") + # Create dict to store the expanded nuclides and abundances abundances = {} diff --git a/openmc/filter.py b/openmc/filter.py index 311db94d190..e005140eeb3 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1,11 +1,12 @@ +from __future__ import annotations from abc import ABCMeta from collections.abc import Iterable import hashlib from itertools import product from numbers import Real, Integral -import lxml.etree as ET import warnings +import lxml.etree as ET import numpy as np import pandas as pd @@ -22,7 +23,7 @@ _FILTER_TYPES = ( 'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy', 'energyout', 'mu', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', - 'energyfunction', 'cellfrom', 'legendre', 'spatiallegendre', + 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', 'collision', 'time' ) @@ -451,7 +452,7 @@ class UniverseFilter(WithIDFilter): Parameters ---------- bins : openmc.UniverseBase, int, or iterable thereof - The Universes to tally. Either openmc.UniverseBase objects or their + The Universes to tally. Either :class:`openmc.UniverseBase` objects or their Integral ID numbers can be used. filter_id : int Unique identifier for the filter @@ -475,7 +476,31 @@ class MaterialFilter(WithIDFilter): Parameters ---------- bins : openmc.Material, Integral, or iterable thereof - The Materials to tally. Either openmc.Material objects or their + The material(s) to tally. Either :class:`openmc.Material` objects or their + Integral ID numbers can be used. + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + bins : Iterable of Integral + openmc.Material IDs. + id : int + Unique identifier for the filter + num_bins : Integral + The number of filter bins + + """ + expected_type = Material + + +class MaterialFromFilter(WithIDFilter): + """Bins tally event locations based on the Material they occurred in. + + Parameters + ---------- + bins : openmc.Material, Integral, or iterable thereof + The material(s) to tally. Either :class:`openmc.Material` objects or their Integral ID numbers can be used. filter_id : int Unique identifier for the filter @@ -499,7 +524,7 @@ class CellFilter(WithIDFilter): Parameters ---------- bins : openmc.Cell, int, or iterable thereof - The cells to tally. Either openmc.Cell objects or their ID numbers can + The cells to tally. Either :class:`openmc.Cell` objects or their ID numbers can be used. filter_id : int Unique identifier for the filter @@ -780,8 +805,8 @@ class MeshFilter(Filter): id : int Unique identifier for the filter translation : Iterable of float - This array specifies a vector that is used to translate (shift) - the mesh for this filter + This array specifies a vector that is used to translate (shift) the mesh + for this filter bins : list of tuple A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1), ...] @@ -822,7 +847,6 @@ def from_hdf5(cls, group, **kwargs): mesh_obj = kwargs['meshes'][mesh_id] filter_id = int(group.name.split('/')[-1].lstrip('filter ')) - out = cls(mesh_obj, filter_id=filter_id) translation = group.get('translation') @@ -840,10 +864,10 @@ def mesh(self, mesh): cv.check_type('filter mesh', mesh, openmc.MeshBase) self._mesh = mesh if isinstance(mesh, openmc.UnstructuredMesh): - if mesh.volumes is None: - self.bins = [] - else: + if mesh.has_statepoint_data: self.bins = list(range(len(mesh.volumes))) + else: + self.bins = [] else: self.bins = list(mesh.indices) @@ -948,7 +972,7 @@ def to_xml_element(self): return element @classmethod - def from_xml_element(cls, elem, **kwargs): + def from_xml_element(cls, elem: ET.Element, **kwargs) -> MeshFilter: mesh_id = int(get_text(elem, 'bins')) mesh_obj = kwargs['meshes'][mesh_id] filter_id = int(elem.get('id')) @@ -960,6 +984,34 @@ def from_xml_element(cls, elem, **kwargs): return out +class MeshBornFilter(MeshFilter): + """Filter events by the mesh cell a particle originated from. + + Parameters + ---------- + mesh : openmc.MeshBase + The mesh object that events will be tallied onto + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + mesh : openmc.MeshBase + The mesh object that events will be tallied onto + id : int + Unique identifier for the filter + translation : Iterable of float + This array specifies a vector that is used to translate (shift) + the mesh for this filter + bins : list of tuple + A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1), + ...] + num_bins : Integral + The number of filter bins + + """ + + class MeshSurfaceFilter(MeshFilter): """Filter events by surface crossings on a mesh. @@ -1325,6 +1377,10 @@ class EnergyFilter(RealFilter): """ units = 'eV' + def __init__(self, values, filter_id=None): + cv.check_length('values', values, 2) + super().__init__(values, filter_id) + def get_bin_index(self, filter_bin): # Use lower energy bound to find index for RealFilters deltas = np.abs(self.bins[:, 1] - filter_bin[1]) / filter_bin[1] @@ -1400,6 +1456,7 @@ def from_group_structure(cls, group_structure): """ + cv.check_value('group_structure', group_structure, openmc.mgxs.GROUP_STRUCTURES.keys()) return cls(openmc.mgxs.GROUP_STRUCTURES[group_structure.upper()]) diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index f05f39fe9b6..f8e677578f1 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -1,4 +1,5 @@ from numbers import Integral, Real + import lxml.etree as ET import openmc.checkvalue as cv @@ -52,6 +53,34 @@ def from_xml_element(cls, elem, **kwargs): order = int(elem.find('order').text) return cls(order, filter_id=filter_id) + def merge(self, other): + """Merge this filter with another. + + This overrides the behavior of the parent Filter class, since its + merging technique is to take the union of the set of bins of each + filter. That technique does not apply to expansion filters, since the + argument should be the maximum filter order rather than the list of all + bins. + + Parameters + ---------- + other : openmc.Filter + Filter to merge with + + Returns + ------- + merged_filter : openmc.Filter + Filter resulting from the merge + + """ + + if not self.can_merge(other): + msg = f'Unable to merge "{type(self)}" with "{type(other)}"' + raise ValueError(msg) + + # Create a new filter with these bins and a new auto-generated ID + return type(self)(max(self.order, other.order)) + class LegendreFilter(ExpansionFilter): r"""Score Legendre expansion moments up to specified order. diff --git a/openmc/geometry.py b/openmc/geometry.py index b2b679de3c8..175cef2bf37 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -293,7 +293,8 @@ def from_xml( if isinstance(materials, (str, os.PathLike)): materials = openmc.Materials.from_xml(materials) - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() return cls.from_xml_element(root, materials) @@ -391,6 +392,20 @@ def get_all_universes(self) -> typing.Dict[int, openmc.Universe]: universes.update(self.root_universe.get_all_universes()) return universes + def get_all_nuclides(self) -> typing.List[str]: + """Return all nuclides within the geometry. + + Returns + ------- + list + Sorted list of all nuclides in materials appearing in the geometry + + """ + all_nuclides = set() + for material in self.get_all_materials().values(): + all_nuclides |= set(material.get_nuclides()) + return sorted(all_nuclides) + def get_all_materials(self) -> typing.Dict[int, openmc.Material]: """Return all materials within the geometry. @@ -686,7 +701,7 @@ def remove_redundant_surfaces(self) -> typing.Dict[int, openmc.Surface]: coeffs = tuple(round(surf._coefficients[k], self.surface_precision) for k in surf._coeff_keys) - key = (surf._type,) + coeffs + key = (surf._type, surf._boundary_type) + coeffs redundancies[key].append(surf) redundant_surfaces = {replace.id: keep @@ -738,7 +753,7 @@ def clone(self) -> Geometry: def plot(self, *args, **kwargs): """Display a slice plot of the geometry. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- diff --git a/openmc/lattice.py b/openmc/lattice.py index 926a016efe3..40b97f6cf5c 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -4,8 +4,8 @@ from math import sqrt, floor from numbers import Real import types -import lxml.etree as ET +import lxml.etree as ET import numpy as np import openmc @@ -1851,7 +1851,7 @@ def _show_indices_y(num_rings): largest_index = 6*(num_rings - 1) n_digits_index = len(str(largest_index)) n_digits_ring = len(str(num_rings - 1)) - str_form = '({{:{}}},{{:{}}})'.format(n_digits_ring, n_digits_index) + str_form = f'({{:{n_digits_ring}}},{{:{n_digits_index}}})' pad = ' '*(n_digits_index + n_digits_ring + 3) # Initialize the list for each row. @@ -1956,7 +1956,7 @@ def _show_indices_x(num_rings): largest_index = 6*(num_rings - 1) n_digits_index = len(str(largest_index)) n_digits_ring = len(str(num_rings - 1)) - str_form = '({{:{}}},{{:{}}})'.format(n_digits_ring, n_digits_index) + str_form = f'({{:{n_digits_ring}}},{{:{n_digits_index}}})' pad = ' '*(n_digits_index + n_digits_ring + 3) # Initialize the list for each row. diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 0e5ad92feb9..42d77944146 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -28,7 +28,7 @@ if os.environ.get('READTHEDOCS', None) != 'True': # Open shared library _filename = pkg_resources.resource_filename( - __name__, 'libopenmc.{}'.format(_suffix)) + __name__, f'libopenmc.{_suffix}') _dll = CDLL(_filename) else: # For documentation builds, we don't actually have the shared library @@ -54,6 +54,9 @@ def _libmesh_enabled(): def _mcpl_enabled(): return c_bool.in_dll(_dll, "MCPL_ENABLED").value +def _uwuw_enabled(): + return c_bool.in_dll(_dll, "UWUW_ENABLED").value + from .error import * from .core import * diff --git a/openmc/lib/cell.py b/openmc/lib/cell.py index 807fe73c773..971a24cba91 100644 --- a/openmc/lib/cell.py +++ b/openmc/lib/cell.py @@ -268,7 +268,7 @@ def rotation(self): return rotation_data[9:] else: raise ValueError( - 'Invalid size of rotation matrix: {}'.format(rot_size)) + f'Invalid size of rotation matrix: {rot_size}') @rotation.setter def rotation(self, rotation_data): diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 8d64426374e..a9a549fa05a 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -95,6 +95,11 @@ class _SourceSite(Structure): _dll.openmc_statepoint_write.argtypes = [c_char_p, POINTER(c_bool)] _dll.openmc_statepoint_write.restype = c_int _dll.openmc_statepoint_write.errcheck = _error_handler +_dll.openmc_statepoint_load.argtypes = [c_char_p] +_dll.openmc_statepoint_load.restype = c_int +_dll.openmc_statepoint_load.errcheck = _error_handler +_dll.openmc_statepoint_write.restype = c_int +_dll.openmc_statepoint_write.errcheck = _error_handler _dll.openmc_global_bounding_box.argtypes = [POINTER(c_double), POINTER(c_double)] _dll.openmc_global_bounding_box.restype = c_int @@ -174,7 +179,7 @@ def export_properties(filename=None, output=True): def export_weight_windows(filename="weight_windows.h5", output=True): """Export weight windows. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -198,7 +203,7 @@ def export_weight_windows(filename="weight_windows.h5", output=True): def import_weight_windows(filename='weight_windows.h5', output=True): """Import weight windows. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -568,6 +573,19 @@ def statepoint_write(filename=None, write_source=True): _dll.openmc_statepoint_write(filename, c_bool(write_source)) +def statepoint_load(filename: PathLike): + """Load a statepoint file. + + Parameters + ---------- + filename : path-like + Path to the statepoint to load. + + """ + filename = c_char_p(str(filename).encode()) + _dll.openmc_statepoint_load(filename) + + @contextmanager def run_in_memory(**kwargs): """Provides context manager for calling OpenMC shared library functions. @@ -611,7 +629,7 @@ def __set__(self, instance, value): class _FortranObject: def __repr__(self): - return "{}[{}]".format(type(self).__name__, self._index) + return f"<{type(self).__name__}(index={self._index})>" class _FortranObjectWithID(_FortranObject): @@ -622,6 +640,9 @@ def __init__(self, uid=None, new=True, index=None): # OutOfBoundsError will be raised here by virtue of referencing self.id self.id + def __repr__(self): + return f"<{type(self).__name__}(id={self.id})>" + @contextmanager def quiet_dll(output=True): diff --git a/openmc/lib/error.py b/openmc/lib/error.py index 89e7b6e3903..dbe08e1ef8b 100644 --- a/openmc/lib/error.py +++ b/openmc/lib/error.py @@ -37,5 +37,5 @@ def errcode(s): warn(msg) elif err < 0: if not msg: - msg = "Unknown error encountered (code {}).".format(err) + msg = f"Unknown error encountered (code {err})." raise exc.OpenMCError(msg) diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 8b175dd02d2..340c2fa3448 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -20,7 +20,8 @@ 'Filter', 'AzimuthalFilter', 'CellFilter', 'CellbornFilter', 'CellfromFilter', 'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter', 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', - 'MaterialFilter', 'MeshFilter', 'MeshSurfaceFilter', 'MuFilter', 'ParticleFilter', + 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', + 'MeshSurfaceFilter', 'MuFilter', 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' ] @@ -89,18 +90,36 @@ _dll.openmc_mesh_filter_set_mesh.argtypes = [c_int32, c_int32] _dll.openmc_mesh_filter_set_mesh.restype = c_int _dll.openmc_mesh_filter_set_mesh.errcheck = _error_handler -_dll.openmc_meshsurface_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)] -_dll.openmc_meshsurface_filter_get_mesh.restype = c_int -_dll.openmc_meshsurface_filter_get_mesh.errcheck = _error_handler -_dll.openmc_meshsurface_filter_set_mesh.argtypes = [c_int32, c_int32] -_dll.openmc_meshsurface_filter_set_mesh.restype = c_int -_dll.openmc_meshsurface_filter_set_mesh.errcheck = _error_handler _dll.openmc_mesh_filter_get_translation.argtypes = [c_int32, POINTER(c_double*3)] _dll.openmc_mesh_filter_get_translation.restype = c_int _dll.openmc_mesh_filter_get_translation.errcheck = _error_handler _dll.openmc_mesh_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)] _dll.openmc_mesh_filter_set_translation.restype = c_int _dll.openmc_mesh_filter_set_translation.errcheck = _error_handler +_dll.openmc_meshborn_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_meshborn_filter_get_mesh.restype = c_int +_dll.openmc_meshborn_filter_get_mesh.errcheck = _error_handler +_dll.openmc_meshborn_filter_set_mesh.argtypes = [c_int32, c_int32] +_dll.openmc_meshborn_filter_set_mesh.restype = c_int +_dll.openmc_meshborn_filter_set_mesh.errcheck = _error_handler +_dll.openmc_meshborn_filter_get_translation.argtypes = [c_int32, POINTER(c_double*3)] +_dll.openmc_meshborn_filter_get_translation.restype = c_int +_dll.openmc_meshborn_filter_get_translation.errcheck = _error_handler +_dll.openmc_meshborn_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)] +_dll.openmc_meshborn_filter_set_translation.restype = c_int +_dll.openmc_meshborn_filter_set_translation.errcheck = _error_handler +_dll.openmc_meshsurface_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_meshsurface_filter_get_mesh.restype = c_int +_dll.openmc_meshsurface_filter_get_mesh.errcheck = _error_handler +_dll.openmc_meshsurface_filter_set_mesh.argtypes = [c_int32, c_int32] +_dll.openmc_meshsurface_filter_set_mesh.restype = c_int +_dll.openmc_meshsurface_filter_set_mesh.errcheck = _error_handler +_dll.openmc_meshsurface_filter_get_translation.argtypes = [c_int32, POINTER(c_double*3)] +_dll.openmc_meshsurface_filter_get_translation.restype = c_int +_dll.openmc_meshsurface_filter_get_translation.errcheck = _error_handler +_dll.openmc_meshsurface_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)] +_dll.openmc_meshsurface_filter_set_translation.restype = c_int +_dll.openmc_meshsurface_filter_set_translation.errcheck = _error_handler _dll.openmc_new_filter.argtypes = [c_char_p, POINTER(c_int32)] _dll.openmc_new_filter.restype = c_int _dll.openmc_new_filter.errcheck = _error_handler @@ -342,7 +361,39 @@ def bins(self, materials): _dll.openmc_material_filter_set_bins(self._index, n, bins) +class MaterialFromFilter(Filter): + filter_type = 'materialfrom' + + class MeshFilter(Filter): + """Mesh filter stored internally. + + This class exposes a Mesh filter that is stored internally in the OpenMC + library. To obtain a view of a Mesh filter with a given ID, use the + :data:`openmc.lib.filters` mapping. + + Parameters + ---------- + mesh : openmc.lib.Mesh + Mesh to use for the filter + uid : int or None + Unique ID of the Mesh filter + new : bool + When `index` is None, this argument controls whether a new object is + created or a view of an existing object is returned. + index : int + Index in the `filters` array. + + Attributes + ---------- + filter_type : str + Type of filter + mesh : openmc.lib.Mesh + Mesh used for the filter + translation : Iterable of float + 3-D coordinates of the translation vector + + """ filter_type = 'mesh' def __init__(self, mesh=None, uid=None, new=True, index=None): @@ -371,7 +422,92 @@ def translation(self, translation): _dll.openmc_mesh_filter_set_translation(self._index, (c_double*3)(*translation)) +class MeshBornFilter(Filter): + """MeshBorn filter stored internally. + + This class exposes a MeshBorn filter that is stored internally in the OpenMC + library. To obtain a view of a MeshBorn filter with a given ID, use the + :data:`openmc.lib.filters` mapping. + + Parameters + ---------- + mesh : openmc.lib.Mesh + Mesh to use for the filter + uid : int or None + Unique ID of the MeshBorn filter + new : bool + When `index` is None, this argument controls whether a new object is + created or a view of an existing object is returned. + index : int + Index in the `filters` array. + + Attributes + ---------- + filter_type : str + Type of filter + mesh : openmc.lib.Mesh + Mesh used for the filter + translation : Iterable of float + 3-D coordinates of the translation vector + + """ + filter_type = 'meshborn' + + def __init__(self, mesh=None, uid=None, new=True, index=None): + super().__init__(uid, new, index) + if mesh is not None: + self.mesh = mesh + + @property + def mesh(self): + index_mesh = c_int32() + _dll.openmc_meshborn_filter_get_mesh(self._index, index_mesh) + return _get_mesh(index_mesh.value) + + @mesh.setter + def mesh(self, mesh): + _dll.openmc_meshborn_filter_set_mesh(self._index, mesh._index) + + @property + def translation(self): + translation = (c_double*3)() + _dll.openmc_meshborn_filter_get_translation(self._index, translation) + return tuple(translation) + + @translation.setter + def translation(self, translation): + _dll.openmc_meshborn_filter_set_translation(self._index, (c_double*3)(*translation)) + + class MeshSurfaceFilter(Filter): + """MeshSurface filter stored internally. + + This class exposes a MeshSurface filter that is stored internally in the + OpenMC library. To obtain a view of a MeshSurface filter with a given ID, + use the :data:`openmc.lib.filters` mapping. + + Parameters + ---------- + mesh : openmc.lib.Mesh + Mesh to use for the filter + uid : int or None + Unique ID of the MeshSurface filter + new : bool + When `index` is None, this argument controls whether a new object is + created or a view of an existing object is returned. + index : int + Index in the `filters` array. + + Attributes + ---------- + filter_type : str + Type of filter + mesh : openmc.lib.Mesh + Mesh used for the filter + translation : Iterable of float + 3-D coordinates of the translation vector + + """ filter_type = 'meshsurface' def __init__(self, mesh=None, uid=None, new=True, index=None): @@ -392,12 +528,12 @@ def mesh(self, mesh): @property def translation(self): translation = (c_double*3)() - _dll.openmc_mesh_filter_get_translation(self._index, translation) + _dll.openmc_meshsurface_filter_get_translation(self._index, translation) return tuple(translation) @translation.setter def translation(self, translation): - _dll.openmc_mesh_filter_set_translation(self._index, (c_double*3)(*translation)) + _dll.openmc_meshsurface_filter_set_translation(self._index, (c_double*3)(*translation)) class MuFilter(Filter): @@ -501,7 +637,9 @@ class ZernikeRadialFilter(ZernikeFilter): 'energyfunction': EnergyFunctionFilter, 'legendre': LegendreFilter, 'material': MaterialFilter, + 'materialfrom': MaterialFromFilter, 'mesh': MeshFilter, + 'meshborn': MeshBornFilter, 'meshsurface': MeshSurfaceFilter, 'mu': MuFilter, 'particle': ParticleFilter, diff --git a/openmc/lib/material.py b/openmc/lib/material.py index fde197d3d8d..0ed8932da8f 100644 --- a/openmc/lib/material.py +++ b/openmc/lib/material.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from ctypes import c_int, c_int32, c_double, c_char_p, POINTER, c_size_t +from ctypes import c_bool, c_int, c_int32, c_double, c_char_p, POINTER, c_size_t from weakref import WeakValueDictionary import numpy as np @@ -60,6 +60,12 @@ _dll.openmc_material_set_volume.argtypes = [c_int32, c_double] _dll.openmc_material_set_volume.restype = c_int _dll.openmc_material_set_volume.errcheck = _error_handler +_dll.openmc_material_get_depletable.argtypes = [c_int32, POINTER(c_bool)] +_dll.openmc_material_get_depletable.restype = c_int +_dll.openmc_material_get_depletable.errcheck = _error_handler +_dll.openmc_material_set_depletable.argtypes = [c_int32, c_bool] +_dll.openmc_material_set_depletable.restype = c_int +_dll.openmc_material_set_depletable.errcheck = _error_handler _dll.n_materials.argtypes = [] _dll.n_materials.restype = c_size_t @@ -89,6 +95,8 @@ class Material(_FortranObjectWithID): List of nuclides in the material densities : numpy.ndarray Array of densities in atom/b-cm + depletable : bool + Whether this material is marked as depletable name : str Name of the material temperature : float @@ -169,6 +177,16 @@ def volume(self): def volume(self, volume): _dll.openmc_material_set_volume(self._index, volume) + @property + def depletable(self): + depletable = c_bool() + _dll.openmc_material_get_depletable(self._index, depletable) + return depletable.value + + @depletable.setter + def depletable(self, depletable): + _dll.openmc_material_set_depletable(self._index, depletable) + @property def nuclides(self): return self._get_densities()[0] diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 72d61cc6650..4da7baba771 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -1,6 +1,8 @@ from collections.abc import Mapping -from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, - create_string_buffer) +from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, + create_string_buffer, c_uint64, c_size_t) +from random import getrandbits +from typing import Optional, List, Tuple, Sequence from weakref import WeakValueDictionary import numpy as np @@ -10,8 +12,21 @@ from . import _dll from .core import _FortranObjectWithID from .error import _error_handler +from .material import Material +from .plot import _Position + +__all__ = [ + 'Mesh', 'RegularMesh', 'RectilinearMesh', 'CylindricalMesh', + 'SphericalMesh', 'UnstructuredMesh', 'meshes' +] + + +class _MaterialVolume(Structure): + _fields_ = [ + ("material", c_int32), + ("volume", c_double) + ] -__all__ = ['RegularMesh', 'RectilinearMesh', 'CylindricalMesh', 'SphericalMesh', 'UnstructuredMesh', 'meshes'] # Mesh functions _dll.openmc_extend_meshes.argtypes = [c_int32, c_char_p, POINTER(c_int32), @@ -24,6 +39,22 @@ _dll.openmc_mesh_set_id.argtypes = [c_int32, c_int32] _dll.openmc_mesh_set_id.restype = c_int _dll.openmc_mesh_set_id.errcheck = _error_handler +_dll.openmc_mesh_get_n_elements.argtypes = [c_int32, POINTER(c_size_t)] +_dll.openmc_mesh_get_n_elements.restype = c_int +_dll.openmc_mesh_get_n_elements.errcheck = _error_handler +_dll.openmc_mesh_get_volumes.argtypes = [c_int32, POINTER(c_double)] +_dll.openmc_mesh_get_volumes.restype = c_int +_dll.openmc_mesh_get_volumes.errcheck = _error_handler +_dll.openmc_mesh_material_volumes.argtypes = [ + c_int32, c_int, c_int, c_int, POINTER(_MaterialVolume), + POINTER(c_int), POINTER(c_uint64)] +_dll.openmc_mesh_material_volumes.restype = c_int +_dll.openmc_mesh_material_volumes.errcheck = _error_handler +_dll.openmc_mesh_get_plot_bins.argtypes = [ + c_int32, _Position, _Position, c_int, POINTER(c_int), POINTER(c_int32) +] +_dll.openmc_mesh_get_plot_bins.restype = c_int +_dll.openmc_mesh_get_plot_bins.errcheck = _error_handler _dll.openmc_get_mesh_index.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_get_mesh_index.restype = c_int _dll.openmc_get_mesh_index.errcheck = _error_handler @@ -123,6 +154,114 @@ def id(self): def id(self, mesh_id): _dll.openmc_mesh_set_id(self._index, mesh_id) + @property + def n_elements(self) -> int: + n = c_size_t() + _dll.openmc_mesh_get_n_elements(self._index, n) + return n.value + + @property + def volumes(self) -> np.ndarray: + volumes = np.empty((self.n_elements,)) + _dll.openmc_mesh_get_volumes( + self._index, volumes.ctypes.data_as(POINTER(c_double))) + return volumes + + def material_volumes( + self, + n_samples: int = 10_000, + prn_seed: Optional[int] = None + ) -> List[List[Tuple[Material, float]]]: + """Determine volume of materials in each mesh element + + .. versionadded:: 0.14.1 + + Parameters + ---------- + n_samples : int + Number of samples in each mesh element + prn_seed : int + Pseudorandom number generator (PRNG) seed; if None, one will be + generated randomly. + + Returns + ------- + List of tuple of (material, volume) for each mesh element. Void volume + is represented by having a value of None in the first element of a + tuple. + + """ + if n_samples <= 0: + raise ValueError("Number of samples must be positive") + if prn_seed is None: + prn_seed = getrandbits(63) + prn_seed = c_uint64(prn_seed) + + # Preallocate space for MaterialVolume results + size = 16 + result = (_MaterialVolume * size)() + + hits = c_int() # Number of materials hit in a given element + volumes = [] + for i_element in range(self.n_elements): + while True: + try: + _dll.openmc_mesh_material_volumes( + self._index, n_samples, i_element, size, result, hits, prn_seed) + except AllocationError: + # Increase size of result array and try again + size *= 2 + result = (_MaterialVolume * size)() + else: + # If no error, break out of loop + break + + volumes.append([ + (Material(index=r.material), r.volume) + for r in result[:hits.value] + ]) + return volumes + + def get_plot_bins( + self, + origin: Sequence[float], + width: Sequence[float], + basis: str, + pixels: Sequence[int] + ) -> np.ndarray: + """Get mesh bin indices for a rasterized plot. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + origin : iterable of float + Origin of the plotting view. Should have length 3. + width : iterable of float + Width of the plotting view. Should have length 2. + basis : {'xy', 'xz', 'yz'} + Plotting basis. + pixels : iterable of int + Number of pixels in each direction. Should have length 2. + + Returns + ------- + 2D numpy array with mesh bin indices corresponding to each pixel within + the plotting view. + + """ + origin = _Position(*origin) + width = _Position(*width) + basis = {'xy': 1, 'xz': 2, 'yz': 3}[basis] + pixel_array = (c_int*2)(*pixels) + img_data = np.zeros((pixels[1], pixels[0]), dtype=np.dtype('int32')) + + _dll.openmc_mesh_get_plot_bins( + self._index, origin, width, basis, pixel_array, + img_data.ctypes.data_as(POINTER(c_int32)) + ) + return img_data + class RegularMesh(Mesh): """RegularMesh stored internally. @@ -150,6 +289,10 @@ class RegularMesh(Mesh): are given, it is assumed that the mesh is an x-y mesh. width : numpy.ndarray The width of mesh cells in each direction. + n_elements : int + Total number of mesh elements. + volumes : numpy.ndarray + Volume of each mesh element in [cm^3] """ mesh_type = 'regular' @@ -232,6 +375,10 @@ class RectilinearMesh(Mesh): The upper-right corner of the structrued mesh. width : numpy.ndarray The width of mesh cells in each direction. + n_elements : int + Total number of mesh elements. + volumes : numpy.ndarray + Volume of each mesh element in [cm^3] """ mesh_type = 'rectilinear' @@ -331,6 +478,10 @@ class CylindricalMesh(Mesh): The upper-right corner of the structrued mesh. width : numpy.ndarray The width of mesh cells in each direction. + n_elements : int + Total number of mesh elements. + volumes : numpy.ndarray + Volume of each mesh element in [cm^3] """ mesh_type = 'cylindrical' @@ -405,6 +556,7 @@ def set_grid(self, r_grid, phi_grid, z_grid): _dll.openmc_cylindrical_mesh_set_grid(self._index, r_grid, nr, phi_grid, nphi, z_grid, nz) + class SphericalMesh(Mesh): """SphericalMesh stored internally. @@ -429,6 +581,10 @@ class SphericalMesh(Mesh): The upper-right corner of the structrued mesh. width : numpy.ndarray The width of mesh cells in each direction. + n_elements : int + Total number of mesh elements. + volumes : numpy.ndarray + Volume of each mesh element in [cm^3] """ mesh_type = 'spherical' diff --git a/openmc/lib/nuclide.py b/openmc/lib/nuclide.py index 399bb346520..8078882cf35 100644 --- a/openmc/lib/nuclide.py +++ b/openmc/lib/nuclide.py @@ -91,7 +91,7 @@ def collapse_rate(self, MT, temperature, energy, flux): energy : iterable of float Energy group boundaries in [eV] flux : iterable of float - Flux in each energt group (not normalized per eV) + Flux in each energy group (not normalized per eV) Returns ------- diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 9f294a42c04..d9863667641 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -31,7 +31,7 @@ def __getitem__(self, idx): elif idx == 2: return self.z else: - raise IndexError("{} index is invalid for _Position".format(idx)) + raise IndexError(f"{idx} index is invalid for _Position") def __setitem__(self, idx, val): if idx == 0: @@ -41,10 +41,10 @@ def __setitem__(self, idx, val): elif idx == 2: self.z = val else: - raise IndexError("{} index is invalid for _Position".format(idx)) + raise IndexError(f"{idx} index is invalid for _Position") def __repr__(self): - return "({}, {}, {})".format(self.x, self.y, self.z) + return f"({self.x}, {self.y}, {self.z})" class _PlotBase(Structure): @@ -127,7 +127,7 @@ def basis(self): elif self.basis_ == 3: return 'yz' - raise ValueError("Plot basis {} is invalid".format(self.basis_)) + raise ValueError(f"Plot basis {self.basis_} is invalid") @basis.setter def basis(self, basis): @@ -135,7 +135,7 @@ def basis(self, basis): valid_bases = ('xy', 'xz', 'yz') basis = basis.lower() if basis not in valid_bases: - raise ValueError("{} is not a valid plot basis.".format(basis)) + raise ValueError(f"{basis} is not a valid plot basis.") if basis == 'xy': self.basis_ = 1 @@ -148,12 +148,11 @@ def basis(self, basis): if isinstance(basis, int): valid_bases = (1, 2, 3) if basis not in valid_bases: - raise ValueError("{} is not a valid plot basis.".format(basis)) + raise ValueError(f"{basis} is not a valid plot basis.") self.basis_ = basis return - raise ValueError("{} of type {} is an" - " invalid plot basis".format(basis, type(basis))) + raise ValueError(f"{basis} of type {type(basis)} is an invalid plot basis") @property def h_res(self): @@ -199,14 +198,14 @@ def __repr__(self): out_str = ["-----", "Plot:", "-----", - "Origin: {}".format(self.origin), - "Width: {}".format(self.width), - "Height: {}".format(self.height), - "Basis: {}".format(self.basis), - "HRes: {}".format(self.h_res), - "VRes: {}".format(self.v_res), - "Color Overlaps: {}".format(self.color_overlaps), - "Level: {}".format(self.level)] + f"Origin: {self.origin}", + f"Width: {self.width}", + f"Height: {self.height}", + f"Basis: {self.basis}", + f"HRes: {self.h_res}", + f"VRes: {self.v_res}", + f"Color Overlaps: {self.color_overlaps}", + f"Level: {self.level}"] return '\n'.join(out_str) diff --git a/openmc/lib/settings.py b/openmc/lib/settings.py index 6a7e5aa124b..062670ef843 100644 --- a/openmc/lib/settings.py +++ b/openmc/lib/settings.py @@ -53,7 +53,7 @@ def run_mode(self, mode): current_idx.value = idx break else: - raise ValueError('Invalid run mode: {}'.format(mode)) + raise ValueError(f'Invalid run mode: {mode}') @property def path_statepoint(self): diff --git a/openmc/lib/tally.py b/openmc/lib/tally.py index 417d084a6e3..d0b34aedc26 100644 --- a/openmc/lib/tally.py +++ b/openmc/lib/tally.py @@ -183,7 +183,7 @@ class Tally(_FortranObjectWithID): multiply_density : bool Whether reaction rates should be multiplied by atom density - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 nuclides : list of str List of nuclides to score results for num_realizations : int diff --git a/openmc/lib/weight_windows.py b/openmc/lib/weight_windows.py index 2984eb23318..d92f019179f 100644 --- a/openmc/lib/weight_windows.py +++ b/openmc/lib/weight_windows.py @@ -109,7 +109,7 @@ class WeightWindows(_FortranObjectWithID): OpenMC library. To obtain a view of a weight windows object with a given ID, use the :data:`openmc.lib.weight_windows` mapping. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -392,4 +392,4 @@ def __repr__(self): def __delitem__(self): raise NotImplementedError("WeightWindows object remove not implemented") -weight_windows = _WeightWindowsMapping() \ No newline at end of file +weight_windows = _WeightWindowsMapping() diff --git a/openmc/material.py b/openmc/material.py index d8bcc209c16..6edc372161c 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -8,8 +8,8 @@ import typing # imported separately as py3.8 requires typing.Iterable import warnings from typing import Optional, List, Union, Dict -import lxml.etree as ET +import lxml.etree as ET import numpy as np import h5py @@ -152,7 +152,7 @@ def __repr__(self) -> str: for nuclide, percent, percent_type in self._nuclides: string += '{: <16}'.format('\t{}'.format(nuclide)) - string += '=\t{: <12} [{}]\n'.format(percent, percent_type) + string += f'=\t{percent: <12} [{percent_type}]\n' if self._macroscopic is not None: string += '{: <16}\n'.format('\tMacroscopic Data') @@ -289,7 +289,7 @@ def get_decay_photon_energy( ) -> Optional[Univariate]: r"""Return energy distribution of decay photons from unstable nuclides. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -469,8 +469,7 @@ def add_volume_information(self, volume_calc): raise ValueError('No volume information found for material ID={}.' .format(self.id)) else: - raise ValueError('No volume information found for material ID={}.' - .format(self.id)) + raise ValueError(f'No volume information found for material ID={self.id}.') def set_density(self, units: str, density: Optional[float] = None): """Set the density of the material @@ -500,7 +499,7 @@ def set_density(self, units: str, density: Optional[float] = None): '"sum" unit'.format(self.id) raise ValueError(msg) - cv.check_type('the density for Material ID="{}"'.format(self.id), + cv.check_type(f'the density for Material ID="{self.id}"', density, Real) self._density = density @@ -571,7 +570,7 @@ def add_components(self, components: dict, percent_type: str = 'ao'): for component, params in components.items(): cv.check_type('component', component, str) - if isinstance(params, float): + if isinstance(params, Real): params = {'percent': params} else: @@ -743,20 +742,18 @@ def add_element(self, element: str, percent: float, percent_type: str = 'ao', el = element.lower() element = openmc.data.ELEMENT_SYMBOL.get(el) if element is None: - msg = 'Element name "{}" not recognised'.format(el) + msg = f'Element name "{el}" not recognised' raise ValueError(msg) else: if element[0].islower(): - msg = 'Element name "{}" should start with an uppercase ' \ - 'letter'.format(element) + msg = f'Element name "{element}" should start with an uppercase letter' raise ValueError(msg) if len(element) == 2 and element[1].isupper(): - msg = 'Element name "{}" should end with a lowercase ' \ - 'letter'.format(element) + msg = f'Element name "{element}" should end with a lowercase letter' raise ValueError(msg) # skips the first entry of ATOMIC_SYMBOL which is n for neutron if element not in list(openmc.data.ATOMIC_SYMBOL.values())[1:]: - msg = 'Element name "{}" not recognised'.format(element) + msg = f'Element name "{element}" not recognised' raise ValueError(msg) if self._macroscopic is not None: @@ -847,8 +844,7 @@ def add_elements_from_formula(self, formula: str, percent_type: str = 'ao', for token in row: if token.isalpha(): if token == "n" or token not in openmc.data.ATOMIC_NUMBER: - msg = 'Formula entry {} not an element symbol.' \ - .format(token) + msg = f'Formula entry {token} not an element symbol.' raise ValueError(msg) elif token not in ['(', ')', ''] and not token.isdigit(): msg = 'Formula must be made from a sequence of ' \ @@ -1373,8 +1369,7 @@ def to_xml_element( subelement.set("value", str(self._density)) subelement.set("units", self._density_units) else: - raise ValueError('Density has not been set for material {}!' - .format(self.id)) + raise ValueError(f'Density has not been set for material {self.id}!') if self._macroscopic is None: # Create nuclide XML subelements @@ -1480,7 +1475,7 @@ def mix_materials(cls, materials, fracs: typing.Iterable[float], # Create the new material with the desired name if name is None: - name = '-'.join(['{}({})'.format(m.name, f) for m, f in + name = '-'.join([f'{m.name}({f})' for m, f in zip(materials, fracs)]) new_mat = openmc.Material(name=name) @@ -1713,7 +1708,7 @@ def export_to_xml(self, path: PathLike = 'materials.xml', self._write_xml(fh, nuclides_to_ignore=nuclides_to_ignore) @classmethod - def from_xml_element(cls, elem) -> Material: + def from_xml_element(cls, elem) -> Materials: """Generate materials collection from XML file Parameters @@ -1740,7 +1735,7 @@ def from_xml_element(cls, elem) -> Material: return materials @classmethod - def from_xml(cls, path: PathLike = 'materials.xml') -> Material: + def from_xml(cls, path: PathLike = 'materials.xml') -> Materials: """Generate materials collection from XML file Parameters @@ -1754,7 +1749,8 @@ def from_xml(cls, path: PathLike = 'materials.xml') -> Material: Materials collection """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() return cls.from_xml_element(root) diff --git a/openmc/mesh.py b/openmc/mesh.py index c0203dcd9ef..8777da32554 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1,11 +1,14 @@ +from __future__ import annotations import typing import warnings from abc import ABC, abstractmethod from collections.abc import Iterable -from math import pi +from functools import wraps +from math import pi, sqrt, atan2 from numbers import Integral, Real from pathlib import Path -from typing import Optional, Sequence, Tuple +import tempfile +from typing import Optional, Sequence, Tuple, List import h5py import lxml.etree as ET @@ -14,6 +17,7 @@ import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike +from openmc.utility_funcs import change_directory from ._xml import get_text from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES @@ -35,7 +39,11 @@ class MeshBase(IDManagerMixin, ABC): Unique identifier for the mesh name : str Name of the mesh - + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. + indices : Iterable of tuple + An iterable of mesh indices for each mesh element, e.g. [(1, 1, 1), (2, 1, 1), ...] """ next_id = 1 @@ -58,6 +66,15 @@ def name(self, name: str): else: self._name = '' + @property + def bounding_box(self) -> openmc.BoundingBox: + return openmc.BoundingBox(self.lower_left, self.upper_right) + + @property + @abstractmethod + def indices(self): + pass + def __repr__(self): string = type(self).__name__ + '\n' string += '{0: <16}{1}{2}\n'.format('\tID', '=\t', self._id) @@ -130,6 +147,94 @@ def from_xml_element(cls, elem: ET.Element): else: raise ValueError(f'Unrecognized mesh type "{mesh_type}" found.') + def get_homogenized_materials( + self, + model: openmc.Model, + n_samples: int = 10_000, + prn_seed: Optional[int] = None, + **kwargs + ) -> List[openmc.Material]: + """Generate homogenized materials over each element in a mesh. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + model : openmc.Model + Model containing materials to be homogenized and the associated + geometry. + n_samples : int + Number of samples in each mesh element. + prn_seed : int, optional + Pseudorandom number generator (PRNG) seed; if None, one will be + generated randomly. + **kwargs + Keyword-arguments passed to :func:`openmc.lib.init`. + + Returns + ------- + list of openmc.Material + Homogenized material in each mesh element + + """ + import openmc.lib + + with change_directory(tmpdir=True): + # In order to get mesh into model, we temporarily replace the + # tallies with a single mesh tally using the current mesh + original_tallies = model.tallies + new_tally = openmc.Tally() + new_tally.filters = [openmc.MeshFilter(self)] + new_tally.scores = ['flux'] + model.tallies = [new_tally] + + # Export model to XML + model.export_to_model_xml() + + # Get material volume fractions + openmc.lib.init(**kwargs) + mesh = openmc.lib.tallies[new_tally.id].filters[0].mesh + mat_volume_by_element = [ + [ + (mat.id if mat is not None else None, volume) + for mat, volume in mat_volume_list + ] + for mat_volume_list in mesh.material_volumes(n_samples, prn_seed) + ] + openmc.lib.finalize() + + # Restore original tallies + model.tallies = original_tallies + + # Create homogenized material for each element + materials = model.geometry.get_all_materials() + homogenized_materials = [] + for mat_volume_list in mat_volume_by_element: + material_ids, volumes = [list(x) for x in zip(*mat_volume_list)] + total_volume = sum(volumes) + + # Check for void material and remove + try: + index_void = material_ids.index(None) + except ValueError: + pass + else: + material_ids.pop(index_void) + volumes.pop(index_void) + + # Compute volume fractions + volume_fracs = np.array(volumes) / total_volume + + # Get list of materials and mix 'em up! + mats = [materials[uid] for uid in material_ids] + homogenized_mat = openmc.Material.mix_materials( + mats, volume_fracs, 'vo' + ) + homogenized_mat.volume = total_volume + homogenized_materials.append(homogenized_mat) + + return homogenized_materials + class StructuredMesh(MeshBase): """A base class for structured mesh functionality @@ -179,18 +284,18 @@ def vertices(self): ------- vertices : numpy.ndarray Returns a numpy.ndarray representing the coordinates of the mesh - vertices with a shape equal to (ndim, dim1 + 1, ..., dimn + 1). Can be - unpacked along the first dimension with xx, yy, zz = mesh.vertices. + vertices with a shape equal to (dim1 + 1, ..., dimn + 1, ndim). X, Y, Z values + can be unpacked with xx, yy, zz = np.rollaxis(mesh.vertices, -1). """ return self._generate_vertices(*self._grids) @staticmethod def _generate_vertices(i_grid, j_grid, k_grid): - """Returns an array with shape (3, i_grid.size+1, j_grid.size+1, k_grid.size+1) + """Returns an array with shape (i_grid.size, j_grid.size, k_grid.size, 3) containing the corner vertices of mesh elements. """ - return np.stack(np.meshgrid(i_grid, j_grid, k_grid, indexing='ij'), axis=0) + return np.stack(np.meshgrid(i_grid, j_grid, k_grid, indexing='ij'), axis=-1) @staticmethod def _generate_edge_midpoints(grids): @@ -206,7 +311,7 @@ def _generate_edge_midpoints(grids): midpoint_grids : list of numpy.ndarray The edge midpoints for the i, j, and k midpoints of each element in i, j, k ordering. The shapes of the resulting grids are - [(3, ni-1, nj, nk), (3, ni, nj-1, nk), (3, ni, nj, nk-1)] + [(ni-1, nj, nk, 3), (ni, nj-1, nk, 3), (ni, nj, nk-1, 3)] """ # generate a set of edge midpoints for each dimension midpoint_grids = [] @@ -216,7 +321,7 @@ def _generate_edge_midpoints(grids): # each grid is comprised of the mid points for one dimension and the # corner vertices of the other two for dims in ((0, 1, 2), (1, 0, 2), (2, 0, 1)): - # compute the midpoints along the first dimension + # compute the midpoints along the last dimension midpoints = grids[dims[0]][:-1] + 0.5 * np.diff(grids[dims[0]]) coords = (midpoints, grids[dims[1]], grids[dims[2]]) @@ -251,17 +356,16 @@ def centroids(self): ------- centroids : numpy.ndarray Returns a numpy.ndarray representing the mesh element centroid - coordinates with a shape equal to (ndim, dim1, ..., dimn). Can be - unpacked along the first dimension with xx, yy, zz = mesh.centroids. - - + coordinates with a shape equal to (dim1, ..., dimn, ndim). X, + Y, Z values can be unpacked with xx, yy, zz = + np.rollaxis(mesh.centroids, -1). """ ndim = self.n_dimension # this line ensures that the vertices aren't adjusted by the origin or # converted to the Cartesian system for cylindrical and spherical meshes vertices = StructuredMesh.vertices.fget(self) - s0 = (slice(None),) + (slice(0, -1),)*ndim - s1 = (slice(None),) + (slice(1, None),)*ndim + s0 = (slice(0, -1),)*ndim + (slice(None),) + s1 = (slice(1, None),)*ndim + (slice(None),) return (vertices[s0] + vertices[s1]) / 2 @property @@ -351,10 +455,8 @@ def _create_vtk_structured_grid(self): import vtk from vtk.util import numpy_support as nps - vertices = self.vertices.T.reshape(-1, 3) - vtkPts = vtk.vtkPoints() - vtkPts.SetData(nps.numpy_to_vtk(vertices, deep=True)) + vtkPts.SetData(nps.numpy_to_vtk(np.swapaxes(self.vertices, 0, 2).reshape(-1, 3), deep=True)) vtk_grid = vtk.vtkStructuredGrid() vtk_grid.SetPoints(vtkPts) vtk_grid.SetDimensions(*[dim + 1 for dim in self.dimension]) @@ -373,7 +475,7 @@ def _create_vtk_unstructured_grid(self): import vtk from vtk.util import numpy_support as nps - corner_vertices = self.vertices.T.reshape(-1, 3) + corner_vertices = np.swapaxes(self.vertices, 0, 2).reshape(-1, 3) vtkPts = vtk.vtkPoints() vtk_grid = vtk.vtkUnstructuredGrid() @@ -415,7 +517,7 @@ def _insert_point(pnt): # list of point IDs midpoint_vertices = self.midpoint_vertices for edge_grid in midpoint_vertices: - for pnt in edge_grid.T.reshape(-1, 3): + for pnt in np.swapaxes(edge_grid, 0, 2).reshape(-1, 3): point_ids.append(_insert_point(pnt)) # determine how many elements in each dimension @@ -448,7 +550,7 @@ def _insert_point(pnt): # initial offset for corner vertices and midpoint dimension flat_idx = corner_vertices.shape[0] + sum(n_midpoint_vertices[:dim]) # generate a flat index into the table of point IDs - midpoint_shape = midpoint_vertices[dim].shape[1:] + midpoint_shape = midpoint_vertices[dim].shape[:-1] flat_idx += np.ravel_multi_index((i+di, j+dj, k+dk), midpoint_shape, order='F') @@ -510,9 +612,9 @@ class RegularMesh(StructuredMesh): upper_right : Iterable of float The upper-right corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. - bounding_box: openmc.BoundingBox - Axis-aligned bounding box of the cell defined by the upper-right and lower- - left coordinates + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. width : Iterable of float The width of mesh cells in each direction. indices : Iterable of tuple @@ -663,12 +765,6 @@ def _grids(self): x1, = self.upper_right return (np.linspace(x0, x1, nx + 1),) - @property - def bounding_box(self): - return openmc.BoundingBox( - np.array(self.lower_left), np.array(self.upper_right) - ) - def __repr__(self): string = super().__repr__() string += '{0: <16}{1}{2}\n'.format('\tDimensions', '=\t', self.n_dimension) @@ -1008,6 +1104,9 @@ class RectilinearMesh(StructuredMesh): indices : Iterable of tuple An iterable of mesh indices for each mesh element, e.g. [(1, 1, 1), (2, 1, 1), ...] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. """ @@ -1059,6 +1158,14 @@ def z_grid(self, grid): def _grids(self): return (self.x_grid, self.y_grid, self.z_grid) + @property + def lower_left(self): + return np.array([self.x_grid[0], self.y_grid[0], self.z_grid[0]]) + + @property + def upper_right(self): + return np.array([self.x_grid[-1], self.y_grid[-1], self.z_grid[-1]]) + @property def volumes(self): """Return Volumes for every mesh cell @@ -1178,7 +1285,7 @@ class CylindricalMesh(StructuredMesh): Parameters ---------- r_grid : numpy.ndarray - 1-D array of mesh boundary points along the r-axis. + 1-D array of mesh boundary points along the r-axis Requirement is r >= 0. z_grid : numpy.ndarray 1-D array of mesh boundary points along the z-axis relative to the @@ -1225,9 +1332,9 @@ class CylindricalMesh(StructuredMesh): upper_right : Iterable of float The upper-right corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. - bounding_box: openmc.OpenMC - Axis-aligned cartesian bounding box of cell defined by upper-right and lower- - left coordinates + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. """ @@ -1324,10 +1431,6 @@ def upper_right(self): self.origin[2] + self.z_grid[-1] )) - @property - def bounding_box(self): - return openmc.BoundingBox(self.lower_left, self.upper_right) - def __repr__(self): fmt = '{0: <16}{1}{2}\n' string = super().__repr__() @@ -1350,6 +1453,66 @@ def __repr__(self): string += fmt.format('\tZ Max:', '=\t', self._z_grid[-1]) return string + def get_indices_at_coords( + self, + coords: Sequence[float] + ) -> Tuple[int, int, int]: + """Finds the index of the mesh voxel at the specified x,y,z coordinates. + + Parameters + ---------- + coords : Sequence[float] + The x, y, z axis coordinates + + Returns + ------- + Tuple[int, int, int] + The r, phi, z indices + + """ + r_value_from_origin = sqrt((coords[0]-self.origin[0])**2 + (coords[1]-self.origin[1])**2) + + if r_value_from_origin < self.r_grid[0] or r_value_from_origin > self.r_grid[-1]: + raise ValueError( + f'The specified x, y ({coords[0]}, {coords[1]}) combine to give an r value of ' + f'{r_value_from_origin} from the origin of {self.origin}.which ' + f'is outside the origin absolute r grid values {self.r_grid}.' + ) + + r_index = np.searchsorted(self.r_grid, r_value_from_origin) - 1 + + z_grid_values = np.array(self.z_grid) + self.origin[2] + + if coords[2] < z_grid_values[0] or coords[2] > z_grid_values[-1]: + raise ValueError( + f'The specified z value ({coords[2]}) from the z origin of ' + f'{self.origin[-1]} is outside of the absolute z grid range {z_grid_values}.' + ) + + z_index = np.argmax(z_grid_values > coords[2]) - 1 + + delta_x = coords[0] - self.origin[0] + delta_y = coords[1] - self.origin[1] + # atan2 returns values in -pi to +pi range + phi_value = atan2(delta_y, delta_x) + if delta_x < 0 and delta_y < 0: + # returned phi_value anticlockwise and negative + phi_value += 2 * pi + if delta_x > 0 and delta_y < 0: + # returned phi_value anticlockwise and negative + phi_value += 2 * pi + + phi_grid_values = np.array(self.phi_grid) + + if phi_value < phi_grid_values[0] or phi_value > phi_grid_values[-1]: + raise ValueError( + f'The phi value ({phi_value}) resulting from the specified x, y ' + f'values is outside of the absolute phi grid range {phi_grid_values}.' + ) + phi_index = np.argmax(phi_grid_values > phi_value) - 1 + + return (r_index, phi_index, z_index) + @classmethod def from_hdf5(cls, group: h5py.Group): mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) @@ -1521,7 +1684,7 @@ def volumes(self): @property def vertices(self): - warnings.warn('Cartesian coordinates are returned from this property as of version 0.13.4') + warnings.warn('Cartesian coordinates are returned from this property as of version 0.14.0') return self._convert_to_cartesian(self.vertices_cylindrical, self.origin) @property @@ -1532,7 +1695,7 @@ def vertices_cylindrical(self): @property def centroids(self): - warnings.warn('Cartesian coordinates are returned from this property as of version 0.13.4') + warnings.warn('Cartesian coordinates are returned from this property as of version 0.14.0') return self._convert_to_cartesian(self.centroids_cylindrical, self.origin) @property @@ -1543,14 +1706,14 @@ def centroids_cylindrical(self): @staticmethod def _convert_to_cartesian(arr, origin: Sequence[float]): - """Converts an array with xyz values in the first dimension (shape (3, ...)) + """Converts an array with r, phi, z values in the last dimension (shape (..., 3)) to Cartesian coordinates. """ - x = arr[0, ...] * np.cos(arr[1, ...]) + origin[0] - y = arr[0, ...] * np.sin(arr[1, ...]) + origin[1] - arr[0, ...] = x - arr[1, ...] = y - arr[2, ...] += origin[2] + x = arr[..., 0] * np.cos(arr[..., 1]) + origin[0] + y = arr[..., 0] * np.sin(arr[..., 1]) + origin[1] + arr[..., 0] = x + arr[..., 1] = y + arr[..., 2] += origin[2] return arr @@ -1609,8 +1772,8 @@ class SphericalMesh(StructuredMesh): The upper-right corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. bounding_box : openmc.BoundingBox - Axis-aligned bounding box of the cell defined by the upper-right and lower- - left coordinates + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. """ @@ -1701,10 +1864,6 @@ def upper_right(self): r = self.r_grid[-1] return np.array((self.origin[0] + r, self.origin[1] + r, self.origin[2] + r)) - @property - def bounding_box(self): - return openmc.BoundingBox(self.lower_left, self.upper_right) - def __repr__(self): fmt = '{0: <16}{1}{2}\n' string = super().__repr__() @@ -1816,7 +1975,7 @@ def volumes(self): @property def vertices(self): - warnings.warn('Cartesian coordinates are returned from this property as of version 0.13.4') + warnings.warn('Cartesian coordinates are returned from this property as of version 0.14.0') return self._convert_to_cartesian(self.vertices_spherical, self.origin) @property @@ -1827,7 +1986,7 @@ def vertices_spherical(self): @property def centroids(self): - warnings.warn('Cartesian coordinates are returned from this property as of version 0.13.4') + warnings.warn('Cartesian coordinates are returned from this property as of version 0.14.0') return self._convert_to_cartesian(self.centroids_spherical, self.origin) @property @@ -1839,19 +1998,30 @@ def centroids_spherical(self): @staticmethod def _convert_to_cartesian(arr, origin: Sequence[float]): - """Converts an array with xyz values in the first dimension (shape (3, ...)) + """Converts an array with r, theta, phi values in the last dimension (shape (..., 3)) to Cartesian coordinates. """ - r_xy = arr[0, ...] * np.sin(arr[1, ...]) - x = r_xy * np.cos(arr[2, ...]) - y = r_xy * np.sin(arr[2, ...]) - z = arr[0, ...] * np.cos(arr[1, ...]) - arr[0, ...] = x + origin[0] - arr[1, ...] = y + origin[1] - arr[2, ...] = z + origin[2] + r_xy = arr[..., 0] * np.sin(arr[..., 1]) + x = r_xy * np.cos(arr[..., 2]) + y = r_xy * np.sin(arr[..., 2]) + z = arr[..., 0] * np.cos(arr[..., 1]) + arr[..., 0] = x + origin[0] + arr[..., 1] = y + origin[1] + arr[..., 2] = z + origin[2] return arr +def require_statepoint_data(func): + @wraps(func) + def wrapper(self: UnstructuredMesh, *args, **kwargs): + if not self._has_statepoint_data: + raise AttributeError(f'The "{func.__name__}" property requires ' + 'information about this mesh to be loaded ' + 'from a statepoint file.') + return func(self, *args, **kwargs) + return wrapper + + class UnstructuredMesh(MeshBase): """A 3D unstructured mesh @@ -1872,6 +2042,11 @@ class UnstructuredMesh(MeshBase): Name of the mesh length_multiplier: float Constant multiplier to apply to mesh coordinates + options : str, optional + Special options that control spatial search data structures used. This + is currently only used to set `parameters + `_ for MOAB's AdaptiveKDTree. If + None, OpenMC internally uses a default of "MAX_DEPTH=20;PLANE_SET=2;". Attributes ---------- @@ -1885,20 +2060,25 @@ class UnstructuredMesh(MeshBase): Multiplicative factor to apply to mesh coordinates library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally + options : str + Special options that control spatial search data structures used. This + is currently only used to set `parameters + `_ for MOAB's AdaptiveKDTree. If + None, OpenMC internally uses a default of "MAX_DEPTH=20;PLANE_SET=2;". output : bool - Indicates whether or not automatic tally output should - be generated for this mesh + Indicates whether or not automatic tally output should be generated for + this mesh volumes : Iterable of float Volumes of the unstructured mesh elements centroids : numpy.ndarray - Centroids of the mesh elements with array shape (3, n_elements) + Centroids of the mesh elements with array shape (n_elements, 3) vertices : numpy.ndarray - Coordinates of the mesh vertices with array shape (3, n_elements) + Coordinates of the mesh vertices with array shape (n_elements, 3) .. versionadded:: 0.13.1 connectivity : numpy.ndarray - Connectivity of the elements with array shape (8, n_elements) + Connectivity of the elements with array shape (n_elements, 8) .. versionadded:: 0.13.1 element_types : Iterable of integers @@ -1907,6 +2087,10 @@ class UnstructuredMesh(MeshBase): .. versionadded:: 0.13.1 total_volume : float Volume of the unstructured mesh in total + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. + """ _UNSUPPORTED_ELEM = -1 @@ -1914,7 +2098,8 @@ class UnstructuredMesh(MeshBase): _LINEAR_HEX = 1 def __init__(self, filename: PathLike, library: str, mesh_id: Optional[int] = None, - name: str = '', length_multiplier: float = 1.0): + name: str = '', length_multiplier: float = 1.0, + options: Optional[str] = None): super().__init__(mesh_id, name) self.filename = filename self._volumes = None @@ -1924,6 +2109,8 @@ def __init__(self, filename: PathLike, library: str, mesh_id: Optional[int] = No self.library = library self._output = False self.length_multiplier = length_multiplier + self.options = options + self._has_statepoint_data = False @property def filename(self): @@ -1944,6 +2131,16 @@ def library(self, lib: str): self._library = lib @property + def options(self) -> Optional[str]: + return self._options + + @options.setter + def options(self, options: Optional[str]): + cv.check_type('options', options, (str, type(None))) + self._options = options + + @property + @require_statepoint_data def size(self): return self._size @@ -1962,6 +2159,7 @@ def output(self, val: bool): self._output = val @property + @require_statepoint_data def volumes(self): """Return Volumes for every mesh cell if populated by a StatePoint file @@ -1980,26 +2178,32 @@ def volumes(self, volumes: typing.Iterable[Real]): self._volumes = volumes @property + @require_statepoint_data def total_volume(self): return np.sum(self.volumes) @property + @require_statepoint_data def vertices(self): - return self._vertices.T + return self._vertices @property + @require_statepoint_data def connectivity(self): - return self._connectivity.T + return self._connectivity @property + @require_statepoint_data def element_types(self): return self._element_types @property + @require_statepoint_data def centroids(self): - return np.array([self.centroid(i) for i in range(self.n_elements)]).T + return np.array([self.centroid(i) for i in range(self.n_elements)]) @property + @require_statepoint_data def n_elements(self): if self._n_elements is None: raise RuntimeError("No information about this mesh has " @@ -2030,6 +2234,15 @@ def dimension(self): def n_dimension(self): return 3 + @property + @require_statepoint_data + def indices(self): + return [(i,) for i in range(self.n_elements)] + + @property + def has_statepoint_data(self) -> bool: + return self._has_statepoint_data + def __repr__(self): string = super().__repr__() string += '{: <16}=\t{}\n'.format('\tFilename', self.filename) @@ -2037,8 +2250,21 @@ def __repr__(self): if self.length_multiplier != 1.0: string += '{: <16}=\t{}\n'.format('\tLength multiplier', self.length_multiplier) + if self.options is not None: + string += '{: <16}=\t{}\n'.format('\tOptions', self.options) return string + @property + @require_statepoint_data + def lower_left(self): + return self.vertices.min(axis=0) + + @property + @require_statepoint_data + def upper_right(self): + return self.vertices.max(axis=0) + + @require_statepoint_data def centroid(self, bin: int): """Return the vertex averaged centroid of an element @@ -2053,11 +2279,11 @@ def centroid(self, bin: int): x, y, z values of the element centroid """ - conn = self.connectivity[:, bin] + conn = self.connectivity[bin] # remove invalid connectivity values conn = conn[conn >= 0] - coords = self.vertices[:, conn] - return coords.mean(axis=1) + coords = self.vertices[conn] + return coords.mean(axis=0) def write_vtk_mesh(self, **kwargs): """Map data to unstructured VTK mesh elements. @@ -2120,12 +2346,11 @@ def write_data_to_vtk( grid = vtk.vtkUnstructuredGrid() vtk_pnts = vtk.vtkPoints() - vtk_pnts.SetData(nps.numpy_to_vtk(self.vertices.T)) + vtk_pnts.SetData(nps.numpy_to_vtk(self.vertices)) grid.SetPoints(vtk_pnts) n_skipped = 0 - elems = [] - for elem_type, conn in zip(self.element_types, self.connectivity.T): + for elem_type, conn in zip(self.element_types, self.connectivity): if elem_type == self._LINEAR_TET: elem = vtk.vtkTetra() elif elem_type == self._LINEAR_HEX: @@ -2138,15 +2363,13 @@ def write_data_to_vtk( if c == -1: break elem.GetPointIds().SetId(i, c) - elems.append(elem) + + grid.InsertNextCell(elem.GetCellType(), elem.GetPointIds()) if n_skipped > 0: warnings.warn(f'{n_skipped} elements were not written because ' 'they are not of type linear tet/hex') - for elem in elems: - grid.InsertNextCell(elem.GetCellType(), elem.GetPointIds()) - # check that datasets are the correct size datasets_out = [] if datasets is not None: @@ -2184,8 +2407,13 @@ def from_hdf5(cls, group: h5py.Group): mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) filename = group['filename'][()].decode() library = group['library'][()].decode() + if 'options' in group.attrs: + options = group.attrs['options'].decode() + else: + options = None - mesh = cls(filename=filename, library=library, mesh_id=mesh_id) + mesh = cls(filename=filename, library=library, mesh_id=mesh_id, options=options) + mesh._has_statepoint_data = True vol_data = group['volumes'][()] mesh.volumes = np.reshape(vol_data, (vol_data.shape[0],)) mesh.n_elements = mesh.volumes.size @@ -2215,6 +2443,8 @@ def to_xml_element(self): element.set("id", str(self._id)) element.set("type", "unstructured") element.set("library", self._library) + if self.options is not None: + element.set('options', self.options) subelement = ET.SubElement(element, "filename") subelement.text = str(self.filename) @@ -2241,8 +2471,9 @@ def from_xml_element(cls, elem: ET.Element): filename = get_text(elem, 'filename') library = get_text(elem, 'library') length_multiplier = float(get_text(elem, 'length_multiplier', 1.0)) + options = elem.get('options') - return cls(filename, library, mesh_id, '', length_multiplier) + return cls(filename, library, mesh_id, '', length_multiplier, options) def _read_meshes(elem): @@ -2259,7 +2490,7 @@ def _read_meshes(elem): A dictionary with mesh IDs as keys and openmc.MeshBase instanaces as values """ - out = dict() + out = {} for mesh_elem in elem.findall('mesh'): mesh = MeshBase.from_xml_element(mesh_elem) out[mesh.id] = mesh diff --git a/openmc/mgxs/groups.py b/openmc/mgxs/groups.py index ef4b78f7ed5..182402ed7fc 100644 --- a/openmc/mgxs/groups.py +++ b/openmc/mgxs/groups.py @@ -17,7 +17,7 @@ class EnergyGroups: The energy group boundaries in [eV] or the name of the group structure (Must be a valid key in the openmc.mgxs.GROUP_STRUCTURES dictionary). - .. versionchanged:: 0.13.4 + .. versionchanged:: 0.14.0 Changed to allow a string specifying the group structure name. Attributes diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index 2fa2e0f0567..12a4630bdc4 100644 --- a/openmc/mgxs/library.py +++ b/openmc/mgxs/library.py @@ -685,7 +685,7 @@ def get_mgxs(self, domain, mgxs_type): # Check that requested domain is included in library if mgxs_type not in self.mgxs_types: - msg = 'Unable to find MGXS type "{0}"'.format(mgxs_type) + msg = f'Unable to find MGXS type "{mgxs_type}"' raise ValueError(msg) return self.all_mgxs[domain_id][mgxs_type] @@ -901,7 +901,7 @@ def dump_to_file(self, filename='mgxs', directory='mgxs'): if not os.path.exists(directory): os.makedirs(directory) - full_filename = os.path.join(directory, '{}.pkl'.format(filename)) + full_filename = os.path.join(directory, f'{filename}.pkl') full_filename = full_filename.replace(' ', '-') # Load and return pickled Library object diff --git a/openmc/mgxs/mdgxs.py b/openmc/mgxs/mdgxs.py index c9d76bc1080..b95a4fbc0eb 100644 --- a/openmc/mgxs/mdgxs.py +++ b/openmc/mgxs/mdgxs.py @@ -20,7 +20,7 @@ 'delayed-nu-fission matrix' ) -# Maximum number of delayed groups, from src/constants.F90 +# Maximum number of delayed groups, from include/openmc/constants.h MAX_DELAYED_GROUPS = 8 @@ -612,7 +612,7 @@ def print_xs(self, subdomains='all', nuclides='all', xs_type='macro'): string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -641,7 +641,7 @@ def print_xs(self, subdomains='all', nuclides='all', xs_type='macro'): string += '{0: <16}=\t{1}\n'.format('\tNuclide', nuclide) # Add the cross section header - string += '{0: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' for delayed_group in self.delayed_groups: @@ -743,9 +743,9 @@ def export_xs_data(self, filename='mgxs', directory='mgxs', df.to_csv(filename + '.csv', index=False) elif format == 'excel': if self.domain_type == 'mesh': - df.to_excel(filename + '.xls') + df.to_excel(filename + '.xlsx') else: - df.to_excel(filename + '.xls', index=False) + df.to_excel(filename + '.xlsx', index=False) elif format == 'pickle': df.to_pickle(filename + '.pkl') elif format == 'latex': @@ -875,7 +875,7 @@ def get_pandas_dataframe(self, groups='all', nuclides='all', # Sort the dataframe by domain type id (e.g., distribcell id) and # energy groups such that data is from fast to thermal if self.domain_type == 'mesh': - mesh_str = 'mesh {0}'.format(self.domain.id) + mesh_str = f'mesh {self.domain.id}' df.sort_values(by=[(mesh_str, 'x'), (mesh_str, 'y'), (mesh_str, 'z')] + columns, inplace=True) else: @@ -2496,7 +2496,7 @@ def print_xs(self, subdomains='all', nuclides='all', xs_type='macro'): string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -2532,7 +2532,7 @@ def print_xs(self, subdomains='all', nuclides='all', xs_type='macro'): string += '{: <16}=\t{}\n'.format('\tNuclide', nuclide) # Build header for cross section type - string += '{: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' if self.delayed_groups is not None: diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index b6840d079ce..588b6f64e2a 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -1427,7 +1427,7 @@ def get_subdomain_avg_xs(self, subdomains='all'): filter_bins=subdomains) avg_xs.tallies[tally_type] = tally_avg - avg_xs._domain_type = 'sum({0})'.format(self.domain_type) + avg_xs._domain_type = f'sum({self.domain_type})' avg_xs.sparse = self.sparse return avg_xs @@ -1478,7 +1478,7 @@ def _get_homogenized_mgxs(self, other_mgxs, denom_score='flux'): # Clone this MGXS to initialize the homogenized version homogenized_mgxs = copy.deepcopy(self) homogenized_mgxs._derived = True - name = 'hom({}, '.format(self.domain.name) + name = f'hom({self.domain.name}, ' # Get the domain filter filter_type = _DOMAIN_TO_FILTER[self.domain_type] @@ -1505,7 +1505,7 @@ def _get_homogenized_mgxs(self, other_mgxs, denom_score='flux'): denom_tally += other_denom_tally # Update the name for the homogenzied MGXS - name += '{}, '.format(mgxs.domain.name) + name += f'{mgxs.domain.name}, ' # Set the properties of the homogenized MGXS homogenized_mgxs._rxn_rate_tally = rxn_rate_tally @@ -1745,7 +1745,7 @@ def print_xs(self, subdomains='all', nuclides='all', xs_type='macro'): string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -1773,7 +1773,7 @@ def print_xs(self, subdomains='all', nuclides='all', xs_type='macro'): string += '{0: <16}=\t{1}\n'.format('\tNuclide', nuclide) # Build header for cross section type - string += '{0: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' template = '{0: <12}Group {1} [{2: <10} - {3: <10}eV]:\t' average_xs = self.get_xs(nuclides=[nuclide], @@ -2001,9 +2001,9 @@ def export_xs_data(self, filename='mgxs', directory='mgxs', df.to_csv(filename + '.csv', index=False) elif format == 'excel': if self.domain_type == 'mesh': - df.to_excel(filename + '.xls') + df.to_excel(filename + '.xlsx') else: - df.to_excel(filename + '.xls', index=False) + df.to_excel(filename + '.xlsx', index=False) elif format == 'pickle': df.to_pickle(filename + '.pkl') elif format == 'latex': @@ -2131,7 +2131,7 @@ def get_pandas_dataframe(self, groups='all', nuclides='all', # Sort the dataframe by domain type id (e.g., distribcell id) and # energy groups such that data is from fast to thermal if self.domain_type == 'mesh': - mesh_str = 'mesh {0}'.format(self.domain.id) + mesh_str = f'mesh {self.domain.id}' df.sort_values(by=[(mesh_str, 'x'), (mesh_str, 'y'), (mesh_str, 'z')] + columns, inplace=True) else: @@ -2472,7 +2472,7 @@ def print_xs(self, subdomains='all', nuclides='all', xs_type='macro'): string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -2508,7 +2508,7 @@ def print_xs(self, subdomains='all', nuclides='all', xs_type='macro'): string += '{0: <16}=\t{1}\n'.format('\tNuclide', nuclide) # Build header for cross section type - string += '{0: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' template = '{0: <12}Group {1} -> Group {2}:\t\t' average_xs = self.get_xs(nuclides=[nuclide], @@ -4476,7 +4476,7 @@ def get_slice(self, nuclides=[], in_groups=[], out_groups=[], slice_xs.legendre_order = legendre_order # Slice the scattering tally - filter_bins = [tuple(['P{}'.format(i) + filter_bins = [tuple([f'P{i}' for i in range(self.legendre_order + 1)])] slice_xs.tallies[self.rxn_type] = \ slice_xs.tallies[self.rxn_type].get_slice( @@ -4613,7 +4613,7 @@ def get_xs(self, in_groups='all', out_groups='all', cv.check_less_than( 'moment', moment, self.legendre_order, equality=True) filters.append(openmc.LegendreFilter) - filter_bins.append(('P{}'.format(moment),)) + filter_bins.append((f'P{moment}',)) num_angle_bins = 1 else: num_angle_bins = self.legendre_order + 1 @@ -4804,7 +4804,7 @@ def print_xs(self, subdomains='all', nuclides='all', cv.check_value('xs_type', xs_type, ['macro', 'micro']) if self.correction != 'P0' and self.scatter_format == SCATTER_LEGENDRE: - rxn_type = '{0} (P{1})'.format(self.mgxs_type, moment) + rxn_type = f'{self.mgxs_type} (P{moment})' else: rxn_type = self.mgxs_type @@ -4815,7 +4815,7 @@ def print_xs(self, subdomains='all', nuclides='all', string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -4851,7 +4851,7 @@ def print_xs(self, subdomains='all', nuclides='all', string += '{0: <16}=\t{1}\n'.format('\tNuclide', nuclide) # Build header for cross section type - string += '{0: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' average_xs = self.get_xs(nuclides=[nuclide], subdomains=[subdomain], @@ -4903,8 +4903,7 @@ def print_groups_and_histogram(avg_xs, err_xs, num_groups, for azi in range(len(azi_bins) - 1): azi_low, azi_high = azi_bins[azi: azi + 2] string += \ - '\t\tPolar Angle: [{0:5f} - {1:5f}]'.format( - pol_low, pol_high) + \ + f'\t\tPolar Angle: [{pol_low:5f} - {pol_high:5f}]' + \ '\tAzimuthal Angle: [{0:5f} - {1:5f}]'.format( azi_low, azi_high) + '\n' string += print_groups_and_histogram( @@ -6226,7 +6225,7 @@ def get_pandas_dataframe(self, groups='all', nuclides='all', if 'group out' in df: df = df[df['group out'].isin(groups)] - mesh_str = 'mesh {0}'.format(self.domain.id) + mesh_str = f'mesh {self.domain.id}' col_key = (mesh_str, 'surf') surfaces = df.pop(col_key) df.insert(len(self.domain.dimension), col_key, surfaces) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 85e15f8ebca..642da83d170 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -1964,7 +1964,7 @@ def to_hdf5(self, file): grp.attrs['fissionable'] = self.fissionable if self.representation is not None: - grp.attrs['representation'] = np.string_(self.representation) + grp.attrs['representation'] = np.bytes_(self.representation) if self.representation == REPRESENTATION_ANGLE: if self.num_azimuthal is not None: grp.attrs['num_azimuthal'] = self.num_azimuthal @@ -1972,9 +1972,9 @@ def to_hdf5(self, file): if self.num_polar is not None: grp.attrs['num_polar'] = self.num_polar - grp.attrs['scatter_shape'] = np.string_("[G][G'][Order]") + grp.attrs['scatter_shape'] = np.bytes_("[G][G'][Order]") if self.scatter_format is not None: - grp.attrs['scatter_format'] = np.string_(self.scatter_format) + grp.attrs['scatter_format'] = np.bytes_(self.scatter_format) if self.order is not None: grp.attrs['order'] = self.order @@ -2516,7 +2516,7 @@ def export_to_hdf5(self, filename='mgxs.h5', libver='earliest'): # Create and write to the HDF5 file file = h5py.File(filename, "w", libver=libver) - file.attrs['filetype'] = np.string_(_FILETYPE_MGXS_LIBRARY) + file.attrs['filetype'] = np.bytes_(_FILETYPE_MGXS_LIBRARY) file.attrs['version'] = [_VERSION_MGXS_LIBRARY, 0] file.attrs['energy_groups'] = self.energy_groups.num_groups file.attrs['delayed_groups'] = self.num_delayed_groups diff --git a/openmc/model/funcs.py b/openmc/model/funcs.py index cbf35665153..41aa920eae7 100644 --- a/openmc/model/funcs.py +++ b/openmc/model/funcs.py @@ -1,11 +1,10 @@ from collections.abc import Iterable -from functools import partial from math import sqrt -from numbers import Real from operator import attrgetter from warnings import warn -from openmc import Plane, Cylinder, Universe, Cell +from openmc import Cylinder, Universe, Cell +from .surface_composite import RectangularPrism, HexagonalPrism from ..checkvalue import (check_type, check_value, check_length, check_less_than, check_iterable_type) import openmc.data @@ -108,280 +107,26 @@ def borated_water(boron_ppm, temperature=293., pressure=0.1013, temp_unit='K', return out -# Define function to create a plane on given axis -def _plane(axis, name, value, boundary_type='transmission'): - cls = getattr(openmc, f'{axis.upper()}Plane') - return cls(value, name=f'{name} {axis}', - boundary_type=boundary_type) def rectangular_prism(width, height, axis='z', origin=(0., 0.), boundary_type='transmission', corner_radius=0.): - """Get an infinite rectangular prism from four planar surfaces. - - .. versionchanged:: 0.11 - This function was renamed from `get_rectangular_prism` to - `rectangular_prism`. - - Parameters - ---------- - width: float - Prism width in units of cm. The width is aligned with the y, x, - or x axes for prisms parallel to the x, y, or z axis, respectively. - height: float - Prism height in units of cm. The height is aligned with the z, z, - or y axes for prisms parallel to the x, y, or z axis, respectively. - axis : {'x', 'y', 'z'} - Axis with which the infinite length of the prism should be aligned. - Defaults to 'z'. - origin: Iterable of two floats - Origin of the prism. The two floats correspond to (y,z), (x,z) or - (x,y) for prisms parallel to the x, y or z axis, respectively. - Defaults to (0., 0.). - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic'} - Boundary condition that defines the behavior for particles hitting the - surfaces comprising the rectangular prism (default is 'transmission'). - corner_radius: float - Prism corner radius in units of cm. Defaults to 0. - - Returns - ------- - openmc.Region - The inside of a rectangular prism - - """ - - check_type('width', width, Real) - check_type('height', height, Real) - check_type('corner_radius', corner_radius, Real) - check_value('axis', axis, ['x', 'y', 'z']) - check_type('origin', origin, Iterable, Real) - - if axis == 'x': - x1, x2 = 'y', 'z' - elif axis == 'y': - x1, x2 = 'x', 'z' - else: - x1, x2 = 'x', 'y' - - # Get cylinder class corresponding to given axis - cyl = getattr(openmc, f'{axis.upper()}Cylinder') - - # Create rectangular region - min_x1 = _plane(x1, 'minimum', -width/2 + origin[0], - boundary_type=boundary_type) - max_x1 = _plane(x1, 'maximum', width/2 + origin[0], - boundary_type=boundary_type) - min_x2 = _plane(x2, 'minimum', -height/2 + origin[1], - boundary_type=boundary_type) - max_x2 = _plane(x2, 'maximum', height/2 + origin[1], - boundary_type=boundary_type) - if boundary_type == 'periodic': - min_x1.periodic_surface = max_x1 - min_x2.periodic_surface = max_x2 - prism = +min_x1 & -max_x1 & +min_x2 & -max_x2 - - # Handle rounded corners if given - if corner_radius > 0.: - if boundary_type == 'periodic': - raise ValueError('Periodic boundary conditions not permitted when ' - 'rounded corners are used.') - - args = {'r': corner_radius, 'boundary_type': boundary_type} - - args[x1 + '0'] = origin[0] - width/2 + corner_radius - args[x2 + '0'] = origin[1] - height/2 + corner_radius - x1_min_x2_min = cyl(name='{} min {} min'.format(x1, x2), **args) - - args[x1 + '0'] = origin[0] - width/2 + corner_radius - args[x2 + '0'] = origin[1] - height/2 + corner_radius - x1_min_x2_min = cyl(name='{} min {} min'.format(x1, x2), **args) - - args[x1 + '0'] = origin[0] - width/2 + corner_radius - args[x2 + '0'] = origin[1] + height/2 - corner_radius - x1_min_x2_max = cyl(name='{} min {} max'.format(x1, x2), **args) - - args[x1 + '0'] = origin[0] + width/2 - corner_radius - args[x2 + '0'] = origin[1] - height/2 + corner_radius - x1_max_x2_min = cyl(name='{} max {} min'.format(x1, x2), **args) - - args[x1 + '0'] = origin[0] + width/2 - corner_radius - args[x2 + '0'] = origin[1] + height/2 - corner_radius - x1_max_x2_max = cyl(name='{} max {} max'.format(x1, x2), **args) - - x1_min = _plane(x1, 'min', -width/2 + origin[0] + corner_radius, - boundary_type=boundary_type) - x1_max = _plane(x1, 'max', width/2 + origin[0] - corner_radius, - boundary_type=boundary_type) - x2_min = _plane(x2, 'min', -height/2 + origin[1] + corner_radius, - boundary_type=boundary_type) - x2_max = _plane(x2, 'max', height/2 + origin[1] - corner_radius, - boundary_type=boundary_type) - - corners = (+x1_min_x2_min & -x1_min & -x2_min) | \ - (+x1_min_x2_max & -x1_min & +x2_max) | \ - (+x1_max_x2_min & +x1_max & -x2_min) | \ - (+x1_max_x2_max & +x1_max & +x2_max) - - prism = prism & ~corners - - return prism - - -def get_rectangular_prism(*args, **kwargs): - warn("get_rectangular_prism(...) has been renamed rectangular_prism(...). " - "Future versions of OpenMC will not accept get_rectangular_prism.", - FutureWarning) - return rectangular_prism(*args, **kwargs) + warn("The rectangular_prism(...) function has been replaced by the " + "RectangularPrism(...) class. Future versions of OpenMC will not " + "accept rectangular_prism.", FutureWarning) + return -RectangularPrism( + width=width, height=height, axis=axis, origin=origin, + boundary_type=boundary_type, corner_radius=corner_radius) def hexagonal_prism(edge_length=1., orientation='y', origin=(0., 0.), boundary_type='transmission', corner_radius=0.): - """Create a hexagon region from six surface planes. - - .. versionchanged:: 0.11 - This function was renamed from `get_hexagonal_prism` to - `hexagonal_prism`. - - Parameters - ---------- - edge_length : float - Length of a side of the hexagon in cm - orientation : {'x', 'y'} - An 'x' orientation means that two sides of the hexagon are parallel to - the x-axis and a 'y' orientation means that two sides of the hexagon are - parallel to the y-axis. - origin: Iterable of two floats - Origin of the prism. Defaults to (0., 0.). - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic'} - Boundary condition that defines the behavior for particles hitting the - surfaces comprising the hexagonal prism (default is 'transmission'). - corner_radius: float - Prism corner radius in units of cm. Defaults to 0. - - Returns - ------- - openmc.Region - The inside of a hexagonal prism - - """ - - l = edge_length - x, y = origin - - if orientation == 'y': - right = openmc.XPlane(x + sqrt(3.)/2*l, boundary_type=boundary_type) - left = openmc.XPlane(x - sqrt(3.)/2*l, boundary_type=boundary_type) - c = sqrt(3.)/3. - - # y = -x/sqrt(3) + a - upper_right = Plane(a=c, b=1., d=l+x*c+y, boundary_type=boundary_type) - - # y = x/sqrt(3) + a - upper_left = Plane(a=-c, b=1., d=l-x*c+y, boundary_type=boundary_type) - - # y = x/sqrt(3) - a - lower_right = Plane(a=-c, b=1., d=-l-x*c+y, boundary_type=boundary_type) - - # y = -x/sqrt(3) - a - lower_left = Plane(a=c, b=1., d=-l+x*c+y, boundary_type=boundary_type) - - prism = -right & +left & -upper_right & -upper_left & \ - +lower_right & +lower_left - - if boundary_type == 'periodic': - right.periodic_surface = left - upper_right.periodic_surface = lower_left - lower_right.periodic_surface = upper_left - - elif orientation == 'x': - top = openmc.YPlane(y0=y + sqrt(3.)/2*l, boundary_type=boundary_type) - bottom = openmc.YPlane(y0=y - sqrt(3.)/2*l, boundary_type=boundary_type) - c = sqrt(3.) - - # y = -sqrt(3)*(x - a) - upper_right = Plane(a=c, b=1., d=c*l+x*c+y, boundary_type=boundary_type) - - # y = sqrt(3)*(x + a) - lower_right = Plane(a=-c, b=1., d=-c*l-x*c+y, - boundary_type=boundary_type) - - # y = -sqrt(3)*(x + a) - lower_left = Plane(a=c, b=1., d=-c*l+x*c+y, boundary_type=boundary_type) - - # y = sqrt(3)*(x + a) - upper_left = Plane(a=-c, b=1., d=c*l-x*c+y, boundary_type=boundary_type) - - prism = -top & +bottom & -upper_right & +lower_right & \ - +lower_left & -upper_left - - if boundary_type == 'periodic': - top.periodic_surface = bottom - upper_right.periodic_surface = lower_left - lower_right.periodic_surface = upper_left - - # Handle rounded corners if given - if corner_radius > 0.: - if boundary_type == 'periodic': - raise ValueError('Periodic boundary conditions not permitted when ' - 'rounded corners are used.') - - c = sqrt(3.)/2 - t = l - corner_radius/c - - # Cylinder with corner radius and boundary type pre-applied - cyl1 = partial(openmc.ZCylinder, r=corner_radius, - boundary_type=boundary_type) - cyl2 = partial(openmc.ZCylinder, r=corner_radius/(2*c), - boundary_type=boundary_type) - - if orientation == 'x': - x_min_y_min_in = cyl1(name='x min y min in', x0=x-t/2, y0=y-c*t) - x_min_y_max_in = cyl1(name='x min y max in', x0=x+t/2, y0=y-c*t) - x_max_y_min_in = cyl1(name='x max y min in', x0=x-t/2, y0=y+c*t) - x_max_y_max_in = cyl1(name='x max y max in', x0=x+t/2, y0=y+c*t) - x_min_in = cyl1(name='x min in', x0=x-t, y0=y) - x_max_in = cyl1(name='x max in', x0=x+t, y0=y) - - x_min_y_min_out = cyl2(name='x min y min out', x0=x-l/2, y0=y-c*l) - x_min_y_max_out = cyl2(name='x min y max out', x0=x+l/2, y0=y-c*l) - x_max_y_min_out = cyl2(name='x max y min out', x0=x-l/2, y0=y+c*l) - x_max_y_max_out = cyl2(name='x max y max out', x0=x+l/2, y0=y+c*l) - x_min_out = cyl2(name='x min out', x0=x-l, y0=y) - x_max_out = cyl2(name='x max out', x0=x+l, y0=y) - - corners = (+x_min_y_min_in & -x_min_y_min_out | - +x_min_y_max_in & -x_min_y_max_out | - +x_max_y_min_in & -x_max_y_min_out | - +x_max_y_max_in & -x_max_y_max_out | - +x_min_in & -x_min_out | - +x_max_in & -x_max_out) - - elif orientation == 'y': - x_min_y_min_in = cyl1(name='x min y min in', x0=x-c*t, y0=y-t/2) - x_min_y_max_in = cyl1(name='x min y max in', x0=x-c*t, y0=y+t/2) - x_max_y_min_in = cyl1(name='x max y min in', x0=x+c*t, y0=y-t/2) - x_max_y_max_in = cyl1(name='x max y max in', x0=x+c*t, y0=y+t/2) - y_min_in = cyl1(name='y min in', x0=x, y0=y-t) - y_max_in = cyl1(name='y max in', x0=x, y0=y+t) - - x_min_y_min_out = cyl2(name='x min y min out', x0=x-c*l, y0=y-l/2) - x_min_y_max_out = cyl2(name='x min y max out', x0=x-c*l, y0=y+l/2) - x_max_y_min_out = cyl2(name='x max y min out', x0=x+c*l, y0=y-l/2) - x_max_y_max_out = cyl2(name='x max y max out', x0=x+c*l, y0=y+l/2) - y_min_out = cyl2(name='y min out', x0=x, y0=y-l) - y_max_out = cyl2(name='y max out', x0=x, y0=y+l) - - corners = (+x_min_y_min_in & -x_min_y_min_out | - +x_min_y_max_in & -x_min_y_max_out | - +x_max_y_min_in & -x_max_y_min_out | - +x_max_y_max_in & -x_max_y_max_out | - +y_min_in & -y_min_out | - +y_max_in & -y_max_out) - - prism = prism & ~corners - - return prism + warn("The hexagonal_prism(...) function has been replaced by the " + "HexagonalPrism(...) class. Future versions of OpenMC will not " + "accept hexagonal_prism.", FutureWarning) + return -HexagonalPrism( + edge_length=edge_length, orientation=orientation, origin=origin, + boundary_type=boundary_type, corner_radius=corner_radius) def get_hexagonal_prism(*args, **kwargs): @@ -476,8 +221,7 @@ def pin(surfaces, items, subdivisions=None, divide_vols=True, center_getter = attrgetter("z0", "y0") else: raise TypeError( - "Not configured to interpret {} surfaces".format( - surf_type.__name__)) + f"Not configured to interpret {surf_type.__name__} surfaces") centers = set() prev_rad = 0 diff --git a/openmc/model/model.py b/openmc/model/model.py index 2381fb8aba0..2160c97e6cc 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,16 +1,15 @@ from __future__ import annotations from collections.abc import Iterable -from contextlib import contextmanager from functools import lru_cache import os from pathlib import Path from numbers import Integral from tempfile import NamedTemporaryFile import warnings -import lxml.etree as ET from typing import Optional, Dict import h5py +import lxml.etree as ET import openmc import openmc._xml as xml @@ -18,18 +17,7 @@ from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value, PathLike from openmc.exceptions import InvalidIDError - - -@contextmanager -def _change_directory(working_dir): - """A context manager for executing in a provided working directory""" - start_dir = Path.cwd() - Path.mkdir(working_dir, parents=True, exist_ok=True) - os.chdir(working_dir) - try: - yield - finally: - os.chdir(start_dir) +from openmc.utility_funcs import change_directory class Model: @@ -253,7 +241,8 @@ def from_model_xml(cls, path='model.xml'): path : str or PathLike Path to model.xml file """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() model = cls() @@ -394,7 +383,7 @@ def deplete(self, timesteps, method='cecm', final_step=True, # Store whether or not the library was initialized when we started started_initialized = self.is_initialized - with _change_directory(Path(directory)): + with change_directory(directory): with openmc.lib.quiet_dll(output): # TODO: Support use of IndependentOperator too depletion_operator = dep.CoupledOperator(self, **op_kwargs) @@ -448,7 +437,7 @@ def export_to_xml(self, directory='.', remove_surfs=False): # Create directory if required d = Path(directory) if not d.is_dir(): - d.mkdir(parents=True) + d.mkdir(parents=True, exist_ok=True) self.settings.export_to_xml(d) self.geometry.export_to_xml(d, remove_surfs=remove_surfs) @@ -487,13 +476,16 @@ def export_to_model_xml(self, path='model.xml', remove_surfs=False): # if the provided path doesn't end with the XML extension, assume the # input path is meant to be a directory. If the directory does not # exist, create it and place a 'model.xml' file there. - if not str(xml_path).endswith('.xml') and not xml_path.exists(): - os.mkdir(xml_path) + if not str(xml_path).endswith('.xml'): + if not xml_path.exists(): + xml_path.mkdir(parents=True, exist_ok=True) + elif not xml_path.is_dir(): + raise FileExistsError(f"File exists and is not a directory: '{xml_path}'") xml_path /= 'model.xml' # if this is an XML file location and the file's parent directory does # not exist, create it before continuing elif not xml_path.parent.exists(): - os.mkdir(xml_path.parent) + xml_path.parent.mkdir(parents=True, exist_ok=True) if remove_surfs: warnings.warn("remove_surfs kwarg will be deprecated soon, please " @@ -673,7 +665,7 @@ def run(self, particles=None, threads=None, geometry_debug=False, last_statepoint = None # Operate in the provided working directory - with _change_directory(Path(cwd)): + with change_directory(cwd): if self.is_initialized: # Handle the run options as applicable # First dont allow ones that must be set via init @@ -708,9 +700,10 @@ def run(self, particles=None, threads=None, geometry_debug=False, self.export_to_model_xml(**export_kwargs) else: self.export_to_xml(**export_kwargs) + path_input = export_kwargs.get("path", None) openmc.run(particles, threads, geometry_debug, restart_file, tracks, output, Path('.'), openmc_exec, mpi_args, - event_based) + event_based, path_input) # Get output directory and return the last statepoint written if self.settings.output and 'path' in self.settings.output: @@ -762,7 +755,7 @@ def calculate_volumes(self, threads=None, output=True, cwd='.', raise ValueError("The Settings.volume_calculations attribute must" " be specified before executing this method!") - with _change_directory(Path(cwd)): + with change_directory(cwd): if self.is_initialized: if threads is not None: msg = "Threads must be set via Model.is_initialized(...)" @@ -787,7 +780,12 @@ def calculate_volumes(self, threads=None, output=True, cwd='.', for i, vol_calc in enumerate(self.settings.volume_calculations): vol_calc.load_results(f"volume_{i + 1}.h5") # First add them to the Python side - self.geometry.add_volume_information(vol_calc) + if vol_calc.domain_type == "material" and self.materials: + for material in self.materials: + if material.id in vol_calc.volumes: + material.add_volume_information(vol_calc) + else: + self.geometry.add_volume_information(vol_calc) # And now repeat for the C API if self.is_initialized and vol_calc.domain_type == 'material': @@ -819,7 +817,7 @@ def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc'): raise ValueError("The Model.plots attribute must be specified " "before executing this method!") - with _change_directory(Path(cwd)): + with change_directory(cwd): if self.is_initialized: # Compute the volumes openmc.lib.plot_geometry(output) @@ -1019,7 +1017,7 @@ def update_material_volumes(self, names_or_ids, volume): def differentiate_depletable_mats(self, diff_volume_method: str): """Assign distribmats for each depletable material - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 279f3ed06a8..6f9123311fb 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1,8 +1,12 @@ from abc import ABC, abstractmethod +from collections.abc import Iterable from copy import copy +from functools import partial from math import sqrt, pi, sin, cos, isclose +from numbers import Real import warnings import operator +from typing import Sequence import numpy as np from scipy.spatial import ConvexHull, Delaunay @@ -42,7 +46,7 @@ def boundary_type(self, boundary_type): getattr(self, name).boundary_type = boundary_type def __repr__(self): - return "<{} at 0x{:x}>".format(type(self).__name__, id(self)) + return f"<{type(self).__name__} at 0x{id(self):x}>" @property @abstractmethod @@ -50,13 +54,13 @@ def _surface_names(self): """Iterable of attribute names corresponding to underlying surfaces.""" @abstractmethod - def __pos__(self): - """Return the positive half-space of the composite surface.""" - - @abstractmethod - def __neg__(self): + def __neg__(self) -> openmc.Region: """Return the negative half-space of the composite surface.""" + def __pos__(self) -> openmc.Region: + """Return the positive half-space of the composite surface.""" + return ~(-self) + class CylinderSector(CompositeSurface): """Infinite cylindrical sector composite surface. @@ -613,7 +617,7 @@ def __pos__(self): class XConeOneSided(CompositeSurface): """One-sided cone parallel the x-axis - A one-sided cone is composed of a normal cone surface and an "ambiguity" + A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is included. This class acts as a proper surface, meaning that unary `+` and `-` operators applied to it will produce a half-space. The negative side is @@ -630,7 +634,9 @@ class XConeOneSided(CompositeSurface): z0 : float, optional z-coordinate of the apex. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -643,7 +649,7 @@ class XConeOneSided(CompositeSurface): cone : openmc.XCone Regular two-sided cone plane : openmc.XPlane - Ambiguity surface + Disambiguation surface up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -671,7 +677,7 @@ def __pos__(self): class YConeOneSided(CompositeSurface): """One-sided cone parallel the y-axis - A one-sided cone is composed of a normal cone surface and an "ambiguity" + A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is included. This class acts as a proper surface, meaning that unary `+` and `-` operators applied to it will produce a half-space. The negative side is @@ -688,7 +694,9 @@ class YConeOneSided(CompositeSurface): z0 : float, optional z-coordinate of the apex. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -701,7 +709,7 @@ class YConeOneSided(CompositeSurface): cone : openmc.YCone Regular two-sided cone plane : openmc.YPlane - Ambiguity surface + Disambiguation surface up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -723,7 +731,7 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): class ZConeOneSided(CompositeSurface): """One-sided cone parallel the z-axis - A one-sided cone is composed of a normal cone surface and an "ambiguity" + A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is included. This class acts as a proper surface, meaning that unary `+` and `-` operators applied to it will produce a half-space. The negative side is @@ -740,7 +748,9 @@ class ZConeOneSided(CompositeSurface): z0 : float, optional z-coordinate of the apex. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -753,7 +763,7 @@ class ZConeOneSided(CompositeSurface): cone : openmc.ZCone Regular two-sided cone plane : openmc.ZPlane - Ambiguity surface + Disambiguation surface up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -836,9 +846,6 @@ def __init__(self, points, basis='rz'): def __neg__(self): return self._region - def __pos__(self): - return ~self._region - @property def _surface_names(self): return self._surfnames @@ -1022,9 +1029,9 @@ def _constrain_triangulation(self, points, depth=0): ------- None """ - # Only attempt the triangulation up to 3 times. - if depth > 2: - raise RuntimeError('Could not create a valid triangulation after 3' + # Only attempt the triangulation up to 5 times. + if depth > 4: + raise RuntimeError('Could not create a valid triangulation after 5' ' attempts') tri = Delaunay(points, qhull_options='QJ') @@ -1032,7 +1039,7 @@ def _constrain_triangulation(self, points, depth=0): # included in the triangulation, break it into two line segments. n = len(points) new_pts = [] - for i, j in zip(range(n), range(1, n +1)): + for i, j in zip(range(n), range(1, n + 1)): # If both vertices of any edge are not found in any simplex, insert # a new point between them. if not any([i in s and j % n in s for s in tri.simplices]): @@ -1070,7 +1077,8 @@ def _group_simplices(self, neighbor_map, group=None): return group # If group is empty, grab the next simplex in the dictionary and recurse if group is None: - sidx = next(iter(neighbor_map)) + # Start with smallest neighbor lists + sidx = sorted(neighbor_map.items(), key=lambda item: len(item[1]))[0][0] return self._group_simplices(neighbor_map, group=[sidx]) # Otherwise use the last simplex in the group else: @@ -1080,13 +1088,16 @@ def _group_simplices(self, neighbor_map, group=None): # For each neighbor check if it is part of the same convex # hull as the rest of the group. If yes, recurse. If no, continue on. for n in neighbors: - if n in group or neighbor_map.get(n, None) is None: + if n in group or neighbor_map.get(n) is None: continue test_group = group + [n] test_point_idx = np.unique(self._tri.simplices[test_group, :]) test_points = self.points[test_point_idx] - # If test_points are convex keep adding to this group - if len(test_points) == len(ConvexHull(test_points).vertices): + test_hull = ConvexHull(test_points, qhull_options='Qc') + pts_on_hull = len(test_hull.vertices) + len(test_hull.coplanar) + # If test_points are convex (including coplanar) keep adding to + # this group + if len(test_points) == pts_on_hull: group = self._group_simplices(neighbor_map, group=test_group) return group @@ -1232,7 +1243,7 @@ class CruciformPrism(CompositeSurface): multiple distances from the center. Equivalent to the 'gcross' derived surface in Serpent. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -1309,5 +1320,297 @@ def __neg__(self): ) return openmc.Union(regions) - def __pos__(self): - return ~(-self) + +# Define function to create a plane on given axis +def _plane(axis, name, value, boundary_type='transmission', albedo=1.0): + cls = getattr(openmc, f'{axis.upper()}Plane') + return cls(value, name=f'{name} {axis}', + boundary_type=boundary_type, albedo=albedo) + + +class RectangularPrism(CompositeSurface): + """Infinite rectangular prism bounded by four planar surfaces. + + .. versionadded:: 0.14.0 + + Parameters + ---------- + width : float + Prism width in units of [cm]. The width is aligned with the x, x, or z + axes for prisms parallel to the x, y, or z axis, respectively. + height : float + Prism height in units of [cm]. The height is aligned with the x, y, or z + axes for prisms parallel to the x, y, or z axis, respectively. + axis : {'x', 'y', 'z'} + Axis with which the infinite length of the prism should be aligned. + origin : Iterable of two floats + Origin of the prism. The two floats correspond to (y,z), (x,z) or (x,y) + for prisms parallel to the x, y or z axis, respectively. + boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + Boundary condition that defines the behavior for particles hitting the + surfaces comprising the rectangular prism. + albedo : float, optional + Albedo of the prism's surfaces as a ratio of particle weight after + interaction with the surface to the initial weight. Values must be + positive. Only applicable if the boundary type is 'reflective', + 'periodic', or 'white'. + corner_radius : float + Prism corner radius in units of [cm]. + + """ + _surface_names = ('min_x1', 'max_x1', 'min_x2', 'max_x2') + + def __init__( + self, + width: float, + height: float, + axis: str = 'z', + origin: Sequence[float] = (0., 0.), + boundary_type: str = 'transmission', + albedo: float = 1., + corner_radius: float = 0. + ): + check_type('width', width, Real) + check_type('height', height, Real) + check_type('albedo', albedo, Real) + check_type('corner_radius', corner_radius, Real) + check_value('axis', axis, ('x', 'y', 'z')) + check_type('origin', origin, Iterable, Real) + + if axis == 'x': + x1, x2 = 'y', 'z' + elif axis == 'y': + x1, x2 = 'x', 'z' + else: + x1, x2 = 'x', 'y' + + # Get cylinder class corresponding to given axis + cyl = getattr(openmc, f'{axis.upper()}Cylinder') + + # Create container for boundary arguments + bc_args = {'boundary_type': boundary_type, 'albedo': albedo} + + # Create rectangular region + self.min_x1 = _plane(x1, 'minimum', -width/2 + origin[0], **bc_args) + self.max_x1 = _plane(x1, 'maximum', width/2 + origin[0], **bc_args) + self.min_x2 = _plane(x2, 'minimum', -height/2 + origin[1], **bc_args) + self.max_x2 = _plane(x2, 'maximum', height/2 + origin[1], **bc_args) + if boundary_type == 'periodic': + self.min_x1.periodic_surface = self.max_x1 + self.min_x2.periodic_surface = self.max_x2 + + # Handle rounded corners if given + if corner_radius > 0.: + if boundary_type == 'periodic': + raise ValueError('Periodic boundary conditions not permitted when ' + 'rounded corners are used.') + + args = {'r': corner_radius, 'boundary_type': boundary_type, 'albedo': albedo} + + args[x1 + '0'] = origin[0] - width/2 + corner_radius + args[x2 + '0'] = origin[1] - height/2 + corner_radius + self.x1_min_x2_min = cyl(name=f'{x1} min {x2} min', **args) + + args[x1 + '0'] = origin[0] - width/2 + corner_radius + args[x2 + '0'] = origin[1] + height/2 - corner_radius + self.x1_min_x2_max = cyl(name=f'{x1} min {x2} max', **args) + + args[x1 + '0'] = origin[0] + width/2 - corner_radius + args[x2 + '0'] = origin[1] - height/2 + corner_radius + self.x1_max_x2_min = cyl(name=f'{x1} max {x2} min', **args) + + args[x1 + '0'] = origin[0] + width/2 - corner_radius + args[x2 + '0'] = origin[1] + height/2 - corner_radius + self.x1_max_x2_max = cyl(name=f'{x1} max {x2} max', **args) + + self.x1_min = _plane(x1, 'min', -width/2 + origin[0] + corner_radius, + **bc_args) + self.x1_max = _plane(x1, 'max', width/2 + origin[0] - corner_radius, + **bc_args) + self.x2_min = _plane(x2, 'min', -height/2 + origin[1] + corner_radius, + **bc_args) + self.x2_max = _plane(x2, 'max', height/2 + origin[1] - corner_radius, + **bc_args) + self._surface_names += ( + 'x1_min_x2_min', 'x1_min_x2_max', 'x1_max_x2_min', + 'x1_max_x2_max', 'x1_min', 'x1_max', 'x2_min', 'x2_max' + ) + + def __neg__(self): + prism = +self.min_x1 & -self.max_x1 & +self.min_x2 & -self.max_x2 + + # Cut out corners if a corner radius was given + if hasattr(self, 'x1_min'): + corners = ( + (+self.x1_min_x2_min & -self.x1_min & -self.x2_min) | + (+self.x1_min_x2_max & -self.x1_min & +self.x2_max) | + (+self.x1_max_x2_min & +self.x1_max & -self.x2_min) | + (+self.x1_max_x2_max & +self.x1_max & +self.x2_max) + ) + prism &= ~corners + + return prism + + +class HexagonalPrism(CompositeSurface): + """Hexagonal prism comoposed of six planar surfaces + + .. versionadded:: 0.14.0 + + Parameters + ---------- + edge_length : float + Length of a side of the hexagon in [cm] + orientation : {'x', 'y'} + An 'x' orientation means that two sides of the hexagon are parallel to + the x-axis and a 'y' orientation means that two sides of the hexagon are + parallel to the y-axis. + origin : Iterable of two floats + Origin of the prism. + boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + Boundary condition that defines the behavior for particles hitting the + surfaces comprising the hexagonal prism. + albedo : float, optional + Albedo of the prism's surfaces as a ratio of particle weight after + interaction with the surface to the initial weight. Values must be + positive. Only applicable if the boundary type is 'reflective', + 'periodic', or 'white'. + corner_radius : float + Prism corner radius in units of [cm]. + + """ + _surface_names = ('plane_max', 'plane_min', 'upper_right', 'upper_left', + 'lower_right', 'lower_left') + + def __init__( + self, + edge_length: float = 1., + orientation: str = 'y', + origin: Sequence[float] = (0., 0.), + boundary_type: str = 'transmission', + albedo: float = 1., + corner_radius: float = 0. + ): + check_type('edge_length', edge_length, Real) + check_type('albedo', albedo, Real) + check_type('corner_radius', corner_radius, Real) + check_value('orientation', orientation, ('x', 'y')) + check_type('origin', origin, Iterable, Real) + + l = edge_length + x, y = origin + + # Create container for boundary arguments + bc_args = {'boundary_type': boundary_type, 'albedo': albedo} + + if orientation == 'y': + # Left and right planes + self.plane_max = openmc.XPlane(x + sqrt(3.)/2*l, **bc_args) + self.plane_min = openmc.XPlane(x - sqrt(3.)/2*l, **bc_args) + c = sqrt(3.)/3. + + # y = -x/sqrt(3) + a + self.upper_right = openmc.Plane(a=c, b=1., d=l+x*c+y, **bc_args) + + # y = x/sqrt(3) + a + self.upper_left = openmc.Plane(a=-c, b=1., d=l-x*c+y, **bc_args) + + # y = x/sqrt(3) - a + self.lower_right = openmc.Plane(a=-c, b=1., d=-l-x*c+y, **bc_args) + + # y = -x/sqrt(3) - a + self.lower_left = openmc.Plane(a=c, b=1., d=-l+x*c+y, **bc_args) + + elif orientation == 'x': + self.plane_max = openmc.YPlane(y + sqrt(3.)/2*l, **bc_args) + self.plane_min = openmc.YPlane(y - sqrt(3.)/2*l, **bc_args) + c = sqrt(3.) + + # Upper-right surface: y = -sqrt(3)*(x - a) + self.upper_right = openmc.Plane(a=c, b=1., d=c*l+x*c+y, **bc_args) + + # Lower-right surface: y = sqrt(3)*(x + a) + self.lower_right = openmc.Plane(a=-c, b=1., d=-c*l-x*c+y, **bc_args) + + # Lower-left surface: y = -sqrt(3)*(x + a) + self.lower_left = openmc.Plane(a=c, b=1., d=-c*l+x*c+y, **bc_args) + + # Upper-left surface: y = sqrt(3)*(x + a) + self.upper_left = openmc.Plane(a=-c, b=1., d=c*l-x*c+y, **bc_args) + + # Handle periodic boundary conditions + if boundary_type == 'periodic': + self.plane_min.periodic_surface = self.plane_max + self.upper_right.periodic_surface = self.lower_left + self.lower_right.periodic_surface = self.upper_left + + # Handle rounded corners if given + if corner_radius > 0.: + if boundary_type == 'periodic': + raise ValueError('Periodic boundary conditions not permitted ' + 'when rounded corners are used.') + + c = sqrt(3.)/2 + t = l - corner_radius/c + + # Cylinder with corner radius and boundary type pre-applied + cyl1 = partial(openmc.ZCylinder, r=corner_radius, **bc_args) + cyl2 = partial(openmc.ZCylinder, r=corner_radius/(2*c), **bc_args) + + if orientation == 'x': + self.x_min_y_min_in = cyl1(name='x min y min in', x0=x-t/2, y0=y-c*t) + self.x_min_y_max_in = cyl1(name='x min y max in', x0=x+t/2, y0=y-c*t) + self.x_max_y_min_in = cyl1(name='x max y min in', x0=x-t/2, y0=y+c*t) + self.x_max_y_max_in = cyl1(name='x max y max in', x0=x+t/2, y0=y+c*t) + self.min_in = cyl1(name='x min in', x0=x-t, y0=y) + self.max_in = cyl1(name='x max in', x0=x+t, y0=y) + + self.x_min_y_min_out = cyl2(name='x min y min out', x0=x-l/2, y0=y-c*l) + self.x_min_y_max_out = cyl2(name='x min y max out', x0=x+l/2, y0=y-c*l) + self.x_max_y_min_out = cyl2(name='x max y min out', x0=x-l/2, y0=y+c*l) + self.x_max_y_max_out = cyl2(name='x max y max out', x0=x+l/2, y0=y+c*l) + self.min_out = cyl2(name='x min out', x0=x-l, y0=y) + self.max_out = cyl2(name='x max out', x0=x+l, y0=y) + + elif orientation == 'y': + self.x_min_y_min_in = cyl1(name='x min y min in', x0=x-c*t, y0=y-t/2) + self.x_min_y_max_in = cyl1(name='x min y max in', x0=x-c*t, y0=y+t/2) + self.x_max_y_min_in = cyl1(name='x max y min in', x0=x+c*t, y0=y-t/2) + self.x_max_y_max_in = cyl1(name='x max y max in', x0=x+c*t, y0=y+t/2) + self.min_in = cyl1(name='y min in', x0=x, y0=y-t) + self.max_in = cyl1(name='y max in', x0=x, y0=y+t) + + self.x_min_y_min_out = cyl2(name='x min y min out', x0=x-c*l, y0=y-l/2) + self.x_min_y_max_out = cyl2(name='x min y max out', x0=x-c*l, y0=y+l/2) + self.x_max_y_min_out = cyl2(name='x max y min out', x0=x+c*l, y0=y-l/2) + self.x_max_y_max_out = cyl2(name='x max y max out', x0=x+c*l, y0=y+l/2) + self.min_out = cyl2(name='y min out', x0=x, y0=y-l) + self.max_out = cyl2(name='y max out', x0=x, y0=y+l) + + # Add to tuple of surface names + for s in ('in', 'out'): + self._surface_names += ( + f'x_min_y_min_{s}', f'x_min_y_max_{s}', + f'x_max_y_min_{s}', f'x_max_y_max_{s}', + f'min_{s}', f'max_{s}') + + def __neg__(self) -> openmc.Region: + prism = ( + -self.plane_max & +self.plane_min & + -self.upper_right & -self.upper_left & + +self.lower_right & +self.lower_left + ) + + # Cut out corners if a corner radius was given + if hasattr(self, 'min_in'): + corners = ( + +self.x_min_y_min_in & -self.x_min_y_min_out | + +self.x_min_y_max_in & -self.x_min_y_max_out | + +self.x_max_y_min_in & -self.x_max_y_min_out | + +self.x_max_y_max_in & -self.x_max_y_max_out | + +self.min_in & -self.min_out | + +self.max_in & -self.max_out + ) + prism &= ~corners + + return prism diff --git a/openmc/plots.py b/openmc/plots.py index 6afe3ea33b8..df4a7663310 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,10 +1,10 @@ from collections.abc import Iterable, Mapping from numbers import Integral, Real from pathlib import Path -import lxml.etree as ET from typing import Optional import h5py +import lxml.etree as ET import numpy as np import openmc @@ -184,7 +184,7 @@ def _get_plot_image(plot, cwd): def voxel_to_vtk(voxel_file: PathLike, output: PathLike = 'plot.vti'): """Converts a voxel HDF5 file to a VTK file - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -465,11 +465,11 @@ def colorize(self, geometry, seed=1): domains = geometry.get_all_cells().values() # Set the seed for the random number generator - np.random.seed(seed) + rng = np.random.RandomState(seed) # Generate random colors for each feature for domain in domains: - self.colors[domain] = np.random.randint(0, 256, (3,)) + self.colors[domain] = rng.randint(0, 256, (3,)) def to_xml_element(self): """Save common plot attributes to XML element @@ -634,8 +634,7 @@ def meshlines(self, meshlines): raise ValueError(msg) elif meshlines['type'] not in ['tally', 'entropy', 'ufs', 'cmfd']: - msg = 'Unable to set the meshlines with ' \ - 'type "{}"'.format(meshlines['type']) + msg = f"Unable to set the meshlines with type \"{meshlines['type']}\"" raise ValueError(msg) if 'id' in meshlines: @@ -943,7 +942,7 @@ def to_vtk(self, output: Optional[PathLike] = None, This method runs OpenMC in plotting mode to produce a .vti file. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -990,6 +989,8 @@ class ProjectionPlot(PlotBase): projections are more similar to a pinhole camera, and orthographic projections preserve parallel lines and distances. + .. versionadded:: 0.14.0 + Parameters ---------- plot_id : int @@ -1506,6 +1507,7 @@ def from_xml(cls, path='plots.xml'): Plots collection """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() return cls.from_xml_element(root) diff --git a/openmc/plotter.py b/openmc/plotter.py index 193ba9cbe67..97ca5f92975 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -58,6 +58,11 @@ def _get_legend_label(this, type): """Gets a label for the element or nuclide or material and reaction plotted""" if isinstance(this, str): + if type in openmc.data.DADZ: + z, a, m = openmc.data.zam(this) + da, dz = openmc.data.DADZ[type] + gnds_name = openmc.data.gnds_name(z + dz, a + da, m) + return f'{this} {type} {gnds_name}' return f'{this} {type}' elif this.name == '': return f'Material {this.id} {type}' @@ -68,24 +73,37 @@ def _get_legend_label(this, type): def _get_yaxis_label(reactions, divisor_types): """Gets a y axis label for the type of data plotted""" - if all(isinstance(item, str) for item in reactions.keys()): - stem = 'Microscopic' - if divisor_types: - mid, units = 'Data', '' - else: - mid, units = 'Cross Section', '[b]' + heat_values = {"heating", "heating-local", "damage-energy"} + + # if all the types are heating a different stem and unit is needed + if all(set(value).issubset(heat_values) for value in reactions.values()): + stem = "Heating" + elif all(isinstance(item, str) for item in reactions.keys()): + for nuc_reactions in reactions.values(): + for reaction in nuc_reactions: + if reaction in heat_values: + raise TypeError( + "Mixture of heating and Microscopic reactions. " + "Invalid type for plotting" + ) + stem = "Microscopic" elif all(isinstance(item, openmc.Material) for item in reactions.keys()): stem = 'Macroscopic' - if divisor_types: - mid, units = 'Data', '' - else: - mid, units = 'Cross Section', '[1/cm]' else: msg = "Mixture of openmc.Material and elements/nuclides. Invalid type for plotting" raise TypeError(msg) - return f'{stem} {mid} {units}' + if divisor_types: + mid, units = "Data", "" + else: + mid = "Cross Section" + units = { + "Macroscopic": "[1/cm]", + "Microscopic": "[b]", + "Heating": "[eV-barn]", + }[stem] + return f'{stem} {mid} {units}' def _get_title(reactions): """Gets a title for the type of data plotted""" @@ -100,7 +118,7 @@ def _get_title(reactions): def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, sab_name=None, ce_cross_sections=None, mg_cross_sections=None, enrichment=None, plot_CE=True, orders=None, divisor_orders=None, - **kwargs): + energy_axis_units="eV", **kwargs): """Creates a figure of continuous-energy cross sections for this item. Parameters @@ -143,6 +161,10 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, **kwargs : All keyword arguments are passed to :func:`matplotlib.pyplot.figure`. + energy_axis_units : {'eV', 'keV', 'MeV'} + Units used on the plot energy axis + + .. versionadded:: 0.14.1 Returns ------- @@ -156,6 +178,9 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, import matplotlib.pyplot as plt cv.check_type("plot_CE", plot_CE, bool) + cv.check_value("energy_axis_units", energy_axis_units, {"eV", "keV", "MeV"}) + + axis_scaling_factor = {"eV": 1.0, "keV": 1e-3, "MeV": 1e-6} # Generate the plot if axis is None: @@ -212,6 +237,8 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, if divisor_types[line] != 'unity': types[line] += ' / ' + divisor_types[line] + E *= axis_scaling_factor[energy_axis_units] + # Plot the data for i in range(len(data)): data[i, :] = np.nan_to_num(data[i, :]) @@ -227,9 +254,12 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, ax.set_xscale('log') ax.set_yscale('log') - ax.set_xlabel('Energy [eV]') + ax.set_xlabel(f"Energy [{energy_axis_units}]") if plot_CE: - ax.set_xlim(_MIN_E, _MAX_E) + ax.set_xlim( + _MIN_E * axis_scaling_factor[energy_axis_units], + _MAX_E * axis_scaling_factor[energy_axis_units], + ) else: ax.set_xlim(E[-1], E[0]) diff --git a/openmc/region.py b/openmc/region.py index 51e1820fee8..f679129c1fc 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -1,9 +1,11 @@ from abc import ABC, abstractmethod from collections.abc import MutableSequence from copy import deepcopy +import warnings import numpy as np +import openmc from .bounding_box import BoundingBox @@ -16,6 +18,11 @@ class Region(ABC): respective classes are typically not instantiated directly but rather are created through operators of the Surface and Region classes. + Attributes + ---------- + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the region + """ def __and__(self, other): return Intersection((self, other)) @@ -30,6 +37,11 @@ def __invert__(self): def __contains__(self, point): pass + @property + @abstractmethod + def bounding_box(self) -> BoundingBox: + pass + @abstractmethod def __str__(self): pass @@ -329,6 +341,59 @@ def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False, return type(self)(n.rotate(rotation, pivot=pivot, order=order, inplace=inplace, memo=memo) for n in self) + def plot(self, *args, **kwargs): + """Display a slice plot of the region. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + origin : iterable of float + Coordinates at the origin of the plot. If left as None then the + bounding box center will be used to attempt to ascertain the origin. + Defaults to (0, 0, 0) if the bounding box is not finite + width : iterable of float + Width of the plot in each basis direction. If left as none then the + bounding box width will be used to attempt to ascertain the plot + width. Defaults to (10, 10) if the bounding box is not finite + pixels : Iterable of int or int + If iterable of ints provided, then this directly sets the number of + pixels to use in each basis direction. If int provided, then this + sets the total number of pixels in the plot and the number of pixels + in each basis direction is calculated from this total and the image + aspect ratio. + basis : {'xy', 'xz', 'yz'} + The basis directions for the plot + seed : int + Seed for the random number generator + openmc_exec : str + Path to OpenMC executable. + axes : matplotlib.Axes + Axes to draw to + outline : bool + Whether outlines between color boundaries should be drawn + axis_units : {'km', 'm', 'cm', 'mm'} + Units used on the plot axis + **kwargs + Keyword arguments passed to :func:`matplotlib.pyplot.imshow` + + Returns + ------- + matplotlib.axes.Axes + Axes containing resulting image + + """ + for key in ('color_by', 'colors', 'legend', 'legend_kwargs'): + if key in kwargs: + warnings.warn(f"The '{key}' argument is present but won't be applied in a region plot") + + # Create cell while not perturbing use of autogenerated IDs + next_id = openmc.Cell.next_id + c = openmc.Cell(region=self) + openmc.Cell.used_ids.remove(c.id) + openmc.Cell.next_id = next_id + return c.plot(*args, **kwargs) + class Intersection(Region, MutableSequence): r"""Intersection of two or more regions. @@ -355,7 +420,7 @@ class Intersection(Region, MutableSequence): Attributes ---------- bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box + Axis-aligned bounding box of the region """ @@ -413,7 +478,7 @@ def __str__(self): return '(' + ' '.join(map(str, self)) + ')' @property - def bounding_box(self): + def bounding_box(self) -> BoundingBox: box = BoundingBox.infinite() for n in self: box &= n.bounding_box @@ -443,7 +508,7 @@ class Union(Region, MutableSequence): Attributes ---------- bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box + Axis-aligned bounding box of the region """ @@ -501,7 +566,7 @@ def __str__(self): return '(' + ' | '.join(map(str, self)) + ')' @property - def bounding_box(self): + def bounding_box(self) -> BoundingBox: bbox = BoundingBox(np.array([np.inf]*3), np.array([-np.inf]*3)) for n in self: @@ -534,7 +599,7 @@ class Complement(Region): node : openmc.Region Regions to take the complement of bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box + Axis-aligned bounding box of the region """ @@ -571,7 +636,7 @@ def node(self, node): self._node = node @property - def bounding_box(self): + def bounding_box(self) -> BoundingBox: # Use De Morgan's laws to distribute the complement operator so that it # only applies to surface half-spaces, thus allowing us to calculate the # bounding box in the usual recursive manner. diff --git a/openmc/settings.py b/openmc/settings.py index 91e42a28b66..63124602908 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1,4 +1,3 @@ -import os from collections.abc import Iterable, Mapping, MutableSequence from enum import Enum import itertools @@ -7,12 +6,12 @@ from pathlib import Path import typing # required to prevent typing.Union namespace overwriting Union from typing import Optional + import lxml.etree as ET import openmc.checkvalue as cv from openmc.stats.multivariate import MeshSpatial - -from . import (RegularMesh, SourceBase, IndependentSource, +from . import (RegularMesh, SourceBase, MeshSource, IndependentSource, VolumeCalculation, WeightWindows, WeightWindowGenerator) from ._xml import clean_indentation, get_text, reorder_attributes from openmc.checkvalue import PathLike @@ -87,7 +86,8 @@ class Settings: .. versionadded:: 0.12 rel_max_lost_particles : float - Maximum number of lost particles, relative to the total number of particles + Maximum number of lost particles, relative to the total number of + particles .. versionadded:: 0.12 inactive : int @@ -110,6 +110,10 @@ class Settings: parallelism. .. versionadded:: 0.12 + max_particle_events : int + Maximum number of allowed particle events per source particle. + + .. versionadded:: 0.14.1 max_order : None or int Maximum scattering order to apply globally when in multi-group mode. max_splits : int @@ -124,7 +128,7 @@ class Settings: Maximum number of particle restart files (per MPI process) to write for lost particles. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 no_reduce : bool Indicate that all user-defined and global tallies should not be reduced across processes in a parallel calculation. @@ -143,6 +147,18 @@ class Settings: Initial seed for randomly generated plot colors. ptables : bool Determine whether probability tables are used. + random_ray : dict + Options for configuring the random ray solver. Acceptable keys are: + + :distance_inactive: + Indicates the total active distance in [cm] a ray should travel + :distance_active: + Indicates the total active distance in [cm] a ray should travel + :ray_source: + Starting ray distribution (must be uniform in space and angle) as + specified by a :class:`openmc.SourceBase` object. + + .. versionadded:: 0.14.1 resonance_scattering : dict Settings for resonance elastic scattering. Accepted keys are 'enable' (bool), 'method' (str), 'energy_min' (float), 'energy_max' (float), and @@ -150,10 +166,10 @@ class Settings: rejection correction) or 'rvs' (relative velocity sampling). If not specified, 'rvs' is the default method. The 'energy_min' and 'energy_max' values indicate the minimum and maximum energies above and - below which the resonance elastic scattering method is to be - applied. The 'nuclides' list indicates what nuclides the method should - be applied to. In its absence, the method will be applied to all - nuclides with 0 K elastic scattering data present. + below which the resonance elastic scattering method is to be applied. + The 'nuclides' list indicates what nuclides the method should be applied + to. In its absence, the method will be applied to all nuclides with 0 K + elastic scattering data present. run_mode : {'eigenvalue', 'fixed source', 'plot', 'volume', 'particle restart'} The type of calculation to perform (default is 'eigenvalue') seed : int @@ -182,26 +198,26 @@ class Settings: :surface_ids: List of surface ids at which crossing particles are to be banked (int) - :max_particles: Maximum number of particles to be banked on - surfaces per process (int) + :max_particles: Maximum number of particles to be banked on surfaces per + process (int) :mcpl: Output in the form of an MCPL-file (bool) survival_biasing : bool Indicate whether survival biasing is to be used tabular_legendre : dict Determines if a multi-group scattering moment kernel expanded via Legendre polynomials is to be converted to a tabular distribution or - not. Accepted keys are 'enable' and 'num_points'. The value for - 'enable' is a bool stating whether the conversion to tabular is - performed; the value for 'num_points' sets the number of points to use - in the tabular distribution, should 'enable' be True. + not. Accepted keys are 'enable' and 'num_points'. The value for 'enable' + is a bool stating whether the conversion to tabular is performed; the + value for 'num_points' sets the number of points to use in the tabular + distribution, should 'enable' be True. temperature : dict Defines a default temperature and method for treating intermediate temperatures at which nuclear data doesn't exist. Accepted keys are 'default', 'method', 'range', 'tolerance', and 'multipole'. The value for 'default' should be a float representing the default temperature in Kelvin. The value for 'method' should be 'nearest' or 'interpolation'. - If the method is 'nearest', 'tolerance' indicates a range of - temperature within which cross sections may be used. If the method is + If the method is 'nearest', 'tolerance' indicates a range of temperature + within which cross sections may be used. If the method is 'interpolation', 'tolerance' indicates the range of temperatures outside of the available cross section temperatures where cross sections will evaluate to the nearer bound. The value for 'range' should be a pair of @@ -240,11 +256,11 @@ class Settings: Indicates the checkpoints for weight window split/roulettes. Valid keys include "collision" and "surface". Values must be of type bool. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 weight_window_generators : WeightWindowGenerator or iterable of WeightWindowGenerator Weight windows generation parameters to apply during simulation - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 create_delayed_neutrons : bool Whether delayed neutrons are created in fission. @@ -258,7 +274,7 @@ class Settings: weight_windows_file: Pathlike Path to a weight window file to load during simulation initialization - .. versionadded::0.13.4 + .. versionadded::0.14.0 write_initial_source : bool Indicate whether to write the initial source distribution to file """ @@ -335,6 +351,7 @@ def __init__(self, **kwargs): self._event_based = None self._max_particles_in_flight = None + self._max_particle_events = None self._write_initial_source = None self._weight_windows = cv.CheckedList(WeightWindows, 'weight windows') self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators') @@ -344,6 +361,8 @@ def __init__(self, **kwargs): self._max_splits = None self._max_tracks = None + self._random_ray = {} + for key, value in kwargs.items(): setattr(self, key, value) @@ -939,6 +958,16 @@ def max_particles_in_flight(self, value: int): cv.check_greater_than('max particles in flight', value, 0) self._max_particles_in_flight = value + @property + def max_particle_events(self) -> int: + return self._max_particle_events + + @max_particle_events.setter + def max_particle_events(self, value: int): + cv.check_type('max particle events', value, Integral) + cv.check_greater_than('max particle events', value, 0) + self._max_particle_events = value + @property def write_initial_source(self) -> bool: return self._write_initial_source @@ -1016,6 +1045,31 @@ def weight_window_generators(self, wwgs): wwgs = [wwgs] self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators', wwgs) + @property + def random_ray(self) -> dict: + return self._random_ray + + @random_ray.setter + def random_ray(self, random_ray: dict): + if not isinstance(random_ray, Mapping): + raise ValueError(f'Unable to set random_ray from "{random_ray}" ' + 'which is not a dict.') + for key in random_ray: + if key == 'distance_active': + cv.check_type('active ray length', random_ray[key], Real) + cv.check_greater_than('active ray length', random_ray[key], 0.0) + elif key == 'distance_inactive': + cv.check_type('inactive ray length', random_ray[key], Real) + cv.check_greater_than('inactive ray length', + random_ray[key], 0.0, True) + elif key == 'ray_source': + cv.check_type('random ray source', random_ray[key], SourceBase) + else: + raise ValueError(f'Unable to set random ray to "{key}" which is ' + 'unsupported by OpenMC') + + self._random_ray = random_ray + def _create_run_mode_subelement(self, root): elem = ET.SubElement(root, "run_mode") elem.text = self._run_mode.value @@ -1072,13 +1126,19 @@ def _create_max_order_subelement(self, root): element = ET.SubElement(root, "max_order") element.text = str(self._max_order) - def _create_source_subelement(self, root): + def _create_source_subelement(self, root, mesh_memo=None): for source in self.source: root.append(source.to_xml_element()) if isinstance(source, IndependentSource) and isinstance(source.space, MeshSpatial): path = f"./mesh[@id='{source.space.mesh.id}']" if root.find(path) is None: root.append(source.space.mesh.to_xml_element()) + if isinstance(source, MeshSource): + path = f"./mesh[@id='{source.mesh.id}']" + if root.find(path) is None: + root.append(source.mesh.to_xml_element()) + if mesh_memo is not None: + mesh_memo.add(source.mesh.id) def _create_volume_calcs_subelement(self, root): for calc in self.volume_calculations: @@ -1336,6 +1396,11 @@ def _create_max_particles_in_flight_subelement(self, root): elem = ET.SubElement(root, "max_particles_in_flight") elem.text = str(self._max_particles_in_flight).lower() + def _create_max_events_subelement(self, root): + if self._max_particle_events is not None: + elem = ET.SubElement(root, "max_particle_events") + elem.text = str(self._max_particle_events).lower() + def _create_material_cell_offsets_subelement(self, root): if self._material_cell_offsets is not None: elem = ET.SubElement(root, "material_cell_offsets") @@ -1365,7 +1430,8 @@ def _create_weight_windows_subelement(self, root, mesh_memo=None): path = f"./mesh[@id='{ww.mesh.id}']" if root.find(path) is None: root.append(ww.mesh.to_xml_element()) - if mesh_memo is not None: mesh_memo.add(ww.mesh.id) + if mesh_memo is not None: + mesh_memo.add(ww.mesh.id) if self._weight_windows_on is not None: elem = ET.SubElement(root, "weight_windows_on") @@ -1416,6 +1482,17 @@ def _create_max_tracks_subelement(self, root): elem = ET.SubElement(root, "max_tracks") elem.text = str(self._max_tracks) + def _create_random_ray_subelement(self, root): + if self._random_ray: + element = ET.SubElement(root, "random_ray") + for key, value in self._random_ray.items(): + if key == 'ray_source' and isinstance(value, SourceBase): + source_element = value.to_xml_element() + element.append(source_element) + else: + subelement = ET.SubElement(element, key) + subelement.text = str(value) + def _eigenvalue_from_xml_element(self, root): elem = root.find('eigenvalue') if elem is not None: @@ -1597,15 +1674,14 @@ def _cutoff_from_xml_element(self, root): if value is not None: self.cutoff[key] = float(value) - def _entropy_mesh_from_xml_element(self, root, meshes=None): + def _entropy_mesh_from_xml_element(self, root, meshes): text = get_text(root, 'entropy_mesh') - if text is not None: - path = f"./mesh[@id='{int(text)}']" - elem = root.find(path) - if elem is not None: - self.entropy_mesh = RegularMesh.from_xml_element(elem) - if meshes is not None and self.entropy_mesh is not None: - meshes[self.entropy_mesh.id] = self.entropy_mesh + if text is None: + return + mesh_id = int(text) + if mesh_id not in meshes: + raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') + self.entropy_mesh = meshes[mesh_id] def _trigger_from_xml_element(self, root): elem = root.find('trigger') @@ -1665,15 +1741,14 @@ def _track_from_xml_element(self, root): values = [int(x) for x in text.split()] self.track = list(zip(values[::3], values[1::3], values[2::3])) - def _ufs_mesh_from_xml_element(self, root, meshes=None): + def _ufs_mesh_from_xml_element(self, root, meshes): text = get_text(root, 'ufs_mesh') - if text is not None: - path = f"./mesh[@id='{int(text)}']" - elem = root.find(path) - if elem is not None: - self.ufs_mesh = RegularMesh.from_xml_element(elem) - if meshes is not None and self.ufs_mesh is not None: - meshes[self.ufs_mesh.id] = self.ufs_mesh + if text is None: + return + mesh_id = int(text) + if mesh_id not in meshes: + raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') + self.ufs_mesh = meshes[mesh_id] def _resonance_scattering_from_xml_element(self, root): elem = root.find('resonance_scattering') @@ -1715,6 +1790,11 @@ def _max_particles_in_flight_from_xml_element(self, root): if text is not None: self.max_particles_in_flight = int(text) + def _max_particle_events_from_xml_element(self, root): + text = get_text(root, 'max_particle_events') + if text is not None: + self.max_particle_events = int(text) + def _material_cell_offsets_from_xml_element(self, root): text = get_text(root, 'material_cell_offsets') if text is not None: @@ -1737,16 +1817,13 @@ def _weight_window_generators_from_xml_element(self, root, meshes=None): def _weight_windows_from_xml_element(self, root, meshes=None): for elem in root.findall('weight_windows'): - ww = WeightWindows.from_xml_element(elem, root) + ww = WeightWindows.from_xml_element(elem, meshes) self.weight_windows.append(ww) text = get_text(root, 'weight_windows_on') if text is not None: self.weight_windows_on = text in ('true', '1') - if meshes is not None and self.weight_windows: - meshes.update({ww.mesh.id: ww.mesh for ww in self.weight_windows}) - def _weight_window_checkpoints_from_xml_element(self, root): elem = root.find('weight_window_checkpoints') if elem is None: @@ -1767,6 +1844,17 @@ def _max_tracks_from_xml_element(self, root): if text is not None: self.max_tracks = int(text) + def _random_ray_from_xml_element(self, root): + elem = root.find('random_ray') + if elem is not None: + self.random_ray = {} + for child in elem: + if child.tag in ('distance_inactive', 'distance_active'): + self.random_ray[child.tag] = float(child.text) + elif child.tag == 'source': + source = SourceBase.from_xml_element(child) + self.random_ray['ray_source'] = source + def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. @@ -1787,7 +1875,7 @@ def to_xml_element(self, mesh_memo=None): self._create_max_write_lost_particles_subelement(element) self._create_generations_per_batch_subelement(element) self._create_keff_trigger_subelement(element) - self._create_source_subelement(element) + self._create_source_subelement(element, mesh_memo) self._create_output_subelement(element) self._create_statepoint_subelement(element) self._create_sourcepoint_subelement(element) @@ -1819,6 +1907,7 @@ def to_xml_element(self, mesh_memo=None): self._create_delayed_photon_scaling_subelement(element) self._create_event_based_subelement(element) self._create_max_particles_in_flight_subelement(element) + self._create_max_events_subelement(element) self._create_material_cell_offsets_subelement(element) self._create_log_grid_bins_subelement(element) self._create_write_initial_source_subelement(element) @@ -1828,6 +1917,7 @@ def to_xml_element(self, mesh_memo=None): self._create_weight_window_checkpoints_subelement(element) self._create_max_splits_subelement(element) self._create_max_tracks_subelement(element) + self._create_random_ray_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) @@ -1874,6 +1964,11 @@ def from_xml_element(cls, elem, meshes=None): Settings object """ + # read all meshes under the settings node and update + settings_meshes = _read_meshes(elem) + meshes = {} if meshes is None else meshes + meshes.update(settings_meshes) + settings = cls() settings._eigenvalue_from_xml_element(elem) settings._run_mode_from_xml_element(elem) @@ -1917,6 +2012,7 @@ def from_xml_element(cls, elem, meshes=None): settings._delayed_photon_scaling_from_xml_element(elem) settings._event_based_from_xml_element(elem) settings._max_particles_in_flight_from_xml_element(elem) + settings._max_particle_events_from_xml_element(elem) settings._material_cell_offsets_from_xml_element(elem) settings._log_grid_bins_from_xml_element(elem) settings._write_initial_source_from_xml_element(elem) @@ -1925,6 +2021,7 @@ def from_xml_element(cls, elem, meshes=None): settings._weight_window_checkpoints_from_xml_element(elem) settings._max_splits_from_xml_element(elem) settings._max_tracks_from_xml_element(elem) + settings._random_ray_from_xml_element(elem) # TODO: Get volume calculations return settings @@ -1946,7 +2043,8 @@ def from_xml(cls, path: PathLike = 'settings.xml'): Settings object """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() meshes = _read_meshes(root) return cls.from_xml_element(root, meshes) diff --git a/openmc/source.py b/openmc/source.py index 0e17938a81d..5deb6b05f50 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -7,8 +7,8 @@ import typing # imported separately as py3.8 requires typing.Iterable # also required to prevent typing.Union namespace overwriting Union from typing import Optional, Sequence -import lxml.etree as ET +import lxml.etree as ET import numpy as np import h5py @@ -18,7 +18,7 @@ from openmc.stats.multivariate import UnitSphere, Spatial from openmc.stats.univariate import Univariate from ._xml import get_text -from .mesh import MeshBase +from .mesh import MeshBase, StructuredMesh, UnstructuredMesh class SourceBase(ABC): @@ -31,7 +31,7 @@ class SourceBase(ABC): Attributes ---------- - type : {'independent', 'file', 'compiled'} + type : {'independent', 'file', 'compiled', 'mesh'} Indicator of source type. strength : float Strength of the source @@ -61,7 +61,6 @@ def populate_xml_element(self, element): XML element containing source data """ - pass def to_xml_element(self) -> ET.Element: """Return XML representation of the source @@ -79,7 +78,7 @@ def to_xml_element(self) -> ET.Element: return element @classmethod - def from_xml_element(cls, elem: ET.Element, meshes=None) -> openmc.SourceBase: + def from_xml_element(cls, elem: ET.Element, meshes=None) -> SourceBase: """Generate source from an XML element Parameters @@ -114,6 +113,8 @@ def from_xml_element(cls, elem: ET.Element, meshes=None) -> openmc.SourceBase: return CompiledSource.from_xml_element(elem) elif source_type == 'file': return FileSource.from_xml_element(elem) + elif source_type == 'mesh': + return MeshSource.from_xml_element(elem, meshes) else: raise ValueError(f'Source type {source_type} is not recognized') @@ -121,6 +122,8 @@ def from_xml_element(cls, elem: ET.Element, meshes=None) -> openmc.SourceBase: class IndependentSource(SourceBase): """Distribution of phase space coordinates for source sites. + .. versionadded:: 0.14.0 + Parameters ---------- space : openmc.stats.Spatial @@ -154,7 +157,7 @@ class IndependentSource(SourceBase): type : str Indicator of source type: 'independent' - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 particle : {'neutron', 'photon'} Source particle type @@ -290,13 +293,13 @@ def domain_type(self, domain_type): def populate_xml_element(self, element): """Add necessary source information to an XML element + Returns ------- element : lxml.etree._Element XML element containing source data """ - super().populate_xml_element(element) element.set("particle", self.particle) if self.space is not None: element.append(self.space.to_xml_element()) @@ -313,7 +316,7 @@ def populate_xml_element(self, element): id_elem.text = ' '.join(str(uid) for uid in self.domain_ids) @classmethod - def from_xml_element(cls, elem: ET.Element, meshes=None) -> 'openmc.SourceBase': + def from_xml_element(cls, elem: ET.Element, meshes=None) -> SourceBase: """Generate source from an XML element Parameters @@ -380,6 +383,157 @@ def from_xml_element(cls, elem: ET.Element, meshes=None) -> 'openmc.SourceBase': return source +class MeshSource(SourceBase): + """A source with a spatial distribution over mesh elements + + This class represents a mesh-based source in which random positions are + uniformly sampled within mesh elements and each element can have independent + angle, energy, and time distributions. The element sampled is chosen based + on the relative strengths of the sources applied to the elements. The + strength of the mesh source as a whole is the sum of all source strengths + applied to the elements. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + mesh : openmc.MeshBase + The mesh over which source sites will be generated. + sources : sequence of openmc.SourceBase + Sources for each element in the mesh. Sources must be specified as + either a 1-D array in the order of the mesh indices or a + multidimensional array whose shape matches the mesh shape. If spatial + distributions are set on any of the source objects, they will be ignored + during source site sampling. + + Attributes + ---------- + mesh : openmc.MeshBase + The mesh over which source sites will be generated. + sources : numpy.ndarray of openmc.SourceBase + Sources to apply to each element + strength : float + Strength of the source + type : str + Indicator of source type: 'mesh' + + """ + def __init__(self, mesh: MeshBase, sources: Sequence[SourceBase]): + self.mesh = mesh + self.sources = sources + + @property + def type(self) -> str: + return "mesh" + + @property + def mesh(self) -> MeshBase: + return self._mesh + + @property + def strength(self) -> float: + return sum(s.strength for s in self.sources) + + @property + def sources(self) -> np.ndarray: + return self._sources + + @mesh.setter + def mesh(self, m): + cv.check_type('source mesh', m, MeshBase) + self._mesh = m + + @sources.setter + def sources(self, s): + cv.check_iterable_type('mesh sources', s, SourceBase, max_depth=3) + + s = np.asarray(s) + + if isinstance(self.mesh, StructuredMesh): + if s.size != self.mesh.num_mesh_cells: + raise ValueError( + f'The length of the source array ({s.size}) does not match ' + f'the number of mesh elements ({self.mesh.num_mesh_cells}).') + + # If user gave a multidimensional array, flatten in the order + # of the mesh indices + if s.ndim > 1: + s = s.ravel(order='F') + + elif isinstance(self.mesh, UnstructuredMesh): + if s.ndim > 1: + raise ValueError('Sources must be a 1-D array for unstructured mesh') + + self._sources = s + for src in self._sources: + if isinstance(src, IndependentSource) and src.space is not None: + warnings.warn('Some sources on the mesh have spatial ' + 'distributions that will be ignored at runtime.') + break + + @strength.setter + def strength(self, val): + cv.check_type('mesh source strength', val, Real) + self.set_total_strength(val) + + def set_total_strength(self, strength: float): + """Scales the element source strengths based on a desired total strength. + + Parameters + ---------- + strength : float + Total source strength + + """ + current_strength = self.strength if self.strength != 0.0 else 1.0 + + for s in self.sources: + s.strength *= strength / current_strength + + def normalize_source_strengths(self): + """Update all element source strengths such that they sum to 1.0.""" + self.set_total_strength(1.0) + + def populate_xml_element(self, elem: ET.Element): + """Add necessary source information to an XML element + + Returns + ------- + element : lxml.etree._Element + XML element containing source data + + """ + elem.set("mesh", str(self.mesh.id)) + + # write in the order of mesh indices + for s in self.sources: + elem.append(s.to_xml_element()) + + @classmethod + def from_xml_element(cls, elem: ET.Element, meshes) -> openmc.MeshSource: + """ + Generate MeshSource from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + meshes : dict + A dictionary with mesh IDs as keys and openmc.MeshBase instances as + values + + Returns + ------- + openmc.MeshSource + MeshSource generated from the XML element + """ + mesh_id = int(get_text(elem, 'mesh')) + mesh = meshes[mesh_id] + + sources = [SourceBase.from_xml_element(e) for e in elem.iterchildren('source')] + return cls(mesh, sources) + + def Source(*args, **kwargs): """ A function for backward compatibility of sources. Will be removed in the @@ -392,7 +546,7 @@ def Source(*args, **kwargs): class CompiledSource(SourceBase): """A source based on a compiled shared library - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -457,15 +611,13 @@ def populate_xml_element(self, element): XML element containing source data """ - super().populate_xml_element(element) - element.set("library", self.library) if self.parameters is not None: element.set("parameters", self.parameters) @classmethod - def from_xml_element(cls, elem: ET.Element, meshes=None) -> openmc.CompiledSource: + def from_xml_element(cls, elem: ET.Element) -> openmc.CompiledSource: """Generate a compiled source from an XML element Parameters @@ -500,7 +652,7 @@ def from_xml_element(cls, elem: ET.Element, meshes=None) -> openmc.CompiledSourc class FileSource(SourceBase): """A source based on particles stored in a file - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -549,8 +701,6 @@ def populate_xml_element(self, element): XML element containing source data """ - super().populate_xml_element(element) - if self.path is not None: element.set("file", self.path) @@ -736,5 +886,39 @@ def write_source_file( # Write array to file kwargs.setdefault('mode', 'w') with h5py.File(filename, **kwargs) as fh: - fh.attrs['filetype'] = np.string_("source") + fh.attrs['filetype'] = np.bytes_("source") fh.create_dataset('source_bank', data=arr, dtype=source_dtype) + + +def read_source_file(filename: PathLike) -> typing.List[SourceParticle]: + """Read a source file and return a list of source particles. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + filename : str or path-like + Path to source file to read + + Returns + ------- + list of SourceParticle + Source particles read from file + + See Also + -------- + openmc.SourceParticle + + """ + with h5py.File(filename, 'r') as fh: + filetype = fh.attrs['filetype'] + arr = fh['source_bank'][...] + + if filetype != b'source': + raise ValueError(f'File {filename} is not a source file') + + source_particles = [] + for *params, particle in arr: + source_particles.append(SourceParticle(*params, ParticleType(particle))) + + return source_particles diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 09a2f8752ff..3922d601aa8 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -710,6 +710,7 @@ def to_xml_element(self): """ element = ET.Element('space') + element.set('type', 'mesh') element.set("mesh_id", str(self.mesh.id)) element.set("volume_normalized", str(self.volume_normalized)) @@ -743,7 +744,7 @@ def from_xml_element(cls, elem, meshes): # check if this mesh has been read in from another location already if mesh_id not in meshes: - raise RuntimeError(f'Could not locate mesh with ID "{mesh_id}"') + raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') volume_normalized = elem.get("volume_normalized") volume_normalized = get_text(elem, 'volume_normalized').lower() == 'true' diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 01c55239d8a..a0e86c3f8f2 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -10,6 +10,7 @@ import lxml.etree as ET import numpy as np +from scipy.integrate import trapezoid import openmc.checkvalue as cv from .._xml import get_text @@ -155,9 +156,9 @@ def cdf(self): return np.insert(np.cumsum(self.p), 0, 0.0) def sample(self, n_samples=1, seed=None): - np.random.seed(seed) + rng = np.random.RandomState(seed) p = self.p / self.p.sum() - return np.random.choice(self.x, n_samples, p=p) + return rng.choice(self.x, n_samples, p=p) def normalize(self): """Normalize the probabilities stored on the distribution""" @@ -266,7 +267,7 @@ def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Discrete: function will remove any low-importance points such that :math:`\sum_i x_i p_i` is preserved to within some threshold. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- @@ -280,6 +281,9 @@ def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Discrete: Discrete distribution with low-importance points removed """ + cv.check_less_than("tolerance", tolerance, 1.0, equality=True) + cv.check_greater_than("tolerance", tolerance, 0.0, equality=True) + # Determine (reversed) sorted order of probabilities intensity = self.p * self.x index_sort = np.argsort(intensity)[::-1] @@ -360,8 +364,8 @@ def to_tabular(self): return t def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - return np.random.uniform(self.a, self.b, n_samples) + rng = np.random.RandomState(seed) + return rng.uniform(self.a, self.b, n_samples) def to_xml_element(self, element_name: str): """Return XML representation of the uniform distribution @@ -379,7 +383,7 @@ def to_xml_element(self, element_name: str): """ element = ET.Element(element_name) element.set("type", "uniform") - element.set("parameters", '{} {}'.format(self.a, self.b)) + element.set("parameters", f'{self.a} {self.b}') return element @classmethod @@ -465,8 +469,8 @@ def n(self, n): self._n = n def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - xi = np.random.rand(n_samples) + rng = np.random.RandomState(seed) + xi = rng.random(n_samples) pwr = self.n + 1 offset = self.a**pwr span = self.b**pwr - offset @@ -546,12 +550,14 @@ def theta(self, theta): self._theta = theta def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - return self.sample_maxwell(self.theta, n_samples) + rng = np.random.RandomState(seed) + return self.sample_maxwell(self.theta, n_samples, rng=rng) @staticmethod - def sample_maxwell(t, n_samples: int): - r1, r2, r3 = np.random.rand(3, n_samples) + def sample_maxwell(t, n_samples: int, rng=None): + if rng is None: + rng = np.random.default_rng() + r1, r2, r3 = rng.random((3, n_samples)) c = np.cos(0.5 * np.pi * r3) return -t * (np.log(r1) + np.log(r2) * c * c) @@ -644,9 +650,9 @@ def b(self, b): self._b = b def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - w = Maxwell.sample_maxwell(self.a, n_samples) - u = np.random.uniform(-1., 1., n_samples) + rng = np.random.RandomState(seed) + w = Maxwell.sample_maxwell(self.a, n_samples, rng=rng) + u = rng.uniform(-1., 1., n_samples) aab = self.a * self.a * self.b return w + 0.25*aab + u*np.sqrt(aab*w) @@ -666,7 +672,7 @@ def to_xml_element(self, element_name: str): """ element = ET.Element(element_name) element.set("type", "watt") - element.set("parameters", '{} {}'.format(self.a, self.b)) + element.set("parameters", f'{self.a} {self.b}') return element @classmethod @@ -737,8 +743,8 @@ def std_dev(self, std_dev): self._std_dev = std_dev def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - return np.random.normal(self.mean_value, self.std_dev, n_samples) + rng = np.random.RandomState(seed) + return rng.normal(self.mean_value, self.std_dev, n_samples) def to_xml_element(self, element_name: str): """Return XML representation of the Normal distribution @@ -756,7 +762,7 @@ def to_xml_element(self, element_name: str): """ element = ET.Element(element_name) element.set("type", "normal") - element.set("parameters", '{} {}'.format(self.mean_value, self.std_dev)) + element.set("parameters", f'{self.mean_value} {self.std_dev}') return element @classmethod @@ -949,8 +955,8 @@ def normalize(self): self.p /= self.cdf().max() def sample(self, n_samples: int = 1, seed: typing.Optional[int] = None): - np.random.seed(seed) - xi = np.random.rand(n_samples) + rng = np.random.RandomState(seed) + xi = rng.random(n_samples) # always use normalized probabilities when sampling cdf = self.cdf() @@ -1066,7 +1072,7 @@ def integral(self): if self.interpolation == 'histogram': return np.sum(np.diff(self.x) * self.p[:-1]) elif self.interpolation == 'linear-linear': - return np.trapz(self.p, self.x) + return trapezoid(self.p, self.x) else: raise NotImplementedError( f'integral() not supported for {self.inteprolation} interpolation') @@ -1182,7 +1188,7 @@ def cdf(self): return np.insert(np.cumsum(self.probability), 0, 0.0) def sample(self, n_samples=1, seed=None): - np.random.seed(seed) + rng = np.random.RandomState(seed) # Get probability of each distribution accounting for its intensity p = np.array([prob*dist.integral() for prob, dist in @@ -1190,8 +1196,7 @@ def sample(self, n_samples=1, seed=None): p /= p.sum() # Sample from the distributions - idx = np.random.choice(range(len(self.distribution)), - n_samples, p=p) + idx = rng.choice(range(len(self.distribution)), n_samples, p=p) # Draw samples from the distributions sampled above out = np.empty_like(idx, dtype=float) @@ -1281,7 +1286,7 @@ def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Mixture: function will remove any low-importance points such that :math:`\sum_i x_i p_i` is preserved to within some threshold. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- diff --git a/openmc/summary.py b/openmc/summary.py index 43224334d0a..6423f8ce1d6 100644 --- a/openmc/summary.py +++ b/openmc/summary.py @@ -240,4 +240,9 @@ def add_volume_information(self, volume_calc): Results from a stochastic volume calculation """ - self.geometry.add_volume_information(volume_calc) + if volume_calc.domain_type == "material" and self.materials: + for material in self.materials: + if material.id in volume_calc.volumes: + material.add_volume_information(volume_calc) + else: + self.geometry.add_volume_information(volume_calc) diff --git a/openmc/surface.py b/openmc/surface.py index dc1068b2291..806331024e2 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -3,18 +3,19 @@ from copy import deepcopy import math from numbers import Real -import lxml.etree as ET from warnings import warn, catch_warnings, simplefilter +import lxml.etree as ET import numpy as np -from .checkvalue import check_type, check_value, check_length +from .checkvalue import check_type, check_value, check_length, check_greater_than from .mixin import IDManagerMixin, IDWarning from .region import Region, Intersection, Union from .bounding_box import BoundingBox _BOUNDARY_TYPES = ['transmission', 'vacuum', 'reflective', 'periodic', 'white'] +_ALBEDO_BOUNDARIES = ['reflective', 'periodic', 'white'] _WARNING_UPPER = """\ "{}(...) accepts an argument named '{}', not '{}'. Future versions of OpenMC \ @@ -123,6 +124,10 @@ class Surface(IDManagerMixin, ABC): freely pass through the surface. Note that periodic boundary conditions can only be applied to x-, y-, and z-planes, and only axis-aligned periodicity is supported. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the surface. If not specified, the name will be the empty string. @@ -132,6 +137,8 @@ class Surface(IDManagerMixin, ABC): boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -147,10 +154,12 @@ class Surface(IDManagerMixin, ABC): used_ids = set() _atol = 1.e-12 - def __init__(self, surface_id=None, boundary_type='transmission', name=''): + def __init__(self, surface_id=None, boundary_type='transmission', + albedo=1., name=''): self.id = surface_id self.name = name self.boundary_type = boundary_type + self.albedo = albedo # A dictionary of the quadratic surface coefficients # Key - coefficient name @@ -165,16 +174,20 @@ def __pos__(self): def __repr__(self): string = 'Surface\n' - string += '{0: <16}{1}{2}\n'.format('\tID', '=\t', self._id) - string += '{0: <16}{1}{2}\n'.format('\tName', '=\t', self._name) - string += '{0: <16}{1}{2}\n'.format('\tType', '=\t', self._type) - string += '{0: <16}{1}{2}\n'.format('\tBoundary', '=\t', self._boundary_type) - - coefficients = '{0: <16}'.format('\tCoefficients') + '\n' + string += '{0: <20}{1}{2}\n'.format('\tID', '=\t', self._id) + string += '{0: <20}{1}{2}\n'.format('\tName', '=\t', self._name) + string += '{0: <20}{1}{2}\n'.format('\tType', '=\t', self._type) + string += '{0: <20}{1}{2}\n'.format('\tBoundary', '=\t', + self._boundary_type) + if (self._boundary_type in _ALBEDO_BOUNDARIES and + not math.isclose(self._albedo, 1.0)): + string += '{0: <20}{1}{2}\n'.format('\tBoundary Albedo', '=\t', + self._albedo) + + coefficients = '{0: <20}'.format('\tCoefficients') + '\n' for coeff in self._coefficients: - coefficients += '{0: <16}{1}{2}\n'.format( - coeff, '=\t', self._coefficients[coeff]) + coefficients += f'{coeff: <20}=\t{self._coefficients[coeff]}\n' string += coefficients @@ -206,6 +219,16 @@ def boundary_type(self, boundary_type): check_value('boundary type', boundary_type, _BOUNDARY_TYPES) self._boundary_type = boundary_type + @property + def albedo(self): + return self._albedo + + @albedo.setter + def albedo(self, albedo): + check_type('albedo', albedo, Real) + check_greater_than('albedo', albedo, 0.0) + self._albedo = float(albedo) + @property def coefficients(self): return self._coefficients @@ -402,6 +425,9 @@ def to_xml_element(self): element.set("type", self._type) if self.boundary_type != 'transmission': element.set("boundary", self.boundary_type) + if (self.boundary_type in _ALBEDO_BOUNDARIES and + not math.isclose(self.albedo, 1.0)): + element.set("albedo", str(self.albedo)) element.set("coeffs", ' '.join([str(self._coefficients.setdefault(key, 0.0)) for key in self._coeff_keys])) @@ -427,10 +453,12 @@ def from_xml_element(elem): surf_type = elem.get('type') cls = _SURFACE_CLASSES[surf_type] - # Determine ID, boundary type, coefficients + # Determine ID, boundary type, boundary albedo, coefficients kwargs = {} kwargs['surface_id'] = int(elem.get('id')) kwargs['boundary_type'] = elem.get('boundary', 'transmission') + if kwargs['boundary_type'] in _ALBEDO_BOUNDARIES: + kwargs['albedo'] = float(elem.get('albedo', 1.0)) kwargs['name'] = elem.get('name') coeffs = [float(x) for x in elem.get('coeffs').split()] kwargs.update(dict(zip(cls._coeff_keys, coeffs))) @@ -462,8 +490,13 @@ def from_hdf5(group): name = group['name'][()].decode() if 'name' in group else '' bc = group['boundary_type'][()].decode() + if 'albedo' in group: + bc_alb = float(group['albedo'][()].decode()) + else: + bc_alb = 1.0 coeffs = group['coefficients'][...] - kwargs = {'boundary_type': bc, 'name': name, 'surface_id': surface_id} + kwargs = {'boundary_type': bc, 'albedo': bc_alb, 'name': name, + 'surface_id': surface_id} surf_type = group['type'][()].decode() cls = _SURFACE_CLASSES[surf_type] @@ -607,7 +640,9 @@ def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False): # Compute new rotated coefficients a, b, c a, b, c = Rmat @ [a, b, c] - kwargs = {'boundary_type': surf.boundary_type, 'name': surf.name} + kwargs = {'boundary_type': surf.boundary_type, + 'albedo': surf.albedo, + 'name': surf.name} if inplace: kwargs['surface_id'] = surf.id @@ -651,6 +686,10 @@ class Plane(PlaneMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the plane. If not specified, the name will be the empty string. surface_id : int, optional @@ -670,6 +709,8 @@ class Plane(PlaneMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight periodic_surface : openmc.Surface If a periodic boundary condition is used, the surface with which this one is periodic with @@ -774,6 +815,10 @@ class XPlane(PlaneMixin, Surface): surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is supported, i.e., x-planes can only be paired with x-planes. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the plane. If not specified, the name will be the empty string. surface_id : int, optional @@ -787,6 +832,8 @@ class XPlane(PlaneMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight periodic_surface : openmc.Surface If a periodic boundary condition is used, the surface with which this one is periodic with @@ -832,7 +879,11 @@ class YPlane(PlaneMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is - supported, i.e., x-planes can only be paired with x-planes. + supported, i.e., y-planes can only be paired with y-planes. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the plane. If not specified, the name will be the empty string. surface_id : int, optional @@ -846,6 +897,8 @@ class YPlane(PlaneMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight periodic_surface : openmc.Surface If a periodic boundary condition is used, the surface with which this one is periodic with @@ -891,7 +944,11 @@ class ZPlane(PlaneMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is - supported, i.e., x-planes can only be paired with x-planes. + supported, i.e., z-planes can only be paired with z-planes. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the plane. If not specified, the name will be the empty string. surface_id : int, optional @@ -905,6 +962,8 @@ class ZPlane(PlaneMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight periodic_surface : openmc.Surface If a periodic boundary condition is used, the surface with which this one is periodic with @@ -1076,7 +1135,8 @@ def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False): else: base_cls = type(tsurf)._virtual_base # Copy necessary surface attributes to new kwargs dictionary - kwargs = {'boundary_type': tsurf.boundary_type, 'name': tsurf.name} + kwargs = {'boundary_type': tsurf.boundary_type, + 'albedo': tsurf.albedo, 'name': tsurf.name} if inplace: kwargs['surface_id'] = tsurf.id kwargs.update({k: getattr(tsurf, k) for k in base_cls._coeff_keys}) @@ -1133,6 +1193,10 @@ class Cylinder(QuadricMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the cylinder. If not specified, the name will be the empty string. @@ -1159,6 +1223,8 @@ class Cylinder(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -1280,8 +1346,8 @@ def to_xml_element(self): # since the C++ layer doesn't support Cylinders right now with catch_warnings(): simplefilter('ignore', IDWarning) - kwargs = {'boundary_type': self.boundary_type, 'name': self.name, - 'surface_id': self.id} + kwargs = {'boundary_type': self.boundary_type, 'albedo': self.albedo, + 'name': self.name, 'surface_id': self.id} quad_rep = Quadric(*self._get_base_coeffs(), **kwargs) return quad_rep.to_xml_element() @@ -1302,6 +1368,10 @@ class XCylinder(QuadricMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the cylinder. If not specified, the name will be the empty string. @@ -1320,6 +1390,8 @@ class XCylinder(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -1394,6 +1466,10 @@ class YCylinder(QuadricMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the cylinder. If not specified, the name will be the empty string. @@ -1412,6 +1488,8 @@ class YCylinder(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -1486,6 +1564,10 @@ class ZCylinder(QuadricMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the cylinder. If not specified, the name will be the empty string. @@ -1504,6 +1586,8 @@ class ZCylinder(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -1579,6 +1663,10 @@ class Sphere(QuadricMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the sphere. If not specified, the name will be the empty string. surface_id : int, optional @@ -1598,6 +1686,8 @@ class Sphere(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -1657,6 +1747,11 @@ def evaluate(self, point): class Cone(QuadricMixin, Surface): """A conical surface parallel to the x-, y-, or z-axis. + .. Note:: + This creates a double cone, which is two one-sided cones that meet at their apex. + For a one-sided cone see :class:`~openmc.model.XConeOneSided`, + :class:`~openmc.model.YConeOneSided`, and :class:`~openmc.model.ZConeOneSided`. + Parameters ---------- x0 : float, optional @@ -1666,7 +1761,9 @@ class Cone(QuadricMixin, Surface): z0 : float, optional z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. dx : float, optional x-component of the vector representing the axis of the cone. Defaults to 0. @@ -1683,6 +1780,11 @@ class Cone(QuadricMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. + name : str Name of the cone. If not specified, the name will be the empty string. @@ -1695,7 +1797,7 @@ class Cone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperature [cm^2] dx : float x-component of the vector representing the axis of the cone. dy : float @@ -1705,6 +1807,8 @@ class Cone(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -1790,7 +1894,9 @@ def to_xml_element(self): # since the C++ layer doesn't support Cones right now with catch_warnings(): simplefilter('ignore', IDWarning) - kwargs = {'boundary_type': self.boundary_type, 'name': self.name, + kwargs = {'boundary_type': self.boundary_type, + 'albedo': self.albedo, + 'name': self.name, 'surface_id': self.id} quad_rep = Quadric(*self._get_base_coeffs(), **kwargs) return quad_rep.to_xml_element() @@ -1800,6 +1906,10 @@ class XCone(QuadricMixin, Surface): """A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 = r^2 (x - x_0)^2`. + .. Note:: + This creates a double cone, which is two one-sided cones that meet at their apex. + For a one-sided cone see :class:`~openmc.model.XConeOneSided`. + Parameters ---------- x0 : float, optional @@ -1809,11 +1919,17 @@ class XCone(QuadricMixin, Surface): z0 : float, optional z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the cone. If not specified, the name will be the empty string. surface_id : int, optional @@ -1833,6 +1949,8 @@ class XCone(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -1889,6 +2007,10 @@ class YCone(QuadricMixin, Surface): """A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 = r^2 (y - y_0)^2`. + .. Note:: + This creates a double cone, which is two one-sided cones that meet at their apex. + For a one-sided cone see :class:`~openmc.model.YConeOneSided`. + Parameters ---------- x0 : float, optional @@ -1898,11 +2020,17 @@ class YCone(QuadricMixin, Surface): z0 : float, optional z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the cone. If not specified, the name will be the empty string. surface_id : int, optional @@ -1922,6 +2050,8 @@ class YCone(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -1978,6 +2108,10 @@ class ZCone(QuadricMixin, Surface): """A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 = r^2 (z - z_0)^2`. + .. Note:: + This creates a double cone, which is two one-sided cones that meet at their apex. + For a one-sided cone see :class:`~openmc.model.ZConeOneSided`. + Parameters ---------- x0 : float, optional @@ -1987,11 +2121,17 @@ class ZCone(QuadricMixin, Surface): z0 : float, optional z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperature [cm^2]. + This is the square of the radius of the cone 1 cm from. + This can also be treated as the square of the slope of the cone relative to its axis. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the cone. If not specified, the name will be the empty string. surface_id : int, optional @@ -2011,6 +2151,8 @@ class ZCone(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -2075,6 +2217,10 @@ class Quadric(QuadricMixin, Surface): Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. + albedo : float, optional + Albedo of the surfaces as a ratio of particle weight after interaction + with the surface to the initial weight. Values must be positive. Only + applicable if the boundary type is 'reflective', 'periodic', or 'white'. name : str, optional Name of the surface. If not specified, the name will be the empty string. surface_id : int, optional @@ -2088,6 +2234,8 @@ class Quadric(QuadricMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -2183,7 +2331,9 @@ def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False): # Create rotated torus kwargs = { - 'boundary_type': surf.boundary_type, 'name': surf.name, + 'boundary_type': surf.boundary_type, + 'albedo': surf.albedo, + 'name': surf.name, 'a': surf.a, 'b': surf.b, 'c': surf.c } if inplace: @@ -2236,6 +2386,8 @@ class XTorus(TorusMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -2309,6 +2461,8 @@ class YTorus(TorusMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int @@ -2382,6 +2536,8 @@ class ZTorus(TorusMixin, Surface): boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. + albedo : float + Boundary albedo as a positive multiplier of particle weight coefficients : dict Dictionary of surface coefficients id : int diff --git a/openmc/tallies.py b/openmc/tallies.py index 5de00f43c3b..04ae00b6a32 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -57,7 +57,7 @@ class Tally(IDManagerMixin): multiply_density : bool Whether reaction rates should be multiplied by atom density - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 filters : list of openmc.Filter List of specified filters for the tally nuclides : list of str @@ -2149,7 +2149,7 @@ def __add__(self, other): new_tally.sparse = self.sparse else: - msg = 'Unable to add "{}" to Tally ID="{}"'.format(other, self.id) + msg = f'Unable to add "{other}" to Tally ID="{self.id}"' raise ValueError(msg) return new_tally @@ -2220,7 +2220,7 @@ def __sub__(self, other): new_tally.sparse = self.sparse else: - msg = 'Unable to subtract "{}" from Tally ID="{}"'.format(other, self.id) + msg = f'Unable to subtract "{other}" from Tally ID="{self.id}"' raise ValueError(msg) return new_tally @@ -2291,7 +2291,7 @@ def __mul__(self, other): new_tally.sparse = self.sparse else: - msg = 'Unable to multiply Tally ID="{}" by "{}"'.format(self.id, other) + msg = f'Unable to multiply Tally ID="{self.id}" by "{other}"' raise ValueError(msg) return new_tally @@ -2362,7 +2362,7 @@ def __truediv__(self, other): new_tally.sparse = self.sparse else: - msg = 'Unable to divide Tally ID="{}" by "{}"'.format(self.id, other) + msg = f'Unable to divide Tally ID="{self.id}" by "{other}"' raise ValueError(msg) return new_tally @@ -2437,7 +2437,7 @@ def __pow__(self, power): new_tally.sparse = self.sparse else: - msg = 'Unable to raise Tally ID="{}" to power "{}"'.format(self.id, power) + msg = f'Unable to raise Tally ID="{self.id}" to power "{power}"' raise ValueError(msg) return new_tally @@ -3105,8 +3105,7 @@ def append(self, tally, merge=False): """ if not isinstance(tally, Tally): - msg = 'Unable to add a non-Tally "{}" to the ' \ - 'Tallies instance'.format(tally) + msg = f'Unable to add a non-Tally "{tally}" to the Tallies instance' raise TypeError(msg) if merge: @@ -3304,6 +3303,7 @@ def from_xml(cls, path='tallies.xml'): Tallies object """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() return cls.from_xml_element(root) diff --git a/openmc/tally_derivative.py b/openmc/tally_derivative.py index 779ca619f2a..ff918c1b3c6 100644 --- a/openmc/tally_derivative.py +++ b/openmc/tally_derivative.py @@ -1,4 +1,5 @@ from numbers import Integral + import lxml.etree as ET import openmc.checkvalue as cv diff --git a/openmc/trigger.py b/openmc/trigger.py index 8a165526cf6..79f5ea5af89 100644 --- a/openmc/trigger.py +++ b/openmc/trigger.py @@ -1,5 +1,6 @@ from collections.abc import Iterable from numbers import Real + import lxml.etree as ET import openmc.checkvalue as cv @@ -16,6 +17,12 @@ class Trigger(EqualityMixin): relative error of scores. threshold : float The threshold for the trigger type. + ignore_zeros : bool + Whether to allow zero tally bins to be ignored. Note that this option + can cause the trigger to fire prematurely if there are zero scores in + any bin at the first evaluation. + + .. versionadded:: 0.14.1 Attributes ---------- @@ -26,18 +33,22 @@ class Trigger(EqualityMixin): The threshold for the trigger type. scores : list of str Scores which should be checked against the trigger + ignore_zeros : bool + Whether to allow zero tally bins to be ignored. """ - def __init__(self, trigger_type: str, threshold: float): + def __init__(self, trigger_type: str, threshold: float, ignore_zeros: bool = False): self.trigger_type = trigger_type self.threshold = threshold + self.ignore_zeros = ignore_zeros self._scores = [] def __repr__(self): string = 'Trigger\n' string += '{: <16}=\t{}\n'.format('\tType', self._trigger_type) string += '{: <16}=\t{}\n'.format('\tThreshold', self._threshold) + string += '{: <16}=\t{}\n'.format('\tIgnore Zeros', self._ignore_zeros) string += '{: <16}=\t{}\n'.format('\tScores', self._scores) return string @@ -60,6 +71,15 @@ def threshold(self, threshold): cv.check_type('tally trigger threshold', threshold, Real) self._threshold = threshold + @property + def ignore_zeros(self): + return self._ignore_zeros + + @ignore_zeros.setter + def ignore_zeros(self, ignore_zeros): + cv.check_type('tally trigger ignores zeros', ignore_zeros, bool) + self._ignore_zeros = ignore_zeros + @property def scores(self): return self._scores @@ -87,6 +107,8 @@ def to_xml_element(self): element = ET.Element("trigger") element.set("type", self._trigger_type) element.set("threshold", str(self._threshold)) + if self._ignore_zeros: + element.set("ignore_zeros", "true") if len(self._scores) != 0: element.set("scores", ' '.join(self._scores)) return element @@ -109,7 +131,10 @@ def from_xml_element(cls, elem: ET.Element): # Generate trigger object trigger_type = elem.get("type") threshold = float(elem.get("threshold")) - trigger = cls(trigger_type, threshold) + ignore_zeros = str(elem.get("ignore_zeros", "false")).lower() + # Try to convert to bool. Let Trigger error out on instantiation. + ignore_zeros = ignore_zeros in ('true', '1') + trigger = cls(trigger_type, threshold, ignore_zeros) # Add scores if present scores = elem.get("scores") diff --git a/openmc/universe.py b/openmc/universe.py index 44f95c1d2fa..2e86ab05a9b 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -1,24 +1,20 @@ import math -import typing from abc import ABC, abstractmethod from collections.abc import Iterable -from copy import deepcopy from numbers import Integral, Real from pathlib import Path from tempfile import TemporaryDirectory -import lxml.etree as ET import warnings import h5py +import lxml.etree as ET import numpy as np import openmc import openmc.checkvalue as cv - from ._xml import get_text from .checkvalue import check_type, check_value from .mixin import IDManagerMixin -from .plots import _SVG_COLORS from .surface import _BOUNDARY_TYPES @@ -349,19 +345,19 @@ def plot(self, origin=None, width=None, pixels=40000, legend : bool Whether a legend showing material or cell names should be drawn - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 legend_kwargs : dict Keyword arguments passed to :func:`matplotlib.pyplot.legend`. - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 outline : bool Whether outlines between color boundaries should be drawn - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 axis_units : {'km', 'm', 'cm', 'mm'} Units used on the plot axis - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 **kwargs Keyword arguments passed to :func:`matplotlib.pyplot.imshow` diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py new file mode 100644 index 00000000000..4eb307c9303 --- /dev/null +++ b/openmc/utility_funcs.py @@ -0,0 +1,38 @@ +from contextlib import contextmanager +import os +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Optional + +from .checkvalue import PathLike + +@contextmanager +def change_directory(working_dir: Optional[PathLike] = None, *, tmpdir: bool = False): + """Context manager for executing in a provided working directory + + Parameters + ---------- + working_dir : path-like + Directory to switch to. + tmpdir : bool + Whether to use a temporary directory instead of a specific working directory + + """ + orig_dir = Path.cwd() + + # Set up temporary directory if requested + if tmpdir: + tmp = TemporaryDirectory() + working_dir = tmp.name + elif working_dir is None: + raise ValueError('Must pass working_dir argument or specify tmpdir=True.') + + working_dir = Path(working_dir) + working_dir.mkdir(parents=True, exist_ok=True) + os.chdir(working_dir) + try: + yield + finally: + os.chdir(orig_dir) + if tmpdir: + tmp.cleanup() diff --git a/openmc/volume.py b/openmc/volume.py index 90882267d7b..df19def1eac 100644 --- a/openmc/volume.py +++ b/openmc/volume.py @@ -1,11 +1,11 @@ from collections.abc import Iterable, Mapping from numbers import Real, Integral -import lxml.etree as ET import warnings +import h5py +import lxml.etree as ET import numpy as np import pandas as pd -import h5py from uncertainties import ufloat import openmc diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index c7649079bc1..96f1db89282 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -12,7 +12,6 @@ from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh import openmc.checkvalue as cv from openmc.checkvalue import PathLike - from ._xml import get_text, clean_indentation from .mixin import IDManagerMixin @@ -354,15 +353,15 @@ def to_xml_element(self) -> ET.Element: return element @classmethod - def from_xml_element(cls, elem: ET.Element, root: ET.Element) -> WeightWindows: + def from_xml_element(cls, elem: ET.Element, meshes: Dict[int, MeshBase]) -> WeightWindows: """Generate weight window settings from an XML element Parameters ---------- elem : lxml.etree._Element XML element - root : lxml.etree._Element - Root element for the file where meshes can be found + meshes : dict + Dictionary mapping IDs to mesh objects Returns ------- @@ -371,10 +370,9 @@ def from_xml_element(cls, elem: ET.Element, root: ET.Element) -> WeightWindows: """ # Get mesh for weight windows mesh_id = int(get_text(elem, 'mesh')) - path = f"./mesh[@id='{mesh_id}']" - mesh_elem = root.find(path) - if mesh_elem is not None: - mesh = MeshBase.from_xml_element(mesh_elem) + if mesh_id not in meshes: + raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') + mesh = meshes[mesh_id] # Read all other parameters lower_ww_bounds = [float(l) for l in get_text(elem, 'lower_ww_bounds').split()] @@ -922,7 +920,7 @@ def from_xml_element(cls, elem: ET.Element, meshes: dict) -> WeightWindowGenerat def hdf5_to_wws(path='weight_windows.h5'): """Create WeightWindows instances from a weight windows HDF5 file - .. versionadded:: 0.13.4 + .. versionadded:: 0.14.0 Parameters ---------- diff --git a/setup.py b/setup.py index 56301e40c54..4f2807d9787 100755 --- a/setup.py +++ b/setup.py @@ -54,25 +54,24 @@ 'Topic :: Scientific/Engineering' 'Programming Language :: C++', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ], # Dependencies - 'python_requires': '>=3.7', + 'python_requires': '>=3.8', 'install_requires': [ 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', - 'pandas', 'lxml', 'uncertainties' + 'pandas', 'lxml', 'uncertainties', 'setuptools' ], 'extras_require': { 'depletion-mpi': ['mpi4py'], 'docs': ['sphinx', 'sphinxcontrib-katex', 'sphinx-numfig', 'jupyter', 'sphinxcontrib-svg2pdfconverter', 'sphinx-rtd-theme'], - 'test': ['pytest', 'pytest-cov', 'colorama'], + 'test': ['pytest', 'pytest-cov', 'colorama', 'openpyxl'], 'vtk': ['vtk'], }, # Cython is used to add resonance reconstruction and fast float_endf diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index 5b842399e84..b58054dce8c 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -6,6 +6,7 @@ #include "openmc/constants.h" #include "openmc/error.h" +#include "openmc/random_ray/random_ray.h" #include "openmc/surface.h" namespace openmc { @@ -16,7 +17,18 @@ namespace openmc { void VacuumBC::handle_particle(Particle& p, const Surface& surf) const { - p.cross_vacuum_bc(surf); + // Random ray and Monte Carlo need different treatments at vacuum BCs + if (settings::solver_type == SolverType::RANDOM_RAY) { + // Reflect ray off of the surface + ReflectiveBC().handle_particle(p, surf); + + // Set ray's angular flux spectrum to vacuum conditions (zero) + RandomRay* r = static_cast(&p); + std::fill(r->angular_flux_.begin(), r->angular_flux_.end(), 0.0); + + } else { + p.cross_vacuum_bc(surf); + } } //============================================================================== @@ -28,6 +40,9 @@ void ReflectiveBC::handle_particle(Particle& p, const Surface& surf) const Direction u = surf.reflect(p.r(), p.u(), &p); u /= u.norm(); + // Handle the effects of the surface albedo on the particle's weight. + BoundaryCondition::handle_albedo(p, surf); + p.cross_reflective_bc(surf, u); } @@ -40,6 +55,9 @@ void WhiteBC::handle_particle(Particle& p, const Surface& surf) const Direction u = surf.diffuse_reflect(p.r(), p.u(), p.current_seed()); u /= u.norm(); + // Handle the effects of the surface albedo on the particle's weight. + BoundaryCondition::handle_albedo(p, surf); + p.cross_reflective_bc(surf, u); } @@ -130,6 +148,9 @@ void TranslationalPeriodicBC::handle_particle( "hitting a surface, but that surface is not recognized by the BC."); } + // Handle the effects of the surface albedo on the particle's weight. + BoundaryCondition::handle_albedo(p, surf); + // Pass the new location and surface to the particle. p.cross_periodic_bc(surf, new_r, p.u(), new_surface); } @@ -264,6 +285,9 @@ void RotationalPeriodicBC::handle_particle( Direction new_u = { cos_theta * u.x - sin_theta * u.y, sin_theta * u.x + cos_theta * u.y, u.z}; + // Handle the effects of the surface albedo on the particle's weight. + BoundaryCondition::handle_albedo(p, surf); + // Pass the new location, direction, and surface to the particle. p.cross_periodic_bc(surf, new_r, new_u, new_surface); } diff --git a/src/cell.cpp b/src/cell.cpp index c96abf39bef..a46f05687eb 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -784,7 +784,7 @@ std::string Region::str() const //============================================================================== std::pair Region::distance( - Position r, Direction u, int32_t on_surface, Particle* p) const + Position r, Direction u, int32_t on_surface) const { double min_dist {INFTY}; int32_t i_surf {std::numeric_limits::max()}; @@ -1020,19 +1020,41 @@ void read_cells(pugi::xml_node node) void populate_universes() { + // Used to map universe index to the index of an implicit complement cell for + // DAGMC universes + std::unordered_map implicit_comp_cells; + // Populate the Universe vector and map. - for (int i = 0; i < model::cells.size(); i++) { - int32_t uid = model::cells[i]->universe_; + for (int index_cell = 0; index_cell < model::cells.size(); index_cell++) { + int32_t uid = model::cells[index_cell]->universe_; auto it = model::universe_map.find(uid); if (it == model::universe_map.end()) { model::universes.push_back(make_unique()); model::universes.back()->id_ = uid; - model::universes.back()->cells_.push_back(i); + model::universes.back()->cells_.push_back(index_cell); model::universe_map[uid] = model::universes.size() - 1; } else { - model::universes[it->second]->cells_.push_back(i); +#ifdef DAGMC + // Skip implicit complement cells for now + Universe* univ = model::universes[it->second].get(); + DAGUniverse* dag_univ = dynamic_cast(univ); + if (dag_univ && (dag_univ->implicit_complement_idx() == index_cell)) { + implicit_comp_cells[it->second] = index_cell; + continue; + } +#endif + + model::universes[it->second]->cells_.push_back(index_cell); } } + + // Add DAGUniverse implicit complement cells last + for (const auto& it : implicit_comp_cells) { + int index_univ = it.first; + int index_cell = it.second; + model::universes[index_univ]->cells_.push_back(index_cell); + } + model::universes.shrink_to_fit(); } @@ -1250,6 +1272,9 @@ struct ParentCellStack { //! compute an instance for the provided distribcell index int32_t compute_instance(int32_t distribcell_index) const { + if (distribcell_index == C_NONE) + return 0; + int32_t instance = 0; for (const auto& parent_cell : this->parent_cells_) { auto& cell = model::cells[parent_cell.cell_index]; @@ -1279,14 +1304,15 @@ vector Cell::find_parent_cells( { // create a temporary particle - Particle dummy_particle {}; + GeometryState dummy_particle {}; dummy_particle.r() = r; dummy_particle.u() = {0., 0., 1.}; return find_parent_cells(instance, dummy_particle); } -vector Cell::find_parent_cells(int32_t instance, Particle& p) const +vector Cell::find_parent_cells( + int32_t instance, GeometryState& p) const { // look up the particle's location exhaustive_find_cell(p); diff --git a/src/dagmc.cpp b/src/dagmc.cpp index e245d4475da..2f2502f6ea1 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -11,7 +11,7 @@ #include "openmc/settings.h" #include "openmc/string_utils.h" -#ifdef DAGMC +#ifdef UWUW #include "uwuw.hpp" #endif #include @@ -29,6 +29,12 @@ const bool DAGMC_ENABLED = true; const bool DAGMC_ENABLED = false; #endif +#ifdef UWUW +const bool UWUW_ENABLED = true; +#else +const bool UWUW_ENABLED = false; +#endif + } // namespace openmc #ifdef DAGMC @@ -49,8 +55,8 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) if (check_for_node(node, "filename")) { filename_ = get_node_value(node, "filename"); - if (!file_exists(filename_)) { - fatal_error(fmt::format("DAGMC file '{}' could not be found", filename_)); + if (!starts_with(filename_, "/")) { + filename_ = dir_name(settings::path_input) + filename_; } } else { fatal_error("Must specify a file for the DAGMC universe"); @@ -85,7 +91,6 @@ DAGUniverse::DAGUniverse(std::shared_ptr dagmc_ptr, { set_id(); init_metadata(); - read_uwuw_materials(); init_geometry(); } @@ -111,8 +116,6 @@ void DAGUniverse::initialize() init_metadata(); - read_uwuw_materials(); - init_geometry(); } @@ -123,7 +126,6 @@ void DAGUniverse::init_dagmc() dagmc_instance_ = std::make_shared(); // load the DAGMC geometry - filename_ = settings::path_input + filename_; if (!file_exists(filename_)) { fatal_error("Geometry DAGMC file '" + filename_ + "' does not exist!"); } @@ -210,20 +212,7 @@ void DAGUniverse::init_geometry() c->material_.push_back(MATERIAL_VOID); } else { if (uses_uwuw()) { - // lookup material in uwuw if present - std::string uwuw_mat = - dmd_ptr->volume_material_property_data_eh[vol_handle]; - if (uwuw_->material_library.count(uwuw_mat) != 0) { - // Note: material numbers are set by UWUW - int mat_number = uwuw_->material_library.get_material(uwuw_mat) - .metadata["mat_number"] - .asInt(); - c->material_.push_back(mat_number); - } else { - fatal_error(fmt::format("Material with value '{}' not found in the " - "UWUW material library", - mat_str)); - } + uwuw_assign_material(vol_handle, c); } else { legacy_assign_material(mat_str, c); } @@ -281,6 +270,10 @@ void DAGUniverse::init_geometry() s->id_ = adjust_geometry_ids_ ? next_surf_id++ : dagmc_instance_->id_by_index(2, i + 1); + // set surface source attribute if needed + if (contains(settings::source_write_surf_id, s->id_)) + s->surf_source_ = true; + // set BCs std::string bc_value = dmd_ptr->get_surface_property("boundary", surf_handle); @@ -289,10 +282,10 @@ void DAGUniverse::init_geometry() bc_value == "transmission") { // set to transmission by default (nullptr) } else if (bc_value == "vacuum") { - s->bc_ = std::make_shared(); + s->bc_ = make_unique(); } else if (bc_value == "reflective" || bc_value == "reflect" || bc_value == "reflecting") { - s->bc_ = std::make_shared(); + s->bc_ = make_unique(); } else if (bc_value == "periodic") { fatal_error("Periodic boundary condition not supported in DAGMC."); } else { @@ -310,7 +303,7 @@ void DAGUniverse::init_geometry() // if this surface belongs to the graveyard if (graveyard && parent_vols.find(graveyard) != parent_vols.end()) { // set graveyard surface BC's to vacuum - s->bc_ = std::make_shared(); + s->bc_ = make_unique(); } // add to global array and map @@ -404,7 +397,7 @@ int32_t DAGUniverse::implicit_complement_idx() const return cell_idx_offset_ + dagmc_instance_->index_by_handle(ic) - 1; } -bool DAGUniverse::find_cell(Particle& p) const +bool DAGUniverse::find_cell(GeometryState& p) const { // if the particle isn't in any of the other DagMC // cells, place it in the implicit complement @@ -436,11 +429,16 @@ void DAGUniverse::to_hdf5(hid_t universes_group) const bool DAGUniverse::uses_uwuw() const { +#ifdef UWUW return uwuw_ && !uwuw_->material_library.empty(); +#else + return false; +#endif // UWUW } std::string DAGUniverse::get_uwuw_materials_xml() const { +#ifdef UWUW if (!uses_uwuw()) { throw std::runtime_error("This DAGMC Universe does not use UWUW materials"); } @@ -458,10 +456,14 @@ std::string DAGUniverse::get_uwuw_materials_xml() const ss << ""; return ss.str(); +#else + fatal_error("DAGMC was not configured with UWUW."); +#endif // UWUW } void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const { +#ifdef UWUW if (!uses_uwuw()) { throw std::runtime_error( "This DAGMC universe does not use UWUW materials."); @@ -472,6 +474,9 @@ void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const std::ofstream mats_xml(outfile); mats_xml << xml_str; mats_xml.close(); +#else + fatal_error("DAGMC was not configured with UWUW."); +#endif } void DAGUniverse::legacy_assign_material( @@ -533,6 +538,7 @@ void DAGUniverse::legacy_assign_material( void DAGUniverse::read_uwuw_materials() { +#ifdef UWUW // If no filename was provided, don't read UWUW materials if (filename_ == "") return; @@ -570,8 +576,35 @@ void DAGUniverse::read_uwuw_materials() for (pugi::xml_node material_node : root.children("material")) { model::materials.push_back(std::make_unique(material_node)); } +#else + fatal_error("DAGMC was not configured with UWUW."); +#endif } +void DAGUniverse::uwuw_assign_material( + moab::EntityHandle vol_handle, std::unique_ptr& c) const +{ +#ifdef UWUW + // read materials from uwuw material file + read_uwuw_materials(); + + // lookup material in uwuw if present + std::string uwuw_mat = dmd_ptr->volume_material_property_data_eh[vol_handle]; + if (uwuw_->material_library.count(uwuw_mat) != 0) { + // Note: material numbers are set by UWUW + int mat_number = uwuw_->material_library.get_material(uwuw_mat) + .metadata["mat_number"] + .asInt(); + c->material_.push_back(mat_number); + } else { + fatal_error(fmt::format("Material with value '{}' not found in the " + "UWUW material library", + mat_str)); + } +#else + fatal_error("DAGMC was not configured with UWUW."); +#endif +} //============================================================================== // DAGMC Cell implementation //============================================================================== @@ -583,9 +616,8 @@ DAGCell::DAGCell(std::shared_ptr dag_ptr, int32_t dag_idx) }; std::pair DAGCell::distance( - Position r, Direction u, int32_t on_surface, Particle* p) const + Position r, Direction u, int32_t on_surface, GeometryState* p) const { - Expects(p); // if we've changed direction or we're not on a surface, // reset the history and update last direction if (u != p->last_dir()) { @@ -635,10 +667,9 @@ std::pair DAGCell::distance( p->material() == MATERIAL_VOID ? "-1 (VOID)" : std::to_string(model::materials[p->material()]->id()); - auto lost_particle_msg = fmt::format( + p->mark_as_lost(fmt::format( "No intersection found with DAGMC cell {}, filled with material {}", id_, - material_id); - p->mark_as_lost(lost_particle_msg); + material_id)); } return {dist, surf_idx}; @@ -723,7 +754,7 @@ Direction DAGSurface::normal(Position r) const return dir; } -Direction DAGSurface::reflect(Position r, Direction u, Particle* p) const +Direction DAGSurface::reflect(Position r, Direction u, GeometryState* p) const { Expects(p); p->history().reset_to_last_intersection(); diff --git a/src/distribution.cpp b/src/distribution.cpp index 9c76147ee27..3026630b335 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -27,17 +27,17 @@ DiscreteIndex::DiscreteIndex(pugi::xml_node node) auto params = get_node_array(node, "parameters"); std::size_t n = params.size() / 2; - assign(params.data() + n, n); + assign({params.data() + n, n}); } -DiscreteIndex::DiscreteIndex(const double* p, int n) +DiscreteIndex::DiscreteIndex(gsl::span p) { - assign(p, n); + assign(p); } -void DiscreteIndex::assign(const double* p, int n) +void DiscreteIndex::assign(gsl::span p) { - prob_.assign(p, p + n); + prob_.assign(p.begin(), p.end()); this->init_alias(); } @@ -126,7 +126,7 @@ Discrete::Discrete(pugi::xml_node node) : di_(node) x_.assign(params.begin(), params.begin() + n); } -Discrete::Discrete(const double* x, const double* p, int n) : di_(p, n) +Discrete::Discrete(const double* x, const double* p, size_t n) : di_({p, n}) { x_.assign(x, x + n); diff --git a/src/distribution_multi.cpp b/src/distribution_multi.cpp index 0735f0994ed..b7b3efe5268 100644 --- a/src/distribution_multi.cpp +++ b/src/distribution_multi.cpp @@ -12,6 +12,25 @@ namespace openmc { +unique_ptr UnitSphereDistribution::create( + pugi::xml_node node) +{ + // Check for type of angular distribution + std::string type; + if (check_for_node(node, "type")) + type = get_node_value(node, "type", true, true); + if (type == "isotropic") { + return UPtrAngle {new Isotropic()}; + } else if (type == "monodirectional") { + return UPtrAngle {new Monodirectional(node)}; + } else if (type == "mu-phi") { + return UPtrAngle {new PolarAzimuthal(node)}; + } else { + fatal_error(fmt::format( + "Invalid angular distribution for external source: {}", type)); + } +} + //============================================================================== // UnitSphereDistribution implementation //============================================================================== diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index e1a4c20c21c..8f34ea6b936 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -8,6 +8,36 @@ namespace openmc { +//============================================================================== +// SpatialDistribution implementation +//============================================================================== + +unique_ptr SpatialDistribution::create(pugi::xml_node node) +{ + // Check for type of spatial distribution and read + std::string type; + if (check_for_node(node, "type")) + type = get_node_value(node, "type", true, true); + if (type == "cartesian") { + return UPtrSpace {new CartesianIndependent(node)}; + } else if (type == "cylindrical") { + return UPtrSpace {new CylindricalIndependent(node)}; + } else if (type == "spherical") { + return UPtrSpace {new SphericalIndependent(node)}; + } else if (type == "mesh") { + return UPtrSpace {new MeshSpatial(node)}; + } else if (type == "box") { + return UPtrSpace {new SpatialBox(node)}; + } else if (type == "fission") { + return UPtrSpace {new SpatialBox(node, true)}; + } else if (type == "point") { + return UPtrSpace {new SpatialPoint(node)}; + } else { + fatal_error(fmt::format( + "Invalid spatial distribution for external source: {}", type)); + } +} + //============================================================================== // CartesianIndependent implementation //============================================================================== @@ -189,27 +219,23 @@ Position SphericalIndependent::sample(uint64_t* seed) const MeshSpatial::MeshSpatial(pugi::xml_node node) { + + if (get_node_value(node, "type", true, true) != "mesh") { + fatal_error(fmt::format( + "Incorrect spatial type '{}' for a MeshSpatial distribution")); + } + // No in-tet distributions implemented, could include distributions for the // barycentric coords Read in unstructured mesh from mesh_id value int32_t mesh_id = std::stoi(get_node_value(node, "mesh_id")); // Get pointer to spatial distribution mesh_idx_ = model::mesh_map.at(mesh_id); - auto mesh_ptr = - dynamic_cast(model::meshes.at(mesh_idx_).get()); - if (!mesh_ptr) { - fatal_error("Only unstructured mesh is supported for source sampling."); - } + const auto mesh_ptr = model::meshes.at(mesh_idx_).get(); - // ensure that the unstructured mesh contains only linear tets - for (int bin = 0; bin < mesh_ptr->n_bins(); bin++) { - if (mesh_ptr->element_type(bin) != ElementType::LINEAR_TET) { - fatal_error( - "Mesh specified for source must contain only linear tetrahedra."); - } - } + check_element_types(); - int32_t n_bins = this->n_sources(); + size_t n_bins = this->n_sources(); std::vector strengths(n_bins, 1.0); // Create cdfs for sampling for an element over a mesh @@ -227,18 +253,44 @@ MeshSpatial::MeshSpatial(pugi::xml_node node) if (get_node_value_bool(node, "volume_normalized")) { for (int i = 0; i < n_bins; i++) { - strengths[i] *= mesh()->volume(i); + strengths[i] *= this->mesh()->volume(i); } } - elem_idx_dist_.assign(strengths.data(), n_bins); + elem_idx_dist_.assign(strengths); } -Position MeshSpatial::sample(uint64_t* seed) const +MeshSpatial::MeshSpatial(int32_t mesh_idx, gsl::span strengths) + : mesh_idx_(mesh_idx) +{ + check_element_types(); + elem_idx_dist_.assign(strengths); +} + +void MeshSpatial::check_element_types() const +{ + const auto umesh_ptr = dynamic_cast(this->mesh()); + if (umesh_ptr) { + // ensure that the unstructured mesh contains only linear tets + for (int bin = 0; bin < umesh_ptr->n_bins(); bin++) { + if (umesh_ptr->element_type(bin) != ElementType::LINEAR_TET) { + fatal_error( + "Mesh specified for source must contain only linear tetrahedra."); + } + } + } +} + +std::pair MeshSpatial::sample_mesh(uint64_t* seed) const { - // Sample over the CDF defined in initialization above + // Sample the CDF defined in initialization above int32_t elem_idx = elem_idx_dist_.sample(seed); - return mesh()->sample(seed, elem_idx); + return {elem_idx, mesh()->sample_element(elem_idx, seed)}; +} + +Position MeshSpatial::sample(uint64_t* seed) const +{ + return this->sample_mesh(seed).second; } //============================================================================== diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index 5584210a1fc..d5410094b18 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -57,16 +57,28 @@ void calculate_generation_keff() double keff_reduced; #ifdef OPENMC_MPI - // Combine values across all processors - MPI_Allreduce(&simulation::keff_generation, &keff_reduced, 1, MPI_DOUBLE, - MPI_SUM, mpi::intracomm); + if (settings::solver_type != SolverType::RANDOM_RAY) { + // Combine values across all processors + MPI_Allreduce(&simulation::keff_generation, &keff_reduced, 1, MPI_DOUBLE, + MPI_SUM, mpi::intracomm); + } else { + // If using random ray, MPI parallelism is provided by domain replication. + // As such, all fluxes will be reduced at the end of each transport sweep, + // such that all ranks have identical scalar flux vectors, and will all + // independently compute the same value of k. Thus, there is no need to + // perform any additional MPI reduction here. + keff_reduced = simulation::keff_generation; + } #else keff_reduced = simulation::keff_generation; #endif // Normalize single batch estimate of k // TODO: This should be normalized by total_weight, not by n_particles - keff_reduced /= settings::n_particles; + if (settings::solver_type != SolverType::RANDOM_RAY) { + keff_reduced /= settings::n_particles; + } + simulation::k_generation.push_back(keff_reduced); } @@ -370,7 +382,8 @@ int openmc_get_keff(double* k_combined) // Special case for n <=3. Notice that at the end, // there is a N-3 term in a denominator. - if (simulation::n_realizations <= 3) { + if (simulation::n_realizations <= 3 || + settings::solver_type == SolverType::RANDOM_RAY) { k_combined[0] = simulation::keff; k_combined[1] = simulation::keff_std; if (simulation::n_realizations <= 1) { diff --git a/src/event.cpp b/src/event.cpp index e9490a77f13..ae914c8be34 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -51,7 +51,7 @@ void dispatch_xs_event(int64_t buffer_idx) { Particle& p = simulation::particles[buffer_idx]; if (p.material() == MATERIAL_VOID || - !model::materials[p.material()]->fissionable_) { + !model::materials[p.material()]->fissionable()) { simulation::calculate_nonfuel_xs_queue.thread_safe_append({p, buffer_idx}); } else { simulation::calculate_fuel_xs_queue.thread_safe_append({p, buffer_idx}); diff --git a/src/file_utils.cpp b/src/file_utils.cpp index 8a078fe18b3..197c9767cb0 100644 --- a/src/file_utils.cpp +++ b/src/file_utils.cpp @@ -26,6 +26,12 @@ bool file_exists(const std::string& filename) return s.good(); } +std::string dir_name(const std::string& filename) +{ + size_t pos = filename.find_last_of("\\/"); + return (std::string::npos == pos) ? "" : filename.substr(0, pos + 1); +} + std::string get_file_extension(const std::string& filename) { // try our best to work on windows... diff --git a/src/finalize.cpp b/src/finalize.cpp index b5b23b9d5bc..26efc9723a5 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -63,6 +63,9 @@ using namespace openmc; int openmc_finalize() { + if (simulation::initialized) + openmc_simulation_finalize(); + // Clear results openmc_reset(); @@ -85,17 +88,28 @@ int openmc_finalize() settings::legendre_to_tabular = true; settings::legendre_to_tabular_points = -1; settings::material_cell_offsets = true; + settings::max_lost_particles = 10; + settings::max_order = 0; settings::max_particles_in_flight = 100000; + settings::max_particle_events = 1000000; settings::max_splits = 1000; settings::max_tracks = 1000; settings::max_write_lost_particles = -1; + settings::n_log_bins = 8000; settings::n_inactive = 0; settings::n_particles = -1; settings::output_summary = true; settings::output_tallies = true; settings::particle_restart_run = false; + settings::path_cross_sections.clear(); + settings::path_input.clear(); + settings::path_output.clear(); + settings::path_particle_restart.clear(); + settings::path_sourcepoint.clear(); + settings::path_statepoint.clear(); settings::photon_transport = false; settings::reduce_tallies = true; + settings::rel_max_lost_particles = 1.0e-6; settings::res_scat_on = false; settings::res_scat_method = ResScatMethod::rvs; settings::res_scat_energy_min = 0.01; @@ -120,6 +134,7 @@ int openmc_finalize() settings::verbosity = 7; settings::weight_cutoff = 0.25; settings::weight_survive = 1.0; + settings::weight_windows_file.clear(); settings::weight_windows_on = false; settings::write_all_tracks = false; settings::write_initial_source = false; diff --git a/src/geometry.cpp b/src/geometry.cpp index 47152e17295..445b19faac1 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -32,7 +32,7 @@ vector overlap_check_count; // Non-member functions //============================================================================== -bool check_cell_overlap(Particle& p, bool error) +bool check_cell_overlap(GeometryState& p, bool error) { int n_coord = p.n_coord(); @@ -63,7 +63,7 @@ bool check_cell_overlap(Particle& p, bool error) //============================================================================== -int cell_instance_at_level(const Particle& p, int level) +int cell_instance_at_level(const GeometryState& p, int level) { // throw error if the requested level is too deep for the geometry if (level > model::n_coord_levels) { @@ -99,7 +99,8 @@ int cell_instance_at_level(const Particle& p, int level) //============================================================================== -bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) +bool find_cell_inner( + GeometryState& p, const NeighborList* neighbor_list, bool verbose) { // Find which cell of this universe the particle is in. Use the neighbor list // to shorten the search if one was provided. @@ -156,7 +157,7 @@ bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) i_cell = p.lowest_coord().cell; // Announce the cell that the particle is entering. - if (found && (settings::verbosity >= 10 || p.trace())) { + if (found && verbose) { auto msg = fmt::format(" Entering cell {}", model::cells[i_cell]->id_); write_message(msg, 1); } @@ -173,17 +174,9 @@ bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) // Set the material and temperature. p.material_last() = p.material(); - if (c.material_.size() > 1) { - p.material() = c.material_[p.cell_instance()]; - } else { - p.material() = c.material_[0]; - } + p.material() = c.material(p.cell_instance()); p.sqrtkT_last() = p.sqrtkT(); - if (c.sqrtkT_.size() > 1) { - p.sqrtkT() = c.sqrtkT_[p.cell_instance()]; - } else { - p.sqrtkT() = c.sqrtkT_[0]; - } + p.sqrtkT() = c.sqrtkT(p.cell_instance()); return true; @@ -243,10 +236,9 @@ bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) if (lat.outer_ != NO_OUTER_UNIVERSE) { coord.universe = lat.outer_; } else { - warning(fmt::format("Particle {} is outside lattice {} but the " - "lattice has no defined outer universe.", + p.mark_as_lost(fmt::format( + "Particle {} left lattice {}, but it has no outer definition.", p.id(), lat.id_)); - return false; } } } @@ -259,7 +251,7 @@ bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) //============================================================================== -bool neighbor_list_find_cell(Particle& p) +bool neighbor_list_find_cell(GeometryState& p, bool verbose) { // Reset all the deeper coordinate levels. @@ -274,20 +266,20 @@ bool neighbor_list_find_cell(Particle& p) // Search for the particle in that cell's neighbor list. Return if we // found the particle. - bool found = find_cell_inner(p, &c.neighbors_); + bool found = find_cell_inner(p, &c.neighbors_, verbose); if (found) return found; // The particle could not be found in the neighbor list. Try searching all // cells in this universe, and update the neighbor list if we find a new // neighboring cell. - found = find_cell_inner(p, nullptr); + found = find_cell_inner(p, nullptr, verbose); if (found) c.neighbors_.push_back(p.coord(coord_lvl).cell); return found; } -bool exhaustive_find_cell(Particle& p) +bool exhaustive_find_cell(GeometryState& p, bool verbose) { int i_universe = p.lowest_coord().universe; if (i_universe == C_NONE) { @@ -299,17 +291,17 @@ bool exhaustive_find_cell(Particle& p) for (int i = p.n_coord(); i < model::n_coord_levels; i++) { p.coord(i).reset(); } - return find_cell_inner(p, nullptr); + return find_cell_inner(p, nullptr, verbose); } //============================================================================== -void cross_lattice(Particle& p, const BoundaryInfo& boundary) +void cross_lattice(GeometryState& p, const BoundaryInfo& boundary, bool verbose) { auto& coord {p.lowest_coord()}; auto& lat {*model::lattices[coord.lattice]}; - if (settings::verbosity >= 10 || p.trace()) { + if (verbose) { write_message( fmt::format(" Crossing lattice {}. Current position ({},{},{}). r={}", lat.id_, coord.lattice_i[0], coord.lattice_i[1], coord.lattice_i[2], @@ -336,10 +328,11 @@ void cross_lattice(Particle& p, const BoundaryInfo& boundary) // The particle is outside the lattice. Search for it from the base coords. p.n_coord() = 1; bool found = exhaustive_find_cell(p); - if (!found && p.alive()) { - p.mark_as_lost(fmt::format("Could not locate particle {} after " - "crossing a lattice boundary", - p.id())); + + if (!found) { + p.mark_as_lost(fmt::format("Particle {} could not be located after " + "crossing a boundary of lattice {}", + p.id(), lat.id_)); } } else { @@ -352,10 +345,10 @@ void cross_lattice(Particle& p, const BoundaryInfo& boundary) // this case, search for it from the base coords. p.n_coord() = 1; bool found = exhaustive_find_cell(p); - if (!found && p.alive()) { - p.mark_as_lost(fmt::format("Could not locate particle {} after " - "crossing a lattice boundary", - p.id())); + if (!found) { + p.mark_as_lost(fmt::format("Particle {} could not be located after " + "crossing a boundary of lattice {}", + p.id(), lat.id_)); } } } @@ -363,7 +356,7 @@ void cross_lattice(Particle& p, const BoundaryInfo& boundary) //============================================================================== -BoundaryInfo distance_to_boundary(Particle& p) +BoundaryInfo distance_to_boundary(GeometryState& p) { BoundaryInfo info; double d_lat = INFINITY; @@ -408,8 +401,9 @@ BoundaryInfo distance_to_boundary(Particle& p) level_lat_trans = lattice_distance.second; if (d_lat < 0) { - p.mark_as_lost(fmt::format( - "Particle {} had a negative distance to a lattice boundary", p.id())); + p.mark_as_lost(fmt::format("Particle {} had a negative distance " + "to a lattice boundary.", + p.id())); } } @@ -463,18 +457,19 @@ BoundaryInfo distance_to_boundary(Particle& p) extern "C" int openmc_find_cell( const double* xyz, int32_t* index, int32_t* instance) { - Particle p; + GeometryState geom_state; - p.r() = Position {xyz}; - p.u() = {0.0, 0.0, 1.0}; + geom_state.r() = Position {xyz}; + geom_state.u() = {0.0, 0.0, 1.0}; - if (!exhaustive_find_cell(p)) { - set_errmsg(fmt::format("Could not find cell at position {}.", p.r())); + if (!exhaustive_find_cell(geom_state)) { + set_errmsg( + fmt::format("Could not find cell at position {}.", geom_state.r())); return OPENMC_E_GEOMETRY; } - *index = p.lowest_coord().cell; - *instance = p.cell_instance(); + *index = geom_state.lowest_coord().cell; + *instance = geom_state.cell_instance(); return 0; } diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 98dfc12a763..10b239be838 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -539,11 +539,20 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, // The desired cell is the first cell that gives an offset smaller or // equal to the target offset. - if (temp_offset <= target_offset) + if (temp_offset <= target_offset - c.offset_[map]) break; } } + // if we get through the loop without finding an appropriate entry, throw + // an error + if (cell_it == search_univ.cells_.crend()) { + fatal_error( + fmt::format("Failed to generate a text label for distribcell with ID {}." + "The current label is: '{}'", + model::cells[target_cell]->id_, path.str())); + } + // Add the cell to the path string. Cell& c = *model::cells[*cell_it]; path << "c" << c.id_ << "->"; @@ -561,11 +570,11 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, for (ReverseLatticeIter it = lat.rbegin(); it != lat.rend(); ++it) { int32_t indx = lat.universes_.size() * map + it.indx_; int32_t temp_offset = offset + lat.offsets_[indx]; - if (temp_offset <= target_offset) { + if (temp_offset <= target_offset - c.offset_[map]) { offset = temp_offset; path << "(" << lat.index_to_string(it.indx_) << ")->"; - path << distribcell_path_inner( - target_cell, map, target_offset, *model::universes[*it], offset); + path << distribcell_path_inner(target_cell, map, target_offset, + *model::universes[*it], offset + c.offset_[map]); return path.str(); } } diff --git a/src/initialize.cpp b/src/initialize.cpp index cafcd582846..cc1eac9cf35 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -23,6 +23,7 @@ #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" +#include "openmc/openmp_interface.h" #include "openmc/output.h" #include "openmc/plot.h" #include "openmc/random_lcg.h" @@ -63,13 +64,7 @@ int openmc_init(int argc, char* argv[], const void* intracomm) return err; #ifdef LIBMESH - -#ifdef _OPENMP - int n_threads = omp_get_max_threads(); -#else - int n_threads = 1; -#endif - + const int n_threads = num_threads(); // initialize libMesh if it hasn't been initialized already // (if initialized externally, the libmesh_init object needs to be provided // also) @@ -309,8 +304,9 @@ int parse_command_line(int argc, char* argv[]) settings::path_input)); } - // Add slash at end of directory if it isn't the - if (!ends_with(settings::path_input, "/")) { + // Add slash at end of directory if it isn't there + if (!ends_with(settings::path_input, "/") && + dir_exists(settings::path_input)) { settings::path_input += "/"; } } @@ -320,18 +316,11 @@ int parse_command_line(int argc, char* argv[]) bool read_model_xml() { - std::string model_filename = - settings::path_input.empty() ? "." : settings::path_input; - - // some string cleanup - // a trailing "/" is applied to path_input if it's specified, - // remove it for the first attempt at reading the input file - if (ends_with(model_filename, "/")) - model_filename.pop_back(); + std::string model_filename = settings::path_input; // if the current filename is a directory, append the default model filename - if (dir_exists(model_filename)) - model_filename += "/model.xml"; + if (model_filename.empty() || dir_exists(model_filename)) + model_filename += "model.xml"; // if this file doesn't exist, stop here if (!file_exists(model_filename)) diff --git a/src/main.cpp b/src/main.cpp index 02a850ead84..88251ac7232 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "openmc/error.h" #include "openmc/message_passing.h" #include "openmc/particle_restart.h" +#include "openmc/random_ray/random_ray_simulation.h" #include "openmc/settings.h" int main(int argc, char* argv[]) @@ -31,7 +32,15 @@ int main(int argc, char* argv[]) switch (settings::run_mode) { case RunMode::FIXED_SOURCE: case RunMode::EIGENVALUE: - err = openmc_run(); + switch (settings::solver_type) { + case SolverType::MONTE_CARLO: + err = openmc_run(); + break; + case SolverType::RANDOM_RAY: + openmc_run_random_ray(); + err = 0; + break; + } break; case RunMode::PLOTTING: err = openmc_plot_geometry(); diff --git a/src/material.cpp b/src/material.cpp index 7466f198163..aad7008e703 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -372,8 +372,8 @@ Material& Material::clone() mat->density_ = density_; mat->density_gpcc_ = density_gpcc_; mat->volume_ = volume_; - mat->fissionable_ = fissionable_; - mat->depletable_ = depletable_; + mat->fissionable() = fissionable_; + mat->depletable() = depletable_; mat->p0_ = p0_; mat->mat_nuclide_index_ = mat_nuclide_index_; mat->thermal_tables_ = thermal_tables_; @@ -1068,7 +1068,7 @@ void Material::to_hdf5(hid_t group) const { hid_t material_group = create_group(group, "material " + std::to_string(id_)); - write_attribute(material_group, "depletable", static_cast(depletable_)); + write_attribute(material_group, "depletable", static_cast(depletable())); if (volume_ > 0.0) { write_attribute(material_group, "volume", volume_); } @@ -1550,6 +1550,30 @@ extern "C" int openmc_material_set_volume(int32_t index, double volume) } } +extern "C" int openmc_material_get_depletable(int32_t index, bool* depletable) +{ + if (index < 0 || index >= model::materials.size()) { + set_errmsg("Index in materials array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + *depletable = model::materials[index]->depletable(); + + return 0; +} + +extern "C" int openmc_material_set_depletable(int32_t index, bool depletable) +{ + if (index < 0 || index >= model::materials.size()) { + set_errmsg("Index in materials array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + model::materials[index]->depletable() = depletable; + + return 0; +} + extern "C" int openmc_extend_materials( int32_t n, int32_t* index_start, int32_t* index_end) { diff --git a/src/mesh.cpp b/src/mesh.cpp index 12052bed62f..c57dd5dcc31 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -8,9 +8,7 @@ #ifdef OPENMC_MPI #include "mpi.h" #endif -#ifdef _OPENMP -#include -#endif + #include "xtensor/xbuilder.hpp" #include "xtensor/xeval.hpp" #include "xtensor/xmath.hpp" @@ -24,13 +22,20 @@ #include "openmc/container_util.h" #include "openmc/error.h" #include "openmc/file_utils.h" +#include "openmc/geometry.h" #include "openmc/hdf5_interface.h" +#include "openmc/material.h" #include "openmc/memory.h" #include "openmc/message_passing.h" +#include "openmc/openmp_interface.h" +#include "openmc/particle_data.h" +#include "openmc/plot.h" +#include "openmc/random_dist.h" #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/tally.h" +#include "openmc/volume_calc.h" #include "openmc/xml_interface.h" #ifdef LIBMESH @@ -39,6 +44,10 @@ #include "libmesh/numeric_vector.h" #endif +#ifdef DAGMC +#include "moab/FileOptions.hpp" +#endif + namespace openmc { //============================================================================== @@ -141,6 +150,92 @@ vector Mesh::volumes() const return volumes; } +int Mesh::material_volumes( + int n_sample, int bin, gsl::span result, uint64_t* seed) const +{ + vector materials; + vector hits; + +#pragma omp parallel + { + vector local_materials; + vector local_hits; + GeometryState geom; + +#pragma omp for + for (int i = 0; i < n_sample; ++i) { + // Get seed for i-th sample + uint64_t seed_i = future_seed(3 * i, *seed); + + // Sample position and set geometry state + geom.r() = this->sample_element(bin, &seed_i); + geom.u() = {1., 0., 0.}; + geom.n_coord() = 1; + + // If this location is not in the geometry at all, move on to next block + if (!exhaustive_find_cell(geom)) + continue; + + int i_material = geom.material(); + + // Check if this material was previously hit and if so, increment count + auto it = + std::find(local_materials.begin(), local_materials.end(), i_material); + if (it == local_materials.end()) { + local_materials.push_back(i_material); + local_hits.push_back(1); + } else { + local_hits[it - local_materials.begin()]++; + } + } // omp for + + // Reduce index/hits lists from each thread into a single copy + reduce_indices_hits(local_materials, local_hits, materials, hits); + } // omp parallel + + // Advance RNG seed + advance_prn_seed(3 * n_sample, seed); + + // Make sure span passed in is large enough + if (hits.size() > result.size()) { + return -1; + } + + // Convert hits to fractions + for (int i_mat = 0; i_mat < hits.size(); ++i_mat) { + double fraction = double(hits[i_mat]) / n_sample; + result[i_mat].material = materials[i_mat]; + result[i_mat].volume = fraction * this->volume(bin); + } + return hits.size(); +} + +vector Mesh::material_volumes( + int n_sample, int bin, uint64_t* seed) const +{ + // Create result vector with space for 8 pairs + vector result; + result.reserve(8); + + int size = -1; + while (true) { + // Get material volumes + size = this->material_volumes( + n_sample, bin, {result.data(), result.data() + result.capacity()}, seed); + + // If capacity was sufficient, resize the vector and return + if (size >= 0) { + result.resize(size); + break; + } + + // Otherwise, increase capacity of the vector + result.reserve(2 * result.capacity()); + } + + return result; +} + //============================================================================== // Structured Mesh implementation //============================================================================== @@ -165,6 +260,23 @@ xt::xtensor StructuredMesh::get_x_shape() const return xt::adapt(tmp_shape, {n_dimension_}); } +Position StructuredMesh::sample_element( + const MeshIndex& ijk, uint64_t* seed) const +{ + // lookup the lower/upper bounds for the mesh element + double x_min = negative_grid_boundary(ijk, 0); + double x_max = positive_grid_boundary(ijk, 0); + + double y_min = (n_dimension_ >= 2) ? negative_grid_boundary(ijk, 1) : 0.0; + double y_max = (n_dimension_ >= 2) ? positive_grid_boundary(ijk, 1) : 0.0; + + double z_min = (n_dimension_ == 3) ? negative_grid_boundary(ijk, 2) : 0.0; + double z_max = (n_dimension_ == 3) ? positive_grid_boundary(ijk, 2) : 0.0; + + return {x_min + (x_max - x_min) * prn(seed), + y_min + (y_max - y_min) * prn(seed), z_min + (z_max - z_min) * prn(seed)}; +} + //============================================================================== // Unstructured Mesh implementation //============================================================================== @@ -183,7 +295,6 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) // check if a length unit multiplier was specified if (check_for_node(node, "length_multiplier")) { length_multiplier_ = std::stod(get_node_value(node, "length_multiplier")); - specified_length_multiplier_ = true; } // get the filename of the unstructured mesh to load @@ -197,6 +308,10 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) "No filename supplied for unstructured mesh with ID: {}", id_)); } + if (check_for_node(node, "options")) { + options_ = get_node_value(node, "options"); + } + // check if mesh tally data should be written with // statepoint files if (check_for_node(node, "output")) { @@ -259,8 +374,11 @@ void UnstructuredMesh::to_hdf5(hid_t group) const write_dataset(mesh_group, "type", mesh_type); write_dataset(mesh_group, "filename", filename_); write_dataset(mesh_group, "library", this->library()); + if (!options_.empty()) { + write_attribute(mesh_group, "options", options_); + } - if (specified_length_multiplier_) + if (length_multiplier_ > 0.0) write_dataset(mesh_group, "length_multiplier", length_multiplier_); // write vertex coordinates @@ -320,9 +438,6 @@ void UnstructuredMesh::to_hdf5(hid_t group) const void UnstructuredMesh::set_length_multiplier(double length_multiplier) { length_multiplier_ = length_multiplier; - - if (length_multiplier_ != 1.0) - specified_length_multiplier_ = true; } ElementType UnstructuredMesh::element_type(int bin) const @@ -381,11 +496,6 @@ StructuredMesh::MeshIndex StructuredMesh::get_indices_from_bin(int bin) const return ijk; } -Position StructuredMesh::sample(uint64_t* seed, int32_t bin) const -{ - fatal_error("Position sampling on structured meshes is not yet implemented"); -} - int StructuredMesh::get_bin(Position r) const { // Determine indices @@ -479,7 +589,7 @@ void StructuredMesh::raytrace_mesh( // Compute the length of the entire track. double total_distance = (r1 - r0).norm(); - if (total_distance == 0.0) + if (total_distance == 0.0 && settings::solver_type != SolverType::RANDOM_RAY) return; const int n = n_dimension_; @@ -724,7 +834,7 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} fatal_error("Must specify either or on a mesh."); } - // Set volume fraction + // Set material volumes volume_frac_ = 1.0 / xt::prod(shape)(); element_volume_ = 1.0; @@ -1023,7 +1133,7 @@ double RectilinearMesh::volume(const MeshIndex& ijk) const double vol {1.0}; for (int i = 0; i < n_dimension_; i++) { - vol *= grid_[i][ijk[i] + 1] - grid_[i][ijk[i]]; + vol *= grid_[i][ijk[i]] - grid_[i][ijk[i] - 1]; } return vol; } @@ -1077,6 +1187,30 @@ StructuredMesh::MeshIndex CylindricalMesh::get_indices( return idx; } +Position CylindricalMesh::sample_element( + const MeshIndex& ijk, uint64_t* seed) const +{ + double r_min = this->r(ijk[0] - 1); + double r_max = this->r(ijk[0]); + + double phi_min = this->phi(ijk[1] - 1); + double phi_max = this->phi(ijk[1]); + + double z_min = this->z(ijk[2] - 1); + double z_max = this->z(ijk[2]); + + double r_min_sq = r_min * r_min; + double r_max_sq = r_max * r_max; + double r = std::sqrt(uniform_distribution(r_min_sq, r_max_sq, seed)); + double phi = uniform_distribution(phi_min, phi_max, seed); + double z = uniform_distribution(z_min, z_max, seed); + + double x = r * std::cos(phi); + double y = r * std::sin(phi); + + return origin_ + Position(x, y, z); +} + double CylindricalMesh::find_r_crossing( const Position& r, const Direction& u, double l, int shell) const { @@ -1338,6 +1472,33 @@ StructuredMesh::MeshIndex SphericalMesh::get_indices( return idx; } +Position SphericalMesh::sample_element( + const MeshIndex& ijk, uint64_t* seed) const +{ + double r_min = this->r(ijk[0] - 1); + double r_max = this->r(ijk[0]); + + double theta_min = this->theta(ijk[1] - 1); + double theta_max = this->theta(ijk[1]); + + double phi_min = this->phi(ijk[2] - 1); + double phi_max = this->phi(ijk[2]); + + double cos_theta = uniform_distribution(theta_min, theta_max, seed); + double sin_theta = std::sin(std::acos(cos_theta)); + double phi = uniform_distribution(phi_min, phi_max, seed); + double r_min_cub = std::pow(r_min, 3); + double r_max_cub = std::pow(r_max, 3); + // might be faster to do rejection here? + double r = std::cbrt(uniform_distribution(r_min_cub, r_max_cub, seed)); + + double x = r * std::cos(phi) * sin_theta; + double y = r * std::sin(phi) * sin_theta; + double z = r * cos_theta; + + return origin_ + Position(x, y, z); +} + double SphericalMesh::find_r_crossing( const Position& r, const Direction& u, double l, int shell) const { @@ -1718,6 +1879,101 @@ extern "C" int openmc_mesh_set_id(int32_t index, int32_t id) return 0; } +//! Get the number of elements in a mesh +extern "C" int openmc_mesh_get_n_elements(int32_t index, size_t* n) +{ + if (int err = check_mesh(index)) + return err; + *n = model::meshes[index]->n_bins(); + return 0; +} + +//! Get the volume of each element in the mesh +extern "C" int openmc_mesh_get_volumes(int32_t index, double* volumes) +{ + if (int err = check_mesh(index)) + return err; + for (int i = 0; i < model::meshes[index]->n_bins(); ++i) { + volumes[i] = model::meshes[index]->volume(i); + } + return 0; +} + +extern "C" int openmc_mesh_material_volumes(int32_t index, int n_sample, + int bin, int result_size, void* result, int* hits, uint64_t* seed) +{ + auto result_ = reinterpret_cast(result); + if (!result_) { + set_errmsg("Invalid result pointer passed to openmc_mesh_material_volumes"); + return OPENMC_E_INVALID_ARGUMENT; + } + + if (int err = check_mesh(index)) + return err; + + int n = model::meshes[index]->material_volumes( + n_sample, bin, {result_, result_ + result_size}, seed); + *hits = n; + return (n == -1) ? OPENMC_E_ALLOCATE : 0; +} + +extern "C" int openmc_mesh_get_plot_bins(int32_t index, Position origin, + Position width, int basis, int* pixels, int32_t* data) +{ + if (int err = check_mesh(index)) + return err; + const auto& mesh = model::meshes[index].get(); + + int pixel_width = pixels[0]; + int pixel_height = pixels[1]; + + // get pixel size + double in_pixel = (width[0]) / static_cast(pixel_width); + double out_pixel = (width[1]) / static_cast(pixel_height); + + // setup basis indices and initial position centered on pixel + int in_i, out_i; + Position xyz = origin; + enum class PlotBasis { xy = 1, xz = 2, yz = 3 }; + PlotBasis basis_enum = static_cast(basis); + switch (basis_enum) { + case PlotBasis::xy: + in_i = 0; + out_i = 1; + break; + case PlotBasis::xz: + in_i = 0; + out_i = 2; + break; + case PlotBasis::yz: + in_i = 1; + out_i = 2; + break; + default: + UNREACHABLE(); + } + + // set initial position + xyz[in_i] = origin[in_i] - width[0] / 2. + in_pixel / 2.; + xyz[out_i] = origin[out_i] + width[1] / 2. - out_pixel / 2.; + +#pragma omp parallel + { + Position r = xyz; + +#pragma omp for + for (int y = 0; y < pixel_height; y++) { + r[out_i] = xyz[out_i] - out_pixel * y; + for (int x = 0; x < pixel_width; x++) { + r[in_i] = xyz[in_i] + in_pixel * x; + data[pixel_width * y + x] = mesh->get_bin(r); + } + } + } + + return 0; +} + //! Get the dimension of a regular mesh extern "C" int openmc_regular_mesh_get_dimension( int32_t index, int** dims, int* n) @@ -1772,6 +2028,11 @@ extern "C" int openmc_regular_mesh_set_params( return err; RegularMesh* m = dynamic_cast(model::meshes[index].get()); + if (m->n_dimension_ == -1) { + set_errmsg("Need to set mesh dimension before setting parameters."); + return OPENMC_E_UNASSIGNED; + } + vector shape = {static_cast(n)}; if (ll && ur) { m->lower_left_ = xt::adapt(ll, n, xt::no_ownership(), shape); @@ -1790,6 +2051,16 @@ extern "C" int openmc_regular_mesh_set_params( return OPENMC_E_INVALID_ARGUMENT; } + // Set material volumes + + // TODO: incorporate this into method in RegularMesh that can be called from + // here and from constructor + m->volume_frac_ = 1.0 / xt::prod(m->get_x_shape())(); + m->element_volume_ = 1.0; + for (int i = 0; i < m->n_dimension_; i++) { + m->element_volume_ *= m->width_[i]; + } + return 0; } @@ -1967,7 +2238,7 @@ void MOABMesh::initialize() fatal_error("Failed to add tetrahedra to an entity set."); } - if (specified_length_multiplier_) { + if (length_multiplier_ > 0.0) { // get the connectivity of all tets moab::Range adj; rval = mbi_->get_adjacencies(ehs_, 0, true, adj, moab::Interface::UNION); @@ -1994,6 +2265,13 @@ void MOABMesh::initialize() } } } +} + +void MOABMesh::prepare_for_tallies() +{ + // if the KDTree has already been constructed, do nothing + if (kdtree_) + return; // build acceleration data structures compute_barycentric_data(ehs_); @@ -2020,6 +2298,7 @@ void MOABMesh::build_kdtree(const moab::Range& all_tets) { moab::Range all_tris; int adj_dim = 2; + write_message("Getting tet adjacencies...", 7); moab::ErrorCode rval = mbi_->get_adjacencies( all_tets, adj_dim, true, all_tris, moab::Interface::UNION); if (rval != moab::MB_SUCCESS) { @@ -2038,10 +2317,20 @@ void MOABMesh::build_kdtree(const moab::Range& all_tets) all_tets_and_tris.merge(all_tris); // create a kd-tree instance + write_message("Building adaptive k-d tree for tet mesh...", 7); kdtree_ = make_unique(mbi_.get()); - // build the tree - rval = kdtree_->build_tree(all_tets_and_tris, &kdtree_root_); + // Determine what options to use + std::ostringstream options_stream; + if (options_.empty()) { + options_stream << "MAX_DEPTH=20;PLANE_SET=2;"; + } else { + options_stream << options_; + } + moab::FileOptions file_opts(options_stream.str().c_str()); + + // Build the k-d tree + rval = kdtree_->build_tree(all_tets_and_tris, &kdtree_root_, &file_opts); if (rval != moab::MB_SUCCESS) { fatal_error("Failed to construct KDTree for the " "unstructured mesh file: " + @@ -2185,7 +2474,7 @@ std::string MOABMesh::library() const } // Sample position within a tet for MOAB type tets -Position MOABMesh::sample(uint64_t* seed, int32_t bin) const +Position MOABMesh::sample_element(int32_t bin, uint64_t* seed) const { moab::EntityHandle tet_ent = get_ent_handle_from_bin(bin); @@ -2628,7 +2917,7 @@ void LibMesh::initialize() // assuming that unstructured meshes used in OpenMC are 3D n_dimension_ = 3; - if (specified_length_multiplier_) { + if (length_multiplier_ > 0.0) { libMesh::MeshTools::Modification::scale(*m_, length_multiplier_); } // if OpenMC is managing the libMesh::MeshBase instance, prepare the mesh. @@ -2651,13 +2940,7 @@ void LibMesh::initialize() libMesh::ExplicitSystem& eq_sys = equation_systems_->add_system(eq_system_name_); -#ifdef _OPENMP - int n_threads = omp_get_max_threads(); -#else - int n_threads = 1; -#endif - - for (int i = 0; i < n_threads; i++) { + for (int i = 0; i < num_threads(); i++) { pl_.emplace_back(m_->sub_point_locator()); pl_.back()->set_contains_point_tol(FP_COINCIDENT); pl_.back()->enable_out_of_mesh_mode(); @@ -2672,7 +2955,7 @@ void LibMesh::initialize() } // Sample position within a tet for LibMesh type tets -Position LibMesh::sample(uint64_t* seed, int32_t bin) const +Position LibMesh::sample_element(int32_t bin, uint64_t* seed) const { const auto& elem = get_element_from_bin(bin); // Get tet vertex coordinates from LibMesh @@ -2832,13 +3115,7 @@ int LibMesh::get_bin(Position r) const return -1; } -#ifdef _OPENMP - int thread_num = omp_get_thread_num(); -#else - int thread_num = 0; -#endif - - const auto& point_locator = pl_.at(thread_num); + const auto& point_locator = pl_.at(thread_num()); const auto elem_ptr = (*point_locator)(p); return elem_ptr ? get_bin_from_element(elem_ptr) : -1; diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index 77085d57c83..d54211ecaf1 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -284,7 +284,7 @@ void mark_fissionable_mgxs_materials() for (const auto& mat : model::materials) { for (int i_nuc : mat->nuclide_) { if (data::mg.nuclides_[i_nuc].fissionable) { - mat->fissionable_ = true; + mat->fissionable() = true; } } } diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 6b70ad9e323..91adc077799 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -62,6 +62,10 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) read_attribute(group, "metastable", metastable_); read_attribute(group, "atomic_weight_ratio", awr_); + if (settings::run_mode == RunMode::VOLUME) { + return; + } + // Determine temperatures available hid_t kT_group = open_group(group, "kTs"); auto dset_names = dataset_names(kT_group); @@ -176,14 +180,14 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) if (!contains(temps_to_read, temps_available.front())) { temps_to_read.push_back(std::round(temps_available.front())); } - break; + continue; } if (std::abs(T_desired - temps_available.back()) <= settings::temperature_tolerance) { if (!contains(temps_to_read, temps_available.back())) { temps_to_read.push_back(std::round(temps_available.back())); } - break; + continue; } fatal_error( "Nuclear data library does not contain cross sections for " + name_ + @@ -630,16 +634,23 @@ void Nuclide::calculate_xs( } } - // Ensure these values are set - // Note, the only time either is used is in one of 4 places: - // 1. physics.cpp - scatter - For inelastic scatter. - // 2. physics.cpp - sample_fission - For partial fissions. - // 3. tally.F90 - score_general - For tallying on MTxxx reactions. - // 4. nuclide.cpp - calculate_urr_xs - For unresolved purposes. - // It is worth noting that none of these occur in the resolved - // resonance range, so the value here does not matter. index_temp is - // set to -1 to force a segfault in case a developer messes up and tries - // to use it with multipole. + /* + * index_temp, index_grid, and interp_factor are used only in the + * following places: + * 1. physics.cpp - scatter - For inelastic scatter. + * 2. physics.cpp - sample_fission - For partial fissions. + * 3. tallies/tally_scoring.cpp - score_general - + * For tallying on MTxxx reactions. + * 4. nuclide.cpp - calculate_urr_xs - For unresolved purposes. + * It is worth noting that none of these occur in the resolved resonance + * range, so the value here does not matter. index_temp is set to -1 to + * force a segfault in case a developer messes up and tries to use it with + * multipole. + * + * However, a segfault is not necessarily guaranteed with an out-of-bounds + * access, so this technique should be replaced by something more robust + * in the future. + */ micro.index_temp = -1; micro.index_grid = -1; micro.interp_factor = 0.0; diff --git a/src/output.cpp b/src/output.cpp index 17e158d3f32..5fdbea1304e 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -31,6 +31,7 @@ #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" #include "openmc/plot.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/reaction.h" #include "openmc/settings.h" #include "openmc/simulation.h" @@ -74,7 +75,7 @@ void title() // Write version information fmt::print( " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2023 MIT, UChicago Argonne LLC, and contributors\n" + " Copyright | 2011-2024 MIT, UChicago Argonne LLC, and contributors\n" " License | https://docs.openmc.org/en/latest/license.html\n" " Version | {}.{}.{}{}\n", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : ""); @@ -295,7 +296,7 @@ void print_version() #ifdef GIT_SHA1 fmt::print("Git SHA1: {}\n", GIT_SHA1); #endif - fmt::print("Copyright (c) 2011-2023 MIT, UChicago Argonne LLC, and " + fmt::print("Copyright (c) 2011-2024 MIT, UChicago Argonne LLC, and " "contributors\nMIT/X license at " "\n"); } @@ -317,6 +318,7 @@ void print_build_info() std::string coverage(n); std::string mcpl(n); std::string ncrystal(n); + std::string uwuw(n); #ifdef PHDF5 phdf5 = y; @@ -345,6 +347,9 @@ void print_build_info() #ifdef COVERAGEBUILD coverage = y; #endif +#ifdef UWUW + uwuw = y; +#endif // Wraps macro variables in quotes #define STRINGIFY(x) STRINGIFY2(x) @@ -363,6 +368,7 @@ void print_build_info() fmt::print("NCrystal support: {}\n", ncrystal); fmt::print("Coverage testing: {}\n", coverage); fmt::print("Profiling flags: {}\n", profiling); + fmt::print("UWUW support: {}\n", uwuw); } } @@ -409,7 +415,7 @@ void print_generation() //============================================================================== -void show_time(const char* label, double secs, int indent_level = 0) +void show_time(const char* label, double secs, int indent_level) { int width = 33 - indent_level * 2; fmt::print("{0:{1}} {2:<{3}} = {4:>10.4e} seconds\n", "", 2 * indent_level, diff --git a/src/particle.cpp b/src/particle.cpp index e26e25e6759..a91113c61ad 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -59,11 +59,20 @@ double Particle::speed() const break; } - // Calculate inverse of Lorentz factor - const double inv_gamma = mass / (this->E() + mass); + if (this->E() < 1.0e-9 * mass) { + // If the energy is much smaller than the mass, revert to non-relativistic + // formula. The 1e-9 criterion is specifically chosen as the point below + // which the error from using the non-relativistic formula is less than the + // round-off eror when using the relativistic formula (see analysis at + // https://gist.github.com/paulromano/da3b473fe3df33de94b265bdff0c7817) + return C_LIGHT * std::sqrt(2 * this->E() / mass); + } else { + // Calculate inverse of Lorentz factor + const double inv_gamma = mass / (this->E() + mass); - // Calculate speed via v = c * sqrt(1 - γ^-2) - return C_LIGHT * std::sqrt(1 - inv_gamma * inv_gamma); + // Calculate speed via v = c * sqrt(1 - γ^-2) + return C_LIGHT * std::sqrt(1 - inv_gamma * inv_gamma); + } } void Particle::move_distance(double length) @@ -112,6 +121,7 @@ void Particle::from_source(const SourceSite* src) wgt_last() = src->wgt; r() = src->r; u() = src->u; + r_born() = src->r; r_last_current() = src->r; r_last() = src->r; u_last() = src->u; @@ -268,7 +278,9 @@ void Particle::event_cross_surface() boundary().lattice_translation[1] != 0 || boundary().lattice_translation[2] != 0) { // Particle crosses lattice boundary - cross_lattice(*this, boundary()); + + bool verbose = settings::verbosity >= 10 || trace(); + cross_lattice(*this, boundary(), verbose); event() = TallyEvent::LATTICE; } else { // Particle crosses surface @@ -369,7 +381,7 @@ void Particle::event_revive_from_secondary() { // If particle has too many events, display warning and kill it ++n_event(); - if (n_event() == MAX_EVENTS) { + if (n_event() == settings::max_particle_events) { warning("Particle " + std::to_string(id()) + " underwent maximum number of events."); wgt() = 0.0; @@ -397,7 +409,8 @@ void Particle::event_revive_from_secondary() // have to determine it before the energy of the secondary particle can be // removed from the pulse-height of this cell. if (lowest_coord().cell == C_NONE) { - if (!exhaustive_find_cell(*this)) { + bool verbose = settings::verbosity >= 10 || trace(); + if (!exhaustive_find_cell(*this, verbose)) { mark_as_lost("Could not find the cell containing particle " + std::to_string(id())); return; @@ -547,7 +560,8 @@ void Particle::cross_surface() } #endif - if (neighbor_list_find_cell(*this)) + bool verbose = settings::verbosity >= 10 || trace(); + if (neighbor_list_find_cell(*this, verbose)) return; // ========================================================================== @@ -555,7 +569,7 @@ void Particle::cross_surface() // Remove lower coordinate levels n_coord() = 1; - bool found = exhaustive_find_cell(*this); + bool found = exhaustive_find_cell(*this, verbose); if (settings::run_mode != RunMode::PLOTTING && (!found)) { // If a cell is still not found, there are two possible causes: 1) there is @@ -570,7 +584,7 @@ void Particle::cross_surface() // Couldn't find next cell anywhere! This probably means there is an actual // undefined region in the geometry. - if (!exhaustive_find_cell(*this)) { + if (!exhaustive_find_cell(*this, verbose)) { mark_as_lost("After particle " + std::to_string(id()) + " crossed surface " + std::to_string(surf->id_) + " it could not be located in any cell and it did not leak."); @@ -645,8 +659,8 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // (unless we're using a dagmc model, which has exactly one universe) n_coord() = 1; if (surf.geom_type_ != GeometryType::DAG && !neighbor_list_find_cell(*this)) { - this->mark_as_lost("Couldn't find particle after reflecting from surface " + - std::to_string(surf.id_) + "."); + mark_as_lost("Couldn't find particle after reflecting from surface " + + std::to_string(surf.id_) + "."); return; } diff --git a/src/particle_data.cpp b/src/particle_data.cpp index 0fc7202c5bf..d8a5bb9d99c 100644 --- a/src/particle_data.cpp +++ b/src/particle_data.cpp @@ -1,6 +1,9 @@ #include "openmc/particle_data.h" +#include + #include "openmc/cell.h" +#include "openmc/error.h" #include "openmc/geometry.h" #include "openmc/material.h" #include "openmc/nuclide.h" @@ -12,6 +15,21 @@ namespace openmc { +void GeometryState::mark_as_lost(const std::string& message) +{ + mark_as_lost(message.c_str()); +} + +void GeometryState::mark_as_lost(const std::stringstream& message) +{ + mark_as_lost(message.str()); +} + +void GeometryState::mark_as_lost(const char* message) +{ + fatal_error(message); +} + void LocalCoord::rotate(const vector& rotation) { r = r.rotate(rotation); @@ -30,13 +48,16 @@ void LocalCoord::reset() rotated = false; } -ParticleData::ParticleData() +GeometryState::GeometryState() { // Create and clear coordinate levels coord_.resize(model::n_coord_levels); cell_last_.resize(model::n_coord_levels); clear(); +} +ParticleData::ParticleData() +{ zero_delayed_bank(); // Every particle starts with no accumulated flux derivative. Note that in diff --git a/src/particle_restart.cpp b/src/particle_restart.cpp index 32d187dd823..6b7778211af 100644 --- a/src/particle_restart.cpp +++ b/src/particle_restart.cpp @@ -87,8 +87,10 @@ void run_particle_restart() read_particle_restart(p, previous_run_mode); // write track if that was requested on command line - if (settings::write_all_tracks) + if (settings::write_all_tracks) { + open_track_file(); p.write_track() = true; + } // Set all tallies to 0 for now (just tracking errors) model::tallies.clear(); @@ -123,6 +125,10 @@ void run_particle_restart() // Write output if particle made it print_particle(p); + + if (settings::write_all_tracks) { + close_track_file(); + } } } // namespace openmc diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 43e3cfa7455..361cf5affcf 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -43,7 +43,7 @@ void sample_reaction(Particle& p) // change when sampling fission sites. The following block handles all // absorption (including fission) - if (model::materials[p.material()]->fissionable_) { + if (model::materials[p.material()]->fissionable()) { if (settings::run_mode == RunMode::EIGENVALUE || (settings::run_mode == RunMode::FIXED_SOURCE && settings::create_fission_neutrons)) { diff --git a/src/plot.cpp b/src/plot.cpp index e2f07c47cf0..bf733cff48f 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -23,6 +23,7 @@ #include "openmc/material.h" #include "openmc/mesh.h" #include "openmc/message_passing.h" +#include "openmc/openmp_interface.h" #include "openmc/output.h" #include "openmc/particle.h" #include "openmc/progress_bar.h" @@ -44,7 +45,7 @@ constexpr int32_t OVERLAP {-3}; IdData::IdData(size_t h_res, size_t v_res) : data_({v_res, h_res, 3}, NOT_FOUND) {} -void IdData::set_value(size_t y, size_t x, const Particle& p, int level) +void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) { // set cell data if (p.n_coord() <= level) { @@ -77,7 +78,8 @@ PropertyData::PropertyData(size_t h_res, size_t v_res) : data_({v_res, h_res, 2}, NOT_FOUND) {} -void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level) +void PropertyData::set_value( + size_t y, size_t x, const GeometryState& p, int level) { Cell* c = model::cells.at(p.lowest_coord().cell).get(); data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; @@ -1083,7 +1085,7 @@ void ProjectionPlot::set_output_path(pugi::xml_node node) // Advances to the next boundary from outside the geometry // Returns -1 if no intersection found, and the surface index // if an intersection was found. -int ProjectionPlot::advance_to_boundary_from_void(Particle& p) +int ProjectionPlot::advance_to_boundary_from_void(GeometryState& p) { constexpr double scoot = 1e-5; double min_dist = {INFINITY}; @@ -1218,11 +1220,7 @@ void ProjectionPlot::create_output() const * Note that a vector of vectors is required rather than a 2-tensor, * since the stack size varies within each column. */ -#ifdef _OPENMP - const int n_threads = omp_get_max_threads(); -#else - const int n_threads = 1; -#endif + const int n_threads = num_threads(); std::vector>> this_line_segments( n_threads); for (int t = 0; t < n_threads; ++t) { @@ -1234,29 +1232,11 @@ void ProjectionPlot::create_output() const #pragma omp parallel { + const int n_threads = num_threads(); + const int tid = thread_num(); -#ifdef _OPENMP - const int n_threads = omp_get_max_threads(); - const int tid = omp_get_thread_num(); -#else - int n_threads = 1; - int tid = 0; -#endif - - SourceSite s; // Where particle starts from (camera) - s.E = 1; - s.wgt = 1; - s.delayed_group = 0; - s.particle = ParticleType::photon; // just has to be something reasonable - s.parent_id = 1; - s.progeny_id = 2; - s.r = camera_position_; - - Particle p; - s.u.x = 1.0; - s.u.y = 0.0; - s.u.z = 0.0; - p.from_source(&s); + GeometryState p; + p.u() = {1.0, 0.0, 0.0}; int vert = tid; for (int iter = 0; iter <= pixels_[1] / n_threads; iter++) { @@ -1272,6 +1252,10 @@ void ProjectionPlot::create_output() const for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + // Projection mode below decides ray starting conditions + Position init_r; + Direction init_u; + // Generate the starting position/direction of the ray if (orthographic_width_ == 0.0) { // perspective projection double this_phi = @@ -1282,18 +1266,22 @@ void ProjectionPlot::create_output() const camera_local_vec.x = std::cos(this_phi) * std::sin(this_mu); camera_local_vec.y = std::sin(this_phi) * std::sin(this_mu); camera_local_vec.z = std::cos(this_mu); - s.u = camera_local_vec.rotate(camera_to_model); + init_u = camera_local_vec.rotate(camera_to_model); + init_r = camera_position_; } else { // orthographic projection - s.u = looking_direction; + init_u = looking_direction; double x_pix_coord = (static_cast(horiz) - p0 / 2.0) / p0; double y_pix_coord = (static_cast(vert) - p1 / 2.0) / p0; - s.r = camera_position_ + - cam_yaxis * x_pix_coord * orthographic_width_ + - cam_zaxis * y_pix_coord * orthographic_width_; + + init_r = camera_position_; + init_r += cam_yaxis * x_pix_coord * orthographic_width_; + init_r += cam_zaxis * y_pix_coord * orthographic_width_; } - p.from_source(&s); // put particle at camera + // Resets internal geometry state of particle + p.init_from_r_u(init_r, init_u); + bool hitsomething = false; bool intersection_found = true; int loop_counter = 0; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp new file mode 100644 index 00000000000..5e1194aa92a --- /dev/null +++ b/src/random_ray/flat_source_domain.cpp @@ -0,0 +1,686 @@ +#include "openmc/random_ray/flat_source_domain.h" + +#include "openmc/cell.h" +#include "openmc/geometry.h" +#include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" +#include "openmc/output.h" +#include "openmc/plot.h" +#include "openmc/random_ray/random_ray.h" +#include "openmc/simulation.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/tally.h" +#include "openmc/tallies/tally_scoring.h" +#include "openmc/timer.h" + +#include + +namespace openmc { + +//============================================================================== +// FlatSourceDomain implementation +//============================================================================== + +FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) +{ + // Count the number of source regions, compute the cell offset + // indices, and store the material type The reason for the offsets is that + // some cell types may not have material fills, and therefore do not + // produce FSRs. Thus, we cannot index into the global arrays directly + for (const auto& c : model::cells) { + if (c->type_ != Fill::MATERIAL) { + source_region_offsets_.push_back(-1); + } else { + source_region_offsets_.push_back(n_source_regions_); + n_source_regions_ += c->n_instances_; + n_source_elements_ += c->n_instances_ * negroups_; + } + } + + // Initialize cell-wise arrays + lock_.resize(n_source_regions_); + material_.resize(n_source_regions_); + position_recorded_.assign(n_source_regions_, 0); + position_.resize(n_source_regions_); + volume_.assign(n_source_regions_, 0.0); + volume_t_.assign(n_source_regions_, 0.0); + was_hit_.assign(n_source_regions_, 0); + + // Initialize element-wise arrays + scalar_flux_new_.assign(n_source_elements_, 0.0); + scalar_flux_old_.assign(n_source_elements_, 1.0); + scalar_flux_final_.assign(n_source_elements_, 0.0); + source_.resize(n_source_elements_); + tally_task_.resize(n_source_elements_); + + // Initialize material array + int64_t source_region_id = 0; + for (int i = 0; i < model::cells.size(); i++) { + Cell& cell = *model::cells[i]; + if (cell.type_ == Fill::MATERIAL) { + for (int j = 0; j < cell.n_instances_; j++) { + material_[source_region_id++] = cell.material(j); + } + } + } + + // Sanity check + if (source_region_id != n_source_regions_) { + fatal_error("Unexpected number of source regions"); + } +} + +void FlatSourceDomain::batch_reset() +{ + // Reset scalar fluxes, iteration volume tallies, and region hit flags to + // zero + parallel_fill(scalar_flux_new_, 0.0f); + parallel_fill(volume_, 0.0); + parallel_fill(was_hit_, 0); +} + +void FlatSourceDomain::accumulate_iteration_flux() +{ +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + scalar_flux_final_[se] += scalar_flux_new_[se]; + } +} + +// Compute new estimate of scattering + fission sources in each source region +// based on the flux estimate from the previous iteration. +void FlatSourceDomain::update_neutron_source(double k_eff) +{ + simulation::time_update_src.start(); + + double inverse_k_eff = 1.0 / k_eff; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + + for (int e_out = 0; e_out < negroups_; e_out++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + float scatter_source = 0.0f; + float fission_source = 0.0f; + + for (int e_in = 0; e_in < negroups_; e_in++) { + float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + float sigma_s = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); + float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); + float chi = data::mg.macro_xs_[material].get_xs( + MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + scatter_source += sigma_s * scalar_flux; + fission_source += nu_sigma_f * scalar_flux * chi; + } + + fission_source *= inverse_k_eff; + float new_isotropic_source = (scatter_source + fission_source) / sigma_t; + source_[sr * negroups_ + e_out] = new_isotropic_source; + } + } + + simulation::time_update_src.stop(); +} + +// Normalizes flux and updates simulation-averaged volume estimate +void FlatSourceDomain::normalize_scalar_flux_and_volumes( + double total_active_distance_per_iteration) +{ + float normalization_factor = 1.0 / total_active_distance_per_iteration; + double volume_normalization_factor = + 1.0 / (total_active_distance_per_iteration * simulation::current_batch); + +// Normalize scalar flux to total distance travelled by all rays this iteration +#pragma omp parallel for + for (int64_t e = 0; e < scalar_flux_new_.size(); e++) { + scalar_flux_new_[e] *= normalization_factor; + } + +// Accumulate cell-wise ray length tallies collected this iteration, then +// update the simulation-averaged cell-wise volume estimates +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + volume_t_[sr] += volume_[sr]; + volume_[sr] = volume_t_[sr] * volume_normalization_factor; + } +} + +// Combine transport flux contributions and flat source contributions from the +// previous iteration to generate this iteration's estimate of scalar flux. +int64_t FlatSourceDomain::add_source_to_scalar_flux() +{ + int64_t n_hits = 0; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for reduction(+ : n_hits) + for (int sr = 0; sr < n_source_regions_; sr++) { + + // Check if this cell was hit this iteration + int was_cell_hit = was_hit_[sr]; + if (was_cell_hit) { + n_hits++; + } + + double volume = volume_[sr]; + int material = material_[sr]; + for (int g = 0; g < negroups_; g++) { + int64_t idx = (sr * negroups_) + g; + + // There are three scenarios we need to consider: + if (was_cell_hit) { + // 1. If the FSR was hit this iteration, then the new flux is equal to + // the flat source from the previous iteration plus the contributions + // from rays passing through the source region (computed during the + // transport sweep) + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a); + scalar_flux_new_[idx] /= (sigma_t * volume); + scalar_flux_new_[idx] += source_[idx]; + } else if (volume > 0.0) { + // 2. If the FSR was not hit this iteration, but has been hit some + // previous iteration, then we simply set the new scalar flux to be + // equal to the contribution from the flat source alone. + scalar_flux_new_[idx] = source_[idx]; + } else { + // If the FSR was not hit this iteration, and it has never been hit in + // any iteration (i.e., volume is zero), then we want to set this to 0 + // to avoid dividing anything by a zero volume. + scalar_flux_new_[idx] = 0.0f; + } + } + } + + // Return the number of source regions that were hit this iteration + return n_hits; +} + +// Generates new estimate of k_eff based on the differences between this +// iteration's estimate of the scalar flux and the last iteration's estimate. +double FlatSourceDomain::compute_k_eff(double k_eff_old) const +{ + double fission_rate_old = 0; + double fission_rate_new = 0; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new) + for (int sr = 0; sr < n_source_regions_; sr++) { + + // If simulation averaged volume is zero, don't include this cell + double volume = volume_[sr]; + if (volume == 0.0) { + continue; + } + + int material = material_[sr]; + + double sr_fission_source_old = 0; + double sr_fission_source_new = 0; + + for (int g = 0; g < negroups_; g++) { + int64_t idx = (sr * negroups_) + g; + double nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, g, nullptr, nullptr, nullptr, t, a); + sr_fission_source_old += nu_sigma_f * scalar_flux_old_[idx]; + sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx]; + } + + fission_rate_old += sr_fission_source_old * volume; + fission_rate_new += sr_fission_source_new * volume; + } + + double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old); + + return k_eff_new; +} + +// This function is responsible for generating a mapping between random +// ray flat source regions (cell instances) and tally bins. The mapping +// takes the form of a "TallyTask" object, which accounts for one single +// score being applied to a single tally. Thus, a single source region +// may have anywhere from zero to many tally tasks associated with it --- +// meaning that the global "tally_task" data structure is in 2D. The outer +// dimension corresponds to the source element (i.e., each entry corresponds +// to a specific energy group within a specific source region), and the +// inner dimension corresponds to the tallying task itself. Mechanically, +// the mapping between FSRs and spatial filters is done by considering +// the location of a single known ray midpoint that passed through the +// FSR. I.e., during transport, the first ray to pass through a given FSR +// will write down its midpoint for use with this function. This is a cheap +// and easy way of mapping FSRs to spatial tally filters, but comes with +// the downside of adding the restriction that spatial tally filters must +// share boundaries with the physical geometry of the simulation (so as +// not to subdivide any FSR). It is acceptable for a spatial tally region +// to contain multiple FSRs, but not the other way around. + +// TODO: In future work, it would be preferable to offer a more general +// (but perhaps slightly more expensive) option for handling arbitrary +// spatial tallies that would be allowed to subdivide FSRs. + +// Besides generating the mapping structure, this function also keeps track +// of whether or not all flat source regions have been hit yet. This is +// required, as there is no guarantee that all flat source regions will +// be hit every iteration, such that in the first few iterations some FSRs +// may not have a known position within them yet to facilitate mapping to +// spatial tally filters. However, after several iterations, if all FSRs +// have been hit and have had a tally map generated, then this status will +// be passed back to the caller to alert them that this function doesn't +// need to be called for the remainder of the simulation. + +void FlatSourceDomain::convert_source_regions_to_tallies() +{ + openmc::simulation::time_tallies.start(); + + // Tracks if we've generated a mapping yet for all source regions. + bool all_source_regions_mapped = true; + +// Attempt to generate mapping for all source regions +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + + // If this source region has not been hit by a ray yet, then + // we aren't going to be able to map it, so skip it. + if (!position_recorded_[sr]) { + all_source_regions_mapped = false; + continue; + } + + // A particle located at the recorded midpoint of a ray + // crossing through this source region is used to estabilish + // the spatial location of the source region + Particle p; + p.r() = position_[sr]; + p.r_last() = position_[sr]; + bool found = exhaustive_find_cell(p); + + // Loop over energy groups (so as to support energy filters) + for (int g = 0; g < negroups_; g++) { + + // Set particle to the current energy + p.g() = g; + p.g_last() = g; + p.E() = data::mg.energy_bin_avg_[p.g()]; + p.E_last() = p.E(); + + int64_t source_element = sr * negroups_ + g; + + // If this task has already been populated, we don't need to do + // it again. + if (tally_task_[source_element].size() > 0) { + continue; + } + + // Loop over all active tallies. This logic is essentially identical + // to what happens when scanning for applicable tallies during + // MC transport. + for (auto i_tally : model::active_tallies) { + Tally& tally {*model::tallies[i_tally]}; + + // Initialize an iterator over valid filter bin combinations. + // If there are no valid combinations, use a continue statement + // to ensure we skip the assume_separate break below. + auto filter_iter = FilterBinIter(tally, p); + auto end = FilterBinIter(tally, true, &p.filter_matches()); + if (filter_iter == end) + continue; + + // Loop over filter bins. + for (; filter_iter != end; ++filter_iter) { + auto filter_index = filter_iter.index_; + auto filter_weight = filter_iter.weight_; + + // Loop over scores + for (auto score_index = 0; score_index < tally.scores_.size(); + score_index++) { + auto score_bin = tally.scores_[score_index]; + // If a valid tally, filter, and score cobination has been found, + // then add it to the list of tally tasks for this source element. + tally_task_[source_element].emplace_back( + i_tally, filter_index, score_index, score_bin); + } + } + } + // Reset all the filter matches for the next tally event. + for (auto& match : p.filter_matches()) + match.bins_present_ = false; + } + } + openmc::simulation::time_tallies.stop(); + + mapped_all_tallies_ = all_source_regions_mapped; +} + +// Tallying in random ray is not done directly during transport, rather, +// it is done only once after each power iteration. This is made possible +// by way of a mapping data structure that relates spatial source regions +// (FSRs) to tally/filter/score combinations. The mechanism by which the +// mapping is done (and the limitations incurred) is documented in the +// "convert_source_regions_to_tallies()" function comments above. The present +// tally function simply traverses the mapping data structure and executes +// the scoring operations to OpenMC's native tally result arrays. + +void FlatSourceDomain::random_ray_tally() const +{ + openmc::simulation::time_tallies.start(); + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +// We loop over all source regions and energy groups. For each +// element, we check if there are any scores needed and apply +// them. +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + double volume = volume_[sr]; + double material = material_[sr]; + for (int g = 0; g < negroups_; g++) { + int idx = sr * negroups_ + g; + double flux = scalar_flux_new_[idx] * volume; + for (auto& task : tally_task_[idx]) { + double score; + switch (task.score_type) { + + case SCORE_FLUX: + score = flux; + break; + + case SCORE_TOTAL: + score = flux * data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + break; + + case SCORE_FISSION: + score = flux * data::mg.macro_xs_[material].get_xs( + MgxsType::FISSION, g, NULL, NULL, NULL, t, a); + break; + + case SCORE_NU_FISSION: + score = flux * data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a); + break; + + case SCORE_EVENTS: + score = 1.0; + break; + + default: + fatal_error("Invalid score specified in tallies.xml. Only flux, " + "total, fission, nu-fission, and events are supported in " + "random ray mode."); + break; + } + Tally& tally {*model::tallies[task.tally_idx]}; +#pragma omp atomic + tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) += + score; + } + } + } +} + +void FlatSourceDomain::all_reduce_replicated_source_regions() +{ +#ifdef OPENMC_MPI + + // If we only have 1 MPI rank, no need + // to reduce anything. + if (mpi::n_procs <= 1) + return; + + simulation::time_bank_sendrecv.start(); + + // The "position_recorded" variable needs to be allreduced (and maxed), + // as whether or not a cell was hit will affect some decisions in how the + // source is calculated in the next iteration so as to avoid dividing + // by zero. We take the max rather than the sum as the hit values are + // expected to be zero or 1. + MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), n_source_regions_, + MPI_INT, MPI_MAX, mpi::intracomm); + + // The position variable is more complicated to reduce than the others, + // as we do not want the sum of all positions in each cell, rather, we + // want to just pick any single valid position. Thus, we perform a gather + // and then pick the first valid position we find for all source regions + // that have had a position recorded. This operation does not need to + // be broadcast back to other ranks, as this value is only used for the + // tally conversion operation, which is only performed on the master rank. + // While this is expensive, it only needs to be done for active batches, + // and only if we have not mapped all the tallies yet. Once tallies are + // fully mapped, then the position vector is fully populated, so this + // operation can be skipped. + + // First, we broadcast the fully mapped tally status variable so that + // all ranks are on the same page + int mapped_all_tallies_i = static_cast(mapped_all_tallies_); + MPI_Bcast(&mapped_all_tallies_i, 1, MPI_INT, 0, mpi::intracomm); + + // Then, we perform the gather of position data, if needed + if (simulation::current_batch > settings::n_inactive && + !mapped_all_tallies_i) { + + // Master rank will gather results and pick valid positions + if (mpi::master) { + // Initialize temporary vector for receiving positions + vector> all_position; + all_position.resize(mpi::n_procs); + for (int i = 0; i < mpi::n_procs; i++) { + all_position[i].resize(n_source_regions_); + } + + // Copy master rank data into gathered vector for convenience + all_position[0] = position_; + + // Receive all data into gather vector + for (int i = 1; i < mpi::n_procs; i++) { + MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i, + 0, mpi::intracomm, MPI_STATUS_IGNORE); + } + + // Scan through gathered data and pick first valid cell posiiton + for (int sr = 0; sr < n_source_regions_; sr++) { + if (position_recorded_[sr] == 1) { + for (int i = 0; i < mpi::n_procs; i++) { + if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 || + all_position[i][sr].z != 0.0) { + position_[sr] = all_position[i][sr]; + break; + } + } + } + } + } else { + // Other ranks just send in their data + MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0, + mpi::intracomm); + } + } + + // For the rest of the source region data, we simply perform an all reduce, + // as these values will be needed on all ranks for transport during the + // next iteration. + MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, + MPI_SUM, mpi::intracomm); + + MPI_Allreduce(MPI_IN_PLACE, was_hit_.data(), n_source_regions_, MPI_INT, + MPI_SUM, mpi::intracomm); + + MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_elements_, + MPI_FLOAT, MPI_SUM, mpi::intracomm); + + simulation::time_bank_sendrecv.stop(); +#endif +} + +// Outputs all basic material, FSR ID, multigroup flux, and +// fission source data to .vtk file that can be directly +// loaded and displayed by Paraview. Note that .vtk binary +// files require big endian byte ordering, so endianness +// is checked and flipped if necessary. +void FlatSourceDomain::output_to_vtk() const +{ + // Rename .h5 plot filename(s) to .vtk filenames + for (int p = 0; p < model::plots.size(); p++) { + PlottableInterface* plot = model::plots[p].get(); + plot->path_plot() = + plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk"; + } + + // Print header information + print_plot(); + + // Outer loop over plots + for (int p = 0; p < model::plots.size(); p++) { + + // Get handle to OpenMC plot object and extract params + Plot* openmc_plot = dynamic_cast(model::plots[p].get()); + + // Random ray plots only support voxel plots + if (!openmc_plot) { + warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting " + "is allowed in random ray mode.", + p)); + continue; + } else if (openmc_plot->type_ != Plot::PlotType::voxel) { + warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting " + "is allowed in random ray mode.", + p)); + continue; + } + + int Nx = openmc_plot->pixels_[0]; + int Ny = openmc_plot->pixels_[1]; + int Nz = openmc_plot->pixels_[2]; + Position origin = openmc_plot->origin_; + Position width = openmc_plot->width_; + Position ll = origin - width / 2.0; + double x_delta = width.x / Nx; + double y_delta = width.y / Ny; + double z_delta = width.z / Nz; + std::string filename = openmc_plot->path_plot(); + + // Perform sanity checks on file size + uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float); + write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)", + openmc_plot->id(), filename, bytes / 1.0e6); + if (bytes / 1.0e9 > 1.0) { + warning("Voxel plot specification is very large (>1 GB). Plotting may be " + "slow."); + } else if (bytes / 1.0e9 > 100.0) { + fatal_error("Voxel plot specification is too large (>100 GB). Exiting."); + } + + // Relate voxel spatial locations to random ray source regions + vector voxel_indices(Nx * Ny * Nz); + +#pragma omp parallel for collapse(3) + for (int z = 0; z < Nz; z++) { + for (int y = 0; y < Ny; y++) { + for (int x = 0; x < Nx; x++) { + Position sample; + sample.z = ll.z + z_delta / 2.0 + z * z_delta; + sample.y = ll.y + y_delta / 2.0 + y * y_delta; + sample.x = ll.x + x_delta / 2.0 + x * x_delta; + Particle p; + p.r() = sample; + bool found = exhaustive_find_cell(p); + int i_cell = p.lowest_coord().cell; + int64_t source_region_idx = + source_region_offsets_[i_cell] + p.cell_instance(); + voxel_indices[z * Ny * Nx + y * Nx + x] = source_region_idx; + } + } + } + + // Open file for writing + std::FILE* plot = std::fopen(filename.c_str(), "wb"); + + // Write vtk metadata + std::fprintf(plot, "# vtk DataFile Version 2.0\n"); + std::fprintf(plot, "Dataset File\n"); + std::fprintf(plot, "BINARY\n"); + std::fprintf(plot, "DATASET STRUCTURED_POINTS\n"); + std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz); + std::fprintf(plot, "ORIGIN 0 0 0\n"); + std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta); + std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz); + + // Plot multigroup flux data + for (int g = 0; g < negroups_; g++) { + std::fprintf(plot, "SCALARS flux_group_%d float\n", g); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int fsr : voxel_indices) { + int64_t source_element = fsr * negroups_ + g; + float flux = scalar_flux_final_[source_element]; + flux /= (settings::n_batches - settings::n_inactive); + flux = convert_to_big_endian(flux); + std::fwrite(&flux, sizeof(float), 1, plot); + } + } + + // Plot FSRs + std::fprintf(plot, "SCALARS FSRs float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int fsr : voxel_indices) { + float value = future_prn(10, fsr); + value = convert_to_big_endian(value); + std::fwrite(&value, sizeof(float), 1, plot); + } + + // Plot Materials + std::fprintf(plot, "SCALARS Materials int\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int fsr : voxel_indices) { + int mat = material_[fsr]; + mat = convert_to_big_endian(mat); + std::fwrite(&mat, sizeof(int), 1, plot); + } + + // Plot fission source + std::fprintf(plot, "SCALARS total_fission_source float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int fsr : voxel_indices) { + float total_fission = 0.0; + int mat = material_[fsr]; + for (int g = 0; g < negroups_; g++) { + int64_t source_element = fsr * negroups_ + g; + float flux = scalar_flux_final_[source_element]; + flux /= (settings::n_batches - settings::n_inactive); + float Sigma_f = data::mg.macro_xs_[mat].get_xs( + MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0); + total_fission += Sigma_f * flux; + } + total_fission = convert_to_big_endian(total_fission); + std::fwrite(&total_fission, sizeof(float), 1, plot); + } + + std::fclose(plot); + } +} + +} // namespace openmc diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp new file mode 100644 index 00000000000..63d728cce8b --- /dev/null +++ b/src/random_ray/random_ray.cpp @@ -0,0 +1,289 @@ +#include "openmc/random_ray/random_ray.h" + +#include "openmc/geometry.h" +#include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" +#include "openmc/random_ray/flat_source_domain.h" +#include "openmc/search.h" +#include "openmc/settings.h" +#include "openmc/simulation.h" +#include "openmc/source.h" + +namespace openmc { + +//============================================================================== +// Non-method functions +//============================================================================== + +// returns 1 - exp(-tau) +// Equivalent to -(_expm1f(-tau)), but faster +// Written by Colin Josey. +float cjosey_exponential(float tau) +{ + constexpr float c1n = -1.0000013559236386308f; + constexpr float c2n = 0.23151368626911062025f; + constexpr float c3n = -0.061481916409314966140f; + constexpr float c4n = 0.0098619906458127653020f; + constexpr float c5n = -0.0012629460503540849940f; + constexpr float c6n = 0.00010360973791574984608f; + constexpr float c7n = -0.000013276571933735820960f; + + constexpr float c0d = 1.0f; + constexpr float c1d = -0.73151337729389001396f; + constexpr float c2d = 0.26058381273536471371f; + constexpr float c3d = -0.059892419041316836940f; + constexpr float c4d = 0.0099070188241094279067f; + constexpr float c5d = -0.0012623388962473160860f; + constexpr float c6d = 0.00010361277635498731388f; + constexpr float c7d = -0.000013276569500666698498f; + + float x = -tau; + + float den = c7d; + den = den * x + c6d; + den = den * x + c5d; + den = den * x + c4d; + den = den * x + c3d; + den = den * x + c2d; + den = den * x + c1d; + den = den * x + c0d; + + float num = c7n; + num = num * x + c6n; + num = num * x + c5n; + num = num * x + c4n; + num = num * x + c3n; + num = num * x + c2n; + num = num * x + c1n; + num = num * x; + + return num / den; +} + +//============================================================================== +// RandomRay implementation +//============================================================================== + +// Static Variable Declarations +double RandomRay::distance_inactive_; +double RandomRay::distance_active_; +unique_ptr RandomRay::ray_source_; + +RandomRay::RandomRay() + : angular_flux_(data::mg.num_energy_groups_), + delta_psi_(data::mg.num_energy_groups_), + negroups_(data::mg.num_energy_groups_) +{} + +RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay() +{ + initialize_ray(ray_id, domain); +} + +// Transports ray until termination criteria are met +uint64_t RandomRay::transport_history_based_single_ray() +{ + using namespace openmc; + while (alive()) { + event_advance_ray(); + if (!alive()) + break; + event_cross_surface(); + } + + return n_event(); +} + +// Transports ray across a single source region +void RandomRay::event_advance_ray() +{ + // Find the distance to the nearest boundary + boundary() = distance_to_boundary(*this); + double distance = boundary().distance; + + if (distance <= 0.0) { + mark_as_lost("Negative transport distance detected for particle " + + std::to_string(id())); + return; + } + + if (is_active_) { + // If the ray is in the active length, need to check if it has + // reached its maximum termination distance. If so, reduce + // the ray traced length so that the ray does not overrun the + // maximum numerical length (so as to avoid numerical bias). + if (distance_travelled_ + distance >= distance_active_) { + distance = distance_active_ - distance_travelled_; + wgt() = 0.0; + } + + distance_travelled_ += distance; + attenuate_flux(distance, true); + } else { + // If the ray is still in the dead zone, need to check if it + // has entered the active phase. If so, split into two segments (one + // representing the final part of the dead zone, the other representing the + // first part of the active length) and attenuate each. Otherwise, if the + // full length of the segment is within the dead zone, attenuate as normal. + if (distance_travelled_ + distance >= distance_inactive_) { + is_active_ = true; + double distance_dead = distance_inactive_ - distance_travelled_; + attenuate_flux(distance_dead, false); + + double distance_alive = distance - distance_dead; + + // Ensure we haven't travelled past the active phase as well + if (distance_alive > distance_active_) { + distance_alive = distance_active_; + wgt() = 0.0; + } + + attenuate_flux(distance_alive, true); + distance_travelled_ = distance_alive; + } else { + distance_travelled_ += distance; + attenuate_flux(distance, false); + } + } + + // Advance particle + for (int j = 0; j < n_coord(); ++j) { + coord(j).r += distance * coord(j).u; + } +} + +// This function forms the inner loop of the random ray transport process. +// It is responsible for several tasks. Based on the incoming angular flux +// of the ray and the source term in the region, the outgoing angular flux +// is computed. The delta psi between the incoming and outgoing fluxes is +// contributed to the estimate of the total scalar flux in the source region. +// Additionally, the contribution of the ray path to the stochastically +// estimated volume is also kept track of. All tasks involving writing +// to the data for the source region are done with a lock over the entire +// source region. Locks are used instead of atomics as all energy groups +// must be written, such that locking once is typically much more efficient +// than use of many atomic operations corresponding to each energy group +// individually (at least on CPU). Several other bookkeeping tasks are also +// performed when inside the lock. +void RandomRay::attenuate_flux(double distance, bool is_active) +{ + // The number of geometric intersections is counted for reporting purposes + n_event()++; + + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The source region is the spatial region index + int64_t source_region = + domain_->source_region_offsets_[i_cell] + cell_instance(); + + // The source element is the energy-specific region index + int64_t source_element = source_region * negroups_; + int material = this->material(); + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + + // MOC incoming flux attenuation + source contribution/attenuation equation + for (int g = 0; g < negroups_; g++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + float tau = sigma_t * distance; + float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) + float new_delta_psi = + (angular_flux_[g] - domain_->source_[source_element + g]) * exponential; + delta_psi_[g] = new_delta_psi; + angular_flux_[g] -= new_delta_psi; + } + + // If ray is in the active phase (not in dead zone), make contributions to + // source region bookkeeping + if (is_active) { + + // Aquire lock for source region + domain_->lock_[source_region].lock(); + + // Accumulate delta psi into new estimate of source region flux for + // this iteration + for (int g = 0; g < negroups_; g++) { + domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; + } + + // If the source region hasn't been hit yet this iteration, + // indicate that it now has + if (domain_->was_hit_[source_region] == 0) { + domain_->was_hit_[source_region] = 1; + } + + // Accomulate volume (ray distance) into this iteration's estimate + // of the source region's volume + domain_->volume_[source_region] += distance; + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!domain_->position_recorded_[source_region]) { + Position midpoint = r() + u() * (distance / 2.0); + domain_->position_[source_region] = midpoint; + domain_->position_recorded_[source_region] = 1; + } + + // Release lock + domain_->lock_[source_region].unlock(); + } +} + +void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) +{ + domain_ = domain; + + // Reset particle event counter + n_event() = 0; + + is_active_ = (distance_inactive_ <= 0.0); + + wgt() = 1.0; + + // set identifier for particle + id() = simulation::work_index[mpi::rank] + ray_id; + + // set random number seed + int64_t particle_seed = + (simulation::current_batch - 1) * settings::n_particles + id(); + init_particle_seeds(particle_seed, seeds()); + stream() = STREAM_TRACKING; + + // Sample from ray source distribution + SourceSite site {ray_source_->sample(current_seed())}; + site.E = lower_bound_index( + data::mg.rev_energy_bins_.begin(), data::mg.rev_energy_bins_.end(), site.E); + site.E = negroups_ - site.E - 1.; + this->from_source(&site); + + // Locate ray + if (lowest_coord().cell == C_NONE) { + if (!exhaustive_find_cell(*this)) { + this->mark_as_lost( + "Could not find the cell containing particle " + std::to_string(id())); + } + + // Set birth cell attribute + if (cell_born() == C_NONE) + cell_born() = lowest_coord().cell; + } + + // Initialize ray's starting angular flux to starting location's isotropic + // source + int i_cell = lowest_coord().cell; + int64_t source_region_idx = + domain_->source_region_offsets_[i_cell] + cell_instance(); + + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] = domain_->source_[source_region_idx * negroups_ + g]; + } +} + +} // namespace openmc diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp new file mode 100644 index 00000000000..b9fd93a48f6 --- /dev/null +++ b/src/random_ray/random_ray_simulation.cpp @@ -0,0 +1,397 @@ +#include "openmc/random_ray/random_ray_simulation.h" + +#include "openmc/eigenvalue.h" +#include "openmc/geometry.h" +#include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" +#include "openmc/output.h" +#include "openmc/plot.h" +#include "openmc/random_ray/random_ray.h" +#include "openmc/simulation.h" +#include "openmc/source.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/tally.h" +#include "openmc/tallies/tally_scoring.h" +#include "openmc/timer.h" + +namespace openmc { + +//============================================================================== +// Non-member functions +//============================================================================== + +void openmc_run_random_ray() +{ + // Initialize OpenMC general data structures + openmc_simulation_init(); + + // Validate that inputs meet requirements for random ray mode + if (mpi::master) + validate_random_ray_inputs(); + + // Initialize Random Ray Simulation Object + RandomRaySimulation sim; + + // Begin main simulation timer + simulation::time_total.start(); + + // Execute random ray simulation + sim.simulate(); + + // End main simulation timer + openmc::simulation::time_total.stop(); + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Reduce variables across MPI ranks + sim.reduce_simulation_statistics(); + + // Output all simulation results + sim.output_simulation_results(); +} + +// Enforces restrictions on inputs in random ray mode. While there are +// many features that don't make sense in random ray mode, and are therefore +// unsupported, we limit our testing/enforcement operations only to inputs +// that may cause erroneous/misleading output or crashes from the solver. +void validate_random_ray_inputs() +{ + // Validate tallies + /////////////////////////////////////////////////////////////////// + for (auto& tally : model::tallies) { + + // Validate score types + for (auto score_bin : tally->scores_) { + switch (score_bin) { + case SCORE_FLUX: + case SCORE_TOTAL: + case SCORE_FISSION: + case SCORE_NU_FISSION: + case SCORE_EVENTS: + break; + default: + fatal_error( + "Invalid score specified. Only flux, total, fission, nu-fission, and " + "event scores are supported in random ray mode."); + } + } + + // Validate filter types + for (auto f : tally->filters()) { + auto& filter = *model::tally_filters[f]; + + switch (filter.type()) { + case FilterType::CELL: + case FilterType::CELL_INSTANCE: + case FilterType::DISTRIBCELL: + case FilterType::ENERGY: + case FilterType::MATERIAL: + case FilterType::MESH: + case FilterType::UNIVERSE: + break; + default: + fatal_error("Invalid filter specified. Only cell, cell_instance, " + "distribcell, energy, material, mesh, and universe filters " + "are supported in random ray mode."); + } + } + } + + // Validate MGXS data + /////////////////////////////////////////////////////////////////// + for (auto& material : data::mg.macro_xs_) { + if (!material.is_isotropic) { + fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets " + "supported in random ray mode."); + } + if (material.get_xsdata().size() > 1) { + fatal_error("Non-isothermal MGXS detected. Only isothermal XS data sets " + "supported in random ray mode."); + } + } + + // Validate solver mode + /////////////////////////////////////////////////////////////////// + if (settings::run_mode == RunMode::FIXED_SOURCE) { + fatal_error( + "Invalid run mode. Fixed source not yet supported in random ray mode."); + } + + // Validate ray source + /////////////////////////////////////////////////////////////////// + + // Check for independent source + IndependentSource* is = + dynamic_cast(RandomRay::ray_source_.get()); + if (!is) { + fatal_error( + "Invalid ray source definition. Ray source must be IndependentSource."); + } + + // Check for box source + SpatialDistribution* space_dist = is->space(); + SpatialBox* sb = dynamic_cast(space_dist); + if (!sb) { + fatal_error( + "Invalid source definition -- only box sources are allowed in random " + "ray " + "mode. If no source is specified, OpenMC default is an isotropic point " + "source at the origin, which is invalid in random ray mode."); + } + + // Check that box source is not restricted to fissionable areas + if (sb->only_fissionable()) { + fatal_error("Invalid source definition -- fissionable spatial distribution " + "not allowed for random ray source."); + } + + // Check for isotropic source + UnitSphereDistribution* angle_dist = is->angle(); + Isotropic* id = dynamic_cast(angle_dist); + if (!id) { + fatal_error("Invalid source definition -- only isotropic sources are " + "allowed for random ray source."); + } + + // Validate plotting files + /////////////////////////////////////////////////////////////////// + for (int p = 0; p < model::plots.size(); p++) { + + // Get handle to OpenMC plot object + Plot* openmc_plot = dynamic_cast(model::plots[p].get()); + + // Random ray plots only support voxel plots + if (!openmc_plot) { + warning(fmt::format( + "Plot {} will not be used for end of simulation data plotting -- only " + "voxel plotting is allowed in random ray mode.", + p)); + continue; + } else if (openmc_plot->type_ != Plot::PlotType::voxel) { + warning(fmt::format( + "Plot {} will not be used for end of simulation data plotting -- only " + "voxel plotting is allowed in random ray mode.", + p)); + continue; + } + } + + // Warn about slow MPI domain replication, if detected + /////////////////////////////////////////////////////////////////// +#ifdef OPENMC_MPI + if (mpi::n_procs > 1) { + warning( + "Domain replication in random ray is supported, but suffers from poor " + "scaling of source all-reduce operations. Performance may severely " + "degrade beyond just a few MPI ranks. Domain decomposition may be " + "implemented in the future to provide efficient scaling."); + } +#endif +} + +//============================================================================== +// RandomRaySimulation implementation +//============================================================================== + +RandomRaySimulation::RandomRaySimulation() + : negroups_(data::mg.num_energy_groups_) +{ + // There are no source sites in random ray mode, so be sure to disable to + // ensure we don't attempt to write source sites to statepoint + settings::source_write = false; + + // Random ray mode does not have an inner loop over generations within a + // batch, so set the current gen to 1 + simulation::current_gen = 1; +} + +void RandomRaySimulation::simulate() +{ + // Random ray power iteration loop + while (simulation::current_batch < settings::n_batches) { + + // Initialize the current batch + initialize_batch(); + initialize_generation(); + + // Reset total starting particle weight used for normalizing tallies + simulation::total_weight = 1.0; + + // Update source term (scattering + fission) + domain_.update_neutron_source(k_eff_); + + // Reset scalar fluxes, iteration volume tallies, and region hit flags to + // zero + domain_.batch_reset(); + + // Start timer for transport + simulation::time_transport.start(); + +// Transport sweep over all random rays for the iteration +#pragma omp parallel for schedule(dynamic) \ + reduction(+ : total_geometric_intersections_) + for (int i = 0; i < simulation::work_per_rank; i++) { + RandomRay ray(i, &domain_); + total_geometric_intersections_ += + ray.transport_history_based_single_ray(); + } + + simulation::time_transport.stop(); + + // If using multiple MPI ranks, perform all reduce on all transport results + domain_.all_reduce_replicated_source_regions(); + + // Normalize scalar flux and update volumes + domain_.normalize_scalar_flux_and_volumes( + settings::n_particles * RandomRay::distance_active_); + + // Add source to scalar flux, compute number of FSR hits + int64_t n_hits = domain_.add_source_to_scalar_flux(); + + // Compute random ray k-eff + k_eff_ = domain_.compute_k_eff(k_eff_); + + // Store random ray k-eff into OpenMC's native k-eff variable + global_tally_tracklength = k_eff_; + + // Execute all tallying tasks, if this is an active batch + if (simulation::current_batch > settings::n_inactive && mpi::master) { + + // Generate mapping between source regions and tallies + if (!domain_.mapped_all_tallies_) { + domain_.convert_source_regions_to_tallies(); + } + + // Use above mapping to contribute FSR flux data to appropriate tallies + domain_.random_ray_tally(); + + // Add this iteration's scalar flux estimate to final accumulated estimate + domain_.accumulate_iteration_flux(); + } + + // Set phi_old = phi_new + domain_.scalar_flux_old_.swap(domain_.scalar_flux_new_); + + // Check for any obvious insabilities/nans/infs + instability_check(n_hits, k_eff_, avg_miss_rate_); + + // Finalize the current batch + finalize_generation(); + finalize_batch(); + } // End random ray power iteration loop +} + +void RandomRaySimulation::reduce_simulation_statistics() +{ + // Reduce number of intersections +#ifdef OPENMC_MPI + if (mpi::n_procs > 1) { + uint64_t total_geometric_intersections_reduced = 0; + MPI_Reduce(&total_geometric_intersections_, + &total_geometric_intersections_reduced, 1, MPI_UNSIGNED_LONG, MPI_SUM, 0, + mpi::intracomm); + total_geometric_intersections_ = total_geometric_intersections_reduced; + } +#endif +} + +void RandomRaySimulation::output_simulation_results() const +{ + // Print random ray results + if (mpi::master) { + print_results_random_ray(total_geometric_intersections_, + avg_miss_rate_ / settings::n_batches, negroups_, + domain_.n_source_regions_); + if (model::plots.size() > 0) { + domain_.output_to_vtk(); + } + } +} + +// Apply a few sanity checks to catch obvious cases of numerical instability. +// Instability typically only occurs if ray density is extremely low. +void RandomRaySimulation::instability_check( + int64_t n_hits, double k_eff, double& avg_miss_rate) const +{ + double percent_missed = ((domain_.n_source_regions_ - n_hits) / + static_cast(domain_.n_source_regions_)) * + 100.0; + avg_miss_rate += percent_missed; + + if (percent_missed > 10.0) { + warning(fmt::format( + "Very high FSR miss rate detected ({:.3f}%). Instability may occur. " + "Increase ray density by adding more rays and/or active distance.", + percent_missed)); + } else if (percent_missed > 0.01) { + warning(fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing " + "ray density by adding more rays and/or active " + "distance may improve simulation efficiency.", + percent_missed)); + } + + if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) { + fatal_error("Instability detected"); + } +} + +// Print random ray simulation results +void RandomRaySimulation::print_results_random_ray( + uint64_t total_geometric_intersections, double avg_miss_rate, int negroups, + int64_t n_source_regions) const +{ + using namespace simulation; + + if (settings::verbosity >= 6) { + double total_integrations = total_geometric_intersections * negroups; + double time_per_integration = + simulation::time_transport.elapsed() / total_integrations; + double misc_time = time_total.elapsed() - time_update_src.elapsed() - + time_transport.elapsed() - time_tallies.elapsed() - + time_bank_sendrecv.elapsed(); + + header("Simulation Statistics", 4); + fmt::print( + " Total Iterations = {}\n", settings::n_batches); + fmt::print(" Flat Source Regions (FSRs) = {}\n", n_source_regions); + fmt::print(" Total Geometric Intersections = {:.4e}\n", + static_cast(total_geometric_intersections)); + fmt::print(" Avg per Iteration = {:.4e}\n", + static_cast(total_geometric_intersections) / settings::n_batches); + fmt::print(" Avg per Iteration per FSR = {:.2f}\n", + static_cast(total_geometric_intersections) / + static_cast(settings::n_batches) / n_source_regions); + fmt::print(" Avg FSR Miss Rate per Iteration = {:.4f}%\n", avg_miss_rate); + fmt::print(" Energy Groups = {}\n", negroups); + fmt::print( + " Total Integrations = {:.4e}\n", total_integrations); + fmt::print(" Avg per Iteration = {:.4e}\n", + total_integrations / settings::n_batches); + + header("Timing Statistics", 4); + show_time("Total time for initialization", time_initialize.elapsed()); + show_time("Reading cross sections", time_read_xs.elapsed(), 1); + show_time("Total simulation time", time_total.elapsed()); + show_time("Transport sweep only", time_transport.elapsed(), 1); + show_time("Source update only", time_update_src.elapsed(), 1); + show_time("Tally conversion only", time_tallies.elapsed(), 1); + show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1); + show_time("Other iteration routines", misc_time, 1); + if (settings::run_mode == RunMode::EIGENVALUE) { + show_time("Time in inactive batches", time_inactive.elapsed()); + } + show_time("Time in active batches", time_active.elapsed()); + show_time("Time writing statepoints", time_statepoint.elapsed()); + show_time("Total time for finalization", time_finalize.elapsed()); + show_time("Time per integration", time_per_integration); + } + + if (settings::verbosity >= 4) { + header("Results", 4); + fmt::print(" k-effective = {:.5f} +/- {:.5f}\n", + simulation::keff, simulation::keff_std); + } +} + +} // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index c0a3aacf3cc..6790f2cc0f7 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -24,6 +24,7 @@ #include "openmc/output.h" #include "openmc/plot.h" #include "openmc/random_lcg.h" +#include "openmc/random_ray/random_ray.h" #include "openmc/simulation.h" #include "openmc/source.h" #include "openmc/string_utils.h" @@ -96,6 +97,7 @@ int32_t gen_per_batch {1}; int64_t n_particles {-1}; int64_t max_particles_in_flight {100000}; +int max_particle_events {1000000}; ElectronTreatment electron_treatment {ElectronTreatment::TTB}; array energy_cutoff {0.0, 1000.0, 0.0, 0.0}; @@ -112,6 +114,7 @@ double res_scat_energy_min {0.01}; double res_scat_energy_max {1000.0}; vector res_scat_nuclides; RunMode run_mode {RunMode::UNSET}; +SolverType solver_type {SolverType::MONTE_CARLO}; std::unordered_set sourcepoint_batch; std::unordered_set statepoint_batch; std::unordered_set source_write_surf_id; @@ -156,6 +159,12 @@ void get_run_parameters(pugi::xml_node node_base) std::stoll(get_node_value(node_base, "max_particles_in_flight")); } + // Get maximum number of events allowed per particle + if (check_for_node(node_base, "max_particle_events")) { + max_particle_events = + std::stoll(get_node_value(node_base, "max_particle_events")); + } + // Get number of basic batches if (check_for_node(node_base, "batches")) { n_batches = std::stoi(get_node_value(node_base, "batches")); @@ -226,6 +235,38 @@ void get_run_parameters(pugi::xml_node node_base) } } } + + // Random ray variables + if (solver_type == SolverType::RANDOM_RAY) { + xml_node random_ray_node = node_base.child("random_ray"); + if (check_for_node(random_ray_node, "distance_active")) { + RandomRay::distance_active_ = + std::stod(get_node_value(random_ray_node, "distance_active")); + if (RandomRay::distance_active_ <= 0.0) { + fatal_error("Random ray active distance must be greater than 0"); + } + } else { + fatal_error("Specify random ray active distance in settings XML"); + } + if (check_for_node(random_ray_node, "distance_inactive")) { + RandomRay::distance_inactive_ = + std::stod(get_node_value(random_ray_node, "distance_inactive")); + if (RandomRay::distance_inactive_ < 0) { + fatal_error( + "Random ray inactive distance must be greater than or equal to 0"); + } + } else { + fatal_error("Specify random ray inactive distance in settings XML"); + } + if (check_for_node(random_ray_node, "source")) { + xml_node source_node = random_ray_node.child("source"); + // Get point to list of elements and make sure there is at least + // one + RandomRay::ray_source_ = Source::create(source_node); + } else { + fatal_error("Specify random ray source in settings XML"); + } + } } void read_settings_xml() @@ -382,6 +423,14 @@ void read_settings_xml(pugi::xml_node root) } } + // Check solver type + if (check_for_node(root, "random_ray")) { + solver_type = SolverType::RANDOM_RAY; + if (run_CE) + fatal_error("multi-group energy mode must be specified in settings XML " + "when using the random ray solver."); + } + if (run_mode == RunMode::EIGENVALUE || run_mode == RunMode::FIXED_SOURCE) { // Read run parameters get_run_parameters(node_mode); @@ -457,28 +506,7 @@ void read_settings_xml(pugi::xml_node root) // Get point to list of elements and make sure there is at least one for (pugi::xml_node node : root.children("source")) { - if (check_for_node(node, "file")) { - auto path = get_node_value(node, "file", false, true); - if (ends_with(path, ".mcpl") || ends_with(path, ".mcpl.gz")) { - auto sites = mcpl_source_sites(path); - model::external_sources.push_back(make_unique(sites)); - } else { - model::external_sources.push_back(make_unique(path)); - } - } else if (check_for_node(node, "library")) { - // Get shared library path and parameters - auto path = get_node_value(node, "library", false, true); - std::string parameters; - if (check_for_node(node, "parameters")) { - parameters = get_node_value(node, "parameters", false, true); - } - - // Create custom source - model::external_sources.push_back( - make_unique(path, parameters)); - } else { - model::external_sources.push_back(make_unique(node)); - } + model::external_sources.push_back(Source::create(node)); } // Check if the user has specified to read surface source diff --git a/src/simulation.cpp b/src/simulation.cpp index a7aea69424d..43d92060669 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -54,8 +54,14 @@ int openmc_run() openmc::simulation::time_total.start(); openmc_simulation_init(); - int err = 0; + // Ensure that a batch isn't executed in the case that the maximum number of + // batches has already been run in a restart statepoint file int status = 0; + if (openmc::simulation::current_batch >= openmc::settings::n_max_batches) { + status = openmc::STATUS_EXIT_MAX_BATCH; + } + + int err = 0; while (status == 0 && err == 0) { err = openmc_next_batch(&status); } @@ -122,7 +128,8 @@ int openmc_simulation_init() write_message("Resuming simulation...", 6); } else { // Only initialize primary source bank for eigenvalue simulations - if (settings::run_mode == RunMode::EIGENVALUE) { + if (settings::run_mode == RunMode::EIGENVALUE && + settings::solver_type == SolverType::MONTE_CARLO) { initialize_source(); } } @@ -132,7 +139,11 @@ int openmc_simulation_init() if (settings::run_mode == RunMode::FIXED_SOURCE) { header("FIXED SOURCE TRANSPORT SIMULATION", 3); } else if (settings::run_mode == RunMode::EIGENVALUE) { - header("K EIGENVALUE SIMULATION", 3); + if (settings::solver_type == SolverType::MONTE_CARLO) { + header("K EIGENVALUE SIMULATION", 3); + } else if (settings::solver_type == SolverType::RANDOM_RAY) { + header("K EIGENVALUE SIMULATION (RANDOM RAY SOLVER)", 3); + } if (settings::verbosity >= 7) print_columns(); } @@ -196,10 +207,12 @@ int openmc_simulation_finalize() simulation::time_finalize.stop(); simulation::time_total.stop(); if (mpi::master) { - if (settings::verbosity >= 6) - print_runtime(); - if (settings::verbosity >= 4) - print_results(); + if (settings::solver_type != SolverType::RANDOM_RAY) { + if (settings::verbosity >= 6) + print_runtime(); + if (settings::verbosity >= 4) + print_results(); + } } if (settings::check_overlaps) print_overlap_check(); @@ -309,7 +322,8 @@ vector work_index; void allocate_banks() { - if (settings::run_mode == RunMode::EIGENVALUE) { + if (settings::run_mode == RunMode::EIGENVALUE && + settings::solver_type == SolverType::MONTE_CARLO) { // Allocate source bank simulation::source_bank.resize(simulation::work_per_rank); @@ -493,7 +507,8 @@ void finalize_generation() } global_tally_leakage = 0.0; - if (settings::run_mode == RunMode::EIGENVALUE) { + if (settings::run_mode == RunMode::EIGENVALUE && + settings::solver_type == SolverType::MONTE_CARLO) { // If using shared memory, stable sort the fission bank (by parent IDs) // so as to allow for reproducibility regardless of which order particles // are run in. @@ -501,6 +516,9 @@ void finalize_generation() // Distribute fission bank across processors evenly synchronize_bank(); + } + + if (settings::run_mode == RunMode::EIGENVALUE) { // Calculate shannon entropy if (settings::entropy_on) diff --git a/src/source.cpp b/src/source.cpp index 3251ca0d0d8..5ddac7f4287 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -22,6 +22,7 @@ #include "openmc/geometry.h" #include "openmc/hdf5_interface.h" #include "openmc/material.h" +#include "openmc/mcpl_interface.h" #include "openmc/memory.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" @@ -31,6 +32,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/state_point.h" +#include "openmc/string_utils.h" #include "openmc/xml_interface.h" namespace openmc { @@ -44,6 +46,39 @@ namespace model { vector> external_sources; } +//============================================================================== +// Source create implementation +//============================================================================== + +unique_ptr Source::create(pugi::xml_node node) +{ + // if the source type is present, use it to determine the type + // of object to create + if (check_for_node(node, "type")) { + std::string source_type = get_node_value(node, "type"); + if (source_type == "independent") { + return make_unique(node); + } else if (source_type == "file") { + return make_unique(node); + } else if (source_type == "compiled") { + return make_unique(node); + } else if (source_type == "mesh") { + return make_unique(node); + } else { + fatal_error(fmt::format("Invalid source type '{}' found.", source_type)); + } + } else { + // support legacy source format + if (check_for_node(node, "file")) { + return make_unique(node); + } else if (check_for_node(node, "library")) { + return make_unique(node); + } else { + return make_unique(node); + } + } +} + //============================================================================== // IndependentSource implementation //============================================================================== @@ -81,32 +116,7 @@ IndependentSource::IndependentSource(pugi::xml_node node) // Spatial distribution for external source if (check_for_node(node, "space")) { - // Get pointer to spatial distribution - pugi::xml_node node_space = node.child("space"); - - // Check for type of spatial distribution and read - std::string type; - if (check_for_node(node_space, "type")) - type = get_node_value(node_space, "type", true, true); - if (type == "cartesian") { - space_ = UPtrSpace {new CartesianIndependent(node_space)}; - } else if (type == "cylindrical") { - space_ = UPtrSpace {new CylindricalIndependent(node_space)}; - } else if (type == "spherical") { - space_ = UPtrSpace {new SphericalIndependent(node_space)}; - } else if (type == "mesh") { - space_ = UPtrSpace {new MeshSpatial(node_space)}; - } else if (type == "box") { - space_ = UPtrSpace {new SpatialBox(node_space)}; - } else if (type == "fission") { - space_ = UPtrSpace {new SpatialBox(node_space, true)}; - } else if (type == "point") { - space_ = UPtrSpace {new SpatialPoint(node_space)}; - } else { - fatal_error(fmt::format( - "Invalid spatial distribution for external source: {}", type)); - } - + space_ = SpatialDistribution::create(node.child("space")); } else { // If no spatial distribution specified, make it a point source space_ = UPtrSpace {new SpatialPoint()}; @@ -114,24 +124,7 @@ IndependentSource::IndependentSource(pugi::xml_node node) // Determine external source angular distribution if (check_for_node(node, "angle")) { - // Get pointer to angular distribution - pugi::xml_node node_angle = node.child("angle"); - - // Check for type of angular distribution - std::string type; - if (check_for_node(node_angle, "type")) - type = get_node_value(node_angle, "type", true, true); - if (type == "isotropic") { - angle_ = UPtrAngle {new Isotropic()}; - } else if (type == "monodirectional") { - angle_ = UPtrAngle {new Monodirectional(node_angle)}; - } else if (type == "mu-phi") { - angle_ = UPtrAngle {new PolarAzimuthal(node_angle)}; - } else { - fatal_error(fmt::format( - "Invalid angular distribution for external source: {}", type)); - } - + angle_ = UnitSphereDistribution::create(node.child("angle")); } else { angle_ = UPtrAngle {new Isotropic()}; } @@ -208,7 +201,7 @@ SourceSite IndependentSource::sample(uint64_t* seed) const if (mat_index == MATERIAL_VOID) { found = false; } else { - found = model::materials[mat_index]->fissionable_; + found = model::materials[mat_index]->fissionable(); } } } @@ -222,10 +215,10 @@ SourceSite IndependentSource::sample(uint64_t* seed) const found = contains(domain_ids_, model::materials[mat_index]->id()); } } else { - for (const auto& coord : p.coord()) { + for (int i = 0; i < p.n_coord(); i++) { auto id = (domain_type_ == DomainType::CELL) - ? model::cells[coord.cell]->id_ - : model::universes[coord.universe]->id_; + ? model::cells[p.coord(i).cell]->id_ + : model::universes[p.coord(i).universe]->id_; if ((found = contains(domain_ids_, id))) break; } @@ -250,36 +243,40 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // Sample angle site.u = angle_->sample(seed); - // Check for monoenergetic source above maximum particle energy - auto p = static_cast(particle_); - auto energy_ptr = dynamic_cast(energy_.get()); - if (energy_ptr) { - auto energies = xt::adapt(energy_ptr->x()); - if (xt::any(energies > data::energy_max[p])) { - fatal_error("Source energy above range of energies of at least " - "one cross section table"); + // Sample energy and time for neutron and photon sources + if (settings::solver_type != SolverType::RANDOM_RAY) { + // Check for monoenergetic source above maximum particle energy + auto p = static_cast(particle_); + auto energy_ptr = dynamic_cast(energy_.get()); + if (energy_ptr) { + auto energies = xt::adapt(energy_ptr->x()); + if (xt::any(energies > data::energy_max[p])) { + fatal_error("Source energy above range of energies of at least " + "one cross section table"); + } } - } - while (true) { - // Sample energy spectrum - site.E = energy_->sample(seed); + while (true) { + // Sample energy spectrum + site.E = energy_->sample(seed); - // Resample if energy falls above maximum particle energy - if (site.E < data::energy_max[p]) - break; + // Resample if energy falls above maximum particle energy + if (site.E < data::energy_max[p]) + break; - n_reject++; - if (n_reject >= EXTSRC_REJECT_THRESHOLD && - static_cast(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) { - fatal_error("More than 95% of external source sites sampled were " - "rejected. Please check your external source energy spectrum " - "definition."); + n_reject++; + if (n_reject >= EXTSRC_REJECT_THRESHOLD && + static_cast(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) { + fatal_error( + "More than 95% of external source sites sampled were " + "rejected. Please check your external source energy spectrum " + "definition."); + } } - } - // Sample particle creation time - site.time = time_->sample(seed); + // Sample particle creation time + site.time = time_->sample(seed); + } // Increment number of accepted samples ++n_accept; @@ -290,8 +287,22 @@ SourceSite IndependentSource::sample(uint64_t* seed) const //============================================================================== // FileSource implementation //============================================================================== +FileSource::FileSource(pugi::xml_node node) +{ + auto path = get_node_value(node, "file", false, true); + if (ends_with(path, ".mcpl") || ends_with(path, ".mcpl.gz")) { + sites_ = mcpl_source_sites(path); + } else { + this->load_sites_from_file(path); + } +} -FileSource::FileSource(std::string path) +FileSource::FileSource(const std::string& path) +{ + load_sites_from_file(path); +} + +void FileSource::load_sites_from_file(const std::string& path) { // Check if source file exists if (!file_exists(path)) { @@ -328,9 +339,19 @@ SourceSite FileSource::sample(uint64_t* seed) const //============================================================================== // CompiledSourceWrapper implementation //============================================================================== +CompiledSourceWrapper::CompiledSourceWrapper(pugi::xml_node node) +{ + // Get shared library path and parameters + auto path = get_node_value(node, "library", false, true); + std::string parameters; + if (check_for_node(node, "parameters")) { + parameters = get_node_value(node, "parameters", false, true); + } + setup(path, parameters); +} -CompiledSourceWrapper::CompiledSourceWrapper( - std::string path, std::string parameters) +void CompiledSourceWrapper::setup( + const std::string& path, const std::string& parameters) { #ifdef HAS_DYNAMIC_LINKING // Open the library @@ -378,6 +399,50 @@ CompiledSourceWrapper::~CompiledSourceWrapper() #endif } +//============================================================================== +// MeshSource implementation +//============================================================================== + +MeshSource::MeshSource(pugi::xml_node node) +{ + int32_t mesh_id = stoi(get_node_value(node, "mesh")); + int32_t mesh_idx = model::mesh_map.at(mesh_id); + const auto& mesh = model::meshes[mesh_idx]; + + std::vector strengths; + // read all source distributions and populate strengths vector for MeshSpatial + // object + for (auto source_node : node.children("source")) { + sources_.emplace_back(Source::create(source_node)); + strengths.push_back(sources_.back()->strength()); + } + + // the number of source distributions should either be one or equal to the + // number of mesh elements + if (sources_.size() > 1 && sources_.size() != mesh->n_bins()) { + fatal_error(fmt::format("Incorrect number of source distributions ({}) for " + "mesh source with {} elements.", + sources_.size(), mesh->n_bins())); + } + + space_ = std::make_unique(mesh_idx, strengths); +} + +SourceSite MeshSource::sample(uint64_t* seed) const +{ + // sample location and element from mesh + auto mesh_location = space_->sample_mesh(seed); + + // Sample source for the chosen element + int32_t element = mesh_location.first; + SourceSite site = source(element)->sample(seed); + + // Replace spatial position with the one already sampled + site.r = mesh_location.second; + + return site; +} + //============================================================================== // Non-member functions //============================================================================== diff --git a/src/state_point.cpp b/src/state_point.cpp index bff62132060..c7b7d6ad85c 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -364,11 +364,27 @@ void restart_set_keff() void load_state_point() { - // Write message - write_message("Loading state point " + settings::path_statepoint + "...", 5); + write_message( + fmt::format("Loading state point {}...", settings::path_statepoint_c), 5); + openmc_statepoint_load(settings::path_statepoint.c_str()); +} +void statepoint_version_check(hid_t file_id) +{ + // Read revision number for state point file and make sure it matches with + // current version + array version_array; + read_attribute(file_id, "version", version_array); + if (version_array != VERSION_STATEPOINT) { + fatal_error( + "State point version does not match current version in OpenMC."); + } +} + +extern "C" int openmc_statepoint_load(const char* filename) +{ // Open file for reading - hid_t file_id = file_open(settings::path_statepoint.c_str(), 'r', true); + hid_t file_id = file_open(filename, 'r', true); // Read filetype std::string word; @@ -377,14 +393,7 @@ void load_state_point() fatal_error("OpenMC tried to restart from a non-statepoint file."); } - // Read revision number for state point file and make sure it matches with - // current version - array array; - read_attribute(file_id, "version", array); - if (array != VERSION_STATEPOINT) { - fatal_error( - "State point version does not match current version in OpenMC."); - } + statepoint_version_check(file_id); // Read and overwrite random number seed int64_t seed; @@ -421,9 +430,10 @@ void load_state_point() read_dataset(file_id, "current_batch", simulation::restart_batch); if (simulation::restart_batch >= settings::n_max_batches) { - fatal_error(fmt::format( - "The number of batches specified for simulation ({}) is smaller" - " than the number of batches in the restart statepoint file ({})", + warning(fmt::format( + "The number of batches specified for simulation ({}) is smaller " + "than or equal to the number of batches in the restart statepoint file " + "({})", settings::n_max_batches, simulation::restart_batch)); } @@ -489,7 +499,6 @@ void load_state_point() if (internal) { tally->writable_ = false; } else { - auto& results = tally->results_; read_tally_results(tally_group, results.shape()[0], results.shape()[1], results.data()); @@ -497,7 +506,6 @@ void load_state_point() close_group(tally_group); } } - close_group(tallies_group); } } @@ -525,6 +533,8 @@ void load_state_point() // Close file file_close(file_id); + + return 0; } hid_t h5banktype() diff --git a/src/surface.cpp b/src/surface.cpp index 01e2f275eb0..12fef070e18 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -80,19 +80,36 @@ Surface::Surface(pugi::xml_node surf_node) if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) { // Leave the bc_ a nullptr } else if (surf_bc == "vacuum") { - bc_ = std::make_shared(); + bc_ = make_unique(); } else if (surf_bc == "reflective" || surf_bc == "reflect" || surf_bc == "reflecting") { - bc_ = std::make_shared(); + bc_ = make_unique(); } else if (surf_bc == "white") { - bc_ = std::make_shared(); + bc_ = make_unique(); } else if (surf_bc == "periodic") { - // periodic BC's are handled separately + // Periodic BCs are handled separately } else { fatal_error(fmt::format("Unknown boundary condition \"{}\" specified " "on surface {}", surf_bc, id_)); } + + if (check_for_node(surf_node, "albedo") && bc_) { + double surf_alb = std::stod(get_node_value(surf_node, "albedo")); + + if (surf_alb < 0.0) + fatal_error(fmt::format("Surface {} has an albedo of {}. " + "Albedo values must be positive.", + id_, surf_alb)); + + if (surf_alb > 1.0) + warning(fmt::format("Surface {} has an albedo of {}. " + "Albedos greater than 1 may cause " + "unphysical behaviour.", + id_, surf_alb)); + + bc_->set_albedo(surf_alb); + } } } @@ -112,7 +129,7 @@ bool Surface::sense(Position r, Direction u) const return f > 0.0; } -Direction Surface::reflect(Position r, Direction u, Particle* p) const +Direction Surface::reflect(Position r, Direction u, GeometryState* p) const { // Determine projection of direction onto normal and squared magnitude of // normal. @@ -123,7 +140,7 @@ Direction Surface::reflect(Position r, Direction u, Particle* p) const } Direction Surface::diffuse_reflect( - Position r, Direction u, uint64_t* seed) const + Position r, Direction u, uint64_t* seed, GeometryState* p) const { // Diffuse reflect direction according to the normal. // cosine distribution @@ -154,6 +171,7 @@ void Surface::to_hdf5(hid_t group_id) const if (bc_) { write_string(surf_group, "boundary_type", bc_->type(), false); + bc_->to_hdf5(surf_group); } else { write_string(surf_group, "boundary_type", "transmission", false); } @@ -1156,9 +1174,10 @@ void read_surfaces(pugi::xml_node node) } // Loop over XML surface elements and populate the array. Keep track of - // periodic surfaces. + // periodic surfaces and their albedos. model::surfaces.reserve(n_surfaces); std::set> periodic_pairs; + std::unordered_map albedo_map; { pugi::xml_node surf_node; int i_surf; @@ -1221,6 +1240,12 @@ void read_surfaces(pugi::xml_node node) if (check_for_node(surf_node, "boundary")) { std::string surf_bc = get_node_value(surf_node, "boundary", true, true); if (surf_bc == "periodic") { + // Check for surface albedo. Skip sanity check as it is already done + // in the Surface class's constructor. + if (check_for_node(surf_node, "albedo")) { + albedo_map[model::surfaces.back()->id_] = + std::stod(get_node_value(surf_node, "albedo")); + } if (check_for_node(surf_node, "periodic_surface_id")) { int i_periodic = std::stoi(get_node_value(surf_node, "periodic_surface_id")); @@ -1284,7 +1309,7 @@ void read_surfaces(pugi::xml_node node) periodic_pairs.erase(second_unresolved); } - // Assign the periodic boundary conditions + // Assign the periodic boundary conditions with albedos for (auto periodic_pair : periodic_pairs) { int i_surf = model::surface_map[periodic_pair.first]; int j_surf = model::surface_map[periodic_pair.second]; @@ -1302,11 +1327,19 @@ void read_surfaces(pugi::xml_node node) // planes are parallel which indicates a translational periodic boundary // condition. Otherwise, it is a rotational periodic BC. if (std::abs(1.0 - dot_prod) < FP_PRECISION) { - surf1.bc_ = std::make_shared(i_surf, j_surf); - surf2.bc_ = surf1.bc_; + surf1.bc_ = make_unique(i_surf, j_surf); + surf2.bc_ = make_unique(i_surf, j_surf); } else { - surf1.bc_ = std::make_shared(i_surf, j_surf); - surf2.bc_ = surf1.bc_; + surf1.bc_ = make_unique(i_surf, j_surf); + surf2.bc_ = make_unique(i_surf, j_surf); + } + + // If albedo data is present in albedo map, set the boundary albedo. + if (albedo_map.count(surf1.id_)) { + surf1.bc_->set_albedo(albedo_map[surf1.id_]); + } + if (albedo_map.count(surf2.id_)) { + surf2.bc_->set_albedo(albedo_map[surf2.id_]); } } } diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index d8efd590f96..ff7a3416b90 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -21,7 +21,9 @@ #include "openmc/tallies/filter_energyfunc.h" #include "openmc/tallies/filter_legendre.h" #include "openmc/tallies/filter_material.h" +#include "openmc/tallies/filter_materialfrom.h" #include "openmc/tallies/filter_mesh.h" +#include "openmc/tallies/filter_meshborn.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_mu.h" #include "openmc/tallies/filter_particle.h" @@ -121,8 +123,12 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "material") { return Filter::create(id); + } else if (type == "materialfrom") { + return Filter::create(id); } else if (type == "mesh") { return Filter::create(id); + } else if (type == "meshborn") { + return Filter::create(id); } else if (type == "meshsurface") { return Filter::create(id); } else if (type == "mu") { diff --git a/src/tallies/filter_distribcell.cpp b/src/tallies/filter_distribcell.cpp index 89349b61f08..c754dbd44ab 100644 --- a/src/tallies/filter_distribcell.cpp +++ b/src/tallies/filter_distribcell.cpp @@ -48,7 +48,8 @@ void DistribcellFilter::get_all_bins( auto& lat {*model::lattices[p.coord(i + 1).lattice]}; const auto& i_xyz {p.coord(i + 1).lattice_i}; if (lat.are_valid_indices(i_xyz)) { - offset += lat.offset(distribcell_index, i_xyz); + offset += + lat.offset(distribcell_index, i_xyz) + c.offset_[distribcell_index]; } } if (cell_ == p.coord(i).cell) { diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 48448cb1f05..4767dd175f0 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -3,7 +3,7 @@ #include #include "openmc/capi.h" -#include "openmc/constants.h" // For F90_NONE +#include "openmc/constants.h" // For C_NONE #include "openmc/mgxs_interface.h" #include "openmc/search.h" #include "openmc/settings.h" @@ -59,7 +59,7 @@ void EnergyFilter::set_bins(gsl::span bins) void EnergyFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - if (p.g() != F90_NONE && matches_transport_groups_) { + if (p.g() != C_NONE && matches_transport_groups_) { if (estimator == TallyEstimator::TRACKLENGTH) { match.bins_.push_back(data::mg.num_energy_groups_ - p.g() - 1); } else { @@ -98,7 +98,7 @@ std::string EnergyFilter::text_label(int bin) const void EnergyoutFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - if (p.g() != F90_NONE && matches_transport_groups_) { + if (p.g() != C_NONE && matches_transport_groups_) { match.bins_.push_back(data::mg.num_energy_groups_ - p.g() - 1); match.weights_.push_back(1.0); diff --git a/src/tallies/filter_materialfrom.cpp b/src/tallies/filter_materialfrom.cpp new file mode 100644 index 00000000000..91f03aef85e --- /dev/null +++ b/src/tallies/filter_materialfrom.cpp @@ -0,0 +1,24 @@ +#include "openmc/tallies/filter_materialfrom.h" + +#include "openmc/cell.h" +#include "openmc/material.h" + +namespace openmc { + +void MaterialFromFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + auto search = map_.find(p.material_last()); + if (search != map_.end()) { + match.bins_.push_back(search->second); + match.weights_.push_back(1.0); + } +} + +std::string MaterialFromFilter::text_label(int bin) const +{ + return "Material from " + + std::to_string(model::materials[materials_[bin]]->id_); +} + +} // namespace openmc diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index deb143346c5..5b01da1f65a 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -77,8 +77,10 @@ std::string MeshFilter::text_label(int bin) const void MeshFilter::set_mesh(int32_t mesh) { + // perform any additional perparation for mesh tallies here mesh_ = mesh; n_bins_ = model::meshes[mesh_]->n_bins(); + model::meshes[mesh_]->prepare_for_tallies(); } void MeshFilter::set_translation(const Position& translation) @@ -159,6 +161,7 @@ extern "C" int openmc_mesh_filter_get_translation( // Check the filter type const auto& filter = model::tally_filters[index]; if (filter->type() != FilterType::MESH && + filter->type() != FilterType::MESHBORN && filter->type() != FilterType::MESH_SURFACE) { set_errmsg("Tried to get a translation from a non-mesh-based filter."); return OPENMC_E_INVALID_TYPE; @@ -184,6 +187,7 @@ extern "C" int openmc_mesh_filter_set_translation( const auto& filter = model::tally_filters[index]; // Check the filter type if (filter->type() != FilterType::MESH && + filter->type() != FilterType::MESHBORN && filter->type() != FilterType::MESH_SURFACE) { set_errmsg("Tried to set mesh on a non-mesh-based filter."); return OPENMC_E_INVALID_TYPE; diff --git a/src/tallies/filter_meshborn.cpp b/src/tallies/filter_meshborn.cpp new file mode 100644 index 00000000000..c95dc3dc78a --- /dev/null +++ b/src/tallies/filter_meshborn.cpp @@ -0,0 +1,61 @@ +#include "openmc/tallies/filter_meshborn.h" + +#include "openmc/capi.h" +#include "openmc/constants.h" +#include "openmc/error.h" +#include "openmc/mesh.h" + +namespace openmc { + +void MeshBornFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + Position r_born = p.r_born(); + + // apply translation if present + if (translated_) { + r_born -= translation(); + } + + auto bin = model::meshes[mesh_]->get_bin(r_born); + if (bin >= 0) { + match.bins_.push_back(bin); + match.weights_.push_back(1.0); + } +} + +std::string MeshBornFilter::text_label(int bin) const +{ + auto& mesh = *model::meshes.at(mesh_); + return mesh.bin_label(bin) + " (born)"; +} + +//============================================================================== +// C-API functions +//============================================================================== + +extern "C" int openmc_meshborn_filter_get_mesh( + int32_t index, int32_t* index_mesh) +{ + return openmc_mesh_filter_get_mesh(index, index_mesh); +} + +extern "C" int openmc_meshborn_filter_set_mesh( + int32_t index, int32_t index_mesh) +{ + return openmc_mesh_filter_set_mesh(index, index_mesh); +} + +extern "C" int openmc_meshborn_filter_get_translation( + int32_t index, double translation[3]) +{ + return openmc_mesh_filter_get_translation(index, translation); +} + +extern "C" int openmc_meshborn_filter_set_translation( + int32_t index, double translation[3]) +{ + return openmc_mesh_filter_set_translation(index, translation); +} + +} // namespace openmc diff --git a/src/tallies/filter_meshsurface.cpp b/src/tallies/filter_meshsurface.cpp index b22085ebbfe..b26cd198b32 100644 --- a/src/tallies/filter_meshsurface.cpp +++ b/src/tallies/filter_meshsurface.cpp @@ -100,4 +100,16 @@ extern "C" int openmc_meshsurface_filter_set_mesh( return openmc_mesh_filter_set_mesh(index, index_mesh); } +extern "C" int openmc_meshsurface_filter_get_translation( + int32_t index, double translation[3]) +{ + return openmc_mesh_filter_get_translation(index, translation); +} + +extern "C" int openmc_meshsurface_filter_set_translation( + int32_t index, double translation[3]) +{ + return openmc_mesh_filter_set_translation(index, translation); +} + } // namespace openmc diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 7fd444a58b4..674987b8f13 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -26,6 +26,7 @@ #include "openmc/tallies/filter_energy.h" #include "openmc/tallies/filter_legendre.h" #include "openmc/tallies/filter_mesh.h" +#include "openmc/tallies/filter_meshborn.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_sph_harm.h" @@ -689,6 +690,12 @@ void Tally::init_triggers(pugi::xml_node node) "Must specify trigger threshold for tally {} in tally XML file", id_)); } + // Read whether to allow zero-tally bins to be ignored. + bool ignore_zeros = false; + if (check_for_node(trigger_node, "ignore_zeros")) { + ignore_zeros = get_node_value_bool(trigger_node, "ignore_zeros"); + } + // Read the trigger scores. vector trigger_scores; if (check_for_node(trigger_node, "scores")) { @@ -702,7 +709,7 @@ void Tally::init_triggers(pugi::xml_node node) if (score_str == "all") { triggers_.reserve(triggers_.size() + this->scores_.size()); for (auto i_score = 0; i_score < this->scores_.size(); ++i_score) { - triggers_.push_back({metric, threshold, i_score}); + triggers_.push_back({metric, threshold, ignore_zeros, i_score}); } } else { int i_score = 0; @@ -716,7 +723,7 @@ void Tally::init_triggers(pugi::xml_node node) "{} but it was listed in a trigger on that tally", score_str, id_)); } - triggers_.push_back({metric, threshold, i_score}); + triggers_.push_back({metric, threshold, ignore_zeros, i_score}); } } } @@ -756,6 +763,10 @@ void Tally::accumulate() double norm = total_source / (settings::n_particles * settings::gen_per_batch); + if (settings::solver_type == SolverType::RANDOM_RAY) { + norm = 1.0; + } + // Accumulate each result #pragma omp parallel for for (int i = 0; i < results_.shape()[0]; ++i) { @@ -952,8 +963,9 @@ void accumulate_tallies() { #ifdef OPENMC_MPI // Combine tally results onto master process - if (mpi::n_procs > 1) + if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) { reduce_tally_results(); + } #endif // Increase number of realizations (only used for global tallies) diff --git a/src/tallies/trigger.cpp b/src/tallies/trigger.cpp index 87f298baa10..f1f83e2982c 100644 --- a/src/tallies/trigger.cpp +++ b/src/tallies/trigger.cpp @@ -77,9 +77,9 @@ void check_tally_triggers(double& ratio, int& tally_id, int& score) auto uncert_pair = get_tally_uncertainty(i_tally, trigger.score_index, filter_index); - // if there is a score without contributions, set ratio to inf and - // exit early - if (uncert_pair.first == -1) { + // If there is a score without contributions, set ratio to inf and + // exit early, unless zero scores are ignored for this trigger. + if (uncert_pair.first == -1 && !trigger.ignore_zeros) { ratio = INFINITY; score = t.scores_[trigger.score_index]; tally_id = t.id_; diff --git a/src/timer.cpp b/src/timer.cpp index 86436758a3f..6d692d4fbf6 100644 --- a/src/timer.cpp +++ b/src/timer.cpp @@ -26,6 +26,7 @@ Timer time_event_advance_particle; Timer time_event_surface_crossing; Timer time_event_collision; Timer time_event_death; +Timer time_update_src; } // namespace simulation @@ -85,6 +86,7 @@ void reset_timers() simulation::time_event_surface_crossing.reset(); simulation::time_event_collision.reset(); simulation::time_event_death.reset(); + simulation::time_update_src.reset(); } } // namespace openmc diff --git a/src/universe.cpp b/src/universe.cpp index 67e1d1354c3..b4ef6b4b265 100644 --- a/src/universe.cpp +++ b/src/universe.cpp @@ -3,6 +3,7 @@ #include #include "openmc/hdf5_interface.h" +#include "openmc/particle.h" namespace openmc { @@ -36,7 +37,7 @@ void Universe::to_hdf5(hid_t universes_group) const close_group(group); } -bool Universe::find_cell(Particle& p) const +bool Universe::find_cell(GeometryState& p) const { const auto& cells { !partitioner_ ? cells_ : partitioner_->get_cells(p.r_local(), p.u_local())}; diff --git a/src/volume_calc.cpp b/src/volume_calc.cpp index 8cd697a9d84..8b5c27f14eb 100644 --- a/src/volume_calc.cpp +++ b/src/volume_calc.cpp @@ -10,18 +10,16 @@ #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" +#include "openmc/openmp_interface.h" #include "openmc/output.h" #include "openmc/random_lcg.h" #include "openmc/settings.h" #include "openmc/timer.h" #include "openmc/xml_interface.h" -#include -#ifdef _OPENMP -#include -#endif #include "xtensor/xadapt.hpp" #include "xtensor/xview.hpp" +#include #include // for copy #include // for pow, sqrt @@ -161,7 +159,7 @@ vector VolumeCalculation::execute() const p.n_coord() = 1; Position xi {prn(&seed), prn(&seed), prn(&seed)}; p.r() = lower_left_ + xi * (upper_right_ - lower_left_); - p.u() = {0.5, 0.5, 0.5}; + p.u() = {1. / std::sqrt(3.), 1. / std::sqrt(3.), 1. / std::sqrt(3.)}; // If this location is not in the geometry at all, move on to next block if (!exhaustive_find_cell(p)) @@ -205,37 +203,9 @@ vector VolumeCalculation::execute() const // At this point, each thread has its own pair of index/hits lists and we // now need to reduce them. OpenMP is not nearly smart enough to do this // on its own, so we have to manually reduce them - -#ifdef _OPENMP - int n_threads = omp_get_num_threads(); -#else - int n_threads = 1; -#endif - -#pragma omp for ordered schedule(static) - for (int i = 0; i < n_threads; ++i) { -#pragma omp ordered - for (int i_domain = 0; i_domain < n; ++i_domain) { - for (int j = 0; j < indices[i_domain].size(); ++j) { - // Check if this material has been added to the master list and if - // so, accumulate the number of hits - bool already_added = false; - for (int k = 0; k < master_indices[i_domain].size(); k++) { - if (indices[i_domain][j] == master_indices[i_domain][k]) { - master_hits[i_domain][k] += hits[i_domain][j]; - already_added = true; - break; - } - } - if (!already_added) { - // If we made it here, the material hasn't yet been added to the - // master list, so add entries to the master indices and master - // hits lists - master_indices[i_domain].push_back(indices[i_domain][j]); - master_hits[i_domain].push_back(hits[i_domain][j]); - } - } - } + for (int i_domain = 0; i_domain < n; ++i_domain) { + reduce_indices_hits(indices[i_domain], hits[i_domain], + master_indices[i_domain], master_hits[i_domain]); } } // omp parallel diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 50ae9a20821..24ec1b7a90e 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -2,6 +2,7 @@ set(TEST_NAMES test_distribution test_file_utils test_tally + test_interpolate # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_interpolate.cpp b/tests/cpp_unit_tests/test_interpolate.cpp new file mode 100644 index 00000000000..4f19f2b63f3 --- /dev/null +++ b/tests/cpp_unit_tests/test_interpolate.cpp @@ -0,0 +1,51 @@ +#include +#include + +#include +#include + +#include "openmc/interpolate.h" +#include "openmc/search.h" + +using namespace openmc; + +TEST_CASE("Test Lagranian Interpolation") +{ + std::vector xs {0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + std::vector ys {0.0, 1.0, 1.0, 2.0, 3.0, 3.0, 5.0}; + + // ensure we get data points back at the x values + for (int n = 1; n <= 6; n++) { + for (int i = 0; i < xs.size(); i++) { + double x = xs[i]; + double y = ys[i]; + + size_t idx = lower_bound_index(xs.begin(), xs.end(), x); + idx = std::min(idx, xs.size() - n - 1); + double out = interpolate_lagrangian(xs, ys, idx, x, n); + REQUIRE(out == y); + } + } + + // spot checks based on an independent implementation of Lagrangian + // interpolation + std::map>> checks; + checks[1] = {{0.5, 0.5}, {4.5, 3.0}, {2.5, 1.5}, {5.5, 4.0}}; + checks[2] = {{2.5, 1.5}, {4.5, 2.75}, {4.9999, 3.0}, {4.00001, 3.0}}; + checks[3] = {{2.5592, 1.5}, {4.5, 2.9375}, {4.9999, 3.0}, {4.00001, 3.0}}; + + for (auto check_set : checks) { + int order = check_set.first; + auto checks = check_set.second; + + for (auto check : checks) { + double input = check.first; + double exp_output = check.second; + + size_t idx = lower_bound_index(xs.begin(), xs.end(), input); + idx = std::min(idx, xs.size() - order - 1); + double out = interpolate_lagrangian(xs, ys, idx, input, order); + REQUIRE_THAT(out, Catch::Matchers::WithinAbs(exp_output, 1e-04)); + } + } +} \ No newline at end of file diff --git a/tests/regression_tests/adj_cell_rotation/test.py b/tests/regression_tests/adj_cell_rotation/test.py index 9b867c18e1d..3fe24053665 100644 --- a/tests/regression_tests/adj_cell_rotation/test.py +++ b/tests/regression_tests/adj_cell_rotation/test.py @@ -24,14 +24,14 @@ def model(): # Create one cell on top of the other. Only one # has a rotation - box = openmc.rectangular_prism(15., 15., 'z', boundary_type='vacuum') + box = openmc.model.RectangularPrism(15., 15., 'z', boundary_type='vacuum') lower_z = openmc.ZPlane(-7.5, boundary_type='vacuum') upper_z = openmc.ZPlane(22.5, boundary_type='vacuum') middle_z = openmc.ZPlane(7.5) - lower_cell = openmc.Cell(fill=univ, region=box & +lower_z & -middle_z) + lower_cell = openmc.Cell(fill=univ, region=-box & +lower_z & -middle_z) lower_cell.rotation = (10, 20, 30) - upper_cell = openmc.Cell(fill=univ, region=box & +middle_z & -upper_z) + upper_cell = openmc.Cell(fill=univ, region=-box & +middle_z & -upper_z) upper_cell.translation = (0, 0, 15) model.geometry = openmc.Geometry(root=[lower_cell, upper_cell]) diff --git a/tests/regression_tests/albedo_box/__init__.py b/tests/regression_tests/albedo_box/__init__.py new file mode 100755 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/albedo_box/geometry.xml b/tests/regression_tests/albedo_box/geometry.xml new file mode 100644 index 00000000000..7d0e9b5f5dd --- /dev/null +++ b/tests/regression_tests/albedo_box/geometry.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/tests/regression_tests/albedo_box/materials.xml b/tests/regression_tests/albedo_box/materials.xml new file mode 100644 index 00000000000..2472a747174 --- /dev/null +++ b/tests/regression_tests/albedo_box/materials.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/regression_tests/albedo_box/results_true.dat b/tests/regression_tests/albedo_box/results_true.dat new file mode 100644 index 00000000000..0f571f2459d --- /dev/null +++ b/tests/regression_tests/albedo_box/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.590800E+00 4.251788E-03 diff --git a/tests/regression_tests/albedo_box/settings.xml b/tests/regression_tests/albedo_box/settings.xml new file mode 100644 index 00000000000..cc66683057c --- /dev/null +++ b/tests/regression_tests/albedo_box/settings.xml @@ -0,0 +1,9 @@ + + + + eigenvalue + 10 + 5 + 1000 + + diff --git a/tests/regression_tests/albedo_box/test.py b/tests/regression_tests/albedo_box/test.py new file mode 100755 index 00000000000..179f58e5b36 --- /dev/null +++ b/tests/regression_tests/albedo_box/test.py @@ -0,0 +1,6 @@ +from tests.testing_harness import TestHarness + + +def test_albedo_box(): + harness = TestHarness('statepoint.10.h5') + harness.main() diff --git a/tests/regression_tests/cpp_driver/test.py b/tests/regression_tests/cpp_driver/test.py index e8b1c62accb..b80e82ee0e1 100644 --- a/tests/regression_tests/cpp_driver/test.py +++ b/tests/regression_tests/cpp_driver/test.py @@ -20,7 +20,7 @@ def cpp_driver(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_cpp_driver CXX) add_executable(cpp_driver driver.cpp) find_package(OpenMC REQUIRED HINTS {}) @@ -92,10 +92,9 @@ def model(): lattice.pitch = (4.0, 4.0) lattice.lower_left = (-4.0, -4.0) lattice.universes = [[extra_univ, extra_univ], [extra_univ, extra_univ]] - lattice_region = openmc.model.rectangular_prism(8.0, - 8.0, - boundary_type='reflective') - lattice_cell = openmc.Cell(fill=lattice, region=lattice_region) + lattice_prism = openmc.model.RectangularPrism( + 8.0, 8.0, boundary_type='reflective') + lattice_cell = openmc.Cell(fill=lattice, region=-lattice_prism) model.geometry = openmc.Geometry([lattice_cell]) diff --git a/tests/regression_tests/dagmc/external/main.cpp b/tests/regression_tests/dagmc/external/main.cpp index 42f7d97c90a..3765cf79ae2 100644 --- a/tests/regression_tests/dagmc/external/main.cpp +++ b/tests/regression_tests/dagmc/external/main.cpp @@ -5,6 +5,7 @@ #include "openmc/geometry.h" #include "openmc/geometry_aux.h" #include "openmc/material.h" +#include "openmc/message_passing.h" #include "openmc/nuclide.h" #include @@ -14,7 +15,12 @@ int main(int argc, char* argv[]) int openmc_err; // Initialise OpenMC +#ifdef OPENMC_MPI + MPI_Comm world = MPI_COMM_WORLD; + openmc_err = openmc_init(argc, argv, &world); +#else openmc_err = openmc_init(argc, argv, nullptr); +#endif if (openmc_err == -1) { // This happens for the -h and -v flags return EXIT_SUCCESS; @@ -33,7 +39,7 @@ int main(int argc, char* argv[]) // Initialize acceleration data structures rval = dag_ptr->init_OBBTree(); if (rval != moab::MB_SUCCESS) { - fatal_error("Failed to initialise OBB tree"); + fatal_error("Failed to initialize OBB tree"); } // Get rid of existing geometry @@ -62,6 +68,20 @@ int main(int argc, char* argv[]) // Add cells to universes openmc::populate_universes(); + // Make sure implicit complement appears last + auto dag_univ = dynamic_cast(model::universes.back().get()); + int n = dag_univ->cells_.size(); + for (int i = 0; i < n - 1; ++i) { + if (dag_univ->cells_[i] == dag_univ->implicit_complement_idx()) { + fatal_error("Implicit complement cell should appear last in vector of " + "cells for DAGMC universe."); + } + } + if (dag_univ->cells_.back() != dag_univ->implicit_complement_idx()) { + fatal_error( + "Last cell in DAGMC universe is not an implicit complement cell."); + } + // Set root universe openmc::model::root_universe = openmc::find_root_universe(); openmc::check_dagmc_root_univ(); @@ -90,5 +110,9 @@ int main(int argc, char* argv[]) if (openmc_err) fatal_error(openmc_err_msg); +#ifdef OPENMC_MPI + MPI_Finalize(); +#endif + return EXIT_SUCCESS; } diff --git a/tests/regression_tests/dagmc/external/test.py b/tests/regression_tests/dagmc/external/test.py index 7993772e8b6..57bc9ea7fd6 100644 --- a/tests/regression_tests/dagmc/external/test.py +++ b/tests/regression_tests/dagmc/external/test.py @@ -25,7 +25,7 @@ def cpp_driver(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_cpp_driver CXX) add_executable(main main.cpp) find_package(OpenMC REQUIRED HINTS {}) diff --git a/tests/regression_tests/dagmc/legacy/test.py b/tests/regression_tests/dagmc/legacy/test.py index 7e4e6e1340b..884264f30ef 100644 --- a/tests/regression_tests/dagmc/legacy/test.py +++ b/tests/regression_tests/dagmc/legacy/test.py @@ -1,9 +1,13 @@ +from pathlib import Path + import openmc import openmc.lib -from pathlib import Path +import h5py +import numpy as np import pytest -from tests.testing_harness import PyAPITestHarness + +from tests.testing_harness import PyAPITestHarness, config pytestmark = pytest.mark.skipif( not openmc.lib._dagmc_enabled(), @@ -13,7 +17,7 @@ def model(): openmc.reset_auto_ids() - model = openmc.model.Model() + model = openmc.Model() # settings model.settings.batches = 5 @@ -73,6 +77,32 @@ def test_missing_material_name(model): assert exp_error_msg in str(exec_info.value) +def test_surf_source(model): + # create a surface source read on this model to ensure + # particles are being generated correctly + n = 100 + model.settings.surf_source_write = {'surface_ids': [1], 'max_particles': n} + + # If running in MPI mode, setup proper keyword arguments for run() + kwargs = {'openmc_exec': config['exe']} + if config['mpi']: + kwargs['mpi_args'] = [config['mpiexec'], '-n', config['mpi_np']] + model.run(**kwargs) + + with h5py.File('surface_source.h5') as fh: + assert fh.attrs['filetype'] == b'source' + arr = fh['source_bank'][...] + expected_size = n * int(config['mpi_np']) if config['mpi'] else n + assert arr.size == expected_size + + # check that all particles are on surface 1 (radius = 7) + xs = arr[:]['r']['x'] + ys = arr[:]['r']['y'] + rad = np.sqrt(xs**2 + ys**2) + assert np.allclose(rad, 7.0) + + def test_dagmc(model): harness = PyAPITestHarness('statepoint.5.h5', model) harness.main() + diff --git a/tests/regression_tests/dagmc/refl/test.py b/tests/regression_tests/dagmc/refl/test.py index 03c1c407bbb..a13acc0256a 100644 --- a/tests/regression_tests/dagmc/refl/test.py +++ b/tests/regression_tests/dagmc/refl/test.py @@ -6,8 +6,8 @@ from tests.testing_harness import PyAPITestHarness pytestmark = pytest.mark.skipif( - not openmc.lib._dagmc_enabled(), - reason="DAGMC CAD geometry is not enabled.") + not openmc.lib._uwuw_enabled(), + reason="UWUW is not enabled.") class UWUWTest(PyAPITestHarness): def __init__(self, *args, **kwargs): diff --git a/tests/regression_tests/dagmc/uwuw/test.py b/tests/regression_tests/dagmc/uwuw/test.py index 38c335a5ed4..bea464cfabc 100644 --- a/tests/regression_tests/dagmc/uwuw/test.py +++ b/tests/regression_tests/dagmc/uwuw/test.py @@ -6,8 +6,8 @@ from tests.testing_harness import PyAPITestHarness pytestmark = pytest.mark.skipif( - not openmc.lib._dagmc_enabled(), - reason="DAGMC CAD geometry is not enabled.") + not openmc.lib._uwuw_enabled(), + reason="UWUW is not enabled.") class UWUWTest(PyAPITestHarness): def __init__(self, *args, **kwargs): diff --git a/tests/regression_tests/deplete_decay_only/test.py b/tests/regression_tests/deplete_decay_only/test.py index cb716a5ad2d..4345b86b898 100644 --- a/tests/regression_tests/deplete_decay_only/test.py +++ b/tests/regression_tests/deplete_decay_only/test.py @@ -36,8 +36,8 @@ def model(): pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] pin_univ = openmc.model.pin(pin_surfaces, materials) - bound_box = openmc.rectangular_prism(1.24, 1.24, boundary_type="reflective") - root_cell = openmc.Cell(fill=pin_univ, region=bound_box) + bound_box = openmc.model.RectangularPrism(1.24, 1.24, boundary_type="reflective") + root_cell = openmc.Cell(fill=pin_univ, region=-bound_box) geometry = openmc.Geometry([root_cell]) settings = openmc.Settings() diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 index 4f270afab00..b9be7634513 100644 Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 differ diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 index 5ee1d696d3e..4b24bed52ad 100644 Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 differ diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 index 791447253c5..51173f778b9 100644 Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 differ diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 index e7ea6555432..d53adc7d841 100644 Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 differ diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 index 7a3b95c812c..921755e1559 100644 Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 differ diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 index 7d68d1d5f85..469149f6742 100644 Binary files a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 and b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 differ diff --git a/tests/regression_tests/deplete_with_transfer_rates/test.py b/tests/regression_tests/deplete_with_transfer_rates/test.py index 4084a47fe0d..669fd4bea01 100644 --- a/tests/regression_tests/deplete_with_transfer_rates/test.py +++ b/tests/regression_tests/deplete_with_transfer_rates/test.py @@ -40,9 +40,9 @@ def model(): geometry = openmc.Geometry([cell_f, cell_w]) settings = openmc.Settings() - settings.particles = 500 + settings.particles = 100 settings.inactive = 0 - settings.batches = 2 + settings.batches = 10 return openmc.Model(geometry, materials, settings) @@ -63,6 +63,7 @@ def test_transfer_rates(run_in_tmpdir, model, rate, dest_mat, power, ref_result) transfer_elements = ['Xe'] op = CoupledOperator(model, chain_file) + op.round_number = True integrator = openmc.deplete.PredictorIntegrator( op, [1], power, timestep_units = 'd') integrator.add_transfer_rate('f', transfer_elements, rate, @@ -82,5 +83,5 @@ def test_transfer_rates(run_in_tmpdir, model, rate, dest_mat, power, ref_result) res_ref = openmc.deplete.Results(path_reference) res_test = openmc.deplete.Results(path_test) - assert_atoms_equal(res_ref, res_test, 1e-4) + assert_atoms_equal(res_ref, res_test, 1e-6) assert_reaction_rates_equal(res_ref, res_test) diff --git a/tests/regression_tests/external_moab/test.py b/tests/regression_tests/external_moab/test.py index 90bff69ed00..ce4e78a2c25 100644 --- a/tests/regression_tests/external_moab/test.py +++ b/tests/regression_tests/external_moab/test.py @@ -32,7 +32,7 @@ def cpp_driver(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_cpp_driver CXX) add_executable(main main.cpp) find_package(OpenMC REQUIRED HINTS {}) diff --git a/tests/regression_tests/filter_cellinstance/test.py b/tests/regression_tests/filter_cellinstance/test.py index 0d6ad93669e..61f17d88a66 100644 --- a/tests/regression_tests/filter_cellinstance/test.py +++ b/tests/regression_tests/filter_cellinstance/test.py @@ -60,8 +60,8 @@ def model(): [u3, u3, u2, u3], [u3, u3, u3, u2] ] - box = openmc.model.rectangular_prism(8.0, 8.0, boundary_type='reflective') - main_cell = openmc.Cell(fill=lat, region=box) + box = openmc.model.RectangularPrism(8.0, 8.0, boundary_type='reflective') + main_cell = openmc.Cell(fill=lat, region=-box) model.geometry.root_universe = openmc.Universe(cells=[main_cell]) model.geometry.determine_paths() diff --git a/tests/regression_tests/filter_mesh/test.py b/tests/regression_tests/filter_mesh/test.py index 62f0ed015d3..6214e0486c0 100644 --- a/tests/regression_tests/filter_mesh/test.py +++ b/tests/regression_tests/filter_mesh/test.py @@ -19,12 +19,12 @@ def model(): zr.add_nuclide('Zr90', 1.0) model.materials.extend([fuel, zr]) - box1 = openmc.model.rectangular_prism(10.0, 10.0) - box2 = openmc.model.rectangular_prism(20.0, 20.0, boundary_type='reflective') + box1 = openmc.model.RectangularPrism(10.0, 10.0) + box2 = openmc.model.RectangularPrism(20.0, 20.0, boundary_type='reflective') top = openmc.ZPlane(z0=10.0, boundary_type='vacuum') bottom = openmc.ZPlane(z0=-10.0, boundary_type='vacuum') - cell1 = openmc.Cell(fill=fuel, region=box1 & +bottom & -top) - cell2 = openmc.Cell(fill=zr, region=~box1 & box2 & +bottom & -top) + cell1 = openmc.Cell(fill=fuel, region=-box1 & +bottom & -top) + cell2 = openmc.Cell(fill=zr, region=+box1 & -box2 & +bottom & -top) model.geometry = openmc.Geometry([cell1, cell2]) model.settings.batches = 5 diff --git a/tests/regression_tests/filter_meshborn/__init__.py b/tests/regression_tests/filter_meshborn/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/filter_meshborn/inputs_true.dat b/tests/regression_tests/filter_meshborn/inputs_true.dat new file mode 100644 index 00000000000..3ba38d56e60 --- /dev/null +++ b/tests/regression_tests/filter_meshborn/inputs_true.dat @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + fixed source + 2000 + 8 + + + 0.0 -10.0 -10.0 10.0 10.0 10.0 + + + + + + 2 2 1 + -10.0 -10.0 -10.0 + 10.0 10.0 10.0 + + + 1 + + + 1 + + + 1 2 + scatter + + + 1 + scatter + + + 2 + scatter + + + scatter + + + diff --git a/tests/regression_tests/filter_meshborn/results_true.dat b/tests/regression_tests/filter_meshborn/results_true.dat new file mode 100644 index 00000000000..62f2707ffc3 --- /dev/null +++ b/tests/regression_tests/filter_meshborn/results_true.dat @@ -0,0 +1,54 @@ +tally 1: +0.000000E+00 +0.000000E+00 +2.631246E+01 +8.845079E+01 +0.000000E+00 +0.000000E+00 +2.450265E+00 +9.462266E-01 +0.000000E+00 +0.000000E+00 +3.878380E+02 +1.881752E+04 +0.000000E+00 +0.000000E+00 +2.932956E+01 +1.091674E+02 +0.000000E+00 +0.000000E+00 +1.837753E+00 +5.195343E-01 +0.000000E+00 +0.000000E+00 +2.944919E+01 +1.095819E+02 +0.000000E+00 +0.000000E+00 +2.921731E+01 +1.097387E+02 +0.000000E+00 +0.000000E+00 +4.019442E+02 +2.021184E+04 +tally 2: +2.876273E+01 +1.060244E+02 +4.171676E+02 +2.176683E+04 +3.128695E+01 +1.238772E+02 +4.311615E+02 +2.325871E+04 +tally 3: +0.000000E+00 +0.000000E+00 +4.452055E+02 +2.478148E+04 +0.000000E+00 +0.000000E+00 +4.631732E+02 +2.683862E+04 +tally 4: +9.083787E+02 +1.031695E+05 diff --git a/tests/regression_tests/filter_meshborn/test.py b/tests/regression_tests/filter_meshborn/test.py new file mode 100644 index 00000000000..ff4adbc9f04 --- /dev/null +++ b/tests/regression_tests/filter_meshborn/test.py @@ -0,0 +1,107 @@ +"""Test the meshborn filter using a fixed source calculation on a H1 sphere. + +""" + +from numpy.testing import assert_allclose +import numpy as np +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +RTOL = 1.0e-7 +ATOL = 0.0 + + +@pytest.fixture +def model(): + """Sphere of H1 with one hemisphere containing the source (x>0) and one + hemisphere with no source (x<0). + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # Materials + h1 = openmc.Material() + h1.add_nuclide("H1", 1.0) + h1.set_density("g/cm3", 1.0) + model.materials = openmc.Materials([h1]) + + # Core geometry + r = 10.0 + sphere = openmc.Sphere(r=r, boundary_type="reflective") + core = openmc.Cell(fill=h1, region=-sphere) + model.geometry = openmc.Geometry([core]) + + # Settings + model.settings.run_mode = 'fixed source' + model.settings.particles = 2000 + model.settings.batches = 8 + distribution = openmc.stats.Box((0., -r, -r), (r, r, r)) + model.settings.source = openmc.IndependentSource(space=distribution) + + # Tallies + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2, 1) + mesh.lower_left = (-r, -r, -r) + mesh.upper_right = (r, r, r) + + f_1 = openmc.MeshFilter(mesh) + f_2 = openmc.MeshBornFilter(mesh) + + t_1 = openmc.Tally(name="scatter") + t_1.filters = [f_1, f_2] + t_1.scores = ["scatter"] + + t_2 = openmc.Tally(name="scatter-mesh") + t_2.filters = [f_1] + t_2.scores = ["scatter"] + + t_3 = openmc.Tally(name="scatter-meshborn") + t_3.filters = [f_2] + t_3.scores = ["scatter"] + + t_4 = openmc.Tally(name="scatter-total") + t_4.scores = ["scatter"] + + model.tallies = [t_1, t_2, t_3, t_4] + + return model + + +class MeshBornFilterTest(PyAPITestHarness): + + def _compare_results(self): + """Additional unit tests on the tally results to check consistency.""" + with openmc.StatePoint(self.statepoint_name) as sp: + + t1 = sp.get_tally(name="scatter").mean.reshape(4, 4) + t2 = sp.get_tally(name="scatter-mesh").mean.reshape(4) + t3 = sp.get_tally(name="scatter-meshborn").mean.reshape(4) + t4 = sp.get_tally(name="scatter-total").mean.reshape(1) + + # Consistency between mesh+meshborn matrix tally and meshborn tally + for i in range(4): + assert_allclose(t1[:, i].sum(), t3[i], rtol=RTOL, atol=ATOL) + + # Consistency between mesh+meshborn matrix tally and mesh tally + for i in range(4): + assert_allclose(t1[i, :].sum(), t2[i], rtol=RTOL, atol=ATOL) + + # Mesh cells in x<0 do not contribute to meshborn + assert_allclose(t1[:, 0].sum(), np.zeros(4), rtol=RTOL, atol=ATOL) + assert_allclose(t1[:, 2].sum(), np.zeros(4), rtol=RTOL, atol=ATOL) + + # Consistency with total scattering + assert_allclose(t1.sum(), t4, rtol=RTOL, atol=ATOL) + assert_allclose(t2.sum(), t4, rtol=RTOL, atol=ATOL) + assert_allclose(t3.sum(), t4, rtol=RTOL, atol=ATOL) + + super()._compare_results() + + +def test_filter_meshborn(model): + harness = MeshBornFilterTest("statepoint.8.h5", model) + harness.main() diff --git a/tests/regression_tests/filter_translations/test.py b/tests/regression_tests/filter_translations/test.py index d61667b0145..4f0fe7141bf 100644 --- a/tests/regression_tests/filter_translations/test.py +++ b/tests/regression_tests/filter_translations/test.py @@ -19,12 +19,12 @@ def model(): zr.add_nuclide('Zr90', 1.0) model.materials.extend([fuel, zr]) - box1 = openmc.model.rectangular_prism(10.0, 10.0) - box2 = openmc.model.rectangular_prism(20.0, 20.0, boundary_type='reflective') + box1 = openmc.model.RectangularPrism(10.0, 10.0) + box2 = openmc.model.RectangularPrism(20.0, 20.0, boundary_type='reflective') top = openmc.ZPlane(z0=10.0, boundary_type='vacuum') bottom = openmc.ZPlane(z0=-10.0, boundary_type='vacuum') - cell1 = openmc.Cell(fill=fuel, region=box1 & +bottom & -top) - cell2 = openmc.Cell(fill=zr, region=~box1 & box2 & +bottom & -top) + cell1 = openmc.Cell(fill=fuel, region=-box1 & +bottom & -top) + cell2 = openmc.Cell(fill=zr, region=+box1 & -box2 & +bottom & -top) model.geometry = openmc.Geometry([cell1, cell2]) model.settings.batches = 5 diff --git a/tests/regression_tests/lattice_hex_coincident/inputs_true.dat b/tests/regression_tests/lattice_hex_coincident/inputs_true.dat index 7a83fb471f6..ac82de0adf9 100644 --- a/tests/regression_tests/lattice_hex_coincident/inputs_true.dat +++ b/tests/regression_tests/lattice_hex_coincident/inputs_true.dat @@ -38,7 +38,7 @@ - + 1.4 3 diff --git a/tests/regression_tests/lattice_hex_coincident/test.py b/tests/regression_tests/lattice_hex_coincident/test.py index 5a4760cd7c0..f971098c06a 100644 --- a/tests/regression_tests/lattice_hex_coincident/test.py +++ b/tests/regression_tests/lattice_hex_coincident/test.py @@ -104,10 +104,10 @@ def __init__(self, *args, **kwargs): inf_mat_univ = openmc.Universe(cells=[inf_mat,]) # a hex surface for the core to go inside of - hexprism = openmc.model.hexagonal_prism(edge_length=edge_length, - origin=(0.0, 0.0), - boundary_type = 'reflective', - orientation='x') + hexprism = openmc.model.HexagonalPrism(edge_length=edge_length, + origin=(0.0, 0.0), + boundary_type = 'reflective', + orientation='x') pincell_only_lattice = openmc.HexLattice(name="regular fuel assembly") pincell_only_lattice.center = (0., 0.) @@ -120,7 +120,7 @@ def __init__(self, *args, **kwargs): pincell_only_lattice.universes = [ring1, ring0] pincell_only_cell = openmc.Cell(name="container cell") - pincell_only_cell.region = hexprism & +fuel_btm & -fuel_top + pincell_only_cell.region = -hexprism & +fuel_btm & -fuel_top pincell_only_cell.fill = pincell_only_lattice root_univ = openmc.Universe(name="root universe", cells=[pincell_only_cell,]) diff --git a/tests/regression_tests/lattice_hex_x/inputs_true.dat b/tests/regression_tests/lattice_hex_x/inputs_true.dat index 2946258e8b1..252fbfc0b5d 100644 --- a/tests/regression_tests/lattice_hex_x/inputs_true.dat +++ b/tests/regression_tests/lattice_hex_x/inputs_true.dat @@ -42,7 +42,7 @@ - + 1.235 5.0 4 diff --git a/tests/regression_tests/lattice_hex_x/test.py b/tests/regression_tests/lattice_hex_x/test.py index 23aec77d3b3..dd5c53d0c02 100644 --- a/tests/regression_tests/lattice_hex_x/test.py +++ b/tests/regression_tests/lattice_hex_x/test.py @@ -139,11 +139,11 @@ def __init__(self, *args, **kwargs): # a hex surface for the core to go inside of - hexprism = openmc.model.hexagonal_prism(edge_length=edge_length, - origin=(0.0, 0.0), - boundary_type='reflective', - orientation='x') - region = hexprism & +fuel_bottom & -fuel_top + hexprism = openmc.model.HexagonalPrism(edge_length=edge_length, + origin=(0.0, 0.0), + boundary_type='reflective', + orientation='x') + region = -hexprism & +fuel_bottom & -fuel_top inf_mat = openmc.Cell(cell_id=12) inf_mat.fill = coolant diff --git a/tests/regression_tests/lattice_multiple/test.py b/tests/regression_tests/lattice_multiple/test.py index c287c01027a..10d9e50bc68 100644 --- a/tests/regression_tests/lattice_multiple/test.py +++ b/tests/regression_tests/lattice_multiple/test.py @@ -42,8 +42,8 @@ def model(): lattice.pitch = (2*d, 2*d) lattice.universes = np.full((2, 2), inner_univ) - box = openmc.model.rectangular_prism(4*d, 4*d, boundary_type='reflective') - main_cell = openmc.Cell(fill=lattice, region=box) + box = openmc.model.RectangularPrism(4*d, 4*d, boundary_type='reflective') + main_cell = openmc.Cell(fill=lattice, region=-box) model.geometry = openmc.Geometry([main_cell]) model.settings.batches = 10 diff --git a/tests/regression_tests/mg_temperature_multi/test.py b/tests/regression_tests/mg_temperature_multi/test.py index 2090f1b0c2c..404c8c0cd05 100755 --- a/tests/regression_tests/mg_temperature_multi/test.py +++ b/tests/regression_tests/mg_temperature_multi/test.py @@ -108,14 +108,14 @@ def test_mg_temperature_multi(): # Create a region represented as the inside of a rectangular prism pitch = 1.26 - box = openmc.rectangular_prism(pitch, pitch, boundary_type='reflective') + box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective') # Instantiate Cells fuel_inner = openmc.Cell(fill=uo2, region=-fuel_ir, name='fuel inner') fuel_inner.temperature = 600.0 fuel_outer = openmc.Cell(fill=uo2, region=+fuel_ir & -fuel_or, name='fuel outer') fuel_outer.temperature = 294.0 - moderator = openmc.Cell(fill=water, region=+fuel_or & box, name='moderator') + moderator = openmc.Cell(fill=water, region=+fuel_or & -box, name='moderator') # Create a geometry with the two cells and export to XML geometry = openmc.Geometry([fuel_inner, fuel_outer, moderator]) diff --git a/tests/regression_tests/mgxs_library_condense/results_true.dat b/tests/regression_tests/mgxs_library_condense/results_true.dat index 0792d92da4c..ae5bff6e35b 100644 --- a/tests/regression_tests/mgxs_library_condense/results_true.dat +++ b/tests/regression_tests/mgxs_library_condense/results_true.dat @@ -168,10 +168,10 @@ 3 2 2 1 1 total 1.0 0.130701 mesh 1 group in nuclide mean std. dev. x y z -0 1 1 1 1 total 5.304284e-07 2.560239e-08 -2 1 2 1 1 total 4.940320e-07 2.410416e-08 -1 2 1 1 1 total 5.587366e-07 3.382787e-08 -3 2 2 1 1 total 5.232209e-07 2.482800e-08 +0 1 1 1 1 total 5.304285e-07 2.560239e-08 +2 1 2 1 1 total 4.940321e-07 2.410417e-08 +1 2 1 1 1 total 5.587365e-07 3.382787e-08 +3 2 2 1 1 total 5.232210e-07 2.482800e-08 mesh 1 group in nuclide mean std. dev. x y z 0 1 1 1 1 total 0.026032 0.001172 diff --git a/tests/regression_tests/mgxs_library_distribcell/results_true.dat b/tests/regression_tests/mgxs_library_distribcell/results_true.dat index ad2aef475e1..ef7cb2b96b6 100644 --- a/tests/regression_tests/mgxs_library_distribcell/results_true.dat +++ b/tests/regression_tests/mgxs_library_distribcell/results_true.dat @@ -51,7 +51,7 @@ sum(distribcell) group out nuclide mean std. dev. 0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 1.0 0.084331 sum(distribcell) group in nuclide mean std. dev. -0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 5.253873e-07 2.168461e-08 +0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 5.253873e-07 2.168462e-08 sum(distribcell) group in nuclide mean std. dev. 0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 0.094147 0.003701 sum(distribcell) group in group out nuclide mean std. dev. diff --git a/tests/regression_tests/mgxs_library_hdf5/results_true.dat b/tests/regression_tests/mgxs_library_hdf5/results_true.dat index 28a352fdf93..b2ef8eae921 100644 --- a/tests/regression_tests/mgxs_library_hdf5/results_true.dat +++ b/tests/regression_tests/mgxs_library_hdf5/results_true.dat @@ -97,8 +97,8 @@ domain=1 type=chi-prompt [1.00000000e+00 0.00000000e+00] [1.03333203e-01 0.00000000e+00] domain=1 type=inverse-velocity -[5.72461488e-08 3.00999716e-06] -[2.80644406e-09 1.80993425e-07] +[5.72461488e-08 3.00999757e-06] +[2.80644407e-09 1.80993426e-07] domain=1 type=prompt-nu-fission [5.64594959e-03 1.32859920e-01] [3.68364598e-04 7.79430310e-03] diff --git a/tests/regression_tests/mgxs_library_hdf5/test.py b/tests/regression_tests/mgxs_library_hdf5/test.py index 2f3c9d149de..06625c25f9e 100644 --- a/tests/regression_tests/mgxs_library_hdf5/test.py +++ b/tests/regression_tests/mgxs_library_hdf5/test.py @@ -54,6 +54,11 @@ def _get_results(self, hash_output=False): # Export the MGXS Library to an HDF5 file self.mgxs_lib.build_hdf5_store(directory='.') + # Test export of the MGXS Library to an Excel spreadsheet + for mgxs in self.mgxs_lib.all_mgxs.values(): + for xs in mgxs.values(): + xs.export_xs_data('mgxs', xs_type='macro', format='excel') + # Open the MGXS HDF5 file with h5py.File('mgxs.h5', 'r') as f: @@ -76,9 +81,8 @@ def _get_results(self, hash_output=False): def _cleanup(self): super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) + files = ['mgxs.h5', 'mgxs.xlsx'] + (os.remove(f) for f in files if os.path.exists(f)) def test_mgxs_library_hdf5(): diff --git a/tests/regression_tests/mgxs_library_mesh/test.py b/tests/regression_tests/mgxs_library_mesh/test.py index 3660d3eb74d..89c68a75a33 100644 --- a/tests/regression_tests/mgxs_library_mesh/test.py +++ b/tests/regression_tests/mgxs_library_mesh/test.py @@ -19,12 +19,12 @@ def model(): zr.add_nuclide('Zr90', 1.0) model.materials.extend([fuel, zr]) - box1 = openmc.model.rectangular_prism(10.0, 10.0) - box2 = openmc.model.rectangular_prism(20.0, 20.0, boundary_type='reflective') + box1 = openmc.model.RectangularPrism(10.0, 10.0) + box2 = openmc.model.RectangularPrism(20.0, 20.0, boundary_type='reflective') top = openmc.ZPlane(z0=10.0, boundary_type='vacuum') bottom = openmc.ZPlane(z0=-10.0, boundary_type='vacuum') - cell1 = openmc.Cell(fill=fuel, region=box1 & +bottom & -top) - cell2 = openmc.Cell(fill=zr, region=~box1 & box2 & +bottom & -top) + cell1 = openmc.Cell(fill=fuel, region=-box1 & +bottom & -top) + cell2 = openmc.Cell(fill=zr, region=+box1 & -box2 & +bottom & -top) model.geometry = openmc.Geometry([cell1, cell2]) model.settings.batches = 5 diff --git a/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat b/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat index 7ab85f6003b..c0dba7c0fb5 100644 --- a/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat +++ b/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat @@ -143,7 +143,7 @@ chi-prompt inverse-velocity material group in nuclide mean std. dev. 1 1 1 total 5.956290e-08 2.255751e-09 -0 1 2 total 2.886630e-06 7.418455e-08 +0 1 2 total 2.886630e-06 7.418452e-08 prompt-nu-fission material group in nuclide mean std. dev. 1 1 1 total 0.018067 0.000817 @@ -449,7 +449,7 @@ chi-prompt inverse-velocity material group in nuclide mean std. dev. 1 2 1 total 6.076563e-08 2.727519e-09 -0 2 2 total 2.996176e-06 1.110140e-07 +0 2 2 total 2.996176e-06 1.110139e-07 prompt-nu-fission material group in nuclide mean std. dev. 1 2 1 total 0.0 0.0 diff --git a/tests/regression_tests/mgxs_library_nuclides/results_true.dat b/tests/regression_tests/mgxs_library_nuclides/results_true.dat index c238903239f..c2188997d7e 100644 --- a/tests/regression_tests/mgxs_library_nuclides/results_true.dat +++ b/tests/regression_tests/mgxs_library_nuclides/results_true.dat @@ -1 +1 @@ -c4a4cb4e00f09ef62222a0e66df817af87bf324a2ac0e57a82ff1337535a223c7077d660a60d0ce9ac7113c47d37b3296347f3ba212558ff09ef5fc586e1dd28 \ No newline at end of file +4ae3b5a70ad72b1be261aee3ab19e0261d1c12f4d4ca50b712d1ba76041bd5387c69fa9fa326619ad588db206cd9aaf464b0025d71ea9b8b1137af0102bca87f \ No newline at end of file diff --git a/tests/regression_tests/mgxs_library_specific_nuclides/results_true.dat b/tests/regression_tests/mgxs_library_specific_nuclides/results_true.dat index 05ee4090225..eaf56b96669 100644 --- a/tests/regression_tests/mgxs_library_specific_nuclides/results_true.dat +++ b/tests/regression_tests/mgxs_library_specific_nuclides/results_true.dat @@ -1 +1 @@ -3e86542d1166b8a0bcc61742b88c9e931d63733477f3274b32b4da64d8f05413fa6330d5615f44f24735c2c2f77d3071f3dce38faba284c321ceab81c1064480 \ No newline at end of file +08d5c199c51496f86fdd739bf7ee0e143a9a159da0f4d364ec970557e5c1fc92a202d906dcae91812e665fd2e88dd7db1e4913ef6b91f456f23b52093c83f483 \ No newline at end of file diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py index dbdb1c8fe15..a35150a1b80 100644 --- a/tests/regression_tests/microxs/test.py +++ b/tests/regression_tests/microxs/test.py @@ -34,8 +34,8 @@ def model(): pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] pin_univ = openmc.model.pin(pin_surfaces, materials) - bound_box = openmc.rectangular_prism(1.24, 1.24, boundary_type="reflective") - root_cell = openmc.Cell(fill=pin_univ, region=bound_box) + bound_box = openmc.model.RectangularPrism(1.24, 1.24, boundary_type="reflective") + root_cell = openmc.Cell(fill=pin_univ, region=-bound_box) geometry = openmc.Geometry([root_cell]) settings = openmc.Settings() diff --git a/tests/regression_tests/periodic_hex/test.py b/tests/regression_tests/periodic_hex/test.py index a21819b1ada..db9f6cfd5b0 100644 --- a/tests/regression_tests/periodic_hex/test.py +++ b/tests/regression_tests/periodic_hex/test.py @@ -12,8 +12,8 @@ def hex_model(): fuel.add_nuclide('U235', 1.0) fuel.set_density('g/cc', 4.5) - hex_region = openmc.model.hexagonal_prism(10.0, boundary_type='periodic') - cell = openmc.Cell(fill=fuel, region=hex_region) + hex_prism = openmc.model.HexagonalPrism(10.0, boundary_type='periodic') + cell = openmc.Cell(fill=fuel, region=-hex_prism) model.geometry = openmc.Geometry([cell]) # Define settings diff --git a/tests/regression_tests/random_ray_basic/__init__.py b/tests/regression_tests/random_ray_basic/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_basic/inputs_true.dat b/tests/regression_tests/random_ray_basic/inputs_true.dat new file mode 100644 index 00000000000..97b7906f7b2 --- /dev/null +++ b/tests/regression_tests/random_ray_basic/inputs_true.dat @@ -0,0 +1,108 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_basic/results_true.dat b/tests/regression_tests/random_ray_basic/results_true.dat new file mode 100644 index 00000000000..802e78a828b --- /dev/null +++ b/tests/regression_tests/random_ray_basic/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.400322E-01 8.023349E-03 +tally 1: +1.260220E+00 +3.179889E-01 +1.484289E-01 +4.411066E-03 +3.612463E-01 +2.612843E-02 +7.086707E-01 +1.006119E-01 +3.342483E-02 +2.238499E-04 +8.134936E-02 +1.325949E-03 +4.194328E-01 +3.558669E-02 +4.287776E-03 +3.717447E-06 +1.043559E-02 +2.201986E-05 +5.878720E-01 +7.045887E-02 +6.147757E-03 +7.701173E-06 +1.496241E-02 +4.561699E-05 +1.768113E+00 +6.356917E-01 +6.513486E-03 +8.628535E-06 +1.585272E-02 +5.111136E-05 +5.063704E+00 +5.152401E+00 +2.440293E-03 +1.196869E-06 +6.038334E-03 +7.328193E-06 +3.253717E+00 +2.117655E+00 +1.389120E-02 +3.859385E-05 +3.863767E-02 +2.985798E-04 +1.876994E+00 +7.046366E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.390875E-01 +1.408791E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.513839E-01 +4.139640E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.682186E-01 +9.116003E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.849034E+00 +6.944337E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.523425E+00 +4.112118E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.821432E+00 +1.592568E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.159618E+00 +2.694138E-01 +1.354028E-01 +3.672094E-03 +3.295432E-01 +2.175122E-02 +6.880334E-01 +9.491215E-02 +3.234611E-02 +2.097782E-04 +7.872396E-02 +1.242596E-03 +4.184841E-01 +3.536436E-02 +4.274305E-03 +3.687209E-06 +1.040280E-02 +2.184074E-05 +5.810180E-01 +6.872944E-02 +6.060273E-03 +7.476500E-06 +1.474949E-02 +4.428617E-05 +1.782580E+00 +6.457892E-01 +6.552384E-03 +8.730345E-06 +1.594739E-02 +5.171444E-05 +5.278155E+00 +5.596601E+00 +2.546878E-03 +1.303010E-06 +6.302072E-03 +7.978072E-06 +3.420419E+00 +2.340454E+00 +1.465798E-02 +4.299061E-05 +4.077042E-02 +3.325951E-04 +1.279417E+00 +3.278133E-01 +1.509073E-01 +4.561836E-03 +3.672782E-01 +2.702150E-02 +7.212777E-01 +1.042487E-01 +3.411552E-02 +2.332877E-04 +8.303035E-02 +1.381852E-03 +4.269473E-01 +3.685202E-02 +4.378540E-03 +3.872997E-06 +1.065649E-02 +2.294124E-05 +5.973530E-01 +7.266946E-02 +6.260881E-03 +7.976490E-06 +1.523773E-02 +4.724780E-05 +1.795373E+00 +6.547440E-01 +6.635941E-03 +8.945067E-06 +1.615075E-02 +5.298634E-05 +5.161876E+00 +5.353441E+00 +2.505311E-03 +1.261399E-06 +6.199218E-03 +7.723296E-06 +3.344042E+00 +2.236603E+00 +1.443089E-02 +4.166228E-05 +4.013879E-02 +3.223186E-04 diff --git a/tests/regression_tests/random_ray_basic/test.py b/tests/regression_tests/random_ray_basic/test.py new file mode 100644 index 00000000000..1727a63716c --- /dev/null +++ b/tests/regression_tests/random_ray_basic/test.py @@ -0,0 +1,232 @@ +import os + +import numpy as np +import openmc + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def random_ray_model() -> openmc.Model: + ############################################################################### + # Create multigroup data + + # Instantiate the energy group data + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges) + + # Instantiate the 7-group (C5G7) cross section data + uo2_xsdata = openmc.XSdata('UO2', groups) + uo2_xsdata.order = 0 + uo2_xsdata.set_total( + [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) + uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, + 3.0020e-02, 1.1126e-01, 2.8278e-01]) + scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + uo2_xsdata.set_scatter_matrix(scatter_matrix) + uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, + 1.85648e-02, 1.78084e-02, 8.30348e-02, + 2.16004e-01]) + uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) + uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, + 0.718, 1.2544497, 2.650379]) + h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, + 1.9406e-03, 5.7416e-03, 1.5001e-02, + 3.7239e-02]) + scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + + mg_cross_sections = openmc.MGXSLibrary(groups) + mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) + mg_cross_sections.export_to_hdf5() + + ############################################################################### + # Create materials for the problem + + # Instantiate some Materials and register the appropriate macroscopic data + uo2 = openmc.Material(name='UO2 fuel') + uo2.set_density('macro', 1.0) + uo2.add_macroscopic('UO2') + + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic('LWTR') + + # Instantiate a Materials collection and export to XML + materials = openmc.Materials([uo2, water]) + materials.cross_sections = "mgxs.h5" + + ############################################################################### + # Define problem geometry + + ######################################## + # Define an unbounded pincell universe + + pitch = 1.26 + + # Create a surface for the fuel outer radius + fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') + inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') + inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') + outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') + outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') + + # Instantiate Cells + fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') + fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b') + fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c') + moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') + moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') + moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c') + + # Create pincell universe + pincell_base = openmc.Universe() + + # Register Cells with Universe + pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) + + # Create planes for azimuthal sectors + azimuthal_planes = [] + for i in range(8): + angle = 2 * i * openmc.pi / 8 + normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) + azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) + + # Create a cell for each azimuthal sector + azimuthal_cells = [] + for i in range(8): + azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') + azimuthal_cell.fill = pincell_base + azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cells.append(azimuthal_cell) + + # Create a geometry with the azimuthal universes + pincell = openmc.Universe(cells=azimuthal_cells) + + ######################################## + # Define a moderator lattice universe + + moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + mu = openmc.Universe() + mu.add_cells([moderator_infinite]) + + lattice = openmc.RectLattice() + lattice.lower_left = [-pitch/2.0, -pitch/2.0] + lattice.pitch = [pitch/10.0, pitch/10.0] + lattice.universes = np.full((10, 10), mu) + + mod_lattice_cell = openmc.Cell(fill=lattice) + + mod_lattice_uni = openmc.Universe() + + mod_lattice_uni.add_cells([mod_lattice_cell]) + + ######################################## + # Define 2x2 outer lattice + lattice2x2 = openmc.RectLattice() + lattice2x2.lower_left = (-pitch, -pitch) + lattice2x2.pitch = (pitch, pitch) + lattice2x2.universes = [ + [pincell, pincell], + [pincell, mod_lattice_uni] + ] + + ######################################## + # Define cell containing lattice and other stuff + box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='reflective') + + assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') + + # Create a geometry with the top-level cell + geometry = openmc.Geometry([assembly]) + + ############################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 10 + settings.inactive = 5 + settings.particles = 100 + + # Create an initial uniform spatial source distribution over fissionable zones + lower_left = (-pitch, -pitch, -1) + upper_right = (pitch, pitch, 1) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist) + + settings.random_ray['distance_active'] = 100.0 + settings.random_ray['distance_inactive'] = 20.0 + settings.random_ray['ray_source'] = rr_source + + ############################################################################### + # Define tallies + + # Create a mesh that will be used for tallying + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + + # Create a mesh filter that can be used in a tally + mesh_filter = openmc.MeshFilter(mesh) + + # Create an energy group filter as well + energy_filter = openmc.EnergyFilter(group_edges) + + # Now use the mesh filter in a tally and indicate what scores are desired + tally = openmc.Tally(name="Mesh tally") + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux', 'fission', 'nu-fission'] + tally.estimator = 'analog' + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + + ############################################################################### + # Exporting to OpenMC model + ############################################################################### + + model = openmc.Model() + model.geometry = geometry + model.materials = materials + model.settings = settings + model.tallies = tallies + return model + + +def test_random_ray_basic(): + harness = MGXSTestHarness('statepoint.10.h5', random_ray_model()) + harness.main() diff --git a/tests/regression_tests/random_ray_vacuum/__init__.py b/tests/regression_tests/random_ray_vacuum/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_vacuum/inputs_true.dat b/tests/regression_tests/random_ray_vacuum/inputs_true.dat new file mode 100644 index 00000000000..4ef10942005 --- /dev/null +++ b/tests/regression_tests/random_ray_vacuum/inputs_true.dat @@ -0,0 +1,108 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_vacuum/results_true.dat b/tests/regression_tests/random_ray_vacuum/results_true.dat new file mode 100644 index 00000000000..744ce6cef28 --- /dev/null +++ b/tests/regression_tests/random_ray_vacuum/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +1.010455E-01 1.585558E-02 +tally 1: +1.849176E-01 +7.634332E-03 +2.181815E-02 +1.062861E-04 +5.310100E-02 +6.295730E-04 +4.048251E-02 +3.851890E-04 +1.893676E-03 +8.448769E-07 +4.608828E-03 +5.004529E-06 +4.063643E-03 +4.022442E-06 +4.112970E-05 +4.186661E-10 +1.001015E-04 +2.479919E-09 +7.467029E-03 +1.178864E-05 +7.688748E-05 +1.266903E-09 +1.871288E-04 +7.504350E-09 +3.870644E-02 +3.010745E-04 +1.375240E-04 +3.807356E-09 +3.347099E-04 +2.255298E-08 +4.524967E-01 +4.098857E-02 +2.437418E-04 +1.190325E-08 +6.031220E-04 +7.288126E-08 +4.989226E-01 +4.993728E-02 +2.374296E-03 +1.135824E-06 +6.603983E-03 +8.787258E-06 +3.899991E-01 +3.308783E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.108982E-02 +1.144390E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.295259E-03 +6.352159E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.852001E-03 +1.984406E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.414391E-02 +3.905201E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.571668E-01 +1.323140E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.752932E-01 +1.517930E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.465446E-01 +4.901884E-03 +1.700791E-02 +6.587674E-05 +4.139385E-02 +3.902131E-04 +3.424032E-02 +2.841338E-04 +1.598985E-03 +6.216312E-07 +3.891610E-03 +3.682159E-06 +4.067582E-03 +4.209468E-06 +4.152829E-05 +4.494649E-10 +1.010715E-04 +2.662352E-09 +7.526712E-03 +1.225032E-05 +7.769969E-05 +1.328443E-09 +1.891055E-04 +7.868877E-09 +4.008649E-02 +3.246821E-04 +1.417944E-04 +4.070719E-09 +3.451035E-04 +2.411301E-08 +4.859902E-01 +4.747592E-02 +2.606214E-04 +1.369749E-08 +6.448895E-04 +8.386705E-08 +5.475198E-01 +6.061269E-02 +2.625477E-03 +1.405458E-06 +7.302631E-03 +1.087327E-05 +1.909660E-01 +8.147906E-03 +2.269063E-02 +1.149570E-04 +5.522446E-02 +6.809342E-04 +4.196583E-02 +4.141620E-04 +1.980406E-03 +9.227119E-07 +4.819913E-03 +5.465576E-06 +4.247004E-03 +4.420116E-06 +4.341806E-05 +4.691518E-10 +1.056709E-04 +2.778965E-09 +7.742814E-03 +1.272112E-05 +8.039606E-05 +1.389209E-09 +1.956679E-04 +8.228817E-09 +3.982370E-02 +3.190931E-04 +1.427171E-04 +4.103942E-09 +3.473492E-04 +2.430981E-08 +4.849535E-01 +4.707014E-02 +2.678327E-04 +1.438540E-08 +6.627333E-04 +8.807897E-08 +5.493457E-01 +6.069440E-02 +2.717400E-03 +1.501450E-06 +7.558312E-03 +1.161591E-05 diff --git a/tests/regression_tests/random_ray_vacuum/test.py b/tests/regression_tests/random_ray_vacuum/test.py new file mode 100644 index 00000000000..e9ca2252144 --- /dev/null +++ b/tests/regression_tests/random_ray_vacuum/test.py @@ -0,0 +1,235 @@ +import os + +import numpy as np +import openmc + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def random_ray_model() -> openmc.Model: + ############################################################################### + # Create multigroup data + + # Instantiate the energy group data + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges) + + # Instantiate the 7-group (C5G7) cross section data + uo2_xsdata = openmc.XSdata('UO2', groups) + uo2_xsdata.order = 0 + uo2_xsdata.set_total( + [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) + uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, + 3.0020e-02, 1.1126e-01, 2.8278e-01]) + scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + uo2_xsdata.set_scatter_matrix(scatter_matrix) + uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, + 1.85648e-02, 1.78084e-02, 8.30348e-02, + 2.16004e-01]) + uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) + uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, + 0.718, 1.2544497, 2.650379]) + h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, + 1.9406e-03, 5.7416e-03, 1.5001e-02, + 3.7239e-02]) + scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + + mg_cross_sections = openmc.MGXSLibrary(groups) + mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) + mg_cross_sections.export_to_hdf5() + + ############################################################################### + # Create materials for the problem + + # Instantiate some Materials and register the appropriate Macroscopic objects + uo2 = openmc.Material(name='UO2 fuel') + uo2.set_density('macro', 1.0) + uo2.add_macroscopic('UO2') + + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic('LWTR') + + # Instantiate a Materials collection and export to XML + materials = openmc.Materials([uo2, water]) + materials.cross_sections = "mgxs.h5" + + ############################################################################### + # Define problem geometry + + ######################################## + # Define an unbounded pincell universe + + pitch = 1.26 + + # Create a surface for the fuel outer radius + fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') + inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') + inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') + outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') + outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') + + # Instantiate Cells + fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') + fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b') + fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c') + moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') + moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') + moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c') + + # Create pincell universe + pincell_base = openmc.Universe() + + # Register Cells with Universe + pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) + + # Create planes for azimuthal sectors + azimuthal_planes = [] + for i in range(8): + angle = 2 * i * openmc.pi / 8 + normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) + azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) + + # Create a cell for each azimuthal sector + azimuthal_cells = [] + for i in range(8): + azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') + azimuthal_cell.fill = pincell_base + azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cells.append(azimuthal_cell) + + # Create a geometry with the azimuthal universes + pincell = openmc.Universe(cells=azimuthal_cells) + + ######################################## + # Define a moderator lattice universe + + moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + mu = openmc.Universe() + mu.add_cells([moderator_infinite]) + + lattice = openmc.RectLattice() + lattice.lower_left = [-pitch/2.0, -pitch/2.0] + lattice.pitch = [pitch/10.0, pitch/10.0] + lattice.universes = np.full((10, 10), mu) + + mod_lattice_cell = openmc.Cell(fill=lattice) + + mod_lattice_uni = openmc.Universe() + + mod_lattice_uni.add_cells([mod_lattice_cell]) + + ######################################## + # Define 2x2 outer lattice + lattice2x2 = openmc.RectLattice() + lattice2x2.lower_left = [-pitch, -pitch] + lattice2x2.pitch = [pitch, pitch] + lattice2x2.universes = [ + [pincell, pincell], + [pincell, mod_lattice_uni] + ] + + ######################################## + # Define cell containing lattice and other stuff + box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='vacuum') + + assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') + + root = openmc.Universe(name='root universe') + root.add_cell(assembly) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ############################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 10 + settings.inactive = 5 + settings.particles = 100 + + # Create an initial uniform spatial source distribution over fissionable zones + lower_left = (-pitch, -pitch, -1) + upper_right = (pitch, pitch, 1) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist) + + settings.random_ray['distance_active'] = 100.0 + settings.random_ray['distance_inactive'] = 20.0 + settings.random_ray['ray_source'] = rr_source + + ############################################################################### + # Define tallies + + # Create a mesh that will be used for tallying + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + + # Create a mesh filter that can be used in a tally + mesh_filter = openmc.MeshFilter(mesh) + + # Create an energy group filter as well + energy_filter = openmc.EnergyFilter(group_edges) + + # Now use the mesh filter in a tally and indicate what scores are desired + tally = openmc.Tally(name="Mesh tally") + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux', 'fission', 'nu-fission'] + tally.estimator = 'analog' + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + + ############################################################################### + # Exporting to OpenMC model + ############################################################################### + + model = openmc.Model() + model.geometry = geometry + model.materials = materials + model.settings = settings + model.tallies = tallies + return model + + +def test_random_ray_vacuum(): + harness = MGXSTestHarness('statepoint.10.h5', random_ray_model()) + harness.main() diff --git a/tests/regression_tests/score_current/test.py b/tests/regression_tests/score_current/test.py index 1309584d2ce..a338a662669 100644 --- a/tests/regression_tests/score_current/test.py +++ b/tests/regression_tests/score_current/test.py @@ -16,12 +16,12 @@ def model(): zr.add_nuclide('Zr90', 1.0) model.materials.extend([fuel, zr]) - box1 = openmc.model.rectangular_prism(10.0, 10.0) - box2 = openmc.model.rectangular_prism(20.0, 20.0, boundary_type='reflective') + box1 = openmc.model.RectangularPrism(10.0, 10.0) + box2 = openmc.model.RectangularPrism(20.0, 20.0, boundary_type='reflective') top = openmc.ZPlane(z0=10.0, boundary_type='vacuum') bottom = openmc.ZPlane(z0=-10.0, boundary_type='vacuum') - cell1 = openmc.Cell(fill=fuel, region=box1 & +bottom & -top) - cell2 = openmc.Cell(fill=zr, region=~box1 & box2 & +bottom & -top) + cell1 = openmc.Cell(fill=fuel, region=-box1 & +bottom & -top) + cell2 = openmc.Cell(fill=zr, region=+box1 & -box2 & +bottom & -top) model.geometry = openmc.Geometry([cell1, cell2]) model.settings.batches = 5 diff --git a/tests/regression_tests/source_dlopen/test.py b/tests/regression_tests/source_dlopen/test.py index efe942933dd..88ff9dd8509 100644 --- a/tests/regression_tests/source_dlopen/test.py +++ b/tests/regression_tests/source_dlopen/test.py @@ -18,7 +18,7 @@ def compile_source(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_sources CXX) add_library(source SHARED source_sampling.cpp) find_package(OpenMC REQUIRED HINTS {}) diff --git a/tests/regression_tests/source_parameterized_dlopen/test.py b/tests/regression_tests/source_parameterized_dlopen/test.py index c7c5d06b1f9..1cc253528cb 100644 --- a/tests/regression_tests/source_parameterized_dlopen/test.py +++ b/tests/regression_tests/source_parameterized_dlopen/test.py @@ -18,7 +18,7 @@ def compile_source(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_sources CXX) add_library(source SHARED parameterized_source_sampling.cpp) find_package(OpenMC REQUIRED HINTS {}) diff --git a/tests/regression_tests/statepoint_restart/test.py b/tests/regression_tests/statepoint_restart/test.py index 1e98bc480bb..82e514da877 100644 --- a/tests/regression_tests/statepoint_restart/test.py +++ b/tests/regression_tests/statepoint_restart/test.py @@ -1,7 +1,6 @@ from pathlib import Path import openmc -import pytest from tests.testing_harness import TestHarness from tests.regression_tests import config @@ -61,24 +60,35 @@ def test_statepoint_restart(): harness.main() -def test_batch_check(request): +def test_batch_check(request, capsys): xmls = list(request.path.parent.glob('*.xml')) with cdtemp(xmls): model = openmc.Model.from_xml() model.settings.particles = 100 + # run the model - sp_file = model.run() + sp_file = model.run(export_model_xml=False) + assert sp_file is not None # run a restart with the resulting statepoint # and the settings unchanged - with pytest.raises(RuntimeError, match='is smaller than the number of batches'): - model.run(restart_file=sp_file) - - # update the number of batches and run again + model.settings.batches = 6 + # ensure we capture output only from the next run + capsys.readouterr() + sp_file = model.run(export_model_xml=False, restart_file=sp_file) + # indicates that a new statepoint file was not created + assert sp_file is None + + output = capsys.readouterr().out + assert "WARNING" in output + assert "The number of batches specified for simulation" in output + + # update the number of batches and run again, + # this restart run should be successful model.settings.batches = 15 model.settings.statepoint = {} - sp_file = model.run(restart_file=sp_file) + sp_file = model.run(export_model_xml=False, restart_file=sp_file) sp = openmc.StatePoint(sp_file) assert sp.n_batches == 15 diff --git a/tests/regression_tests/surface_tally/inputs_true.dat b/tests/regression_tests/surface_tally/inputs_true.dat index dc65e2a15da..abd01266d48 100644 --- a/tests/regression_tests/surface_tally/inputs_true.dat +++ b/tests/regression_tests/surface_tally/inputs_true.dat @@ -54,7 +54,7 @@ 1 - + 2 diff --git a/tests/regression_tests/surface_tally/test.py b/tests/regression_tests/surface_tally/test.py index 1224ebbb972..8968db9b6fb 100644 --- a/tests/regression_tests/surface_tally/test.py +++ b/tests/regression_tests/surface_tally/test.py @@ -106,19 +106,19 @@ def __init__(self, *args, **kwargs): # Create partial current tallies from water to fuel # Filters - cell_from_filter = openmc.CellFromFilter(water) + mat_from_filter = openmc.MaterialFromFilter(borated_water) cell_filter = openmc.CellFilter(fuel) # Cell to cell filters for partial current cell_to_cell_tally = openmc.Tally(name=str('water_to_fuel_1')) - cell_to_cell_tally.filters = [cell_from_filter, cell_filter, \ + cell_to_cell_tally.filters = [mat_from_filter, cell_filter, \ energy_filter, polar_filter, azimuthal_filter] cell_to_cell_tally.scores = ['current'] tallies_file.append(cell_to_cell_tally) # Cell from + surface filters for partial current cell_to_cell_tally = openmc.Tally(name=str('water_to_fuel_2')) - cell_to_cell_tally.filters = [cell_from_filter, surface_filter, \ + cell_to_cell_tally.filters = [mat_from_filter, surface_filter, \ energy_filter, polar_filter, azimuthal_filter] cell_to_cell_tally.scores = ['current'] tallies_file.append(cell_to_cell_tally) diff --git a/tests/regression_tests/tallies/results_true.dat b/tests/regression_tests/tallies/results_true.dat index 673d2143bee..3dda0f9b211 100644 --- a/tests/regression_tests/tallies/results_true.dat +++ b/tests/regression_tests/tallies/results_true.dat @@ -1 +1 @@ -d1decdbec6cb59df91ba5c42cb37a04f413a34fa7faf7ad1eecfd7d53a14af8cb65b58335c4ede4e88f6d9ab35a1746251983cc991d74eab3e678887c84183bd \ No newline at end of file +ddfbb0a6f5498eb8ff33bb10beb64e244b015861919eb4837bd82855e5fd87c3ff97dfa382d3d60afa81c48d9f857fba8e0b99263e7b35587f8adb75be1fc0ec \ No newline at end of file diff --git a/tests/regression_tests/tally_aggregation/test.py b/tests/regression_tests/tally_aggregation/test.py index a29839a6f89..08d91166064 100644 --- a/tests/regression_tests/tally_aggregation/test.py +++ b/tests/regression_tests/tally_aggregation/test.py @@ -33,8 +33,8 @@ def model(): [pin, pin], [pin, pin], ] - box = openmc.model.rectangular_prism(2*d, 2*d, boundary_type='reflective') - main_cell = openmc.Cell(fill=lattice, region=box) + box = openmc.model.RectangularPrism(2*d, 2*d, boundary_type='reflective') + main_cell = openmc.Cell(fill=lattice, region=-box) model.geometry = openmc.Geometry([main_cell]) model.settings.batches = 10 diff --git a/tests/regression_tests/track_output/results_true.dat b/tests/regression_tests/track_output/results_true.dat index 746f1448fd1..148b54a30b0 100644 --- a/tests/regression_tests/track_output/results_true.dat +++ b/tests/regression_tests/track_output/results_true.dat @@ -97,22 +97,22 @@ neutron [((-9.663085e-01, -6.616522e-01, -9.863690e-01), (-7.513921e-01, 4.14097 ((-1.585080e+01, 1.545037e+00, -2.159072e+01), (6.457458e-01, -1.162934e-01, -7.546444e-01), 4.958073e-01, 5.061748e-06, 1.000000e+00, 22, 2129, 3) ((-1.576406e+01, 1.529415e+00, -2.169209e+01), (6.457458e-01, -1.162934e-01, -7.546444e-01), 4.958073e-01, 5.199673e-06, 1.000000e+00, 23, 2129, 1) ((-1.553972e+01, 1.489015e+00, -2.195425e+01), (6.457458e-01, -1.162934e-01, -7.546444e-01), 4.958073e-01, 5.556377e-06, 1.000000e+00, 23, 2115, 1) - ((-1.546081e+01, 1.474803e+00, -2.204648e+01), (3.257038e-01, -9.420596e-01, -8.025439e-02), 3.076319e-01, 5.681852e-06, 1.000000e+00, 23, 2115, 1) + ((-1.546081e+01, 1.474803e+00, -2.204648e+01), (3.257038e-01, -9.420596e-01, -8.025439e-02), 3.076319e-01, 5.681853e-06, 1.000000e+00, 23, 2115, 1) ((-1.543563e+01, 1.401988e+00, -2.205268e+01), (-3.677195e-01, -8.784218e-01, -3.052172e-01), 8.422973e-02, 5.782605e-06, 1.000000e+00, 23, 2115, 1) ((-1.553972e+01, 1.153338e+00, -2.213907e+01), (-3.677195e-01, -8.784218e-01, -3.052172e-01), 8.422973e-02, 6.487752e-06, 1.000000e+00, 23, 2129, 1) ((-1.565955e+01, 8.670807e-01, -2.223854e+01), (1.611814e-01, 2.692337e-01, -9.494913e-01), 8.559448e-02, 7.299552e-06, 1.000000e+00, 23, 2129, 1) ((-1.563263e+01, 9.120446e-01, -2.239711e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 7.712256e-06, 1.000000e+00, 23, 2129, 1) - ((-1.592604e+01, 1.214620e+00, -2.249695e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 9.406031e-06, 1.000000e+00, 22, 2129, 3) + ((-1.592604e+01, 1.214620e+00, -2.249695e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 9.406032e-06, 1.000000e+00, 22, 2129, 3) ((-1.598742e+01, 1.277922e+00, -2.251784e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 9.760391e-06, 1.000000e+00, 21, 2129, 2) ((-1.670388e+01, 2.016770e+00, -2.276163e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 1.389636e-05, 1.000000e+00, 22, 2129, 3) ((-1.676526e+01, 2.080073e+00, -2.278252e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 1.425072e-05, 1.000000e+00, 23, 2129, 1) ((-1.681575e+01, 2.132139e+00, -2.279970e+01), (4.286076e-02, -9.409799e-01, -3.357375e-01), 1.813618e-02, 1.454218e-05, 1.000000e+00, 23, 2129, 1) ((-1.681374e+01, 2.087933e+00, -2.281547e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.479439e-05, 1.000000e+00, 23, 2129, 1) ((-1.682083e+01, 2.021795e+00, -2.287841e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.532970e-05, 1.000000e+00, 22, 2129, 3) - ((-1.684417e+01, 1.804084e+00, -2.308558e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.709182e-05, 1.000000e+00, 21, 2129, 2) + ((-1.684417e+01, 1.804084e+00, -2.308558e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.709181e-05, 1.000000e+00, 21, 2129, 2) ((-1.686879e+01, 1.574384e+00, -2.330417e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.895097e-05, 1.000000e+00, 22, 2129, 3) - ((-1.689212e+01, 1.356673e+00, -2.351134e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 2.071309e-05, 1.000000e+00, 23, 2129, 1) - ((-1.689233e+01, 1.354754e+00, -2.351317e+01), (6.002458e-01, 4.967948e-01, 6.268172e-01), 8.602342e-03, 2.072862e-05, 1.000000e+00, 23, 2129, 1) + ((-1.689212e+01, 1.356673e+00, -2.351134e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 2.071308e-05, 1.000000e+00, 23, 2129, 1) + ((-1.689233e+01, 1.354754e+00, -2.351317e+01), (6.002458e-01, 4.967948e-01, 6.268172e-01), 8.602342e-03, 2.072861e-05, 1.000000e+00, 23, 2129, 1) ((-1.689148e+01, 1.355453e+00, -2.351229e+01), (6.002458e-01, 4.967948e-01, 6.268172e-01), 8.602342e-03, 2.073958e-05, 1.000000e+00, 22, 2129, 3) ((-1.682182e+01, 1.413107e+00, -2.343954e+01), (6.002458e-01, 4.967948e-01, 6.268172e-01), 8.602342e-03, 2.164421e-05, 1.000000e+00, 21, 2129, 2) ((-1.636364e+01, 1.792326e+00, -2.296107e+01), (9.641593e-01, -1.162836e-01, 2.384846e-01), 7.837617e-03, 2.759442e-05, 1.000000e+00, 21, 2129, 2) @@ -145,60 +145,60 @@ neutron [((-9.469716e-01, -2.580266e-01, 1.414357e-01), (1.438873e-01, 2.365819e ((-3.744933e+00, -1.031047e+00, 2.831062e+00), (-2.916959e-01, -6.494657e-01, -7.022164e-01), 8.590591e+00, 1.876753e-07, 0.000000e+00, 23, 2387, 1)] neutron [((6.474155e+00, -5.192870e+00, 5.413003e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450859e-05, 1.000000e+00, 21, 2367, 2) ((6.037250e+00, -5.003484e+00, 5.468885e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450883e-05, 1.000000e+00, 22, 2367, 3) - ((5.942573e+00, -4.962444e+00, 5.480995e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450888e-05, 1.000000e+00, 23, 2367, 1) - ((5.861800e+00, -4.927431e+00, 5.491326e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450892e-05, 1.000000e+00, 23, 2367, 1) - ((5.725160e+00, -4.971424e+00, 5.491002e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450902e-05, 1.000000e+00, 23, 2366, 1) + ((5.942573e+00, -4.962444e+00, 5.480995e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450889e-05, 1.000000e+00, 23, 2367, 1) + ((5.861800e+00, -4.927431e+00, 5.491326e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450893e-05, 1.000000e+00, 23, 2367, 1) + ((5.725160e+00, -4.971424e+00, 5.491002e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450903e-05, 1.000000e+00, 23, 2366, 1) ((5.494150e+00, -5.045802e+00, 5.490454e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450919e-05, 1.000000e+00, 22, 2366, 3) - ((5.392865e+00, -5.078412e+00, 5.490213e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450926e-05, 1.000000e+00, 21, 2366, 2) - ((4.612759e+00, -5.329579e+00, 5.488362e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450981e-05, 1.000000e+00, 22, 2366, 3) + ((5.392865e+00, -5.078412e+00, 5.490213e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450927e-05, 1.000000e+00, 21, 2366, 2) + ((4.612759e+00, -5.329579e+00, 5.488362e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450982e-05, 1.000000e+00, 22, 2366, 3) ((4.511474e+00, -5.362189e+00, 5.488121e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450989e-05, 1.000000e+00, 23, 2366, 1) ((4.089400e+00, -5.498082e+00, 5.487119e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451019e-05, 1.000000e+00, 23, 2365, 1) ((3.384113e+00, -5.725160e+00, 5.485445e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451069e-05, 1.000000e+00, 23, 2351, 1) - ((2.453640e+00, -6.024740e+00, 5.483237e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451135e-05, 1.000000e+00, 23, 2350, 1) - ((2.086813e+00, -6.142846e+00, 5.482366e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451161e-05, 1.000000e+00, 22, 2350, 3) + ((2.453640e+00, -6.024740e+00, 5.483237e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451136e-05, 1.000000e+00, 23, 2350, 1) + ((2.086813e+00, -6.142846e+00, 5.482366e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451162e-05, 1.000000e+00, 22, 2350, 3) ((1.993594e+00, -6.172859e+00, 5.482145e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451168e-05, 1.000000e+00, 21, 2350, 2) - ((1.325704e+00, -6.387896e+00, 5.480560e+00), (8.986086e-01, -4.380574e-01, -2.466265e-02), 9.775839e+05, 4.451215e-05, 1.000000e+00, 21, 2350, 2) - ((2.061403e+00, -6.746538e+00, 5.460368e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451275e-05, 1.000000e+00, 21, 2350, 2) - ((1.122794e+00, -6.498940e+00, 4.743664e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451374e-05, 1.000000e+00, 22, 2350, 3) - ((1.036483e+00, -6.476172e+00, 4.677758e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451383e-05, 1.000000e+00, 23, 2350, 1) - ((8.178800e-01, -6.418507e+00, 4.510837e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451406e-05, 1.000000e+00, 23, 2349, 1) + ((1.325704e+00, -6.387896e+00, 5.480560e+00), (8.986086e-01, -4.380574e-01, -2.466265e-02), 9.775839e+05, 4.451216e-05, 1.000000e+00, 21, 2350, 2) + ((2.061403e+00, -6.746538e+00, 5.460368e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451276e-05, 1.000000e+00, 21, 2350, 2) + ((1.122794e+00, -6.498940e+00, 4.743664e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451375e-05, 1.000000e+00, 22, 2350, 3) + ((1.036483e+00, -6.476172e+00, 4.677758e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451384e-05, 1.000000e+00, 23, 2350, 1) + ((8.178800e-01, -6.418507e+00, 4.510837e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451407e-05, 1.000000e+00, 23, 2349, 1) ((5.725264e-01, -6.353784e+00, 4.323490e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451432e-05, 1.000000e+00, 22, 2349, 3) - ((4.668297e-01, -6.325902e+00, 4.242782e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451443e-05, 1.000000e+00, 21, 2349, 2) + ((4.668297e-01, -6.325902e+00, 4.242782e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451444e-05, 1.000000e+00, 21, 2349, 2) ((-2.989816e-01, -6.123888e+00, 3.658023e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451524e-05, 1.000000e+00, 22, 2349, 3) ((-4.046782e-01, -6.096006e+00, 3.577315e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451535e-05, 1.000000e+00, 23, 2349, 1) ((-8.040445e-01, -5.990656e+00, 3.272367e+00), (-7.989001e-01, -1.890553e-01, -5.709787e-01), 6.637114e+05, 4.451577e-05, 1.000000e+00, 23, 2349, 1) - ((-8.178800e-01, -5.993930e+00, 3.262478e+00), (-7.989001e-01, -1.890553e-01, -5.709787e-01), 6.637114e+05, 4.451578e-05, 1.000000e+00, 23, 2348, 1) - ((-1.077910e+00, -6.055465e+00, 3.076633e+00), (-4.487118e-01, 1.670254e-01, -8.779295e-01), 4.550608e+05, 4.451607e-05, 1.000000e+00, 23, 2348, 1) - ((-1.215611e+00, -6.004208e+00, 2.807214e+00), (7.872329e-01, 4.481433e-01, -4.235941e-01), 3.544207e+03, 4.451640e-05, 1.000000e+00, 23, 2348, 1) - ((-1.100296e+00, -5.938564e+00, 2.745166e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.451818e-05, 1.000000e+00, 23, 2348, 1) + ((-8.178800e-01, -5.993930e+00, 3.262478e+00), (-7.989001e-01, -1.890553e-01, -5.709787e-01), 6.637114e+05, 4.451579e-05, 1.000000e+00, 23, 2348, 1) + ((-1.077910e+00, -6.055465e+00, 3.076633e+00), (-4.487118e-01, 1.670254e-01, -8.779295e-01), 4.550608e+05, 4.451608e-05, 1.000000e+00, 23, 2348, 1) + ((-1.215611e+00, -6.004208e+00, 2.807214e+00), (7.872329e-01, 4.481433e-01, -4.235941e-01), 3.544207e+03, 4.451641e-05, 1.000000e+00, 23, 2348, 1) + ((-1.100296e+00, -5.938564e+00, 2.745166e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.451819e-05, 1.000000e+00, 23, 2348, 1) ((-8.178800e-01, -5.761448e+00, 2.754541e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.452273e-05, 1.000000e+00, 23, 2349, 1) ((-7.600184e-01, -5.725160e+00, 2.756462e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.452366e-05, 1.000000e+00, 23, 2363, 1) ((-2.947156e-01, -5.433347e+00, 2.771908e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.453115e-05, 1.000000e+00, 22, 2363, 3) - ((-2.073333e-01, -5.378546e+00, 2.774809e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.453255e-05, 1.000000e+00, 21, 2363, 2) - ((-1.746705e-01, -5.358062e+00, 2.775893e+00), (5.278774e-01, 5.459205e-01, 6.506276e-01), 2.806481e+03, 4.453308e-05, 1.000000e+00, 21, 2363, 2) - ((-8.681718e-02, -5.267205e+00, 2.884176e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.453535e-05, 1.000000e+00, 21, 2363, 2) + ((-2.073333e-01, -5.378546e+00, 2.774809e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.453256e-05, 1.000000e+00, 21, 2363, 2) + ((-1.746705e-01, -5.358062e+00, 2.775893e+00), (5.278774e-01, 5.459205e-01, 6.506276e-01), 2.806481e+03, 4.453309e-05, 1.000000e+00, 21, 2363, 2) + ((-8.681718e-02, -5.267205e+00, 2.884176e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.453536e-05, 1.000000e+00, 21, 2363, 2) ((-3.684221e-01, -5.266924e+00, 2.745840e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.453967e-05, 1.000000e+00, 22, 2363, 3) ((-4.840903e-01, -5.266809e+00, 2.689019e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.454144e-05, 1.000000e+00, 23, 2363, 1) - ((-8.178800e-01, -5.266475e+00, 2.525049e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.454655e-05, 1.000000e+00, 23, 2362, 1) + ((-8.178800e-01, -5.266475e+00, 2.525049e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.454656e-05, 1.000000e+00, 23, 2362, 1) ((-1.151175e+00, -5.266142e+00, 2.361321e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.455166e-05, 1.000000e+00, 22, 2362, 3) - ((-1.266464e+00, -5.266027e+00, 2.304687e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.455342e-05, 1.000000e+00, 21, 2362, 2) + ((-1.266464e+00, -5.266027e+00, 2.304687e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.455343e-05, 1.000000e+00, 21, 2362, 2) ((-2.005772e+00, -5.265288e+00, 1.941509e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.456475e-05, 1.000000e+00, 22, 2362, 3) - ((-2.121061e+00, -5.265173e+00, 1.884875e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.456651e-05, 1.000000e+00, 23, 2362, 1) - ((-2.393759e+00, -5.264900e+00, 1.750915e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457069e-05, 1.000000e+00, 23, 2362, 1) - ((-2.453640e+00, -5.275804e+00, 1.764337e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457181e-05, 1.000000e+00, 23, 2361, 1) - ((-2.470658e+00, -5.278903e+00, 1.768152e+00), (-6.728606e-01, -2.916408e-01, -6.798561e-01), 4.873672e+02, 4.457213e-05, 1.000000e+00, 23, 2361, 1) - ((-3.463692e+00, -5.709318e+00, 7.647939e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462046e-05, 1.000000e+00, 23, 2361, 1) - ((-3.467511e+00, -5.725160e+00, 7.600247e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462138e-05, 1.000000e+00, 23, 2347, 1) + ((-2.121061e+00, -5.265173e+00, 1.884875e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.456652e-05, 1.000000e+00, 23, 2362, 1) + ((-2.393759e+00, -5.264900e+00, 1.750915e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457070e-05, 1.000000e+00, 23, 2362, 1) + ((-2.453640e+00, -5.275804e+00, 1.764337e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457182e-05, 1.000000e+00, 23, 2361, 1) + ((-2.470658e+00, -5.278903e+00, 1.768152e+00), (-6.728606e-01, -2.916408e-01, -6.798561e-01), 4.873672e+02, 4.457214e-05, 1.000000e+00, 23, 2361, 1) + ((-3.463692e+00, -5.709318e+00, 7.647939e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462047e-05, 1.000000e+00, 23, 2361, 1) + ((-3.467511e+00, -5.725160e+00, 7.600247e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462139e-05, 1.000000e+00, 23, 2347, 1) ((-3.533785e+00, -6.000066e+00, 6.772677e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.463730e-05, 1.000000e+00, 22, 2347, 3) - ((-3.562245e+00, -6.118119e+00, 6.417291e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.464413e-05, 1.000000e+00, 21, 2347, 2) + ((-3.562245e+00, -6.118119e+00, 6.417291e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.464414e-05, 1.000000e+00, 21, 2347, 2) ((-3.723934e+00, -6.788806e+00, 4.398272e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.468297e-05, 1.000000e+00, 22, 2347, 3) - ((-3.752394e+00, -6.906859e+00, 4.042885e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.468980e-05, 1.000000e+00, 23, 2347, 1) - ((-3.848515e+00, -7.305571e+00, 2.842613e-01), (6.498448e-01, -3.243413e-01, -6.873896e-01), 2.328979e+01, 4.471289e-05, 1.000000e+00, 23, 2347, 1) + ((-3.752394e+00, -6.906859e+00, 4.042885e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.468981e-05, 1.000000e+00, 23, 2347, 1) + ((-3.848515e+00, -7.305571e+00, 2.842613e-01), (6.498448e-01, -3.243413e-01, -6.873896e-01), 2.328979e+01, 4.471290e-05, 1.000000e+00, 23, 2347, 1) ((-3.737619e+00, -7.360920e+00, 1.669579e-01), (6.498448e-01, -3.243413e-01, -6.873896e-01), 2.328979e+01, 4.473846e-05, 1.000000e+00, 11, 1750, 1) - ((-3.574308e+00, -7.442429e+00, -5.788041e-03), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.477610e-05, 1.000000e+00, 11, 1750, 1) - ((-3.398889e+00, -7.360920e+00, -1.767852e-01), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.482291e-05, 1.000000e+00, 23, 2347, 1) - ((-2.904712e+00, -7.131298e+00, -6.585068e-01), (-4.836985e-02, -2.819627e-01, -9.582053e-01), 2.610202e+00, 4.495476e-05, 1.000000e+00, 23, 2347, 1) - ((-2.923579e+00, -7.241285e+00, -1.032280e+00), (-6.089807e-01, -2.347428e-01, -7.576532e-01), 1.671268e+00, 4.512932e-05, 1.000000e+00, 23, 2347, 1) + ((-3.574308e+00, -7.442429e+00, -5.788041e-03), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.477611e-05, 1.000000e+00, 11, 1750, 1) + ((-3.398889e+00, -7.360920e+00, -1.767852e-01), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.482292e-05, 1.000000e+00, 23, 2347, 1) + ((-2.904712e+00, -7.131298e+00, -6.585068e-01), (-4.836985e-02, -2.819627e-01, -9.582053e-01), 2.610202e+00, 4.495477e-05, 1.000000e+00, 23, 2347, 1) + ((-2.923579e+00, -7.241285e+00, -1.032280e+00), (-6.089807e-01, -2.347428e-01, -7.576532e-01), 1.671268e+00, 4.512933e-05, 1.000000e+00, 23, 2347, 1) ((-3.012516e+00, -7.275567e+00, -1.142930e+00), (3.221931e-01, -5.406104e-01, -7.771306e-01), 1.275709e-01, 4.521100e-05, 1.000000e+00, 23, 2347, 1) ((-2.984032e+00, -7.323360e+00, -1.211633e+00), (-8.629564e-01, -1.719529e-01, -4.751195e-01), 1.897424e-02, 4.538995e-05, 1.000000e+00, 23, 2347, 1) ((-3.172528e+00, -7.360920e+00, -1.315413e+00), (-8.629564e-01, -1.719529e-01, -4.751195e-01), 1.897424e-02, 4.653641e-05, 1.000000e+00, 11, 1750, 1) @@ -206,13 +206,13 @@ neutron [((6.474155e+00, -5.192870e+00, 5.413003e+00), (-9.112559e-01, 3.950037e ((-3.625236e+00, -7.552741e+00, -1.449021e+00), (-1.976867e-01, 2.826475e-01, 9.386322e-01), 2.145548e-02, 5.009730e-05, 1.000000e+00, 11, 1750, 1) ((-3.636290e+00, -7.536936e+00, -1.396537e+00), (1.266676e-01, 2.265353e-01, -9.657314e-01), 1.815564e-02, 5.037329e-05, 1.000000e+00, 11, 1750, 1) ((-3.621792e+00, -7.511008e+00, -1.507069e+00), (-4.275550e-01, 8.441443e-01, 3.234457e-01), 1.848049e-02, 5.098741e-05, 1.000000e+00, 11, 1750, 1) - ((-3.697811e+00, -7.360920e+00, -1.449560e+00), (-4.275550e-01, 8.441443e-01, 3.234457e-01), 1.848049e-02, 5.193299e-05, 1.000000e+00, 23, 2347, 1) - ((-3.703436e+00, -7.349814e+00, -1.445305e+00), (-7.832167e-01, 5.713670e-01, 2.451762e-01), 1.861705e-02, 5.200296e-05, 1.000000e+00, 23, 2347, 1) + ((-3.697811e+00, -7.360920e+00, -1.449560e+00), (-4.275550e-01, 8.441443e-01, 3.234457e-01), 1.848049e-02, 5.193300e-05, 1.000000e+00, 23, 2347, 1) + ((-3.703436e+00, -7.349814e+00, -1.445305e+00), (-7.832167e-01, 5.713670e-01, 2.451762e-01), 1.861705e-02, 5.200297e-05, 1.000000e+00, 23, 2347, 1) ((-3.823243e+00, -7.262414e+00, -1.407801e+00), (-9.917106e-01, 5.069164e-02, 1.180695e-01), 4.703334e-02, 5.281350e-05, 1.000000e+00, 23, 2347, 1) ((-4.089400e+00, -7.248809e+00, -1.376113e+00), (-9.917106e-01, 5.069164e-02, 1.180695e-01), 4.703334e-02, 5.370820e-05, 1.000000e+00, 23, 2346, 1) ((-4.131081e+00, -7.246679e+00, -1.371151e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.384831e-05, 1.000000e+00, 23, 2346, 1) ((-4.089400e+00, -7.265067e+00, -1.366848e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.396486e-05, 1.000000e+00, 23, 2347, 1) - ((-3.872134e+00, -7.360920e+00, -1.344418e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.457239e-05, 1.000000e+00, 11, 1750, 1) + ((-3.872134e+00, -7.360920e+00, -1.344418e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.457240e-05, 1.000000e+00, 11, 1750, 1) ((-3.673062e+00, -7.448746e+00, -1.323866e+00), (2.976906e-01, 8.174443e-01, -4.931178e-01), 5.538060e-02, 5.512905e-05, 1.000000e+00, 11, 1750, 1) ((-3.658580e+00, -7.408978e+00, -1.347855e+00), (-5.409120e-01, 8.345378e-01, -1.046943e-01), 8.122576e-02, 5.527851e-05, 1.000000e+00, 11, 1750, 1) - ((-3.682981e+00, -7.371331e+00, -1.352578e+00), (-5.409120e-01, 8.345378e-01, -1.046943e-01), 8.122576e-02, 5.539294e-05, 0.000000e+00, 11, 1750, 1)] + ((-3.682981e+00, -7.371331e+00, -1.352578e+00), (-5.409120e-01, 8.345378e-01, -1.046943e-01), 8.122576e-02, 5.539295e-05, 0.000000e+00, 11, 1750, 1)] diff --git a/tests/regression_tests/unstructured_mesh/inputs_true10.dat b/tests/regression_tests/unstructured_mesh/inputs_true10.dat index ebc5548f54e..81dfbdc52d9 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true10.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true10.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true11.dat b/tests/regression_tests/unstructured_mesh/inputs_true11.dat index e30a5ccfc1e..b224a3ebc36 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true11.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true11.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true12.dat b/tests/regression_tests/unstructured_mesh/inputs_true12.dat index a60b70eab22..a18aee627ce 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true12.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true12.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true13.dat b/tests/regression_tests/unstructured_mesh/inputs_true13.dat index 1b1aa19c312..0e9123c9036 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true13.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true13.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true14.dat b/tests/regression_tests/unstructured_mesh/inputs_true14.dat index d07504b9d28..9d4db490451 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true14.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true14.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true15.dat b/tests/regression_tests/unstructured_mesh/inputs_true15.dat index 8cabbb38d0b..eab97b95227 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true15.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true15.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true8.dat b/tests/regression_tests/unstructured_mesh/inputs_true8.dat index 5f14aa691bc..7994e1add76 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true8.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true8.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true9.dat b/tests/regression_tests/unstructured_mesh/inputs_true9.dat index 92354c75cf0..4fff123d7d4 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true9.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true9.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/test.py b/tests/regression_tests/unstructured_mesh/test.py index 7c7961928e7..0082198ddec 100644 --- a/tests/regression_tests/unstructured_mesh/test.py +++ b/tests/regression_tests/unstructured_mesh/test.py @@ -54,7 +54,7 @@ def _compare_results(self): exp_vertex = (-10.0, -10.0, 10.0) exp_centroid = (-9.0, -9.0, 9.0) - np.testing.assert_array_equal(umesh.vertices[:, 0], exp_vertex) + np.testing.assert_array_equal(umesh.vertices[0], exp_vertex) np.testing.assert_array_equal(umesh.centroid(0), exp_centroid) # loop over the tallies and get data @@ -277,6 +277,8 @@ def test_unstructured_mesh_tets(model, test_opts): # add analagous unstructured mesh tally uscd_mesh = openmc.UnstructuredMesh(mesh_filename, test_opts['library']) + if test_opts['library'] == 'moab': + uscd_mesh.options = 'MAX_DEPTH=15;PLANE_SET=2' uscd_filter = openmc.MeshFilter(mesh=uscd_mesh) # create tallies diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 40c9d37d806..81527452b84 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -384,6 +384,63 @@ def _get_results(self): return super()._get_results(True) +class TolerantPyAPITestHarness(PyAPITestHarness): + """Specialized harness for running tests that involve significant levels + of floating point non-associativity when using shared memory parallelism + due to single precision usage (e.g., as in the random ray solver). + + """ + def _are_files_equal(self, actual_path, expected_path, tolerance): + def isfloat(value): + try: + float(value) + return True + except ValueError: + return False + + def tokenize(line): + return line.strip().split() + + def compare_tokens(token1, token2): + if isfloat(token1) and isfloat(token2): + float1, float2 = float(token1), float(token2) + return abs(float1 - float2) <= tolerance * max(abs(float1), abs(float2)) + else: + return token1 == token2 + + expected = open(expected_path).readlines() + actual = open(actual_path).readlines() + + if len(expected) != len(actual): + return False + + for line1, line2 in zip(expected, actual): + tokens1 = tokenize(line1) + tokens2 = tokenize(line2) + + if len(tokens1) != len(tokens2): + return False + + for token1, token2 in zip(tokens1, tokens2): + if not compare_tokens(token1, token2): + return False + + return True + + def _compare_results(self): + """Make sure the current results agree with the reference.""" + compare = self._are_files_equal('results_test.dat', 'results_true.dat', 1e-6) + if not compare: + expected = open('results_true.dat').readlines() + actual = open('results_test.dat').readlines() + diff = unified_diff(expected, actual, 'results_true.dat', + 'results_test.dat') + print('Result differences:') + print(''.join(colorize(diff))) + os.rename('results_test.dat', 'results_error.dat') + assert compare, 'Results do not agree' + + class PlotTestHarness(TestHarness): """Specialized TestHarness for running OpenMC plotting tests.""" def __init__(self, plot_names, voxel_convert_checks=[]): diff --git a/tests/unit_tests/dagmc/test_h5m_subdir.py b/tests/unit_tests/dagmc/test_h5m_subdir.py new file mode 100644 index 00000000000..dd9c6b043b6 --- /dev/null +++ b/tests/unit_tests/dagmc/test_h5m_subdir.py @@ -0,0 +1,40 @@ +import shutil +from pathlib import Path + +import openmc +import openmc.lib +import pytest + +pytestmark = pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), reason="DAGMC CAD geometry is not enabled." +) + + +@pytest.mark.parametrize("absolute", [True, False]) +def test_model_h5m_in_subdirectory(run_in_tmpdir, request, absolute): + # Create new subdirectory and copy h5m file there + h5m = Path(request.fspath).parent / "dagmc.h5m" + subdir = Path("h5m") + subdir.mkdir() + shutil.copy(h5m, subdir) + + # Create simple model with h5m file in subdirectory + if absolute: + dag_univ = openmc.DAGMCUniverse((subdir / "dagmc.h5m").absolute()) + else: + dag_univ = openmc.DAGMCUniverse(subdir / "dagmc.h5m") + model = openmc.Model() + model.geometry = openmc.Geometry(dag_univ.bounded_universe()) + mat1 = openmc.Material(name="41") + mat1.add_nuclide("H1", 1.0) + mat2 = openmc.Material(name="no-void fuel") + mat2.add_nuclide("U235", 1.0) + model.materials = [mat1, mat2] + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + + # Make sure model can load + model.export_to_model_xml() + openmc.lib.init(["model.xml"]) + openmc.lib.finalize() diff --git a/tests/unit_tests/test_bounding_box.py b/tests/unit_tests/test_bounding_box.py index 89e50427151..57c880092e3 100644 --- a/tests/unit_tests/test_bounding_box.py +++ b/tests/unit_tests/test_bounding_box.py @@ -78,9 +78,9 @@ def test_bounding_box_input_checking(): def test_bounding_box_extents(): - assert test_bb_1.extent['xy'] == (-10., 1., -20., 2.) - assert test_bb_1.extent['xz'] == (-10., 1., -30., 3.) - assert test_bb_1.extent['yz'] == (-20., 2., -30., 3.) + assert test_bb_1.extent["xy"] == (-10.0, 1.0, -20.0, 2.0) + assert test_bb_1.extent["xz"] == (-10.0, 1.0, -30.0, 3.0) + assert test_bb_1.extent["yz"] == (-20.0, 2.0, -30.0, 3.0) def test_bounding_box_methods(): @@ -156,3 +156,35 @@ def test_bounding_box_methods(): assert all(test_bb[0] == [-50.1, -50.1, -12.1]) assert all(test_bb[1] == [50.1, 14.1, 50.1]) + + +@pytest.mark.parametrize( + "bb, other, expected", + [ + (test_bb_1, (0, 0, 0), True), + (test_bb_2, (3, 3, 3), False), + # completely disjoint + (test_bb_1, test_bb_2, False), + # contained but touching border + (test_bb_1, test_bb_3, False), + # Fully contained + (test_bb_1, openmc.BoundingBox((-9, -19, -29), (0, 0, 0)), True), + # intersecting boxes + (test_bb_1, openmc.BoundingBox((-9, -19, -29), (1, 2, 5)), False), + ], +) +def test_bounding_box_contains(bb, other, expected): + assert (other in bb) == expected + + +@pytest.mark.parametrize( + "invalid, ex", + [ + ((1, 0), ValueError), + ((1, 2, 3, 4), ValueError), + ("foo", TypeError), + ], +) +def test_bounding_box_contains_checking(invalid, ex): + with pytest.raises(ex): + invalid in test_bb_1 diff --git a/tests/unit_tests/test_cell.py b/tests/unit_tests/test_cell.py index 888a0ef88f3..95c8249bb33 100644 --- a/tests/unit_tests/test_cell.py +++ b/tests/unit_tests/test_cell.py @@ -1,11 +1,9 @@ import lxml.etree as ET - import numpy as np from uncertainties import ufloat import openmc import pytest - from tests.unit_tests import assert_unbounded from openmc.data import atomic_mass, AVOGADRO diff --git a/tests/unit_tests/test_cell_instance.py b/tests/unit_tests/test_cell_instance.py index 00424f9c5a5..25c20cfef22 100644 --- a/tests/unit_tests/test_cell_instance.py +++ b/tests/unit_tests/test_cell_instance.py @@ -9,6 +9,7 @@ @pytest.fixture(scope='module', autouse=True) def double_lattice_model(): + openmc.reset_auto_ids() model = openmc.Model() # Create a single material @@ -39,6 +40,18 @@ def double_lattice_model(): cell_with_lattice2.translation = (2., 0., 0.) model.geometry = openmc.Geometry([cell_with_lattice1, cell_with_lattice2]) + tally = openmc.Tally() + tally.filters = [openmc.DistribcellFilter(c)] + tally.scores = ['flux'] + model.tallies = [tally] + + # Add box source that covers the model space well + bbox = model.geometry.bounding_box + bbox[0][2] = -0.5 + bbox[1][2] = 0.5 + space = openmc.stats.Box(*bbox) + model.settings.source = openmc.IndependentSource(space=space) + # Add necessary settings and export model.settings.batches = 10 model.settings.inactive = 0 @@ -71,3 +84,9 @@ def double_lattice_model(): def test_cell_instance_multilattice(r, expected_cell_instance): _, cell_instance = openmc.lib.find_cell(r) assert cell_instance == expected_cell_instance + + +def test_cell_instance_multilattice_results(): + openmc.lib.run() + tally_results = openmc.lib.tallies[1].mean + assert (tally_results != 0.0).all() diff --git a/tests/unit_tests/test_cylindrical_mesh.py b/tests/unit_tests/test_cylindrical_mesh.py index df0c5d2bb53..b408bc0e917 100644 --- a/tests/unit_tests/test_cylindrical_mesh.py +++ b/tests/unit_tests/test_cylindrical_mesh.py @@ -98,7 +98,7 @@ def test_offset_mesh(run_in_tmpdir, model, estimator, origin): centroids = mesh.centroids for ijk in mesh.indices: i, j, k = np.array(ijk) - 1 - if model.geometry.find(centroids[:, i, j, k]): + if model.geometry.find(centroids[i, j, k]): mean[i, j, k] == 0.0 else: mean[i, j, k] != 0.0 diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index e4e0231355f..9753a53f738 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -5,6 +5,7 @@ from math import log import os from pathlib import Path +import warnings import numpy as np from openmc.mpi import comm @@ -438,9 +439,9 @@ def test_validate(simple_chain): simple_chain["C"].yield_data = {0.0253: {"A": 1.4, "B": 0.6}} assert simple_chain.validate(strict=True, tolerance=0.0) - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") assert simple_chain.validate(strict=False, quiet=False, tolerance=0.0) - assert len(record) == 0 # Mess up "earlier" nuclide's reactions decay_mode = simple_chain["A"].decay_modes.pop() diff --git a/tests/unit_tests/test_deplete_coupled_operator.py b/tests/unit_tests/test_deplete_coupled_operator.py index 4fa1dac2ed0..fe79d621b12 100644 --- a/tests/unit_tests/test_deplete_coupled_operator.py +++ b/tests/unit_tests/test_deplete_coupled_operator.py @@ -38,8 +38,9 @@ def model(): pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] pin_univ = openmc.model.pin(pin_surfaces, materials) - bound_box = openmc.rectangular_prism(1.24, 1.24, boundary_type="reflective") - root_cell = openmc.Cell(fill=pin_univ, region=bound_box) + bound_box = openmc.model.RectangularPrism( + 1.24, 1.24, boundary_type="reflective") + root_cell = openmc.Cell(fill=pin_univ, region=-bound_box) geometry = openmc.Geometry([root_cell]) settings = openmc.Settings() @@ -95,7 +96,7 @@ def test_diff_volume_method_match_cell(model_with_volumes): chain_file=CHAIN_PATH ) - all_cells = list(operator.geometry.get_all_cells().values()) + all_cells = list(operator.model.geometry.get_all_cells().values()) assert all_cells[0].fill.volume == 4.19 assert all_cells[1].fill.volume == 33.51 # mat2 is not depletable @@ -112,9 +113,8 @@ def test_diff_volume_method_divide_equally(model_with_volumes): chain_file=CHAIN_PATH ) - all_cells = list(operator.geometry.get_all_cells().values()) + all_cells = list(operator.model.geometry.get_all_cells().values()) assert all_cells[0].fill[0].volume == 51 assert all_cells[1].fill[0].volume == 51 # mat2 is not depletable assert all_cells[2].fill.volume is None - diff --git a/tests/unit_tests/test_deplete_decay_products.py b/tests/unit_tests/test_deplete_decay.py similarity index 55% rename from tests/unit_tests/test_deplete_decay_products.py rename to tests/unit_tests/test_deplete_decay.py index 751a96ca6e6..aca812560c7 100644 --- a/tests/unit_tests/test_deplete_decay_products.py +++ b/tests/unit_tests/test_deplete_decay.py @@ -1,3 +1,5 @@ +from pathlib import Path + import openmc.deplete import numpy as np import pytest @@ -45,3 +47,37 @@ def test_deplete_decay_products(run_in_tmpdir): # H1 and He4 assert h1[1] == pytest.approx(1e24) assert he4[1] == pytest.approx(1e24) + + +def test_deplete_decay_step_fissionable(run_in_tmpdir): + """Ensures that power is not computed in zero power cases with + fissionable material present. This tests decay calculations without + power, although this specific example does not exhibit any decay. + + Proves github issue #2963 is fixed + """ + + # Set up a pure decay operator + micro_xs = openmc.deplete.MicroXS(np.empty((0, 0)), [], []) + mat = openmc.Material() + mat.name = 'I do not decay.' + mat.add_nuclide('U238', 1.0, 'ao') + mat.volume = 10.0 + mat.set_density('g/cc', 1.0) + original_atoms = mat.get_nuclide_atoms()['U238'] + + mats = openmc.Materials([mat]) + op = openmc.deplete.IndependentOperator( + mats, [1.0], [micro_xs], Path(__file__).parents[1] / "chain_simple.xml") + + # Create time integrator and integrate + integrator = openmc.deplete.PredictorIntegrator( + op, [1.0], power=[0.0], timestep_units='s' + ) + integrator.integrate() + + # Get concentration of U238. It should be unchanged since this chain has no U238 decay. + results = openmc.deplete.Results('depletion_results.h5') + _, u238 = results.get_atoms("1", "U238") + + assert u238[1] == pytest.approx(original_atoms) diff --git a/tests/unit_tests/test_deplete_fission_yields.py b/tests/unit_tests/test_deplete_fission_yields.py index b603710f34a..1937e61e333 100644 --- a/tests/unit_tests/test_deplete_fission_yields.py +++ b/tests/unit_tests/test_deplete_fission_yields.py @@ -27,8 +27,8 @@ def materials(tmpdir_factory): mfuel.add_nuclide(nuclide, 1.0) openmc.Materials([mfuel]).export_to_xml() # Geometry - box = openmc.rectangular_prism(1.0, 1.0, boundary_type="reflective") - cell = openmc.Cell(fill=mfuel, region=box) + box = openmc.model.RectangularPrism(1.0, 1.0, boundary_type="reflective") + cell = openmc.Cell(fill=mfuel, region=-box) root = openmc.Universe(cells=[cell]) openmc.Geometry(root).export_to_xml() # settings diff --git a/tests/unit_tests/test_deplete_independent_operator.py b/tests/unit_tests/test_deplete_independent_operator.py index 813ac06de80..9129cf0642f 100644 --- a/tests/unit_tests/test_deplete_independent_operator.py +++ b/tests/unit_tests/test_deplete_independent_operator.py @@ -4,8 +4,10 @@ from pathlib import Path -from openmc.deplete import IndependentOperator, MicroXS +import pytest + from openmc import Material, Materials +from openmc.deplete import IndependentOperator, MicroXS CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" @@ -36,3 +38,17 @@ def test_operator_init(): fluxes = [1.0] micros = [micro_xs] IndependentOperator(materials, fluxes, micros, CHAIN_PATH) + + +def test_error_handling(): + micro_xs = MicroXS.from_csv(ONE_GROUP_XS) + fuel = Material(name="oxygen") + fuel.add_element("O", 2) + fuel.set_density("g/cc", 1) + fuel.depletable = True + fuel.volume = 1 + materials = Materials([fuel]) + fluxes = [1.0, 2.0] + micros = [micro_xs] + with pytest.raises(ValueError, match=r"The length of fluxes \(2\)"): + IndependentOperator(materials, fluxes, micros, CHAIN_PATH) diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index ba625a99981..b1d2cb950eb 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -40,7 +40,7 @@ def test_results_save(run_in_tmpdir): stages = 3 - np.random.seed(comm.rank) + rng = np.random.RandomState(comm.rank) # Mock geometry op = MagicMock() @@ -68,26 +68,26 @@ def test_results_save(run_in_tmpdir): x2 = [] for i in range(stages): - x1.append([np.random.rand(2), np.random.rand(2)]) - x2.append([np.random.rand(2), np.random.rand(2)]) + x1.append([rng.random(2), rng.random(2)]) + x2.append([rng.random(2), rng.random(2)]) # Construct r r1 = ReactionRates(burn_list, ["na", "nb"], ["ra", "rb"]) - r1[:] = np.random.rand(2, 2, 2) + r1[:] = rng.random((2, 2, 2)) rate1 = [] rate2 = [] for i in range(stages): rate1.append(copy.deepcopy(r1)) - r1[:] = np.random.rand(2, 2, 2) + r1[:] = rng.random((2, 2, 2)) rate2.append(copy.deepcopy(r1)) - r1[:] = np.random.rand(2, 2, 2) + r1[:] = rng.random((2, 2, 2)) # Create global terms # Col 0: eig, Col 1: uncertainty - eigvl1 = np.random.rand(stages, 2) - eigvl2 = np.random.rand(stages, 2) + eigvl1 = rng.random((stages, 2)) + eigvl2 = rng.random((stages, 2)) eigvl1 = comm.bcast(eigvl1, root=0) eigvl2 = comm.bcast(eigvl2, root=0) diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 582c584654d..ad54026f014 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -51,6 +51,7 @@ def test_from_array(): r'match dimensions of data array of shape \(\d*\, \d*\)'): MicroXS(data[:, 0], nuclides, reactions) + def test_csv(): ref_xs = MicroXS.from_csv(ONE_GROUP_XS) ref_xs.to_csv('temp_xs.csv') @@ -58,3 +59,53 @@ def test_csv(): assert np.all(ref_xs.data == temp_xs.data) remove('temp_xs.csv') + +def test_from_multigroup_flux(): + energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7] + flux = [1.1e-7, 1.2e-6, 1.3e-5, 1.4e-4] + chain_file = Path(__file__).parents[1] / 'chain_simple.xml' + kwargs = {'multigroup_flux': flux, 'chain_file': chain_file} + + # test with energy group structure from string + microxs = MicroXS.from_multigroup_flux(energies='CASMO-4', **kwargs) + assert isinstance(microxs, MicroXS) + + # test with energy group structure as floats + microxs = MicroXS.from_multigroup_flux(energies=energies, **kwargs) + assert isinstance(microxs, MicroXS) + + # test with nuclides provided + microxs = MicroXS.from_multigroup_flux( + energies=energies, nuclides=['Gd157', 'H1'], **kwargs + ) + assert isinstance(microxs, MicroXS) + assert microxs.nuclides == ['Gd157', 'H1'] + + # test with reactions provided + microxs = MicroXS.from_multigroup_flux( + energies=energies, reactions=['fission', '(n,2n)'], **kwargs + ) + assert isinstance(microxs, MicroXS) + assert microxs.reactions == ['fission', '(n,2n)'] + + +def test_multigroup_flux_same(): + chain_file = Path(__file__).parents[1] / 'chain_simple.xml' + + # Generate micro XS based on 4-group flux + energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7] + flux_per_ev = [0.3, 0.3, 1.0, 1.0] + flux = flux_per_ev * np.diff(energies) + microxs_4g = MicroXS.from_multigroup_flux( + energies=energies, multigroup_flux=flux, chain_file=chain_file) + + # Generate micro XS based on 2-group flux, where the boundaries line up with + # the 4 group flux and have the same flux per eV across the full energy + # range + energies = [0., 5.53e3, 2.0e7] + flux_per_ev = [0.3, 1.0] + flux = flux_per_ev * np.diff(energies) + microxs_2g = MicroXS.from_multigroup_flux( + energies=energies, multigroup_flux=flux, chain_file=chain_file) + + assert microxs_4g.data == pytest.approx(microxs_2g.data) diff --git a/tests/unit_tests/test_deplete_nuclide.py b/tests/unit_tests/test_deplete_nuclide.py index 07db57bd6d1..f2bb7d1b65e 100644 --- a/tests/unit_tests/test_deplete_nuclide.py +++ b/tests/unit_tests/test_deplete_nuclide.py @@ -1,7 +1,9 @@ """Tests for the openmc.deplete.Nuclide class.""" -import lxml.etree as ET import copy +import warnings + +import lxml.etree as ET import numpy as np import pytest from openmc.deplete import nuclide @@ -276,9 +278,9 @@ def test_validate(): } # nuclide is good and should have no warnings raise - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") assert nuc.validate(strict=True, quiet=False, tolerance=0.0) - assert len(record) == 0 # invalidate decay modes decay = nuc.decay_modes.pop() diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 190affea99d..b22a786509e 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -111,7 +111,7 @@ def test_get_mass(res): assert n_cm3 == pytest.approx(n_ref / volume) t_min, n_bcm = res.get_mass("1", "Xe135", mass_units="kg", time_units="min") - assert n_bcm == pytest.approx(n_ref * 1e3) + assert n_bcm == pytest.approx(n_ref / 1e3) assert t_min == pytest.approx(t_ref / 60) t_hour, _n = res.get_mass("1", "Xe135", time_units="h") diff --git a/tests/unit_tests/test_element.py b/tests/unit_tests/test_element.py index bacb988b9a9..d3555701e2c 100644 --- a/tests/unit_tests/test_element.py +++ b/tests/unit_tests/test_element.py @@ -1,5 +1,5 @@ import openmc -from pytest import approx, raises +from pytest import approx, raises, warns from openmc.data import NATURAL_ABUNDANCE, atomic_mass @@ -37,6 +37,13 @@ def test_expand_enrichment(): assert isotope[1] == approx(ref[isotope[0]]) +def test_expand_no_isotopes(): + """Test that correct warning is raised for elements with no isotopes""" + with warns(UserWarning, match='No naturally occurring'): + element = openmc.Element('Tc') + element.expand(100.0, 'ao') + + def test_expand_exceptions(): """ Test that correct exceptions are raised for invalid input """ diff --git a/tests/unit_tests/test_endf.py b/tests/unit_tests/test_endf.py index 9e69708673e..1d4982054c8 100644 --- a/tests/unit_tests/test_endf.py +++ b/tests/unit_tests/test_endf.py @@ -23,6 +23,7 @@ def test_float_endf(): assert endf.float_endf('-1.+2') == approx(-100.0) assert endf.float_endf(' ') == 0.0 assert endf.float_endf('9.876540000000000') == approx(9.87654) + assert endf.float_endf('-2.225002+6') == approx(-2.225002e+6) def test_int_endf(): diff --git a/tests/unit_tests/test_filter_mesh.py b/tests/unit_tests/test_filter_mesh.py index 36beed25d1c..a8bd4996dd7 100644 --- a/tests/unit_tests/test_filter_mesh.py +++ b/tests/unit_tests/test_filter_mesh.py @@ -127,9 +127,9 @@ def test_cylindrical_mesh_coincident(scale, run_in_tmpdir): fuel.set_density('g/cm3', 4.5) zcyl = openmc.ZCylinder(r=1.25*scale) - box = openmc.rectangular_prism(4*scale, 4*scale, boundary_type='reflective') + box = openmc.model.RectangularPrism(4*scale, 4*scale, boundary_type='reflective') cell1 = openmc.Cell(fill=fuel, region=-zcyl) - cell2 = openmc.Cell(fill=None, region=+zcyl & box) + cell2 = openmc.Cell(fill=None, region=+zcyl & -box) model = openmc.Model() model.geometry = openmc.Geometry([cell1, cell2]) @@ -193,7 +193,7 @@ def test_spherical_mesh_coincident(scale, run_in_tmpdir): phi_grid=[0., 2*math.pi], theta_grid=[0., math.pi], ) - + sph_mesh_filter = openmc.MeshFilter(sph_mesh) cell_filter = openmc.CellFilter([cell1]) diff --git a/tests/unit_tests/test_filter_meshborn.py b/tests/unit_tests/test_filter_meshborn.py new file mode 100644 index 00000000000..62fa1174e75 --- /dev/null +++ b/tests/unit_tests/test_filter_meshborn.py @@ -0,0 +1,116 @@ +"""Test the meshborn filter using a fixed source calculation on a H1 sphere. + +""" + +import numpy as np +from uncertainties import unumpy +import openmc +import pytest + + +@pytest.fixture +def model(): + """Sphere of H1 with one hemisphere containing the source (x>0) and one + hemisphere with no source (x<0). + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # Materials + h1 = openmc.Material() + h1.add_nuclide("H1", 1.0) + h1.set_density("g/cm3", 1.0) + model.materials = openmc.Materials([h1]) + + # Core geometry + r = 10.0 + sphere = openmc.Sphere(r=r, boundary_type="reflective") + core = openmc.Cell(fill=h1, region=-sphere) + model.geometry = openmc.Geometry([core]) + + # Settings + model.settings.run_mode = 'fixed source' + model.settings.particles = 2000 + model.settings.batches = 8 + + distribution = openmc.stats.Box((0., -r, -r), (r, r, r)) + model.settings.source = openmc.IndependentSource(space=distribution) + + # ============================================================================= + # Tallies + # ============================================================================= + + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2, 1) + mesh.lower_left = (-r, -r, -r) + mesh.upper_right = (r, r, r) + + f = openmc.MeshBornFilter(mesh) + t_1 = openmc.Tally(name="scatter-collision") + t_1.filters = [f] + t_1.scores = ["scatter"] + t_1.estimator = "collision" + + t_2 = openmc.Tally(name="scatter-tracklength") + t_2.filters = [f] + t_2.scores = ["scatter"] + t_2.estimator = "tracklength" + + model.tallies = [t_1, t_2] + + return model + + +def test_estimator_consistency(model, run_in_tmpdir): + """Test that resuts obtained from a tracklength estimator are + consistent with results obtained from a collision estimator. + + """ + # Run OpenMC + sp_filename = model.run() + + # Get radial flux distribution + with openmc.StatePoint(sp_filename) as sp: + scatter_collision = sp.get_tally(name="scatter-collision").mean.ravel() + scatter_collision_std_dev = sp.get_tally(name="scatter-collision").std_dev.ravel() + scatter_tracklength = sp.get_tally(name="scatter-tracklength").mean.ravel() + scatter_tracklength_std_dev = sp.get_tally(name="scatter-tracklength").std_dev.ravel() + + collision = unumpy.uarray(scatter_collision, scatter_collision_std_dev) + tracklength = unumpy.uarray(scatter_tracklength, scatter_tracklength_std_dev) + delta = abs(collision - tracklength) + + diff = unumpy.nominal_values(delta) + std_dev = unumpy.std_devs(delta) + assert np.all(diff <= 3 * std_dev) + + +def test_xml_serialization(): + """Test xml serialization of the meshborn filter.""" + openmc.reset_auto_ids() + + mesh = openmc.RegularMesh() + mesh.dimension = (1, 1, 1) + mesh.lower_left = (0.0, 0.0, 0.0) + mesh.upper_right = (1.0, 1.0, 1.0) + + filter = openmc.MeshBornFilter(mesh) + filter.translation = (2.0, 2.0, 2.0) + assert filter.mesh.id == 1 + assert filter.mesh.dimension == (1, 1, 1) + assert filter.mesh.lower_left == (0.0, 0.0, 0.0) + assert filter.mesh.upper_right == (1.0, 1.0, 1.0) + + repr(filter) + + elem = filter.to_xml_element() + assert elem.tag == 'filter' + assert elem.attrib['type'] == 'meshborn' + assert elem[0].text == "1" + assert elem.get("translation") == "2.0 2.0 2.0" + + meshes = {1: mesh} + new_filter = openmc.Filter.from_xml_element(elem, meshes=meshes) + assert new_filter.bins == filter.bins + np.testing.assert_equal(new_filter.translation, [2.0, 2.0, 2.0]) diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index ff064d5c251..55bb62075bd 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -1,6 +1,6 @@ import numpy as np import openmc -from pytest import fixture, approx +from pytest import fixture, approx, raises @fixture(scope='module') @@ -10,8 +10,8 @@ def box_model(): m.add_nuclide('U235', 1.0) m.set_density('g/cm3', 1.0) - box = openmc.model.rectangular_prism(10., 10., boundary_type='vacuum') - c = openmc.Cell(fill=m, region=box) + box = openmc.model.RectangularPrism(10., 10., boundary_type='vacuum') + c = openmc.Cell(fill=m, region=-box) model.geometry.root_universe = openmc.Universe(cells=[c]) model.settings.particles = 100 @@ -248,6 +248,11 @@ def test_energy(): assert len(f.values) == 710 +def test_energyfilter_error_handling(): + with raises(ValueError): + openmc.EnergyFilter([1e6]) + + def test_lethargy_bin_width(): f = openmc.EnergyFilter.from_group_structure('VITAMIN-J-175') assert len(f.lethargy_bin_width) == 175 diff --git a/tests/unit_tests/test_geometry.py b/tests/unit_tests/test_geometry.py index ebb185642fd..6cc577c820c 100644 --- a/tests/unit_tests/test_geometry.py +++ b/tests/unit_tests/test_geometry.py @@ -213,27 +213,27 @@ def test_get_by_name(): def test_hex_prism(): - hex_prism = openmc.model.hexagonal_prism(edge_length=5.0, - origin=(0.0, 0.0), - orientation='y') + hex_prism = openmc.model.HexagonalPrism(edge_length=5.0, + origin=(0.0, 0.0), + orientation='y') # clear checks - assert (0.0, 0.0, 0.0) in hex_prism - assert (10.0, 10.0, 10.0) not in hex_prism + assert (0.0, 0.0, 0.0) in -hex_prism + assert (10.0, 10.0, 10.0) not in -hex_prism # edge checks - assert (0.0, 5.01, 0.0) not in hex_prism - assert (0.0, 4.99, 0.0) in hex_prism + assert (0.0, 5.01, 0.0) not in -hex_prism + assert (0.0, 4.99, 0.0) in -hex_prism - rounded_hex_prism = openmc.model.hexagonal_prism(edge_length=5.0, - origin=(0.0, 0.0), - orientation='y', - corner_radius=1.0) + rounded_hex_prism = openmc.model.HexagonalPrism(edge_length=5.0, + origin=(0.0, 0.0), + orientation='y', + corner_radius=1.0) # clear checks - assert (0.0, 0.0, 0.0) in rounded_hex_prism - assert (10.0, 10.0, 10.0) not in rounded_hex_prism + assert (0.0, 0.0, 0.0) in -rounded_hex_prism + assert (10.0, 10.0, 10.0) not in -rounded_hex_prism # edge checks - assert (0.0, 5.01, 0.0) not in rounded_hex_prism - assert (0.0, 4.99, 0.0) not in rounded_hex_prism + assert (0.0, 5.01, 0.0) not in -rounded_hex_prism + assert (0.0, 4.99, 0.0) not in -rounded_hex_prism def test_get_lattice_by_name(cell_with_lattice): @@ -378,3 +378,28 @@ def get_cyl_cell(r1, r2, z1, z2, fill): # There should be 0 remaining redundant surfaces n_redundant_surfs = len(geom.remove_redundant_surfaces().keys()) assert n_redundant_surfs == 0 + +def test_get_all_nuclides(): + m1 = openmc.Material() + m1.add_nuclide('Fe56', 1) + m1.add_nuclide('Be9', 1) + m2 = openmc.Material() + m2.add_nuclide('Be9', 1) + s = openmc.Sphere() + c1 = openmc.Cell(fill=m1, region=-s) + c2 = openmc.Cell(fill=m2, region=+s) + geom = openmc.Geometry([c1, c2]) + assert geom.get_all_nuclides() == ['Be9', 'Fe56'] + + +def test_redundant_surfaces(): + # Make sure boundary condition is accounted for + s1 = openmc.Sphere(r=5.0) + s2 = openmc.Sphere(r=5.0, boundary_type="vacuum") + c1 = openmc.Cell(region=-s1) + c2 = openmc.Cell(region=+s1) + u_lower = openmc.Universe(cells=[c1, c2]) + c3 = openmc.Cell(fill=u_lower, region=-s2) + geom = openmc.Geometry([c3]) + redundant_surfs = geom.remove_redundant_surfaces() + assert len(redundant_surfs) == 0 diff --git a/tests/unit_tests/test_lattice.py b/tests/unit_tests/test_lattice.py index 97cd898435f..6fa32760e65 100644 --- a/tests/unit_tests/test_lattice.py +++ b/tests/unit_tests/test_lattice.py @@ -1,6 +1,6 @@ from math import sqrt -import lxml.etree as ET +import lxml.etree as ET import openmc import pytest diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 5f74c7c235d..1310a6a7fa0 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -1,4 +1,5 @@ from collections.abc import Mapping +from math import pi import os import numpy as np @@ -126,7 +127,7 @@ def test_cell(lib_init): cell = openmc.lib.cells[1] assert isinstance(cell.fill, openmc.lib.Material) cell.fill = openmc.lib.materials[1] - assert str(cell) == 'Cell[0]' + assert str(cell) == '' assert cell.name == "Fuel" cell.name = "Not fuel" assert cell.name == "Not fuel" @@ -206,6 +207,10 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" + assert m.depletable == False + m.depletable = True + assert m.depletable == True + def test_properties_density(lib_init): m = openmc.lib.materials[1] @@ -561,6 +566,8 @@ def test_regular_mesh(lib_init): assert mesh.upper_right == pytest.approx(ur) assert mesh.width == pytest.approx(width) + np.testing.assert_allclose(mesh.volumes, 1.0) + meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) assert len(meshes) == 1 @@ -580,6 +587,50 @@ def test_regular_mesh(lib_init): msf.translation = translation assert msf.translation == translation + # Test material volumes + mesh = openmc.lib.RegularMesh() + mesh.dimension = (2, 2, 1) + mesh.set_parameters(lower_left=(-0.63, -0.63, -0.5), + upper_right=(0.63, 0.63, 0.5)) + vols = mesh.material_volumes() + assert len(vols) == 4 + for elem_vols in vols: + assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26 / 4) + + # If the mesh extends beyond the boundaries of the model, the volumes should + # still be reported correctly + mesh.dimension = (1, 1, 1) + mesh.set_parameters(lower_left=(-1.0, -1.0, -0.5), + upper_right=(1.0, 1.0, 0.5)) + vols = mesh.material_volumes(100_000) + for elem_vols in vols: + assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26, 1e-2) + + +def test_regular_mesh_get_plot_bins(lib_init): + mesh: openmc.lib.RegularMesh = openmc.lib.meshes[2] + mesh.dimension = (2, 2, 1) + mesh.set_parameters(lower_left=(-1.0, -1.0, -0.5), + upper_right=(1.0, 1.0, 0.5)) + + # Get bins for a plot view covering only a single mesh bin + mesh_bins = mesh.get_plot_bins((-0.5, -0.5, 0.), (0.1, 0.1), 'xy', (20, 20)) + assert (mesh_bins == 0).all() + mesh_bins = mesh.get_plot_bins((0.5, 0.5, 0.), (0.1, 0.1), 'xy', (20, 20)) + assert (mesh_bins == 3).all() + + # Get bins for a plot view covering all mesh bins. Note that the y direction + # (first dimension) is flipped for plotting purposes + mesh_bins = mesh.get_plot_bins((0., 0., 0.), (2., 2.), 'xy', (20, 20)) + assert (mesh_bins[:10, :10] == 2).all() + assert (mesh_bins[:10, 10:] == 3).all() + assert (mesh_bins[10:, :10] == 0).all() + assert (mesh_bins[10:, 10:] == 1).all() + + # Get bins for a plot view outside of the mesh + mesh_bins = mesh.get_plot_bins((100., 100., 0.), (2., 2.), 'xy', (20, 20)) + assert (mesh_bins == -1).all() + def test_rectilinear_mesh(lib_init): mesh = openmc.lib.RectilinearMesh() @@ -595,12 +646,14 @@ def test_rectilinear_mesh(lib_init): for k, diff_z in enumerate(np.diff(z_grid)): assert np.all(mesh.width[i, j, k, :] == (10, 10, 10)) + np.testing.assert_allclose(mesh.volumes, 1000.0) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.RectilinearMesh(mesh.id) meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) - assert len(meshes) == 2 + assert len(meshes) == 3 mesh = meshes[mesh.id] assert isinstance(mesh, openmc.lib.RectilinearMesh) @@ -611,8 +664,21 @@ def test_rectilinear_mesh(lib_init): msf = openmc.lib.MeshSurfaceFilter(mesh) assert msf.mesh == mesh + # Test material volumes + mesh = openmc.lib.RectilinearMesh() + w = 1.26 + mesh.set_grid([-w/2, -w/4, w/2], [-w/2, -w/4, w/2], [-0.5, 0.5]) + + vols = mesh.material_volumes() + assert len(vols) == 4 + assert sum(f[1] for f in vols[0]) == pytest.approx(w/4 * w/4) + assert sum(f[1] for f in vols[1]) == pytest.approx(w/4 * 3*w/4) + assert sum(f[1] for f in vols[2]) == pytest.approx(3*w/4 * w/4) + assert sum(f[1] for f in vols[3]) == pytest.approx(3*w/4 * 3*w/4) + + def test_cylindrical_mesh(lib_init): - deg2rad = lambda deg: deg*np.pi/180 + deg2rad = lambda deg: deg*pi/180 mesh = openmc.lib.CylindricalMesh() r_grid = [0., 5., 10.] phi_grid = np.radians([0., 10., 20.]) @@ -626,12 +692,15 @@ def test_cylindrical_mesh(lib_init): for k, _ in enumerate(np.diff(z_grid)): assert np.allclose(mesh.width[i, j, k, :], (5, deg2rad(10), 10)) + np.testing.assert_allclose(mesh.volumes[::2], 10/360 * pi * 5**2 * 10) + np.testing.assert_allclose(mesh.volumes[1::2], 10/360 * pi * (10**2 - 5**2) * 10) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.CylindricalMesh(mesh.id) meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) - assert len(meshes) == 3 + assert len(meshes) == 5 mesh = meshes[mesh.id] assert isinstance(mesh, openmc.lib.CylindricalMesh) @@ -642,6 +711,21 @@ def test_cylindrical_mesh(lib_init): msf = openmc.lib.MeshSurfaceFilter(mesh) assert msf.mesh == mesh + # Test material volumes + mesh = openmc.lib.CylindricalMesh() + r_grid = (0., 0.25, 0.5) + phi_grid = np.linspace(0., 2.0*pi, 4) + z_grid = (-0.5, 0.5) + mesh.set_grid(r_grid, phi_grid, z_grid) + + vols = mesh.material_volumes() + assert len(vols) == 6 + for i in range(0, 6, 2): + assert sum(f[1] for f in vols[i]) == pytest.approx(pi * 0.25**2 / 3) + for i in range(1, 6, 2): + assert sum(f[1] for f in vols[i]) == pytest.approx(pi * (0.5**2 - 0.25**2) / 3) + + def test_spherical_mesh(lib_init): deg2rad = lambda deg: deg*np.pi/180 mesh = openmc.lib.SphericalMesh() @@ -657,12 +741,19 @@ def test_spherical_mesh(lib_init): for k, _ in enumerate(np.diff(phi_grid)): assert np.allclose(mesh.width[i, j, k, :], (5, deg2rad(10), deg2rad(10))) + dtheta = lambda d1, d2: np.cos(deg2rad(d1)) - np.cos(deg2rad(d2)) + f = 1/3 * deg2rad(10.) + np.testing.assert_allclose(mesh.volumes[::4], f * 5**3 * dtheta(0., 10.)) + np.testing.assert_allclose(mesh.volumes[1::4], f * (10**3 - 5**3) * dtheta(0., 10.)) + np.testing.assert_allclose(mesh.volumes[2::4], f * 5**3 * dtheta(10., 20.)) + np.testing.assert_allclose(mesh.volumes[3::4], f * (10**3 - 5**3) * dtheta(10., 20.)) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.SphericalMesh(mesh.id) meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) - assert len(meshes) == 4 + assert len(meshes) == 7 mesh = meshes[mesh.id] assert isinstance(mesh, openmc.lib.SphericalMesh) @@ -673,6 +764,24 @@ def test_spherical_mesh(lib_init): msf = openmc.lib.MeshSurfaceFilter(mesh) assert msf.mesh == mesh + # Test material volumes + mesh = openmc.lib.SphericalMesh() + r_grid = (0., 0.25, 0.5) + theta_grid = np.linspace(0., pi, 3) + phi_grid = np.linspace(0., 2.0*pi, 4) + mesh.set_grid(r_grid, theta_grid, phi_grid) + + vols = mesh.material_volumes() + assert len(vols) == 12 + d_theta = theta_grid[1] - theta_grid[0] + d_phi = phi_grid[1] - phi_grid[0] + for i in range(0, 12, 2): + assert sum(f[1] for f in vols[i]) == pytest.approx( + 0.25**3 / 3 * d_theta * d_phi * 2/pi) + for i in range(1, 12, 2): + assert sum(f[1] for f in vols[i]) == pytest.approx( + (0.5**3 - 0.25**3) / 3 * d_theta * d_phi * 2/pi) + def test_restart(lib_init, mpi_intracomm): # Finalize and re-init to make internal state consistent with XML. diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 71945193403..c4993065069 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -48,6 +48,17 @@ def test_regular_mesh_bounding_box(): np.testing.assert_array_equal(bb.upper_right, (2, 3, 5)) +def test_rectilinear_mesh_bounding_box(): + mesh = openmc.RectilinearMesh() + mesh.x_grid = [0., 1., 5., 10.] + mesh.y_grid = [-10., -5., 0.] + mesh.z_grid = [-100., 0., 100.] + bb = mesh.bounding_box + assert isinstance(bb, openmc.BoundingBox) + np.testing.assert_array_equal(bb.lower_left, (0., -10. ,-100.)) + np.testing.assert_array_equal(bb.upper_right, (10., 0., 100.)) + + def test_cylindrical_mesh_bounding_box(): # test with mesh at origin (0, 0, 0) mesh = openmc.CylindricalMesh( @@ -76,6 +87,7 @@ def test_cylindrical_mesh_bounding_box(): np.testing.assert_array_equal(mesh.lower_left, (2, 4, -3)) np.testing.assert_array_equal(mesh.upper_right, (4, 6, 17)) + def test_spherical_mesh_bounding_box(): # test with mesh at origin (0, 0, 0) mesh = openmc.SphericalMesh([0.1, 0.2, 0.5, 1.], origin=(0., 0., 0.)) @@ -162,29 +174,194 @@ def test_centroids(): mesh.lower_left = (1., 2., 3.) mesh.upper_right = (11., 12., 13.) mesh.dimension = (1, 1, 1) - np.testing.assert_array_almost_equal(mesh.centroids[:, 0, 0, 0], [6., 7., 8.]) + np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [6., 7., 8.]) # rectilinear mesh mesh = openmc.RectilinearMesh() mesh.x_grid = [1., 11.] mesh.y_grid = [2., 12.] mesh.z_grid = [3., 13.] - np.testing.assert_array_almost_equal(mesh.centroids[:, 0, 0, 0], [6., 7., 8.]) + np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [6., 7., 8.]) # cylindrical mesh mesh = openmc.CylindricalMesh(r_grid=(0, 10), z_grid=(0, 10), phi_grid=(0, np.pi)) - np.testing.assert_array_almost_equal(mesh.centroids[:, 0, 0, 0], [0.0, 5.0, 5.0]) + np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [0.0, 5.0, 5.0]) # ensure that setting an origin is handled correctly mesh.origin = (5.0, 0, -10) - np.testing.assert_array_almost_equal(mesh.centroids[:, 0, 0, 0], [5.0, 5.0, -5.0]) + np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [5.0, 5.0, -5.0]) # spherical mesh, single element xyz-positive octant mesh = openmc.SphericalMesh(r_grid=[0, 10], theta_grid=[0, 0.5*np.pi], phi_grid=[0, np.pi]) x = 5.*np.cos(0.5*np.pi)*np.sin(0.25*np.pi) y = 5.*np.sin(0.5*np.pi)*np.sin(0.25*np.pi) z = 5.*np.sin(0.25*np.pi) - np.testing.assert_array_almost_equal(mesh.centroids[:, 0, 0, 0], [x, y, z]) + np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [x, y, z]) mesh.origin = (-5.0, -5.0, 5.0) - np.testing.assert_array_almost_equal(mesh.centroids[:, 0, 0, 0], [x-5.0, y-5.0, z+5.0]) + np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [x-5.0, y-5.0, z+5.0]) + + +@pytest.mark.parametrize('mesh_type', ('regular', 'rectilinear', 'cylindrical', 'spherical')) +def test_mesh_vertices(mesh_type): + + ijk = (2, 3, 2) + + # create a new mesh object + if mesh_type == 'regular': + mesh = openmc.RegularMesh() + ll = np.asarray([0.]*3) + width = np.asarray([0.5]*3) + mesh.lower_left = ll + mesh.width = width + mesh.dimension = (5, 7, 9) + + # spot check that an element has the correct vertex coordinates asociated with it + # (using zero-indexing here) + exp_i_j_k = ll + np.asarray(ijk, dtype=float) * width + np.testing.assert_equal(mesh.vertices[ijk], exp_i_j_k) + + # shift the mesh using the llc + shift = np.asarray((3.0, 6.0, 10.0)) + mesh.lower_left += shift + np.testing.assert_equal(mesh.vertices[ijk], exp_i_j_k+shift) + elif mesh_type == 'rectilinear': + mesh = openmc.RectilinearMesh() + w = np.asarray([0.5] * 3) + ll = np.asarray([0.]*3) + dims = (5, 7, 9) + mesh.x_grid = np.linspace(ll[0], w[0]*dims[0], dims[0]) + mesh.y_grid = np.linspace(ll[1], w[1]*dims[1], dims[1]) + mesh.z_grid = np.linspace(ll[2], w[2]*dims[2], dims[2]) + exp_vert = np.asarray((mesh.x_grid[2], mesh.y_grid[3], mesh.z_grid[2])) + np.testing.assert_equal(mesh.vertices[ijk], exp_vert) + elif mesh_type == 'cylindrical': + r_grid = np.linspace(0, 5, 10) + z_grid = np.linspace(-10, 10, 20) + phi_grid = np.linspace(0, 2*np.pi, 8) + mesh = openmc.CylindricalMesh(r_grid=r_grid, z_grid=z_grid, phi_grid=phi_grid) + exp_vert = np.asarray((mesh.r_grid[2], mesh.phi_grid[3], mesh.z_grid[2])) + np.testing.assert_equal(mesh.vertices_cylindrical[ijk], exp_vert) + elif mesh_type == 'spherical': + r_grid = np.linspace(0, 13, 14) + theta_grid = np.linspace(0, np.pi, 11) + phi_grid = np.linspace(0, 2*np.pi, 7) + mesh = openmc.SphericalMesh(r_grid=r_grid, theta_grid=theta_grid, phi_grid=phi_grid) + exp_vert = np.asarray((mesh.r_grid[2], mesh.theta_grid[3], mesh.phi_grid[2])) + np.testing.assert_equal(mesh.vertices_spherical[ijk], exp_vert) + + +def test_CylindricalMesh_get_indices_at_coords(): + # default origin (0, 0, 0) and default phi grid (0, 2*pi) + mesh = openmc.CylindricalMesh(r_grid=(0, 5, 10), z_grid=(0, 5, 10)) + assert mesh.get_indices_at_coords([1, 0, 1]) == (0, 0, 0) + assert mesh.get_indices_at_coords([6, 0, 1]) == (1, 0, 0) + assert mesh.get_indices_at_coords([9, 0, 1]) == (1, 0, 0) + assert mesh.get_indices_at_coords([0, 6, 0]) == (1, 0, 0) + assert mesh.get_indices_at_coords([0, 9, 6]) == (1, 0, 1) + assert mesh.get_indices_at_coords([-2, -2, 9]) == (0, 0, 1) + + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([8, 8, 1]) # resulting r value to large + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([-8, -8, 1]) # resulting r value to large + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([1, 0, -1]) # z value below range + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([1, 0, 11]) # z value above range + + assert mesh.get_indices_at_coords([1, 1, 1]) == (0, 0, 0) + + # negative range on z grid + mesh = openmc.CylindricalMesh( + r_grid=(0, 5, 10), + phi_grid=(0, 0.5 * pi, pi, 1.5 * pi, 1.9 * pi), + z_grid=(-5, 0, 5, 10), + ) + assert mesh.get_indices_at_coords([1, 1, 1]) == (0, 0, 1) # first angle quadrant + assert mesh.get_indices_at_coords([2, 2, 6]) == (0, 0, 2) # first angle quadrant + assert mesh.get_indices_at_coords([-2, 0.1, -1]) == (0, 1, 0) # second angle quadrant + assert mesh.get_indices_at_coords([-2, -0.1, -1]) == (0, 2, 0) # third angle quadrant + assert mesh.get_indices_at_coords([2, -0.9, -1]) == (0, 3, 0) # forth angle quadrant + + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([2, -0.1, 1]) # outside of phi range + + # origin of mesh not default + mesh = openmc.CylindricalMesh( + r_grid=(0, 5, 10), + phi_grid=(0, 0.5 * pi, pi, 1.5 * pi, 1.9 * pi), + z_grid=(-5, 0, 5, 10), + origin=(100, 200, 300), + ) + assert mesh.get_indices_at_coords([101, 201, 301]) == (0, 0, 1) # first angle quadrant + assert mesh.get_indices_at_coords([102, 202, 306]) == (0, 0, 2) # first angle quadrant + assert mesh.get_indices_at_coords([98, 200.1, 299]) == (0, 1, 0) # second angle quadrant + assert mesh.get_indices_at_coords([98, 199.9, 299]) == (0, 2, 0) # third angle quadrant + assert mesh.get_indices_at_coords([102, 199.1, 299]) == (0, 3, 0) # forth angle quadrant + + +def test_umesh_roundtrip(run_in_tmpdir, request): + umesh = openmc.UnstructuredMesh(request.path.parent / 'test_mesh_tets.e', 'moab') + umesh.output = True + + # create a tally using this mesh + mf = openmc.MeshFilter(umesh) + tally = openmc.Tally() + tally.filters = [mf] + tally.scores = ['flux'] + + tallies = openmc.Tallies([tally]) + tallies.export_to_xml() + + xml_tallies = openmc.Tallies.from_xml() + xml_tally = xml_tallies[0] + xml_mesh = xml_tally.filters[0].mesh + + assert umesh.id == xml_mesh.id + + +def test_mesh_get_homogenized_materials(): + """Test the get_homogenized_materials method""" + # Simple model with 1 cm of Fe56 next to 1 cm of H1 + fe = openmc.Material() + fe.add_nuclide('Fe56', 1.0) + fe.set_density('g/cm3', 5.0) + h = openmc.Material() + h.add_nuclide('H1', 1.0) + h.set_density('g/cm3', 1.0) + + x0 = openmc.XPlane(-1.0, boundary_type='vacuum') + x1 = openmc.XPlane(0.0) + x2 = openmc.XPlane(1.0) + x3 = openmc.XPlane(2.0, boundary_type='vacuum') + cell1 = openmc.Cell(fill=fe, region=+x0 & -x1) + cell2 = openmc.Cell(fill=h, region=+x1 & -x2) + cell_empty = openmc.Cell(region=+x2 & -x3) + model = openmc.Model(geometry=openmc.Geometry([cell1, cell2, cell_empty])) + model.settings.particles = 1000 + model.settings.batches = 10 + + mesh = openmc.RegularMesh() + mesh.lower_left = (-1., -1., -1.) + mesh.upper_right = (1., 1., 1.) + mesh.dimension = (3, 1, 1) + m1, m2, m3 = mesh.get_homogenized_materials(model, n_samples=1_000_000) + + # Left mesh element should be only Fe56 + assert m1.get_mass_density('Fe56') == pytest.approx(5.0) + + # Middle mesh element should be 50% Fe56 and 50% H1 + assert m2.get_mass_density('Fe56') == pytest.approx(2.5, rel=1e-2) + assert m2.get_mass_density('H1') == pytest.approx(0.5, rel=1e-2) + + # Right mesh element should be only H1 + assert m3.get_mass_density('H1') == pytest.approx(1.0) + + mesh_void = openmc.RegularMesh() + mesh_void.lower_left = (0.5, 0.5, -1.) + mesh_void.upper_right = (1.5, 1.5, 1.) + mesh_void.dimension = (1, 1, 1) + m4, = mesh_void.get_homogenized_materials(model, n_samples=1_000_000) + # Mesh element that overlaps void should have half density + assert m4.get_mass_density('H1') == pytest.approx(0.5, rel=1e-2) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index f78f9563737..16fa18d453c 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -35,8 +35,8 @@ def pin_model_attributes(): pitch = 1.25984 fuel_or = openmc.ZCylinder(r=0.39218, name='Fuel OR') clad_or = openmc.ZCylinder(r=0.45720, name='Clad OR') - box = openmc.model.rectangular_prism(pitch, pitch, - boundary_type='reflective') + box = openmc.model.RectangularPrism(pitch, pitch, + boundary_type='reflective') # Define cells fuel_inf_cell = openmc.Cell(cell_id=1, name='inf fuel', fill=uo2) @@ -44,7 +44,7 @@ def pin_model_attributes(): fuel = openmc.Cell(cell_id=2, name='fuel', fill=fuel_inf_univ, region=-fuel_or) clad = openmc.Cell(cell_id=3, fill=zirc, region=+fuel_or & -clad_or) - water = openmc.Cell(cell_id=4, fill=borated_water, region=+clad_or & box) + water = openmc.Cell(cell_id=4, fill=borated_water, region=+clad_or & -box) # Define overall geometry geom = openmc.Geometry([fuel, clad, water]) @@ -457,6 +457,20 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert after_xe + after_u == pytest.approx(initial_u, abs=1e-15) assert test_model.is_initialized is False + # check the tally output + def check_tally_output(): + with openmc.StatePoint('openmc_simulation_n0.h5') as sp: + flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0] + fission = sp.get_tally(id=1).get_values( + scores=['fission'])[0, 0, 0] + + # we're mainly just checking that the result was produced, + # so a rough numerical comparison doesn't hurt to have. + assert flux == pytest.approx(13.1, abs=0.2) + assert fission == pytest.approx(0.47, abs=0.2) + + check_tally_output() + # Reset the initial material densities mats[0].nuclides.clear() densities = initial_mat.get_nuclide_atom_densities() @@ -481,6 +495,8 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert after_xe == pytest.approx(after_lib_xe, abs=1e-15) assert after_u == pytest.approx(after_lib_u, abs=1e-15) + check_tally_output() + test_model.finalize_lib() @@ -531,6 +547,7 @@ def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.finalize_lib() + def test_model_xml(run_in_tmpdir): # load a model from examples @@ -549,6 +566,7 @@ def test_model_xml(run_in_tmpdir): # XML files new_model.export_to_xml() + def test_single_xml_exec(run_in_tmpdir): pincell_model = openmc.examples.pwr_pin_cell() @@ -566,4 +584,10 @@ def test_single_xml_exec(run_in_tmpdir): openmc.run(path_input='./inputs/pincell.xml') with pytest.raises(RuntimeError, match='input_dir'): - openmc.run(path_input='input_dir/pincell.xml') \ No newline at end of file + openmc.run(path_input='input_dir/pincell.xml') + + # Make sure path can be specified with run + pincell_model.run(path='my_model.xml') + + os.mkdir('subdir') + pincell_model.run(path='subdir') diff --git a/tests/unit_tests/test_no_visible_boundary.py b/tests/unit_tests/test_no_visible_boundary.py index 8f07b8da9e7..7c53e4e3fa3 100644 --- a/tests/unit_tests/test_no_visible_boundary.py +++ b/tests/unit_tests/test_no_visible_boundary.py @@ -13,9 +13,9 @@ def test_no_visible_boundary(run_in_tmpdir): # disc of copper. Neutrons leaving the back of the disc see no surfaces in # front of them. disc = openmc.model.RightCircularCylinder((0., 0., 1.), 0.1, 1.2) - box = openmc.rectangular_prism(width=10, height=10, boundary_type='vacuum') + box = openmc.model.RectangularPrism(width=10, height=10, boundary_type='vacuum') c1 = openmc.Cell(fill=copper, region=-disc) - c2 = openmc.Cell(fill=air, region=+disc & box) + c2 = openmc.Cell(fill=air, region=+disc & -box) model = openmc.Model() model.geometry = openmc.Geometry([c1, c2]) model.settings.run_mode = 'fixed source' diff --git a/tests/unit_tests/test_plotter.py b/tests/unit_tests/test_plotter.py index 8a1147b4afa..2c195c5e120 100644 --- a/tests/unit_tests/test_plotter.py +++ b/tests/unit_tests/test_plotter.py @@ -77,10 +77,19 @@ def test_plot_xs(this): from matplotlib.figure import Figure assert isinstance(openmc.plot_xs({this: ['total', 'elastic']}), Figure) + def test_plot_xs_mat(test_mat): from matplotlib.figure import Figure assert isinstance(openmc.plot_xs({test_mat: ['total']}), Figure) + +@pytest.mark.parametrize("units", ["eV", "keV", "MeV"]) +def test_plot_xs_energy_axis(units): + plot = openmc.plot_xs({'Be9': ['(n,2n)']}, energy_axis_units=units) + axis_text = plot.get_axes()[0].get_xaxis().get_label().get_text() + assert axis_text == f'Energy [{units}]' + + def test_plot_axes_labels(): # just nuclides axis_label = openmc.plotter._get_yaxis_label( @@ -109,6 +118,21 @@ def test_plot_axes_labels(): ) assert axis_label == 'Microscopic Cross Section [b]' + axis_label = openmc.plotter._get_yaxis_label( + reactions={ + "Li": ["heating", "heating-local"], + "Li7": ["heating"], + "Be": ["damage-energy"], + }, + divisor_types=False, + ) + assert axis_label == "Heating Cross Section [eV-barn]" + + with pytest.raises(TypeError): + axis_label = openmc.plotter.plot_xs( + reactions={"Li": ["heating", "heating-local"], "Be9": ["(n,2n)"]} + ) + # just materials mat1 = openmc.Material() mat1.add_nuclide('Fe56', 1) @@ -156,4 +180,4 @@ def test_get_title(): mat1.set_density('g/cm3', 1) mat1.name = 'my_mat' title = openmc.plotter._get_title(reactions={mat1: [205]}) - assert title == 'Cross Section Plot For my_mat' \ No newline at end of file + assert title == 'Cross Section Plot For my_mat' diff --git a/tests/unit_tests/test_region.py b/tests/unit_tests/test_region.py index 3197a8d7248..8c9e0afe4d9 100644 --- a/tests/unit_tests/test_region.py +++ b/tests/unit_tests/test_region.py @@ -241,3 +241,13 @@ def test_invalid_operands(): with pytest.raises(ValueError, match='must be of type Region'): openmc.Complement(z) + +def test_plot(): + # Create region and plot + region = -openmc.Sphere() & +openmc.XPlane() + c_before = openmc.Cell() + region.plot() + + # Ensure that calling plot doesn't affect cell ID space + c_after = openmc.Cell() + assert c_after.id - 1 == c_before.id diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index b1737c46140..650bfd18680 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -27,8 +27,8 @@ def test_export_to_xml(run_in_tmpdir): s.survival_biasing = True s.cutoff = {'weight': 0.25, 'weight_avg': 0.5, 'energy_neutron': 1.0e-5, 'energy_photon': 1000.0, 'energy_electron': 1.0e-5, - 'energy_positron': 1.0e-5, 'time_neutron': 1.0e-5, - 'time_photon': 1.0e-5, 'time_electron': 1.0e-5, + 'energy_positron': 1.0e-5, 'time_neutron': 1.0e-5, + 'time_photon': 1.0e-5, 'time_electron': 1.0e-5, 'time_positron': 1.0e-5} mesh = openmc.RegularMesh() mesh.lower_left = (-10., -10., -10.) @@ -58,6 +58,15 @@ def test_export_to_xml(run_in_tmpdir): s.electron_treatment = 'led' s.write_initial_source = True s.weight_window_checkpoints = {'surface': True, 'collision': False} + s.random_ray = { + 'distance_inactive': 10.0, + 'distance_active': 100.0, + 'ray_source': openmc.IndependentSource( + space=openmc.stats.Box((-1., -1., -1.), (1., 1., 1.)) + ) + } + + s.max_particle_events = 100 # Make sure exporting XML works s.export_to_xml() @@ -92,7 +101,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.cutoff == {'weight': 0.25, 'weight_avg': 0.5, 'energy_neutron': 1.0e-5, 'energy_photon': 1000.0, 'energy_electron': 1.0e-5, 'energy_positron': 1.0e-5, - 'time_neutron': 1.0e-5, 'time_photon': 1.0e-5, + 'time_neutron': 1.0e-5, 'time_photon': 1.0e-5, 'time_electron': 1.0e-5, 'time_positron': 1.0e-5} assert isinstance(s.entropy_mesh, openmc.RegularMesh) assert s.entropy_mesh.lower_left == [-10., -10., -10.] @@ -128,3 +137,8 @@ def test_export_to_xml(run_in_tmpdir): assert vol.lower_left == (-10., -10., -10.) assert vol.upper_right == (10., 10., 10.) assert s.weight_window_checkpoints == {'surface': True, 'collision': False} + assert s.max_particle_events == 100 + assert s.random_ray['distance_inactive'] == 10.0 + assert s.random_ray['distance_active'] == 100.0 + assert s.random_ray['ray_source'].space.lower_left == [-1., -1., -1.] + assert s.random_ray['ray_source'].space.upper_right == [1., 1., 1.] diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index ec10260bafe..1b5549b008e 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -45,6 +45,30 @@ def test_source_file(run_in_tmpdir): assert np.all(arr['particle'] == 0) + # Ensure sites read in are consistent + sites = openmc.read_source_file('test_source.h5') + + assert filetype == b'source' + xs = np.array([site.r[0] for site in sites]) + ys = np.array([site.r[1] for site in sites]) + zs = np.array([site.r[2] for site in sites]) + assert np.all((xs > 0.0) & (xs < 1.0)) + assert np.all(ys == np.arange(1000)) + assert np.all(zs == 0.0) + u = np.array([s.u for s in sites]) + assert np.all(u[..., 0] == 0.0) + assert np.all(u[..., 1] == 0.0) + assert np.all(u[..., 2] == 1.0) + E = np.array([s.E for s in sites]) + assert np.all(E == n - np.arange(n)) + wgt = np.array([s.wgt for s in sites]) + assert np.all(wgt == 1.0) + dgs = np.array([s.delayed_group for s in sites]) + assert np.all(dgs == 0) + p_types = np.array([s.particle for s in sites]) + assert np.all(p_types == 0) + + def test_wrong_source_attributes(run_in_tmpdir): # Create a source file with animal attributes source_dtype = np.dtype([ @@ -54,7 +78,7 @@ def test_wrong_source_attributes(run_in_tmpdir): ]) arr = np.array([(1.0, 2.0, 3), (4.0, 5.0, 6), (7.0, 8.0, 9)], dtype=source_dtype) with h5py.File('animal_source.h5', 'w') as fh: - fh.attrs['filetype'] = np.string_("source") + fh.attrs['filetype'] = np.bytes_("source") fh.create_dataset('source_bank', data=arr) # Create a simple model that uses this lovely animal source diff --git a/tests/unit_tests/test_source_mesh.py b/tests/unit_tests/test_source_mesh.py index fd8b04148e7..43bb1678c40 100644 --- a/tests/unit_tests/test_source_mesh.py +++ b/tests/unit_tests/test_source_mesh.py @@ -1,6 +1,5 @@ from itertools import product from pathlib import Path -from subprocess import call import pytest import numpy as np @@ -8,9 +7,11 @@ import openmc.lib from tests import cdtemp -from tests.regression_tests import config +################### +# MeshSpatial Tests +################### TETS_PER_VOXEL = 12 # This test uses a geometry file with cells that match a regular mesh. Each cell @@ -48,9 +49,9 @@ def model(): settings.particles = 100 settings.batches = 2 - return openmc.model.Model(geometry=geometry, - materials=materials, - settings=settings) + return openmc.Model(geometry=geometry, + materials=materials, + settings=settings) ### Setup test cases ### param_values = (['libmesh', 'moab'], # mesh libraries @@ -174,6 +175,7 @@ def test_strengths_size_failure(request, model): model.export_to_xml() openmc.run() + def test_roundtrip(run_in_tmpdir, model, request): if not openmc.lib._libmesh_enabled() and not openmc.lib._dagmc_enabled(): pytest.skip("Unstructured mesh is not enabled in this build.") @@ -201,3 +203,196 @@ def test_roundtrip(run_in_tmpdir, model, request): assert space_in.mesh.id == space_out.mesh.id assert space_in.volume_normalized == space_out.volume_normalized + + +################### +# MeshSource tests +################### +@pytest.fixture +def void_model(): + """ + A void model containing a single box + """ + model = openmc.Model() + + box = openmc.model.RectangularParallelepiped(*[-10, 10]*3, boundary_type='vacuum') + model.geometry = openmc.Geometry([openmc.Cell(region=-box)]) + + model.settings.particles = 100 + model.settings.batches = 10 + model.settings.run_mode = 'fixed source' + + return model + + +@pytest.mark.parametrize('mesh_type', ('rectangular', 'cylindrical')) +def test_mesh_source_independent(run_in_tmpdir, void_model, mesh_type): + """ + A void model containing a single box + """ + model = void_model + + # define a 2 x 2 x 2 mesh + if mesh_type == 'rectangular': + mesh = openmc.RegularMesh.from_domain(model.geometry, (2, 2, 2)) + elif mesh_type == 'cylindrical': + mesh = openmc.CylindricalMesh.from_domain(model.geometry, (1, 4, 2)) + + energy = openmc.stats.Discrete([1.e6], [1.0]) + + # create sources with only one non-zero strength for the source in the mesh + # voxel occupying the lowest octant. Direct source particles straight out of + # the problem from there. This demonstrates that + # 1) particles are only being sourced within the intented mesh voxel based + # on source strength + # 2) particles are respecting the angle distributions assigned to each voxel + sources = np.empty(mesh.dimension, dtype=openmc.SourceBase) + centroids = mesh.centroids + x, y, z = np.swapaxes(mesh.centroids, -1, 0) + for i, j, k in mesh.indices: + # mesh.indices is currently one-indexed, adjust for Python arrays + ijk = (i-1, j-1, k-1) + + # get the centroid of the ijk mesh element and use it to set the + # direction of the source directly out of the problem + centroid = centroids[ijk] + vec = np.sign(centroid, dtype=float) + vec /= np.linalg.norm(vec) + angle = openmc.stats.Monodirectional(vec) + sources[ijk] = openmc.IndependentSource(energy=energy, angle=angle, strength=0.0) + + # create and apply the mesh source + mesh_source = openmc.MeshSource(mesh, sources) + model.settings.source = mesh_source + + # tally the flux on the mesh + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally() + tally.filters = [mesh_filter] + tally.scores = ['flux'] + + model.tallies = openmc.Tallies([tally]) + + # for each element, set a single-non zero source with particles + # traveling out of the mesh (and geometry) w/o crossing any other + # mesh elements + for flat_index, (i, j, k) in enumerate(mesh.indices): + ijk = (i-1, j-1, k-1) + # zero-out all source strengths and set the strength + # on the element of interest + mesh_source.strength = 0.0 + mesh_source.sources[flat_index].strength = 1.0 + + sp_file = model.run() + + with openmc.StatePoint(sp_file) as sp: + tally_out = sp.get_tally(id=tally.id) + mean = tally_out.get_reshaped_data(expand_dims=True) + + # remove nuclides and scores axes + mean = mean[..., 0, 0] + # the mesh elment with a non-zero source strength should have a value + assert mean[ijk] != 0 + # all other values should be zero + mean[ijk] = 0 + assert np.all(mean == 0), f'Failed on index {ijk} with centroid {mesh.centroids[ijk]}' + + # test roundtrip + xml_model = openmc.Model.from_model_xml() + xml_source = xml_model.settings.source[0] + assert isinstance(xml_source, openmc.MeshSource) + assert xml_source.strength == 1.0 + assert isinstance(xml_source.mesh, type(mesh_source.mesh)) + assert xml_source.mesh.dimension == mesh_source.mesh.dimension + assert xml_source.mesh.id == mesh_source.mesh.id + assert len(xml_source.sources) == len(mesh_source.sources) + + # check strength adjustment methods + assert mesh_source.strength == 1.0 + mesh_source.strength = 100.0 + assert mesh_source.strength == 100.0 + + mesh_source.normalize_source_strengths() + assert mesh_source.strength == 1.0 + + +@pytest.mark.parametrize("library", ('moab', 'libmesh')) +def test_umesh_source_independent(run_in_tmpdir, request, void_model, library): + import openmc.lib + # skip the test if the library is not enabled + if library == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip("DAGMC (and MOAB) mesh not enabled in this build.") + + if library == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip("LibMesh is not enabled in this build.") + + model = void_model + + mesh_filename = Path(request.fspath).parent / "test_mesh_tets.e" + uscd_mesh = openmc.UnstructuredMesh(mesh_filename, library) + ind_source = openmc.IndependentSource() + n_elements = 12_000 + model.settings.source = openmc.MeshSource(uscd_mesh, n_elements*[ind_source]) + model.export_to_model_xml() + try: + openmc.lib.init() + openmc.lib.simulation_init() + sites = openmc.lib.sample_external_source(10) + openmc.lib.statepoint_write('statepoint.h5') + finally: + openmc.lib.finalize() + + with openmc.StatePoint('statepoint.h5') as sp: + uscd_mesh = sp.meshes[uscd_mesh.id] + + # ensure at least that all sites are inside the mesh + bounding_box = uscd_mesh.bounding_box + for site in sites: + assert site.r in bounding_box + + +def test_mesh_source_file(run_in_tmpdir): + # Creating a source file with a single particle + source_particle = openmc.SourceParticle(time=10.0) + openmc.write_source_file([source_particle], 'source.h5') + file_source = openmc.FileSource('source.h5') + + model = openmc.Model() + + rect_prism = openmc.model.RectangularParallelepiped( + -5.0, 5.0, -5.0, 5.0, -5.0, 5.0, boundary_type='vacuum') + + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + + model.geometry = openmc.Geometry([openmc.Cell(fill=mat, region=-rect_prism)]) + model.settings.particles = 1000 + model.settings.batches = 10 + model.settings.run_mode = 'fixed source' + + mesh = openmc.RegularMesh() + mesh.lower_left = (-1, -2, -3) + mesh.upper_right = (2, 3, 4) + mesh.dimension = (1, 1, 1) + + model.settings.source = openmc.MeshSource(mesh, [file_source]) + + model.export_to_model_xml() + + openmc.lib.init() + openmc.lib.simulation_init() + sites = openmc.lib.sample_external_source(10) + openmc.lib.simulation_finalize() + openmc.lib.finalize() + + # The mesh bounds do not contain the point of the lone source site in the + # file source, so it should not appear in the set of source sites produced + # from the mesh source. Additionally, the source should be located within + # the mesh + bbox = mesh.bounding_box + for site in sites: + assert site.r != (0, 0, 0) + assert site.E == source_particle.E + assert site.u == source_particle.u + assert site.time == source_particle.time + assert site.r in bbox diff --git a/tests/unit_tests/test_spherical_mesh.py b/tests/unit_tests/test_spherical_mesh.py index b491014cc70..0b579be35f7 100644 --- a/tests/unit_tests/test_spherical_mesh.py +++ b/tests/unit_tests/test_spherical_mesh.py @@ -102,7 +102,7 @@ def test_offset_mesh(run_in_tmpdir, model, estimator, origin): centroids = mesh.centroids for ijk in mesh.indices: i, j, k = np.array(ijk) - 1 - if model.geometry.find(centroids[:, i, j, k]): + if model.geometry.find(centroids[i, j, k]): mean[i, j, k] == 0.0 else: mean[i, j, k] != 0.0 diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 8438535ff8c..761f26ab3df 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -4,6 +4,7 @@ import pytest import openmc import openmc.stats +from scipy.integrate import trapezoid def assert_sample_mean(samples, expected_mean): @@ -89,6 +90,12 @@ def test_clip_discrete(): d_same = d.clip(1e-6, inplace=True) assert d_same is d + with pytest.raises(ValueError): + d.clip(-1.) + + with pytest.raises(ValueError): + d.clip(5) + def test_uniform(): a, b = 10.0, 20.0 @@ -220,7 +227,7 @@ def test_legendre(): # Integrating distribution should yield one mu = np.linspace(-1., 1., 1000) - assert np.trapz(d(mu), mu) == pytest.approx(1.0, rel=1e-4) + assert trapezoid(d(mu), mu) == pytest.approx(1.0, rel=1e-4) with pytest.raises(NotImplementedError): d.to_xml_element('distribution') diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 73519c38368..19221212e06 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -399,6 +399,32 @@ def test_polygon(): with pytest.raises(ValueError): openmc.model.Polygon(rz_points) + # Test "M" shaped polygon + points = np.array([[8.5151581, -17.988337], + [10.381711000000001, -17.988337], + [12.744357, -24.288728000000003], + [15.119406000000001, -17.988337], + [16.985959, -17.988337], + [16.985959, -27.246687], + [15.764328, -27.246687], + [15.764328, -19.116951], + [13.376877, -25.466951], + [12.118039, -25.466951], + [9.7305877, -19.116951], + [9.7305877, -27.246687], + [8.5151581, -27.246687]]) + + # Test points inside and outside by using offset method + m_polygon = openmc.model.Polygon(points, basis='xz') + inner_pts = m_polygon.offset(-0.1).points + assert all([(pt[0], 0, pt[1]) in -m_polygon for pt in inner_pts]) + outer_pts = m_polygon.offset(0.1).points + assert all([(pt[0], 0, pt[1]) in +m_polygon for pt in outer_pts]) + + # Offset of -0.2 will cause self-intersection + with pytest.raises(ValueError): + m_polygon.offset(-0.2) + @pytest.mark.parametrize("axis", ["x", "y", "z"]) def test_cruciform_prism(axis): diff --git a/tests/unit_tests/test_tallies.py b/tests/unit_tests/test_tallies.py index 14319a0a29d..54444331257 100644 --- a/tests/unit_tests/test_tallies.py +++ b/tests/unit_tests/test_tallies.py @@ -10,8 +10,9 @@ def test_xml_roundtrip(run_in_tmpdir): mesh.upper_right = (10., 10., 10.,) mesh.dimension = (5, 5, 5) mesh_filter = openmc.MeshFilter(mesh) + meshborn_filter = openmc.MeshBornFilter(mesh) tally = openmc.Tally() - tally.filters = [mesh_filter] + tally.filters = [mesh_filter, meshborn_filter] tally.nuclides = ['U235', 'I135', 'Li6'] tally.scores = ['total', 'fission', 'heating'] tally.derivative = openmc.TallyDerivative( @@ -27,9 +28,11 @@ def test_xml_roundtrip(run_in_tmpdir): assert len(new_tallies) == 1 new_tally = new_tallies[0] assert new_tally.id == tally.id - assert len(new_tally.filters) == 1 + assert len(new_tally.filters) == 2 assert isinstance(new_tally.filters[0], openmc.MeshFilter) assert np.allclose(new_tally.filters[0].mesh.lower_left, mesh.lower_left) + assert isinstance(new_tally.filters[1], openmc.MeshBornFilter) + assert np.allclose(new_tally.filters[1].mesh.lower_left, mesh.lower_left) assert new_tally.nuclides == tally.nuclides assert new_tally.scores == tally.scores assert new_tally.derivative.variable == tally.derivative.variable diff --git a/tests/unit_tests/test_temp_interp.py b/tests/unit_tests/test_temp_interp.py index a28aafb6f88..4c2882347b3 100644 --- a/tests/unit_tests/test_temp_interp.py +++ b/tests/unit_tests/test_temp_interp.py @@ -238,3 +238,38 @@ def test_temperature_interpolation_tolerance(model): # All calculated k-effectives should be equal assert default_k == pytest.approx(interpolated_k) assert interpolated_k == pytest.approx(cell_k) + + +def test_temperature_slightly_above(run_in_tmpdir): + """In this test, we have two materials at temperatures close to actual data + temperatures. However, one is slightly above the highest temperature which + invokes separate logic. The k-effective value should be somewhere between + k=2 (if the temperature were only 600 K) and k=1 (if the temperature were + only 900 K).""" + + make_fake_cross_section() + + model = openmc.Model() + mat1 = openmc.Material() + mat1.add_nuclide('U235', 1.0) + mat1.temperature = 900.1 + mat2 = openmc.Material() + mat2.add_nuclide('U235', 1.0) + mat2.temperature = 600.0 + model.materials.extend([mat1, mat2]) + model.materials.cross_sections = str(Path('cross_sections_fake.xml').resolve()) + + sph1 = openmc.Sphere(r=1.0) + sph2 = openmc.Sphere(r=4.0, boundary_type='reflective') + cell1 = openmc.Cell(fill=mat1, region=-sph1) + cell2 = openmc.Cell(fill=mat2, region=+sph1 & -sph2) + model.geometry = openmc.Geometry([cell1, cell2]) + + model.settings.particles = 1000 + model.settings.inactive = 0 + model.settings.batches = 10 + model.settings.temperature = {'method': 'interpolation'} + + sp_filename = model.run() + with openmc.StatePoint(sp_filename) as sp: + assert 1.1 < sp.keff.n < 1.9 diff --git a/tests/unit_tests/test_tracks.py b/tests/unit_tests/test_tracks.py index 3a017015564..3951a72c63c 100644 --- a/tests/unit_tests/test_tracks.py +++ b/tests/unit_tests/test_tracks.py @@ -1,5 +1,6 @@ from pathlib import Path +import h5py import numpy as np import openmc import pytest @@ -157,3 +158,35 @@ def test_write_to_vtk(sphere_model): assert isinstance(polydata, vtk.vtkPolyData) assert Path('tracks.vtp').is_file() + + +def test_restart_track(run_in_tmpdir, sphere_model): + # cut the sphere model in half with an improper boundary condition + plane = openmc.XPlane(x0=-1.0) + for cell in sphere_model.geometry.get_all_cells().values(): + cell.region &= +plane + + # generate lost particle files + with pytest.raises(RuntimeError, match='Maximum number of lost particles has been reached.'): + sphere_model.run(output=False) + + lost_particle_files = list(Path.cwd().glob('particle_*.h5')) + assert len(lost_particle_files) > 0 + particle_file = lost_particle_files[0] + # restart the lost particle with tracks enabled + sphere_model.run(tracks=True, restart_file=particle_file) + tracks_file = Path('tracks.h5') + assert tracks_file.is_file() + + # check that the last track of the file matches the lost particle file + tracks = openmc.Tracks(tracks_file) + initial_state = tracks[0].particle_tracks[0].states[0] + restart_r = np.array(initial_state['r']) + restart_u = np.array(initial_state['u']) + + with h5py.File(particle_file, 'r') as lost_particle_file: + lost_r = np.array(lost_particle_file['xyz'][()]) + lost_u = np.array(lost_particle_file['uvw'][()]) + + pytest.approx(restart_r, lost_r) + pytest.approx(restart_u, lost_u) diff --git a/tests/unit_tests/test_triggers.py b/tests/unit_tests/test_triggers.py index 4fe6e044ab7..6b9e54eeb72 100644 --- a/tests/unit_tests/test_triggers.py +++ b/tests/unit_tests/test_triggers.py @@ -73,3 +73,43 @@ def test_tally_trigger_null_score(run_in_tmpdir): total_batches = sp.n_realizations + sp.n_inactive assert total_batches == pincell.settings.trigger_max_batches + +def test_tally_trigger_zero_ignored(run_in_tmpdir): + pincell = openmc.examples.pwr_pin_cell() + + # create an energy filter below and around the O-16(n,p) threshold (1.02e7 eV) + e_filter = openmc.EnergyFilter([0.0, 1e7, 2e7]) + + # create a tally with triggers applied + tally = openmc.Tally() + tally.filters = [e_filter] + tally.scores = ['(n,p)'] + tally.nuclides = ["O16"] + + # 100% relative error: should be immediately satisfied in nonzero bin + trigger = openmc.Trigger('rel_err', 1.0) + trigger.scores = ['(n,p)'] + trigger.ignore_zeros = True + + tally.triggers = [trigger] + + pincell.tallies = [tally] + + pincell.settings.particles = 1000 # we need a few more particles for this + pincell.settings.trigger_active = True + pincell.settings.trigger_max_batches = 50 + pincell.settings.trigger_batch_interval = 20 + + sp_file = pincell.run() + + with openmc.StatePoint(sp_file) as sp: + # verify that the first bin is zero and the second is nonzero + tally_out = sp.get_tally(id=tally.id) + below, above = tally_out.mean.squeeze() + assert below == 0.0, "Tally events observed below expected threshold" + assert above > 0, "No tally events observed. Test with more particles." + + # we expect that the trigger fires before max batches are hit + total_batches = sp.n_realizations + sp.n_inactive + assert total_batches < pincell.settings.trigger_max_batches + diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 630e66df3ab..46d4ec3f734 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -1,5 +1,4 @@ import lxml.etree as ET - import numpy as np import openmc import pytest diff --git a/tools/ci/gha-install-vectfit.sh b/tools/ci/gha-install-vectfit.sh index 8444c3036b3..bd38e1ea8cd 100755 --- a/tools/ci/gha-install-vectfit.sh +++ b/tools/ci/gha-install-vectfit.sh @@ -16,8 +16,6 @@ XTENSOR_PYTHON_REPO='https://github.com/xtensor-stack/xtensor-python' XTENSOR_BLAS_BRANCH='0.17.1' XTENSOR_BLAS_REPO='https://github.com/xtensor-stack/xtensor-blas' -sudo apt-get install -y libblas-dev liblapack-dev - cd $HOME git clone -b $PYBIND_BRANCH $PYBIND_REPO cd pybind11 && mkdir build && cd build && cmake .. && sudo make install diff --git a/vendor/xtensor b/vendor/xtensor index 31acec1e90b..3634f2ded19 160000 --- a/vendor/xtensor +++ b/vendor/xtensor @@ -1 +1 @@ -Subproject commit 31acec1e90bbea6d4bc17af0710a123bd5da6689 +Subproject commit 3634f2ded19e0cf38208c8b86cea9e1d7c8e397d diff --git a/vendor/xtl b/vendor/xtl index c19750fb148..a7c1c5444df 160000 --- a/vendor/xtl +++ b/vendor/xtl @@ -1 +1 @@ -Subproject commit c19750fb1488369dc41f6069bc2b8446fc093e75 +Subproject commit a7c1c5444dfc57f76620391af4c94785ff82c8d6