")
+
+
+def _append_content(lines: ViewList, content: list[str], indent: str = "") -> None:
+ for line in content:
+ _append_line(lines, f"{indent}{line}" if line else "")
+
+
+def _converted_docstring(
+ directive: SphinxDirective, obj, public_name: str
+) -> list[str]:
+ docstring = inspect.getdoc(obj) or ""
+ return str(
+ NumpyDocstring(
+ docstring,
+ config=directive.env.config,
+ app=directive.env.app,
+ what="method",
+ name=public_name,
+ obj=obj,
+ )
+ ).splitlines()
+
+
+def _public_signature(function) -> str:
+ signature = inspect.signature(function)
+ parameters = tuple(signature.parameters.values())[1:]
+ return str(
+ signature.replace(
+ parameters=parameters,
+ return_annotation=inspect.Signature.empty,
+ )
+ )
+
+
+def _public_type(annotation) -> str:
+ if isinstance(annotation, type):
+ return annotation.__name__
+ return str(annotation).removeprefix("typing.")
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
index 67b0574f9..d20b9f974 100644
--- a/docs/source/_static/custom.css
+++ b/docs/source/_static/custom.css
@@ -5,18 +5,17 @@ dt.sig.sig-object.py > em.property {
display: none;
}
-/* Container styling */
-.admonition.tip {
- border-left: 5px solid #ffde57; /* Python Yellow border */
+/* Align Simulation-owned settings and techniques with regular class members. */
+.simulation-members {
+ margin-left: 30px;
}
-/* Adding the Green Circle to the title */
-.admonition.tip > .admonition-title::before {
- background-color: #ffde57;
+/* Separate the architecture flow figure from its introductory discussion. */
+.architecture-flow-figure {
+ margin-bottom: 1.5rem;
}
-/* Optional: Change title background to a light Python Yellow */
-.admonition.tip > .admonition-title {
- background-color: #3776ab;
- color: #ffffff;
+/* Separate the architecture component table from its follow-up discussion. */
+.architecture-component-followup {
+ margin-top: 1.5rem;
}
diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json
new file mode 100644
index 000000000..00f7e0216
--- /dev/null
+++ b/docs/source/_static/switcher.json
@@ -0,0 +1,13 @@
+[
+ {
+ "name": "dev",
+ "version": "dev",
+ "url": "https://mcdc.readthedocs.io/en/dev/"
+ },
+ {
+ "name": "0.15 (stable)",
+ "version": "stable",
+ "url": "https://mcdc.readthedocs.io/en/stable/",
+ "preferred": true
+ }
+]
diff --git a/docs/source/_static/theme-toggle.js b/docs/source/_static/theme-toggle.js
new file mode 100644
index 000000000..fe7f4bc8b
--- /dev/null
+++ b/docs/source/_static/theme-toggle.js
@@ -0,0 +1,40 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const buttons = document.querySelectorAll(".mcdc-theme-toggle");
+ if (!buttons.length) {
+ return;
+ }
+
+ const root = document.documentElement;
+
+ const updateButtons = () => {
+ const darkMode = root.dataset.theme === "dark";
+ const nextMode = darkMode ? "light" : "dark";
+
+ buttons.forEach((button) => {
+ button.setAttribute("aria-label", `Switch to ${nextMode} mode`);
+ button.setAttribute("title", `Switch to ${nextMode} mode`);
+ });
+ };
+
+ buttons.forEach((button) => {
+ button.addEventListener("click", () => {
+ const nextMode = root.dataset.theme === "dark" ? "light" : "dark";
+
+ root.dataset.mode = nextMode;
+ root.dataset.theme = nextMode;
+ document.querySelectorAll(".dropdown-menu").forEach((menu) => {
+ menu.classList.toggle("dropdown-menu-dark", nextMode === "dark");
+ });
+
+ localStorage.setItem("mode", nextMode);
+ localStorage.setItem("theme", nextMode);
+ updateButtons();
+ });
+ });
+
+ new MutationObserver(updateButtons).observe(root, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+ updateButtons();
+});
diff --git a/docs/source/_templates/copyright.html b/docs/source/_templates/copyright.html
new file mode 100644
index 000000000..aad76875c
--- /dev/null
+++ b/docs/source/_templates/copyright.html
@@ -0,0 +1,10 @@
+{# Link the two institutions named in the MC/DC copyright notice. #}
+{% if show_copyright %}
+
+ © Copyright 2023-2026,
+ Center for Exascale Monte Carlo Neutron Transport (CEMeNT) ,
+ Center for Advancing the Radiation Resilience of Electronics (CARRE) ,
+ and MC/DC contributors.
+
+
+{% endif %}
diff --git a/docs/source/_templates/simulationclass.rst b/docs/source/_templates/simulationclass.rst
new file mode 100644
index 000000000..d38c66ab9
--- /dev/null
+++ b/docs/source/_templates/simulationclass.rst
@@ -0,0 +1,9 @@
+{{ fullname | escape | underline}}
+
+.. currentmodule:: {{ module }}
+
+.. autoclass:: {{ objname }}
+ :members:
+ :no-special-members:
+
+.. simulation-members::
diff --git a/docs/source/_templates/theme-toggle.html b/docs/source/_templates/theme-toggle.html
new file mode 100644
index 000000000..c8680325b
--- /dev/null
+++ b/docs/source/_templates/theme-toggle.html
@@ -0,0 +1,10 @@
+{# A two-state light/dark toggle that replaces the theme's dropdown. #}
+
+
+
+
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 63c0d3f9b..eba9bddb5 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -18,6 +18,9 @@
PROJECT_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
+EXTENSIONS_ROOT = os.path.join(HERE, "_ext")
+if EXTENSIONS_ROOT not in sys.path:
+ sys.path.insert(0, EXTENSIONS_ROOT)
# -- Project information -----------------------------------------------------
project = "MC/DC"
@@ -37,17 +40,13 @@
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.autosummary",
- "sphinx_toolbox.github",
- "sphinx_toolbox.sidebar_links",
"sphinx.ext.autosectionlabel",
+ "sphinx_design",
+ "simulation_members",
]
autosummary_generate = True
autosectionlabel_prefix_document = True
-github_username = "CEMeNT-PSAAP"
-github_repository = "MCDC"
-github_url = "https://github.com/{github_username}/{github_repository}"
-
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -57,16 +56,73 @@
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
-# The theme to use for HTML and HTML Help pages. See the documentation for
-# a list of builtin themes.
-#
-html_theme = "furo"
-html_logo = "images/home/mcdc.svg"
+# The PyData Sphinx Theme provides the site-wide header, section navigation,
+# in-page table of contents, search, and light/dark mode.
+html_theme = "pydata_sphinx_theme"
+html_title = "MC/DC Documentation"
+html_logo = "../../assets/mcdc-logo.svg"
+html_favicon = "../../assets/mcdc-favicon.svg"
+
+# Read the Docs provides the active version slug during hosted builds. Use the
+# development docs as the local-build default.
+docs_version = os.environ.get("READTHEDOCS_VERSION", "dev")
+switcher_json_url = (
+ "https://mcdc.readthedocs.io/en/dev/_static/switcher.json"
+ if os.environ.get("READTHEDOCS") == "True"
+ else "/_static/switcher.json"
+)
+
+html_theme_options = {
+ "navbar_align": "left",
+ "navbar_end": ["theme-toggle", "version-switcher", "navbar-icon-links"],
+ "navbar_persistent": ["search-button"],
+ "header_links_before_dropdown": 5,
+ "navigation_depth": 4,
+ "show_nav_level": 2,
+ "use_edit_page_button": True,
+ "switcher": {
+ "json_url": switcher_json_url,
+ "version_match": docs_version,
+ },
+ "show_version_warning_banner": docs_version != "stable",
+ # Hosted builds share one version list from the development documentation.
+ # Local builds use the copied static file when served from the HTML output root.
+ "check_switcher": False,
+ "logo": {
+ "alt_text": "MC/DC Documentation - Home",
+ },
+ "icon_links": [
+ {
+ "name": "GitHub",
+ "url": "https://github.com/mcdc-project/mcdc",
+ "icon": "fa-brands fa-square-github",
+ "type": "fontawesome",
+ },
+ {
+ "name": "PyPI",
+ "url": "https://pypi.org/project/mcdc/",
+ "icon": "fa-solid fa-box",
+ "type": "fontawesome",
+ },
+ ],
+}
+
+html_context = {
+ "github_user": "mcdc-project",
+ "github_repo": "mcdc",
+ "github_version": "dev",
+ "doc_path": "docs/source",
+}
-# html_permalinks = ['https://cement-psaap.github.io/', 'https://github.com/CEMeNT-PSAAP/MCDC']
+# The home page already provides purpose-based routing. Section pages use the
+# theme's default collapsible primary sidebar.
+html_sidebars = {
+ "index": [],
+}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static", "images/home"]
+html_static_path = ["_static"]
html_css_files = ["custom.css"]
+html_js_files = ["theme-toggle.js"]
diff --git a/docs/source/contribution_guide/container-dev.rst b/docs/source/contributing/container_development.rst
similarity index 89%
rename from docs/source/contribution_guide/container-dev.rst
rename to docs/source/contributing/container_development.rst
index 7dbfd7334..892fb8bc7 100644
--- a/docs/source/contribution_guide/container-dev.rst
+++ b/docs/source/contributing/container_development.rst
@@ -40,7 +40,7 @@ Option A:
.. code-block:: bash
- apptainer build --sandbox mcdc_sandbox docker://ghcr.io/cement-psaap/mcdc:dev
+ apptainer build --sandbox mcdc_sandbox docker://ghcr.io/mcdc-project/mcdc:dev
Option B:
@@ -127,8 +127,8 @@ On Apple Silicon:
.. code-block:: bash
docker build --platform linux/amd64 -f containers/Dockerfile -t mcdc:dev-amd64 .
- docker tag mcdc:dev-amd64 ghcr.io/cement-psaap/mcdc:dev
- docker push ghcr.io/cement-psaap/mcdc:dev
+ docker tag mcdc:dev-amd64 ghcr.io/mcdc-project/mcdc:dev
+ docker push ghcr.io/mcdc-project/mcdc:dev
.. rubric:: Making the Package Public
@@ -143,5 +143,6 @@ File Overview
containers/
├── Dockerfile
- ├── docker-compose.yml
- └── README.md
+ ├── Dockerfile.cuda
+ ├── Dockerfile.rocm
+ └── docker-compose.yml
diff --git a/docs/source/contribution_guide/ci.rst b/docs/source/contributing/continuous_integration.rst
similarity index 71%
rename from docs/source/contribution_guide/ci.rst
rename to docs/source/contributing/continuous_integration.rst
index 0fb31d7c0..a1493f561 100644
--- a/docs/source/contribution_guide/ci.rst
+++ b/docs/source/contributing/continuous_integration.rst
@@ -1,20 +1,22 @@
-.. _ci:
+.. _continuous_integration:
.. highlight:: none
Continuous Integration
======================
-We use `github actions `_ to host and run most of our CI tests and version release information.
+We use `GitHub Actions `_ to host and run most of our CI tests and version release information.
We run pure python unit tests and regression testing in pure Python, pure Python + MPI, numba, numba + MPI, and numba+GPU+harmonize.
When running regression tests we compare small particle count outputs to saved files in the testing directory.
If the RNG seed has not changed the results should be deterministic.
+The Black workflow runs on Python 3.14 and formats for all supported Python versions, while Pyright checks the typed public API against Python 3.14.
+Automatic runtime tests use Python 3.13, while maintainers manually test Python 3.11, 3.12, and 3.14 in Python and Numba modes before a release.
GPU COE Machine
---------------
-CEMeNT currently has a `CI machine `_ on OSU's campus administered by the college of engineering HPC folks to do export controlled and GPU continuous integration.
+CEMeNT currently has a `CI machine `_ on OSU's campus administered by the college of engineering HPC folks to do export controlled and GPU continuous integration.
It has a single Nvidia A2 (16GB VRAM) and an AMD EPYC 7313P 16-Core Processor with 64 GBs of RAM.
This is a dedicated machine with no additional users other then CEMeNT staff.
@@ -31,11 +33,11 @@ The standard dev env for MC/DC install on this machine can be ascertained with
.. code-block:: bash
- module load cuda/11.8 gcc/10.3 mpich/4.0h_gcc-10 python/3.11
+ module load cuda/11.8 gcc/10.3 mpich/4.0h_gcc-10 python/3.13
python -m venv
- module unload python/3.11
+ module unload python/3.13
source /bin/activate
Then MC/DC and harmonize can be installed there in the normal manner for GPU capabilities.
The runner runs all the time in background of Joanna's account.
-Contact her with any issues or on instructions to set up your own runner!
\ No newline at end of file
+Contact her with any issues or on instructions to set up your own runner!
diff --git a/docs/source/contributing/example_validation.rst b/docs/source/contributing/example_validation.rst
new file mode 100644
index 000000000..4e4084cb6
--- /dev/null
+++ b/docs/source/contributing/example_validation.rst
@@ -0,0 +1,66 @@
+.. _example_validation:
+
+==================
+Example Validation
+==================
+
+The inputs under ``examples/`` are executable documentation. Changes to the
+public API, object-compilation behavior, or example models should validate them
+at three levels.
+
+Compile Every Example
+---------------------
+
+The automated example test executes every ``examples/**/input.py`` while
+replacing transport and visualization with model compilation:
+
+.. code-block:: sh
+
+ python -m pytest test/unit/test_example_inputs.py --mode=python
+
+This check:
+
+- Discovers new example inputs automatically.
+- Executes their model-construction code from the correct working directory.
+- Requires an explicit :class:`mcdc.Simulation`.
+- Allows iterative examples to compile the same simulation more than once.
+- Compiles the complete reachable object graph.
+- Requires at least one cell, source, and tally.
+- Avoids long particle-transport runs and output files.
+
+The test catches removed public interfaces, missing object attachments,
+invalid model references, compilation failures, and example files that no
+longer reach ``simulation.run()`` or ``simulation.visualize_model()``.
+
+Run Representative Transport
+----------------------------
+
+The compile-only test does not validate transport results. Before merging a
+change that affects execution, run the small slab-shielding problem in the
+affected CPU modes:
+
+.. code-block:: sh
+
+ cd examples/slab_shielding
+ python input.py --mode=python --N_particle=100 --N_batch=2
+ python input.py --mode=numba --N_particle=100 --N_batch=2
+
+Check that both commands finish, write ``slab_shielding.h5``, and produce the
+expected ``slab_flux`` tally. Use the normal example settings when evaluating
+statistical agreement rather than only execution.
+
+Validate Specialized Paths
+--------------------------
+
+Run examples that represent the changed capability:
+
+- ``examples/c5g7/k-eigenvalue`` for k-eigenvalue behavior.
+- ``examples/moving_source`` or ``examples/c5g7/transient`` for transient
+ behavior.
+- ``examples/slab_shielding`` or ``examples/fuel_array_packaged`` for
+ visualization.
+- A supported accelerator environment for Numba-GPU changes.
+
+Regression tests remain the authoritative check for numerical results.
+Example validation complements them by ensuring the documented, user-facing
+inputs continue to construct models through the current public API.
diff --git a/docs/source/contributing/index.rst b/docs/source/contributing/index.rst
new file mode 100644
index 000000000..78ba61aaf
--- /dev/null
+++ b/docs/source/contributing/index.rst
@@ -0,0 +1,208 @@
+.. _contributing:
+
+============
+Contributing
+============
+
+This guide describes the repository workflow and quality checks for contributing to MC/DC.
+It is intended for both occasional contributors and project maintainers.
+
+Start with the setup steps below.
+Use :doc:`continuous_integration` to understand automated checks and :doc:`container_development` when developing in the project container.
+Use :doc:`example_validation` when changing the public API or example problems.
+Read :doc:`pull_requests` before preparing a contribution.
+For software architecture and documentation practices, see the :doc:`../developer_guide/index`.
+
+For implementation guidance specific to compiled transport functions, see :doc:`../developer_guide/extending/writing_numba_compatible_transport_code`.
+
+Contributions target the ``dev`` branch.
+Prepare a development checkout with the following steps:
+
+#. Fork ``mcdc-project/mcdc`` to your GitHub account.
+#. ``git clone git@github.com:/mcdc.git``
+#. ``git switch dev``
+#. Create and activate a Python 3.14 environment for contributor tooling.
+#. ``python -m pip install -e ".[dev]"``
+
+Development Workflow
+--------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ continuous_integration
+ container_development
+ example_validation
+ pull_requests
+
+MC/DC documentation is an important part of the project and evolves alongside the codebase.
+The :doc:`../developer_guide/documentation/index` guide describes the documentation philosophy, writing guidelines, and the tools used to build and maintain the documentation.
+
+Please note our `code of conduct `_, which we take seriously.
+
+------------
+Code Styling
+------------
+
+MC/DC uses the `Black code style `_.
+Run Black with Python 3.14 from the repository root before submitting a contribution:
+
+.. code-block:: sh
+
+ black .
+
+Black is included in the ``dev`` optional dependency group installed during development setup.
+Black formats for every supported Python version listed in ``pyproject.toml``.
+
+Public API Typing
+-----------------
+
+MC/DC ships inline type information for its public Python interface.
+Run Pyright from the repository root after changing a public class, annotation, or export:
+
+.. code-block:: sh
+
+ pyright
+
+The strict public API checks are defined in ``test/typecheck/public_api.py``.
+Pyright checks the public API against Python 3.14.
+
+---------
+Debugging
+---------
+
+MCDC includes options to debug the Numba JIT code.
+It does this by toggling Numba options using the numba.config submodule.
+This will result in less performant code and longer compile times but will allow for better error messages from Numba and other packages.
+`See Numba documentation of a list of all possible debug and compiler options. `_
+The most useful set of debug options for MC/DC can be enabled with
+
+.. code-block:: python3
+
+ python input.py --mode=numba_debug
+
+Which will toggle the following debug and compiler options in Numba:
+
+* ``DISABLE_JIT=False`` turns on the jitter
+* ``NUMBA_OPT=0`` forces the compilers to form unoptimized code, while values ``1``, ``2``, and ``3`` enable increasing optimization.
+ Change this option when an error appears only at higher optimization levels.
+* ``DEBUG=False`` controls all debugging options.
+ MC/DC leaves this disabled in ``numba_debug`` because it produces extensive terminal output.
+* ``NUMBA_FULL_TRACEBACKS=1`` allows errors from sub-packages to be printed (i.e. Numpy)
+* ``NUMBA_BOUNDSCHECK=1`` makes Numba check vectors for bounds errors.
+ Without this check, a bounds error can result in a segmentation fault.
+ Together with full tracebacks, this option identifies the location of a bounds error in NumPy operations.
+* ``NUMBA_DEBUG_NRT=1`` enables the `Numba runtime statistics counter `_.
+ This counter helps diagnose memory leaks.
+* ``NUMBA_DEBUG_TYPEINFER= 1`` print out debugging information about type inferences that numba might need to make if a function is ill-defined
+* ``NUMBA_ENABLE_PROFILING=1`` enables profiler use
+* ``NUMBA_DUMP_CFG=1`` prints out a control flow diagram
+
+If extra debug options or alteration to these options are required they can be toggled and passed under the ``mode==numba_debug`` option tree in ``mcdc/config.py``.
+
+-------
+Caching
+-------
+
+MC/DC is a just-in-time (JIT) compiled code.
+This is sometimes disadvantageous, especially for users who might run many versions of the same simulation with slightly different parameters.
+As the JIT compilation scheme will only compile functions that are actually used in a given simulation, it is not a grantee that any one function will be compiled.
+
+Developers should be very cautious about using caching features.
+Numba has a few documented errors around caching.
+The most critical of which is that functions in other files that are called by cached functions will not force a recompile, even if there are changes in those sub-functions.
+In this case caching should be disabled.
+
+In MC/DC the simulation functions (in ``mcdc/transport/simulation.py``) can be configured to use caching.
+Caching behavior is controlled via the ``--caching`` and ``--clear_cache`` command-line flags.
+
+To disable caching, omit the ``--caching`` flag (the default).
+Alternatively a developer could delete the ``__pycache__`` directory or other cache directory which is system dependent (`see more about clearing the numba cache `_)
+
+
+MC/DC may eventually enable `Numba's ahead-of-time compilation capabilities `_.
+The core development team is waiting for planned `upgrades to Numba's AOT functionality `_.
+However if absolutely required by users numba does allow for some `cache sharing `_.
+
+------------------
+Adding a New Input
+------------------
+
+For architectural guidance on adding a model field, embedded configuration, registered object category, or polymorphic subtype, see :doc:`../developer_guide/extending/extending_the_object_model`.
+Public model classes and configuration are primarily defined in ``mcdc/object_/``.
+Common input-related locations include:
+
+#. ``mcdc/object_/settings.py`` — simulation settings and k-eigenvalue parameters
+#. ``mcdc/object_/material.py`` — material and composition definition
+#. ``mcdc/object_/transport_model_data.py`` — particle-specific transport data
+#. ``mcdc/object_/surface.py`` — surface geometry (``Surface`` class methods)
+#. ``mcdc/object_/cell.py`` — cell definitions (``Cell``)
+#. ``mcdc/object_/source.py`` — source specifications (``Source``)
+#. ``mcdc/object_/tally.py`` — tally objects (``Tally``)
+#. ``mcdc/object_/technique.py`` — variance reduction techniques
+#. ``mcdc/config.py`` — command-line argument definitions
+
+-------
+Testing
+-------
+
+See :doc:`continuous_integration` for more information on how we run these tests automatically.
+
+MC/DC has unit and regression test suites that contributions must pass before they are accepted.
+Unit tests exercise focused behavior in both Python and Numba modes.
+Regression tests compare representative simulations against saved reference results.
+GitHub Actions runs the CPU suites on Linux, and a self-hosted runner provides GPU regression coverage.
+
+To run the default fast unit-test suite locally, run,
+
+.. code-block:: sh
+
+ python -m pytest
+
+To run the full unit-test suite in both Python and Numba mode, run,
+
+.. code-block:: sh
+
+ python -m pytest test/unit
+
+To run the regression tests locally, run,
+
+.. code-block:: sh
+
+ python -m pytest test/regression
+
+
+The command runs all regression tests.
+The following options control test selection and execution:
+
+* Run a specific test (with wildcard ``*`` support): ``--name=``
+* Skip a specific test (with wildcard ``*`` support): ``--skip=``
+* Run in Numba mode: ``--mode=numba``
+* Run against the GPU target: ``--target=gpu``
+* Run in multiple MPI ranks (currently support ``mpiexec`` and ``srun``): ``--mpiexec=``
+* Run with Slurm ``srun`` instead of ``mpiexec``: ``--srun=``
+
+The flags can be combined.
+Add a new test with the following steps:
+
+#. Create a folder whose name identifies the test.
+#. Add the input file as ``input.py``.
+#. Add the answer key as ``answer.h5``.
+#. Make sure that the number of particles run is large enough for a good test.
+#. If the test runs longer than 5 seconds, consider decreasing the number of particles.
+
+When adding a new hardware backend a new instantiation of the test suit should be made.
+This is done with github actions.
+See the (``.github/workflows``) for examples.
+
+If a new simulation type is added (e.g. quasi montecarlo w/ davidson's method, residual monte carlo, intrusive uq) more regression tests should be added with your PR.
+If you are wondering accommodations.
+
+--------------------
+Adding Documentation
+--------------------
+
+Documentation is a core part of MC/DC.
+Contributions that introduce new features, modify existing behavior, or change developer workflows should update the relevant documentation accordingly.
+
+See the :doc:`../developer_guide/documentation/index` guide for documentation philosophy, writing guidelines, and instructions for contributing to the documentation.
diff --git a/docs/source/contributing/pull_requests.rst b/docs/source/contributing/pull_requests.rst
new file mode 100644
index 000000000..9c9c55c2f
--- /dev/null
+++ b/docs/source/contributing/pull_requests.rst
@@ -0,0 +1,39 @@
+.. _pull_requests:
+
+=============
+Pull Requests
+=============
+
+MC/DC uses a fork-based contribution workflow. Open pull requests from your
+fork against the ``dev`` branch of ``mcdc-project/mcdc``.
+
+Before opening a pull request, make sure:
+
+- The applicable tests pass.
+- The code follows the project style.
+- Tests and documentation have been added or updated as needed.
+- ``CHANGELOG.md`` has been updated when the change is notable.
+
+Changelog Updates
+-----------------
+
+Add a concise entry under ``[Unreleased]`` in
+`CHANGELOG.md `_
+when a pull request introduces a notable user- or developer-visible change.
+Follow the format described in that file. Release headings, versions, and dates
+are assigned during release preparation.
+
+Pull Request Description
+------------------------
+
+Use the pull request template to summarize:
+
+- The type and purpose of the change.
+- Associated issues or pull requests.
+- Relevant theory or design context.
+- New, changed, deprecated, or removed functionality.
+- Any new dependency.
+- The developers who should be notified.
+
+The description should give reviewers enough context to understand the change,
+verify its scope, and identify any follow-up work.
diff --git a/docs/source/contribution_guide/documentation/index.rst b/docs/source/contribution_guide/documentation/index.rst
deleted file mode 100644
index 4fa72dc1c..000000000
--- a/docs/source/contribution_guide/documentation/index.rst
+++ /dev/null
@@ -1,18 +0,0 @@
-.. _documentation:
-
-Documentation
-=============
-
-High-quality documentation is an essential part of MC/DC. Clear documentation
-helps users perform simulations, enables method developers to understand and
-extend computational methods, and assists framework developers in maintaining
-and evolving the software.
-
-This section explains both the philosophy behind MC/DC's documentation and the
-tools used to build it.
-
-.. toctree::
- :maxdepth: 1
-
- philosophy
- sphinx
diff --git a/docs/source/contribution_guide/documentation/philosophy.rst b/docs/source/contribution_guide/documentation/philosophy.rst
deleted file mode 100644
index 93f28c7cf..000000000
--- a/docs/source/contribution_guide/documentation/philosophy.rst
+++ /dev/null
@@ -1,111 +0,0 @@
-.. _documentation_philosophy:
-
-========================
-Documentation Philosophy
-========================
-
-Vision
-------
-
-The MC/DC documentation should serve the diverse community that develops and
-uses the project. As MC/DC continues to grow, its documentation should be as
-scalable and maintainable as its software architecture.
-
-MC/DC adopts a layered documentation philosophy that balances usability,
-technical depth, and long-term maintainability across the entire project.
-
-This philosophy applies to all forms of MC/DC documentation, including the
-README, User Guide, Theory Guide, Python API documentation, examples,
-tutorials, and API docstrings.
-
-The Layered Documentation Philosophy
-------------------------------------
-
-MC/DC documentation is written for three complementary audiences. Rather than
-maintaining separate documentation for each audience, individual documents
-should progressively layer information from high-level usage to mathematical
-concepts and implementation details. Readers can naturally stop at the level of
-detail appropriate for their needs.
-
-Users
-^^^^^
-
-Users build geometry, define materials and sources, configure simulations,
-execute transport calculations, and analyze results.
-
-Documentation for users should emphasize:
-
-- What MC/DC provides.
-- How to use the public API.
-- Tutorials, examples, and recommended workflows.
-- Best practices for building transport models.
-
-Method Developers
-^^^^^^^^^^^^^^^^^
-
-Method Developers use MC/DC as a platform for developing and evaluating new
-transport methods and computational algorithms.
-
-Documentation for Method Developers should explain:
-
-- Mathematical formulations.
-- Numerical algorithms.
-- Data representations.
-- Design rationale.
-- Extensibility points.
-- Relationships between the public API and transport algorithms.
-
-Framework Developers
-^^^^^^^^^^^^^^^^^^^^
-
-Framework Developers extend and maintain the MC/DC software framework itself.
-
-Documentation for Framework Developers should describe:
-
-- Software architecture.
-- Internal APIs.
-- Preparation pipeline.
-- Memory layout.
-- Compilation workflow.
-- Parallel execution.
-- Performance considerations.
-- Implementation and design decisions.
-
-Guiding Principles
-------------------
-
-Documentation should naturally progress from high-level concepts toward
-implementation details.
-
-A typical progression is:
-
-#. Overview
-#. Usage
-#. Examples
-#. Mathematical concepts
-#. Implementation notes
-
-Not every document requires every section. However, documentation should
-generally present information in this order so that each audience can stop
-reading once they have reached the level of detail they need.
-
-Public behavior should be described before mathematical representation, and
-mathematical representation should be described before implementation details.
-
-API Docstrings
---------------
-
-API docstrings should follow the same layered philosophy. In general:
-
-- The opening description should explain the public purpose of the object,
- function, or module.
-- Parameters, return values, attributes, and examples should focus on the
- public interface.
-- Mathematical representations, algorithms, and design rationale should be
- documented in the ``Notes`` section when they help Method Developers.
-- Framework-specific implementation details should be documented separately as
- implementation notes when appropriate.
-
-Not every API requires all of these sections. The goal is to provide each
-audience with the information it needs while keeping the documentation clear,
-progressive, and easy to navigate.
diff --git a/docs/source/contribution_guide/index.rst b/docs/source/contribution_guide/index.rst
deleted file mode 100644
index 00c6b7c2a..000000000
--- a/docs/source/contribution_guide/index.rst
+++ /dev/null
@@ -1,206 +0,0 @@
-.. _contribution_guide:
-
-==================
-Contribution Guide
-==================
-
-Thank you for looking to contribute to MC/DC!
-We are really excited to see what you bring to this exciting open source project!
-Whether you are here to make a single PR and never return, or want to become a maintainer we are pumped to work with you.
-We have regular developers meetings for any and all who are interested to discuss contributions to this code base.
-
-This describes the processes of contributing to MC/DC for both internal (CEMeNT) and external developers.
-We make contributions to the ``dev`` branch of MC/DC.
-To get started making alterations in a cloned repo
-
-#. fork ``CEMeNT-PSAAP/MCDC`` to your github account
-#. ``git clone git@github.com:/MCDC.git``
-#. ``git switch dev``
-#. run install script which will install MC/DC as an editable package from this directory
-
-Push some particles around!!!!
-
-Development Guides
-------------------
-
-.. toctree::
- :maxdepth: 1
-
- documentation/index
- ci
- container-dev
-
-MC/DC documentation is an important part of the project and evolves alongside
-the codebase. The :doc:`documentation/index` guide describes the documentation
-philosophy, writing guidelines, and the tools used to build and maintain the
-documentation.
-
-Please note our `code of conduct `_, which we take seriously.
-
-------------
-Code Styling
-------------
-
-Our code is auto-linted for the `Black code style `_.
-Your contributions will not be merged unless you follow this code style.
-It's pretty easy to do this locally, just run,
-
-.. code-block:: sh
-
-
- pip install black
- black .
-
-
-in the top level MC/DC directory and all necessary changes will be automatically made for you.
-
----------
-Debugging
----------
-
-MCDC includes options to debug the Numba JIT code.
-It does this by toggling Numba options using the numba.config submodule.
-This will result in less performant code and longer compile times but will allow for better error messages from Numba and other packages.
-`See Numba documentation of a list of all possible debug and compiler options. `_
-The most useful set of debug options for MC/DC can be enabled with
-
-.. code-block:: python3
-
- python input.py --mode=numba_debug
-
-Which will toggle the following debug and compiler options in Numba:
-
-* ``DISABLE_JIT=False`` turns on the jitter
-* ``NUMBA_OPT=0`` Forces the compilers to form un-optimized code (other options for this are ``1``, ``2``, and ``3`` with ``3`` being the most optimized). This option might need to be changed if errors only result from more optimization.
-* ``DEBUG=False`` turns on all debugging options. This is still disabled in ``mcdc numba_debug`` as it will print ALOT of info on your terminal screen
-* ``NUMBA_FULL_TRACEBACKS=1`` allows errors from sub-packages to be printed (i.e. Numpy)
-* ``NUMBA_BOUNDSCHECK=1`` numba will check vectors for bounds errors. If this is disabled it bound errors will result in a ``seg_fault``. This in consort with the previous option allows for the exact location of a bound error to be printed from Numpy subroutines
-* ``NUMBA_DEBUG_NRT=1`` enables the `Numba run time (NRT) statistics counter `_ This helps with debugging memory leaks.
-* ``NUMBA_DEBUG_TYPEINFER= 1`` print out debugging information about type inferences that numba might need to make if a function is ill-defined
-* ``NUMBA_ENABLE_PROFILING=1`` enables profiler use
-* ``NUMBA_DUMP_CFG=1`` prints out a control flow diagram
-
-If extra debug options or alteration to these options are required they can be toggled and passed under the ``mode==numba_debug`` option tree in ``mcdc/config.py``.
-
--------
-Caching
--------
-
-MC/DC is a just-in-time (JIT) compiled code.
-This is sometimes disadvantageous, especially for users who might run many versions of the same simulation with slightly different parameters.
-As the JIT compilation scheme will only compile functions that are actually used in a given simulation, it is not a grantee that any one function will be compiled.
-
-Developers should be very cautious about using caching features.
-Numba has a few documented errors around caching.
-The most critical of which is that functions in other files that are called by cached functions will not force a recompile, even if there are changes in those sub-functions.
-In this case caching should be disabled.
-
-In MC/DC the simulation functions (in ``mcdc/transport/simulation.py``) can be configured to use caching.
-Caching behavior is controlled via the ``--caching`` and ``--clear_cache`` command-line flags.
-
-To disable caching, omit the ``--caching`` flag (the default).
-Alternatively a developer could delete the ``__pycache__`` directory or other cache directory which is system dependent (`see more about clearing the numba cache `_)
-
-
-At some point MC/DC will enable `Numba's Ahead of Time compilation abilities `_. But the core development team is holding off until scheduled `upgrades to AOT functionality in Numba are implemented `_.
-However if absolutely required by users numba does allow for some `cache sharing `_.
-
-------------------
-Adding a New Input
-------------------
-
-To add a new keyword argument such that a user can interface with it in an input deck
-there are a few different places a dev will need to make alterations.
-The input objects are defined as dataclasses in the ``mcdc/object_/`` directory:
-
-#. ``mcdc/object_/settings.py`` — simulation settings and k-eigenvalue parameters
-#. ``mcdc/object_/material.py`` — material definitions (``Material``, ``MaterialMG``)
-#. ``mcdc/object_/surface.py`` — surface geometry (``Surface`` class methods)
-#. ``mcdc/object_/cell.py`` — cell definitions (``Cell``)
-#. ``mcdc/object_/source.py`` — source specifications (``Source``)
-#. ``mcdc/object_/tally.py`` — tally objects (``Tally``)
-#. ``mcdc/object_/technique.py`` — variance reduction techniques
-#. ``mcdc/config.py`` — command-line argument definitions
-
--------
-Testing
--------
-
-Check out the :doc:`ci` for more info on how we run these tests automatically
-
-MC/DC has a robust testing suite that your changes must be able to pass before a PR is accepted.
-Unit tests for functions that have them are ran in a pure python from.
-Mostly this is for ensuring input operability
-A regression test suite (including models with analytical and experimental solutions) is provided to ensure accuracy and precision of MC/DC.
-
-Our test suite runs on every PR, and Push.
-Our github based CI runs for,
-
-* linux-64 (x86)
-* osx-64 (x86, intel based macs)
-
-while we do not have continuous integration we have validated MC/DC on other systems.
-
-To run the regression tests locally, navigate to ``MCDC/test/regression`` and run,
-
-.. code-block:: sh
-
-
- python run.py
-
-
-and all the tests will run. Various option ``OPTION_FLAG`` are accepted to control the tests ran,
-
-* Run a specific test (with wildcard ``*`` support): ``--name=``
-* Run in Numba mode: ``--mode=numba``
-* Run in multiple MPI ranks (currently support ``mpiexec`` and ``srun``): ``--mpiexec=``
-
-Note that flags can be combined. To add a new test:
-
-#. Create a folder. The name of the folder will be the test name.
-#. Add the input file. Name it`input.py`.
-#. Add the answer key file. Name it `answer.h5`.
-#. Make sure that the number of particles run is large enough for a good test.
-#. If the test runs longer than 5 seconds, consider decreasing the number of particles.
-
-When adding a new hardware backend a new instantiation of the test suit should be made.
-This is done with github actions.
-See the (``.github/workflows``) for examples.
-
-If a new simulation type is added (e.g. quasi montecarlo w/ davidson's method, residual monte carlo, intrusive uq) more regression tests should be added with your PR.
-If you are wondering accommodations.
-
-
---------------------
-Adding Documentation
---------------------
-
-Documentation is a core part of MC/DC. Contributions that introduce new
-features, modify existing behavior, or change developer workflows should update
-the relevant documentation accordingly.
-
-See the :doc:`documentation/index` guide for documentation philosophy, writing
-guidelines, and instructions for contributing to the documentation.
-
-
--------------
-Pull Requests
--------------
-
-
-MC/DC works off of a fork workflow in which contributors fork our repo, make alterations, and submit a pull requests.
-You should only submit a pull request once your code passes all tests, is properly linted, you have edited documentation (if necessary), and added any new tests (if needed).
-Open a PR to the ``dev`` branch in Github.
-MC/DC's main branch is only updated for version releases at which time a PR from dev to main is opened, tagged, archived, and published automatically.
-
-Within your pull request documentation please list:
-
-#. Type of PR (e.g. enhancement, bugfix, etc);
-#. Link to any theory to understand what you are doing;
-#. Link to any open/closed issues if applicable;
-#. New functionalities implemented
-#. Depreciated functionalities
-#. New dependencies needed (we don't add these lightly)
-#. Anything else we need to give you the thorough code review you deserve!
-
-If these things aren't listed we will ask for clarifying questions!
diff --git a/docs/source/developer_guide/architecture/index.rst b/docs/source/developer_guide/architecture/index.rst
new file mode 100644
index 000000000..f2c2cd9fa
--- /dev/null
+++ b/docs/source/developer_guide/architecture/index.rst
@@ -0,0 +1,152 @@
+.. _architecture:
+
+============
+Architecture
+============
+
+Architecture documentation explains how MC/DC translates flexible Python model definitions into particle-transport execution:
+
+.. image:: ../../images/developer_guide/architecture/architecture_flow.png
+ :width: 100%
+ :class: architecture-flow-figure
+ :alt: MC/DC's architecture flow from model definition through simulation compilation and runtime preparation to shared transport, which runs with Python, Numba-CPU, or Numba-GPU execution modes.
+
+The architecture flow follows a Monte Carlo transport model from definition through one common preparation path, then branches into the Python, Numba-CPU, or Numba-GPU execution modes.
+The figure above emphasizes three major model-to-execution stages:
+
+#. **Simulation compilation** discovers the Python objects owned by a :class:`mcdc.Simulation`, deduplicates them, and assigns simulation-local identifiers.
+ :doc:`simulation_compilation` explains model ownership, discovery, and finalization.
+#. **Runtime preparation** converts the compiled model into the structured ``simulation`` state and flat ``data`` array consumed by transport.
+ :doc:`runtime_data_layout` explains this numerical representation and its generated access helpers, ``mcdc_get`` and ``mcdc_set``.
+#. **Transport execution** runs the common, adaptable transport implementation in the selected execution mode.
+ :doc:`transport_execution` explains how Python, Numba-CPU, and Numba-GPU execute it, including GPU code generation, memory placement, and Harmonize scheduling.
+
+After transport, ``main.run_simulation`` passes the completed runtime state to ``mcdc.output`` for result aggregation and HDF5 serialization.
+This final results-and-output stage completes the calculation lifecycle but remains outside the shared transport implementation shown in the figure.
+
+Visit :doc:`python_first_numba_accelerated_design` for the rationale, boundaries, and tradeoffs that shape MC/DC's architecture.
+It explains why method development begins in unrestricted Python mode and may progress through Numba-CPU to Numba-GPU.
+
+Component Responsibility
+------------------------
+
+The source tree follows the same separation of responsibilities shown in the architecture flow.
+Model-facing components define, collect, and finalize the simulation.
+Code-factory components coordinate object discovery and generate its runtime representation.
+Transport components implement the numerical algorithms shared by all execution modes.
+Output components aggregate and serialize completed results.
+For :ref:`methods development `, ``object_/`` and ``transport/`` form the primary extension surface.
+The ``code_factory/`` package implements the framework-level compilation and generation bridge between them.
+The table maps each component to its corresponding architecture role.
+Paths in the component column are relative to the top-level ``mcdc/`` package.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 28 22 50
+
+ * - Component
+ - Architecture role
+ - Responsibility
+ * - ``object_/``
+ - Model definition and compilation
+ - Defines Python-side model classes and their object-local finalization hooks.
+ * - :class:`mcdc.Simulation` in ``object_/simulation.py``
+ - Model definition and control
+ - Owns model roots and configuration, resolves model-wide finalization, and controls compilation, visualization, and execution.
+ * - ``config.py``
+ - Calculation configuration
+ - Parses command-line controls, configures process-wide execution behavior, and applies supported simulation-setting overrides before compilation.
+ * - ``constant.py``
+ - Shared static definitions
+ - Defines named numerical codes, event flags, numerical limits, and tolerances used across model, transport, and output components.
+ * - ``code_factory/python_objects_compiler.py``
+ - Simulation compilation
+ - Coordinates recursive discovery, registration, and model-wide finalization.
+ * - ``main.run_simulation``
+ - Calculation orchestration
+ - Coordinates runtime preparation, transport execution, result generation, runtime reporting, and backend finalization.
+ * - ``print_.py``
+ - Diagnostics and reporting
+ - Centralizes fatal errors, master-rank messages, calculation progress, and runtime summaries used across the model, transport, and output stages.
+ * - ``main.prepare``
+ - Runtime preparation
+ - Coordinates framework-level packing, execution-resource allocation, backend configuration, and external runtime state.
+ * - ``code_factory/literals_generator.py`` and ``literals.py``
+ - Runtime preparation
+ - Derive and expose simulation-specific values that compiled transport requires as literals.
+ * - ``code_factory/numba_layers_generator.py``
+ - Runtime preparation
+ - Derives structured dtypes, packs runtime state, generates accessors, and initiates GPU-specific preparation when requested.
+ * - Runtime ``simulation`` and ``data``
+ - Prepared runtime data
+ - Store fixed-layout state and variable-length numerical data generated by ``numba_layers_generator.py``.
+ * - ``mcdc_get/`` and ``mcdc_set/``
+ - Runtime data access
+ - Provide generated access to variable-length fields stored in ``data``.
+ * - ``transport/``
+ - Shared transport
+ - Implements the particle-transport algorithms used by every execution mode.
+ * - ``output.py``
+ - Results and output
+ - Aggregates completed tally results and writes settings, tallies, eigenvalue data, saved particles, and runtime measurements to HDF5.
+
+.. rst-class:: architecture-component-followup
+
+The ``mcdc/object_`` modules, :class:`mcdc.Simulation`, and ``python_objects_compiler.py`` implement the model-definition and simulation-compilation stages.
+:doc:`simulation_compilation` explains their relationships, while :doc:`../extending/extending_the_object_model` explains how contributors can extend them.
+
+``main.prepare``, ``numba_layers_generator.py``, runtime ``simulation`` and ``data``, and generated ``mcdc_get`` and ``mcdc_set`` implement framework-level runtime preparation and form the data boundary between model compilation and transport.
+:doc:`runtime_data_layout` explains their roles.
+
+The ``mcdc/transport`` package implements the shared-transport stage.
+:doc:`transport_execution` explains how the execution modes run it.
+:doc:`../extending/writing_numba_compatible_transport_code` provides practical rules for extending its algorithms.
+
+Results and Output
+------------------
+
+After the selected transport driver returns, ``main.run_simulation`` calls ``output.generate_output`` with the prepared ``simulation`` record, flat ``data`` array, and Python :class:`mcdc.Simulation`.
+``output.py`` reads fixed fields directly, uses ``mcdc_get`` for variable-length runtime data, and retains Python-side settings and names where they form part of the output schema.
+
+The module writes the primary HDF5 file, serializes tally and eigenvalue results, optionally saves particles, recombines census-based tally files, and appends runtime measurements.
+It may reshape or aggregate finalized results for storage, but particle tracking and tally scoring remain responsibilities of ``transport/``.
+
+When a new result must persist after a run, define and prepare its runtime storage first, populate it during the appropriate transport or closeout stage, and add only the serialization step to ``output.py``.
+Changes to the user-visible HDF5 structure should also update the corresponding user documentation, regression coverage, and ``CHANGELOG.md`` entry.
+
+Utility Module Scope
+--------------------
+
+``util`` denotes helpers shared within the package that contains the module.
+The complete import path therefore defines the helper's architectural scope.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 34 66
+
+ * - Module
+ - Scope
+ * - ``util.py``
+ - Contains framework-neutral helpers shared across top-level MC/DC packages, currently the nested-list ``flatten`` operation.
+ * - ``object_/util.py``
+ - Supports Python model construction and finalization, including distribution conversion, validation, motion processing, and model-side reference data.
+ * - ``transport/util.py``
+ - Provides Numba-compatible helpers shared across transport domains, including binning, interpolation, atomic updates, local arrays, and backend-neutral simulation access.
+ * - ``transport/physics/util.py``
+ - Contains numerical helpers shared specifically by neutron and electron physics implementations.
+ * - ``code_factory/gpu/transport/util.py``
+ - Implements GPU-compatible replacements and compiler lowering for the adaptable operations exposed by ``transport/util.py``.
+
+Place a new helper in the narrowest package that contains all of its consumers.
+Model-construction helpers may use ordinary Python and NumPy behavior, while helpers reachable from ``transport/`` must follow the compiled-execution constraints described in :doc:`../extending/writing_numba_compatible_transport_code`.
+Hardware-specific replacements belong in the corresponding backend adaptation.
+Promote a helper to a broader ``util.py`` only when multiple sibling components genuinely share it, and avoid treating any utility module as a collection for otherwise unrelated code.
+
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ python_first_numba_accelerated_design
+ simulation_compilation
+ runtime_data_layout
+ transport_execution
diff --git a/docs/source/developer_guide/architecture/python_first_numba_accelerated_design.rst b/docs/source/developer_guide/architecture/python_first_numba_accelerated_design.rst
new file mode 100644
index 000000000..cf4125acb
--- /dev/null
+++ b/docs/source/developer_guide/architecture/python_first_numba_accelerated_design.rst
@@ -0,0 +1,191 @@
+.. _python_first_numba_accelerated_design:
+
+======================================
+Python-First, Numba-Accelerated Design
+======================================
+
+MC/DC is a Python-based environment for developing Monte Carlo transport methods, but large calculations also require compiled performance.
+*Python-first* means that developers can build and validate a method through MC/DC's normal Python execution while using the full Python language and ecosystem.
+They can address the requirements of accelerated execution later.
+Numba provides the bridge between these goals—but what is Numba, and why is it a good fit for MC/DC?
+
+What Is Numba?
+--------------
+
+`Numba `_ is a just-in-time (JIT) compiler for numerical Python.
+It translates compatible Python functions into machine code as a calculation runs, allowing MC/DC to accelerate Python transport algorithms without maintaining a second implementation in a lower-level language.
+
+Numba does not support every feature of Python.
+A prototype in MC/DC Python may use the full Python language and ecosystem, but an accelerated implementation must express the relevant data and behavior in forms that Numba can compile.
+
+MC/DC uses Numba as the bridge between Python development and accelerated transport.
+Its common transport path supports three *execution backends*, or environments that run the transport functions:
+
+- **Python** executes the functions through the interpreter with JIT compilation disabled.
+- **Numba-CPU** compiles the functions into machine code for the host CPU.
+- **Numba-GPU** compiles functions for an accelerator, with Harmonize providing the supporting GPU runtime.
+
+Why MC/DC Uses Numba
+--------------------
+
+Numba lets MC/DC keep its maintained transport algorithms in Python while specializing them for the model and hardware used by a calculation.
+This supports several project goals:
+
+- New transport methods can begin in the language and scientific ecosystem already used to construct MC/DC models.
+- The Python backend supports unrestricted methods development and ordinary Python debugging within MC/DC's normal model lifecycle.
+- Numba-CPU accelerates those functions without requiring a separate implementation in C, C++, or Fortran.
+- Common transport interfaces provide a shared foundation for CPU and GPU execution, with backend-specific adaptation where the hardware requires it.
+
+The choice therefore preserves an expressive starting point while providing a path to compiled execution.
+Those goals introduce the constraints discussed below.
+
+Staged Methods Development
+--------------------------
+
+MC/DC Python: Prototyping and Validation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A new method can be developed through MC/DC's Python backend.
+It still follows the normal MC/DC path: the user defines a valid model, MC/DC prepares the complete model and its numerical execution data, and transport runs through the Python interpreter with JIT compilation disabled.
+The preparation process is described in :doc:`simulation_compilation` and :doc:`runtime_data_layout`.
+
+Within those minimum requirements, prototype transport code may use arbitrary Python objects, module-level global state, manual imports, dynamic dispatch, callbacks, hard-coded assumptions, or direct edits to transport routines.
+It may call packages such as SciPy or Matplotlib during transport, perform file I/O, or visualize intermediate state.
+It may also reach beyond MC/DC's prepared transport data through ordinary Python mechanisms.
+These choices are acceptable because this stage prioritizes scientific expressiveness and rapid validation over performance, encapsulation, dependency discipline, and compiler compatibility.
+
+No special prototype framework or service is required.
+Researchers may use the most direct implementation within MC/DC Python that answers the question being studied.
+Prototype-only packages do not need to become MC/DC dependencies, and temporary implementation choices are not expected to remain in their original form.
+
+This freedom is permission, not a required style.
+A developer familiar with MC/DC and Numba may begin with an implementation already close to the compiled form, reducing later porting work without making compiler compatibility a requirement for initial exploration.
+Practical guidance is provided in :doc:`../extending/writing_numba_compatible_transport_code`.
+
+Numba-CPU Compilation
+^^^^^^^^^^^^^^^^^^^^^
+
+Once the method is verified and larger calculations require more performance, it can be adapted for Numba-CPU.
+Unless the Python implementation was already written against the compatible subset, reaching this stage requires deliberate porting rather than simply changing an execution option.
+See :doc:`transport_execution` for the execution architecture and :doc:`../extending/writing_numba_compatible_transport_code` for the practical porting requirements.
+
+Numba-GPU Compilation
+^^^^^^^^^^^^^^^^^^^^^
+
+GPU execution is a further stage with additional hardware constraints.
+Some code that works with Numba-CPU therefore requires further adaptation before it can execute on a GPU.
+These additional layers are described in :doc:`transport_execution`.
+
+Valid Stopping Points
+^^^^^^^^^^^^^^^^^^^^^
+
+Each stage can be a useful endpoint:
+
+- **MC/DC Python** is sufficient for prototyping, debugging, and small calculations.
+ Prototype code may use unrestricted Python where needed.
+- **Numba-CPU** is sufficient when compiled CPU performance meets the calculation's needs.
+- **Numba-GPU** provides the final portability stage when accelerator performance is required.
+
+This staged workflow allows researchers to stop when their immediate objective has been achieved.
+A general feature intended to be merged and maintained as part of MC/DC, however, must work in MC/DC Python, Numba-CPU, and Numba-GPU unless the maintainers explicitly accept and document a backend-specific limitation.
+
+The Standard Execution Path
+---------------------------
+
+Every backend follows MC/DC's standard model and execution path.
+What changes between development stages is how transport runs and which Python features it may use:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 24 38 38
+
+ * - Stage
+ - Representation
+ - Execution environment
+ * - Model definition
+ - Flexible Python objects and relationships
+ - Python
+ * - Model compilation
+ - Complete, internally consistent model snapshot
+ - Python
+ * - Runtime preparation
+ - Compact numerical execution data
+ - Python and NumPy
+ * - Transport
+ - Shared numerical algorithms
+ - Python, Numba-CPU, or Numba-GPU
+ * - Output and postprocessing
+ - HDF5 output and analysis objects
+ - Python
+
+Model construction and preparation may use object-oriented interfaces, variable-length collections, validation, and other expressive Python features.
+Transport in the Python backend may also use ordinary Python facilities beyond the prepared execution data.
+The accelerated backends require predictable numerical data and explicit behavior.
+See :doc:`simulation_compilation` and :doc:`runtime_data_layout` for the two preparation stages, then :doc:`transport_execution` for execution.
+
+Architectural Consequences
+--------------------------
+
+Preparing a method for acceleration narrows the freedom available inside transport and has three broad architectural consequences.
+
+**Flexible models become predictable execution data.**
+Python objects remain the natural way to describe a problem, but accelerated transport operates on a numerical representation prepared for one simulation.
+MC/DC therefore replaces Python's implicit object machinery with a purpose-built runtime object model based on structured records, simulation-local IDs, offsets, and generated accessors.
+Mutable runtime records are passed in one-element array containers, providing stable shared storage across Python, Numba-CPU, and Numba-GPU execution.
+See :doc:`simulation_compilation` and :doc:`runtime_data_layout` for this transformation.
+
+**Transport behavior becomes explicit.**
+Dynamic Python techniques that are useful during prototyping must be expressed in forms the compiler can understand when the method is accelerated.
+Use :doc:`../extending/writing_numba_compatible_transport_code` and :doc:`../extending/extending_the_object_model` when implementing these changes.
+
+**CPU and GPU share a portable core.**
+Common transport algorithms use behavior supported by both accelerated targets, while hardware-specific concerns remain isolated.
+The execution modes are described in :doc:`transport_execution`.
+
+Design Tradeoffs
+----------------
+
+The staged approach does not eliminate development cost; it defers some of that cost until a method has demonstrated enough value to justify acceleration and long-term maintenance.
+
+Prototyping through MC/DC Python maximizes scientific expressiveness and minimizes the effort required to test an idea.
+The tradeoff is that parts of the Python implementation may need to be redesigned or rewritten for Numba-CPU.
+Beginning with simple numerical data and operations that Numba supports can reduce that porting effort, but introduces implementation constraints earlier in the research process.
+
+Portability also increases compiler, backend, and validation work.
+MC/DC accepts these costs so that maintained features can share transport logic across Python, CPU, and GPU execution rather than becoming separate implementations that may drift apart.
+Use :doc:`../extending/writing_numba_compatible_transport_code` for practical constraints and :doc:`transport_execution` for mode-specific execution mechanisms.
+
+Related Design Choices
+----------------------
+
+Alternative designs place the boundary between expressiveness and portability elsewhere.
+Requiring Numba compatibility from the beginning would reduce later porting but constrain early exploration.
+Maintaining separate Python and optimized solvers would preserve unrestricted Python behavior but duplicate transport logic.
+Handwritten kernels would provide more low-level control but split methods development across languages.
+
+MC/DC instead uses staged convergence within its standard execution path.
+A method in MC/DC Python may range from an ad hoc implementation to one that is already nearly Numba-compatible.
+When the method needs compiled performance or becomes a maintained MC/DC capability, it converges on common numerical data and transport interfaces.
+This preserves a shared execution path without dictating how the Python implementation must begin.
+The data boundary is described in :doc:`runtime_data_layout`; contributor guidance is provided in :doc:`../extending/writing_numba_compatible_transport_code`.
+
+Future Evolution
+----------------
+
+The Python-first principle does not require the present implementation to remain fixed.
+Future work should make movement between stages easier without narrowing the freedom of prototyping in MC/DC Python.
+
+Potential improvements fall into three areas:
+
+- **Python-mode flexibility** -- make the active Python model and arbitrary development state easier to reach during Python execution without requiring a formal prototype framework.
+- **Porting assistance** -- provide better tools for inspecting a Python implementation and identifying code that an accelerated backend cannot run.
+- **Execution infrastructure** -- make the creation and ownership of prepared data clearer, and keep hardware-specific behavior separated from the common transport algorithms.
+
+Future changes should preserve the ability to stop at any development stage, keep the transition from an MC/DC-Python-only implementation to a portable maintained implementation explicit and reviewable, and retain equivalent physical behavior across the supported backends of a maintained feature.
+
+Where to Go Next
+----------------
+
+For model compilation and runtime preparation, continue with :doc:`simulation_compilation` and :doc:`runtime_data_layout`.
+For execution, read :doc:`transport_execution`.
+Contributors implementing a transport change should use :doc:`../extending/writing_numba_compatible_transport_code`.
diff --git a/docs/source/developer_guide/architecture/runtime_data_layout.rst b/docs/source/developer_guide/architecture/runtime_data_layout.rst
new file mode 100644
index 000000000..01f5b331c
--- /dev/null
+++ b/docs/source/developer_guide/architecture/runtime_data_layout.rst
@@ -0,0 +1,247 @@
+.. _runtime_data_layout:
+
+===================
+Runtime Data Layout
+===================
+
+After model compilation has discovered, finalized, and ordered the Python object graph, ``mcdc.main.prepare`` calls ``generate_numba_layers`` in ``mcdc.code_factory`` to create the runtime representation used by transport.
+The representation has two complementary parts:
+
+``simulation``
+ A fixed-layout NumPy structured record containing scalar state, embedded records, typed object collections, fixed-size arrays, and metadata.
+
+``data``
+ A contiguous one-dimensional NumPy array containing variable-length numerical payloads and lists of object IDs.
+
+The same logical representation is supplied to Python, Numba-CPU, and Numba-GPU execution.
+Portable transport code expresses its state through this prepared representation.
+See :doc:`python_first_numba_accelerated_design` for this development model.
+
+Why Two Structures?
+-------------------
+
+Python model objects may contain arrays whose sizes depend on the problem: energy grids, cross sections, mesh boundaries, motion tables, tally filters, and many others.
+Nested Python object references and variable-sized arrays cannot be embedded directly in the stable NumPy structured dtype required by the Numba execution modes.
+Separating fixed-layout metadata from variable-length values keeps the structured dtype stable while accommodating model-dependent payloads.
+
+Runtime Object Model
+--------------------
+
+Fixed fields become structured-record fields, Python object references become simulation-local IDs, and variable-length fields become offsets into ``data``.
+Generated ``mcdc_get`` and ``mcdc_set`` accessors perform the corresponding lookups and offset calculations.
+
+.. image:: ../../images/developer_guide/architecture/runtime_data_layout.png
+ :width: 100%
+ :alt: Runtime preparation turns a cell, its three boundary surfaces, and a surface-crossing tally into structured records whose metadata points into a flat data array through generated accessors.
+
+The figure follows one connected example from Python model objects into the two runtime layers.
+The cell record locates its three surface IDs in ``data``, the selected surface record locates an attached tally ID, and the base tally record identifies its concrete surface-crossing record.
+Tally scores, bins, and other variable-length payloads share the same flat arena.
+The IDs and offsets shown in the figure are illustrative; their values depend on the compiled model and its packed layout.
+
+The ``data`` arena uses ``float64`` values, one allocation, and one offset space across execution modes.
+Integer values stored in the arena, including object IDs, are restored to their declared types by generated scalar accessors.
+
+Object Collections
+^^^^^^^^^^^^^^^^^^
+
+Registered model objects are stored in collections on ``simulation``.
+The figure demonstrates both forms of runtime collection: direct indexing for non-polymorphic objects and base-to-concrete dispatch for polymorphic objects.
+
+For a non-polymorphic category, an object's simulation-local ID directly indexes its collection.
+A particle's current cell and one of its boundary surfaces are therefore retrieved with:
+
+.. code-block:: python
+
+ cell = simulation["cells"][particle["cell_ID"]]
+ surface_ID = mcdc_get.cell.surface_IDs(0, cell, data)
+ surface = simulation["surfaces"][surface_ID]
+
+A polymorphic category has both a common base collection and a collection for each concrete representation.
+A surface stores the IDs of the surface-crossing tallies attached to it.
+Each ID first selects a base tally record, whose ``sub_type`` and ``sub_ID`` identify the concrete surface-crossing record:
+
+.. code-block:: python
+
+ from mcdc.constant import TALLY_SURFACE_CROSSING
+
+
+ tally_ID = mcdc_get.surface.surface_crossing_tally_IDs(0, surface, data)
+ tally = simulation["tallies"][tally_ID]
+
+ tally["sub_type"] == TALLY_SURFACE_CROSSING # True
+
+ surface_crossing_tally = simulation["surface_crossing_tallies"][tally["sub_ID"]]
+
+The concrete tally record retains ``surface_filter_ID`` and ``cell_filter_ID``, connecting it back to the selected surface and cell.
+All IDs are assigned during model compilation and identify objects only within the current simulation snapshot.
+The hierarchy and ID assignment are described in :doc:`simulation_compilation`.
+
+Variable-Length Fields
+^^^^^^^^^^^^^^^^^^^^^^
+
+Using the illustrative values in the figure, the cell and selected surface records store metadata equivalent to:
+
+.. code-block:: text
+
+ cell.surface_IDs_offset = 0
+ cell.N_surface = 3
+
+ surface.surface_crossing_tally_IDs_offset = 3
+ surface.N_surface_crossing_tally = 1
+
+The corresponding ID lists occupy adjacent regions of ``data`` in the simplified layout:
+
+.. code-block:: text
+
+ data[0:3] = [0, 1, 2] # Cell's surface IDs
+ data[3:4] = [0] # Surface's tally ID
+
+Array Shapes and Generated Access
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Array shape annotations determine whether an array is embedded in a structured record or stored in ``data``.
+For example, ``Cell.translation`` has a completely fixed shape:
+
+.. code-block:: python
+
+ translation: Annotated[NDArray[float64], (3,)]
+
+Because every dimension is an integer literal, the three-component array is embedded directly in the cell record and accessed through ``cell["translation"]``.
+
+``Surface.move_velocities`` combines a model-dependent dimension with a fixed trailing dimension:
+
+.. code-block:: python
+
+ move_velocities: Annotated[NDArray[float64], ("N_move", 3)]
+
+The symbolic dimension ``N_move`` names the surface record field that supplies its size at runtime.
+Because that dimension depends on the model, the array is flattened into ``data``, and the surface record stores its offset and total length.
+Fully symbolic multidimensional arrays use the same mechanism.
+For example, ``NeutronMultigroupData.nu_d`` is shaped ``("G", "J")`` and uses the
+named energy-group and delayed-neutron-group dimensions to reconstruct indexing
+into its flattened payload.
+Generated accessors for arrays stored in ``data`` currently support array ranks from one through four dimensions.
+
+``mcdc.code_factory`` generates modules under ``mcdc/mcdc_get`` and ``mcdc/mcdc_set`` for fields stored in ``data``.
+These helpers hide offset and stride arithmetic from transport code and remain callable from both Python and Numba-compiled functions.
+
+Conceptually, a generated element getter for the cell's surface IDs performs:
+
+.. code-block:: python
+
+ from numpy import int64
+
+
+ def surface_IDs(index, cell, data):
+ offset = cell["surface_IDs_offset"]
+ return int64(data[offset + index])
+
+Transport code can therefore express logical access without depending on where the values reside in ``data``:
+
+.. code-block:: python
+
+ surface_ID = mcdc_get.cell.surface_IDs(index, cell, data)
+
+For ``move_velocities``, the generated accessor uses the fixed trailing dimension as the row stride and reconstructs logical two-dimensional indexing:
+
+.. code-block:: python
+
+ velocity = mcdc_get.surface.move_velocities(
+ move_index, component_index, surface, data
+ )
+
+For ``NeutronMultigroupData.nu_d``, the generated element getter reads the named
+trailing dimension ``J`` from the neutron-model record and uses it as the
+runtime row stride:
+
+.. code-block:: python
+
+ def nu_d(group, delayed_group, neutron_multigroup, data):
+ offset = neutron_multigroup["nu_d_offset"]
+ stride = neutron_multigroup["J"]
+ return data[offset + group * stride + delayed_group]
+
+In the row-major flattened layout, ``J`` determines the stride between energy groups, while ``G`` determines the number of rows.
+Generated helpers also provide operations for complete arrays, final elements, chunks, vectors, and multidimensional elements as appropriate.
+Scalar getters restore the integer type declared by an annotation or implied by an object-ID list.
+Bulk getters continue to return zero-copy ``float64`` views into ``data`` and therefore require explicit conversion when an integer array is needed outside transport.
+
+Deriving the Layout
+-------------------
+
+Classes in ``mcdc/object_`` declare their runtime-visible fields with Python type annotations.
+``generate_numba_layers`` collects those annotations and maps them to runtime fields:
+
+- Scalars become scalar structured fields.
+- Fixed-shape annotated arrays are embedded in structured records.
+- Variable-length arrays become ``_offset`` and ``_length`` metadata plus values in ``data``.
+- Object references become ``_ID`` fields.
+- Lists of object references become ``N_`` and ``_IDs_offset`` metadata plus IDs in ``data``.
+- Members named in a class's ``non_numba`` list are excluded from the packed representation or handled specially.
+
+Packing is performed in two passes:
+
+#. Walk the compiled objects to build records and calculate the required ``data`` size.
+#. Allocate ``data`` and walk the objects again to copy flattened payloads into their assigned regions.
+
+The structured ``simulation`` dtype can then be finalized because collection sizes, particle-bank sizes, and nested record types are known.
+
+.. _simulation_specific_literals:
+
+Static Constants and Simulation-Specific Literals
+-------------------------------------------------
+
+MC/DC distinguishes static implementation constants from values derived for one prepared simulation.
+``mcdc.constant`` defines stable codes, event flags, numerical limits, and tolerances that have the same meaning for every simulation.
+These values can be imported directly by the model, transport, and output layers.
+
+Some compiled operations instead require a simulation-dependent value to be known as a Numba literal.
+During ``mcdc.main.prepare``, ``make_literals`` in ``mcdc.code_factory.literals_generator`` derives those values from the compiled Python model and replaces the placeholder functions in ``mcdc.literals`` with JIT-compatible implementations that return them.
+
+The current example is the work-array size used to evaluate cell-region reverse Polish notation.
+The generator finds the largest required evaluation buffer in the compiled model, and geometry transport obtains that value through:
+
+.. code-block:: python
+
+ value = util.local_array(
+ literals.rpn_evaluation_buffer_size(),
+ np.bool_,
+ )
+
+Use a generated literal only for a single simulation-wide value that compiled code must treat as fixed during the prepared run.
+Store values that vary by object or particle in the structured ``simulation`` state or ``data`` instead.
+Because literals are derived from a particular model snapshot, runtime preparation regenerates them whenever that simulation is prepared again.
+
+The One-element Container
+-------------------------
+
+The generated simulation record is stored in a one-element NumPy array:
+
+.. code-block:: python
+
+ simulation_container, data = prepare(simulation_python)
+ simulation = simulation_container[0]
+
+The container gives Python, Numba, MPI, and GPU paths a consistent mutable reference to the structured state.
+Transport drivers receive the container and ``data``; individual kernels generally operate on the record or its nested objects.
+
+Transport Consumption
+---------------------
+
+Portable transport functions shared across the execution backends consume the runtime representation and primitive transport records.
+They use:
+
+- Direct structured-field access for fixed-size values and metadata.
+- Base and subtype IDs to navigate registered objects.
+- ``mcdc_get`` and ``mcdc_set`` for variable-length values.
+- The same function signatures in Python and Numba-CPU modes.
+
+The layout is fixed for the duration of a prepared run.
+Transport may update allocated values, tally bins, particle banks, and runtime counters, but it cannot resize a field or introduce a new model object.
+Changing the prepared MC/DC model requires a new model compilation and runtime preparation pass.
+
+Execution Backends
+------------------
+
+Continue with :doc:`transport_execution` to see how Python, Numba-CPU, and Numba-GPU consume this shared layout.
diff --git a/docs/source/developer_guide/architecture/simulation_compilation.rst b/docs/source/developer_guide/architecture/simulation_compilation.rst
new file mode 100644
index 000000000..f7886a818
--- /dev/null
+++ b/docs/source/developer_guide/architecture/simulation_compilation.rst
@@ -0,0 +1,299 @@
+.. _simulation_compilation:
+
+======================
+Simulation Compilation
+======================
+
+MC/DC begins with normal Python objects.
+Materials, surfaces, cells, sources, tallies, settings, and techniques can contain nested references and arbitrary-sized arrays.
+A :class:`mcdc.Simulation` owns the roots of one model and turns that connected object graph into a deterministic, simulation-local snapshot.
+
+This first compilation stage is entirely a Python operation.
+It discovers the model, finalizes object-local and model-wide state, and establishes the snapshot that runtime preparation will pack for Python, Numba-CPU, or Numba-GPU execution.
+
+Why a Simulation Context Is Needed
+----------------------------------
+
+A Monte Carlo transport model is a connected system rather than a collection of independent definitions.
+A cell is not complete without its bounding surfaces and fill; a material may depend on nuclides, elements, reactions, and their data; and sources and tallies may refer to distributions, meshes, or geometry objects.
+The same object may also be shared by several parts of the model.
+
+These relationships are assembled incrementally with ordinary Python references.
+While the model is being built, an individual object cannot know its final position among all objects of the same kind, whether another branch of the model will refer to it, or which subtype-specific collection will contain it.
+Finalizing each object when it is created would therefore make model construction depend on definition order and require process-wide registration.
+
+:class:`mcdc.Simulation` provides the owning context for this delayed finalization.
+After the model cells, sources, tallies, settings, and techniques have been configured, compilation can inspect the complete reachable object graph at once.
+It registers shared objects only once, resolves relationships into simulation-local identifiers, and produces a consistent snapshot for runtime preparation.
+
+Here, *context* means the explicit model scope owned by a ``Simulation`` instance, not a Python ``with`` context manager.
+
+Terminology
+-----------
+
+MC/DC uses several related forms of compilation:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 24 38 38
+
+ * - Term
+ - Responsibility
+ - Primary implementation
+ * - Model compilation
+ - Discover objects, register them once, assign local IDs, and finalize model-dependent state.
+ - ``mcdc/object_/``, ``Simulation.compile``, and ``mcdc/code_factory/python_objects_compiler.py``
+ * - Runtime preparation
+ - Pack the finalized model, allocate execution resources, and configure the selected backend.
+ - ``mcdc.main.prepare`` and ``mcdc/code_factory/numba_layers_generator.py``
+ * - Backend compilation
+ - Compile shared transport functions for a CPU or GPU target.
+ - Numba and, for GPUs, Harmonize
+
+Unless otherwise qualified, this page uses *compilation* to mean model compilation.
+
+Configuration Boundary
+----------------------
+
+``mcdc.config`` connects command-line execution controls to the simulation lifecycle.
+When imported, it parses the command-line arguments known to MC/DC, establishes process-wide choices such as Python or Numba mode, CPU or GPU target, caching behavior, and GPU execution options, then configures Numba and generated-code caches accordingly.
+Unknown arguments are retained rather than rejected so MC/DC can run under notebooks, test runners, and higher-level Python drivers.
+
+Simulation settings remain owned by :class:`mcdc.Simulation` and its embedded ``Settings`` object.
+At the beginning of ``Simulation.compile``, ``config.override_settings`` applies the supported command-line overrides before object discovery and model-wide finalization.
+Consequently, derived state such as particle-bank capacities is calculated from the effective settings that will be used for the run.
+
+This separation distinguishes process-wide execution configuration from simulation-owned model configuration.
+A new physical or numerical setting should normally be added to ``Settings`` or another simulation-owned object.
+Add a ``config.py`` option only when that setting also needs a command-line override or when the choice controls the execution framework itself.
+
+Ownership and Roots
+-------------------
+
+Every calculation begins with an explicit simulation:
+
+.. code-block:: python
+
+ simulation = mcdc.Simulation("Shielding")
+ simulation.set_model([source_cell, shield_cell])
+ simulation.set_sources([source])
+ simulation.set_tallies([flux_tally])
+
+The setter calls identify the roots of the user model:
+
+- ``set_model`` places cells in the simulation's root universe.
+- ``set_sources`` retains the sources sampled by transport.
+- ``set_tallies`` retains the requested scoring definitions.
+- Settings and transport techniques are embedded objects already owned by the simulation.
+
+There is no process-wide model singleton.
+Each ``Simulation`` owns the model objects reachable from its roots or embedded configuration.
+
+Ownership Boundary and Process Model
+------------------------------------
+
+MC/DC intentionally uses a serial-in-process execution model: one ``Simulation`` is compiled, prepared, or executed at a time within a Python process.
+A model-object instance belongs to one simulation context.
+Repeated in-process calculations should update and recompile the same ``Simulation``.
+See :ref:`example_iterative_source_reweighting` for a complete iterative simulation and result-comparison example.
+
+References may be shared freely within that context.
+For example, several cells may use the same material, and compilation will register that material once.
+The same material instance must not be attached to a second ``Simulation``.
+Compilation metadata such as ``compile_ID``, ``ID``, and ``sub_ID`` is stored on the Python object and describes only its owning simulation's current snapshot.
+
+This ownership rule keeps ordinary model construction and recursive compilation lightweight.
+Applications that need concurrent calculations should create an independent model graph in each process and manage those processes from an outer Python driver, workflow system, or batch scheduler.
+Process-level orchestration is the supported parallelism boundary for multiple independent simulations.
+
+The MC/DC Object Hierarchy
+--------------------------
+
+The classes in ``mcdc/object_/base.py`` give every part of the model a common compilation lifecycle:
+
+.. code-block:: text
+
+ MCDCBase
+ └── MCDCObject
+ └── MCDCPolymorphic
+
+The distinction between these classes is about identity in a compiled simulation.
+All three can contribute fields to the packed runtime layout, but only ``MCDCObject`` and ``MCDCPolymorphic`` instances are registered as independently addressable model entities.
+
+``MCDCBase``: Embedded State and Traversal
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``MCDCBase`` is the foundation for Python-side model and runtime definitions.
+Each subclass declares a ``label`` and type-annotated fields.
+The annotations serve two related purposes:
+
+- Assignment validation catches incompatible model values during construction.
+- The Numba-layer generator uses the annotations to derive structured fields, object references, and variable-length data access.
+
+An ``MCDCBase`` instance participates in recursive compilation and carries a ``compile_ID``, but it does not receive an object ``ID`` or occupy a simulation-level registry.
+This behavior is appropriate for state that belongs to a parent rather than representing a separately addressable model entity.
+Examples include ``Simulation``, ``Settings``, transport-technique configuration, particle-bank metadata, and GPU metadata.
+``Simulation`` is the special root of this hierarchy: it starts compilation and owns the resulting registries instead of being registered in one.
+
+The default compilation method visits object-valued members and lists of members recursively.
+Fields named in ``non_numba`` are excluded from this default traversal and packed representation; the owning class handles them explicitly when special processing is required.
+
+``MCDCObject``: Registered Model Entities
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``MCDCObject`` extends ``MCDCBase`` for entities that must be collected and referenced by transport.
+Examples include cells, surfaces, universes, lattices, sources, nuclides, and elements.
+
+When compilation reaches an ``MCDCObject``, ``register_object`` places it in the corresponding collection owned by the current ``Simulation`` and assigns its simulation-local ``ID``.
+Registration happens before the object's members are traversed.
+Consequently, another reference to the same Python instance can recognize that it is already registered, which both preserves sharing and terminates cycles.
+
+An ``ID`` identifies object position within one compiled simulation; it is not a permanent identity belonging to the Python object.
+Recompiling its owning simulation may assign a different ``ID``.
+Attaching the same object instance to another simulation is outside the ownership model.
+
+``MCDCPolymorphic``: Base and Concrete Representations
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``MCDCPolymorphic`` extends ``MCDCObject`` for categories with multiple runtime representations.
+Meshes, distributions, tallies, and neutron and electron reactions use this pattern.
+For example, ``Tally`` defines the shared tally category, while ``TallySurfaceCrossing``, ``TallyCollision``, and ``TallyTracklength`` provide estimator-specific representations.
+
+Each concrete polymorphic class declares a ``sub_type`` code.
+During registration, an instance receives two positions:
+
+- ``ID`` locates its base record in the heterogeneous category collection.
+- ``sub_ID`` locates its concrete record among objects with the same ``sub_type``.
+
+By convention, the shared category class uses ``sub_type = -1`` and concrete subclasses use named integer constants.
+Their distinct ``label`` values also name the corresponding structured layouts and generated accessor modules.
+
+The packed base record stores ``sub_type`` and ``sub_ID``; the concrete record stores ``base_ID``.
+Transport can therefore move between the common category view and the subtype-specific data without retaining Python references or depending on Python dynamic dispatch.
+
+For the implementation steps required to add a field, registered category, or polymorphic subtype, see :doc:`../extending/extending_the_object_model`.
+
+Recursive Discovery
+-------------------
+
+``Simulation.compile`` assigns a new ``compile_ID``, clears the previous registries, and walks the object graph:
+
+#. Reserved ``None`` representations are registered.
+#. The root universe recursively reaches cells, regions, surfaces, fills, universes, lattices, materials, nuclear data, and distributions.
+#. Sources and tallies are traversed from their explicit root lists.
+#. Settings, techniques, and their members are traversed from the simulation.
+
+The common lifecycle defined by the object hierarchy makes this traversal uniform: embedded ``MCDCBase`` members are visited in place, while ``MCDCObject`` members are registered before their descendants are explored.
+
+Discovery follows actual Python references.
+For example, a root cell reaches its region, the region reaches its surfaces, and the cell's fill reaches its material or child universe.
+Users therefore attach root cells rather than manually registering every referenced object.
+
+Object and Model Finalization
+-----------------------------
+
+Discovery and finalization occur within the same model-compilation snapshot.
+An object's ``_compile_into_simulation`` hook handles work owned by that object, including canonicalizing Python inputs, compiling excluded references, and deriving fields from assigned IDs.
+
+Some values require the complete discovered model rather than one object.
+After the explicit model roots have been traversed, ``Simulation._finalize_compilation`` resolves these model-wide relationships and invariants, such as completing material data, normalizing source probabilities, adapting tally shapes, deriving particle-bank capacities, and initializing state from the final settings and MPI decomposition.
+The compiler traverses embedded simulation configuration before model-wide finalization, and finalization logic explicitly compiles any runtime-visible dependencies it creates.
+
+This boundary keeps scientific model rules in ``mcdc/object_/``.
+The orchestration in ``python_objects_compiler.py`` changes only when the compilation framework gains a new phase or registered category, while ``mcdc.main.prepare`` remains responsible for framework-level runtime setup after the model is complete.
+
+Neutron Multigroup Finalization
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+For neutron multigroup transport, model-wide finalization selects standard transport only when every material has multigroup data without native composition and all multigroup energy grids are identical.
+An omitted grid is represented internally by a zero-valued ``G + 1`` placeholder, allowing materials that all omit the grid to share one standard multigroup structure.
+All other material combinations select hybrid transport, for which each multigroup dataset requires an explicit physical energy grid.
+Standard transport interprets the particle's ``E`` field as a dimensionless group coordinate, while hybrid transport interprets it as physical energy in eV and maps it through ``energy_grid`` and ``energy_representation``.
+Finalization also validates standard-multigroup source energies and resolves ``energy="all"`` tally filters.
+
+Deduplication and Cycles
+------------------------
+
+An object's ``compile_ID`` records the compilation snapshot in which it most recently participated.
+When the traversal reaches the same object again during that snapshot, registration stops for that branch.
+
+This behavior has two purposes:
+
+- A material, surface, mesh, or other object shared in several places is registered once.
+- Cycles among embedded configuration objects do not cause infinite recursion.
+
+Deduplication is based on Python object identity within one compilation, not on equal field values.
+Two separately constructed materials with identical cross sections remain two objects unless a class performs its own canonicalization.
+
+Simulation-local IDs
+--------------------
+
+Registered objects receive identifiers that are meaningful only within the current compiled simulation:
+
+``ID``
+ Position in the corresponding simulation collection, such as all surfaces or all materials.
+
+``sub_type``
+ Integer code identifying the concrete representation of a polymorphic object, such as a surface-crossing, collision, or track-length tally.
+
+``sub_ID``
+ Position in the collection for that concrete subtype.
+
+The packed child record also carries ``base_ID`` so transport can move between the subtype-specific and base representations.
+
+For example, transport can inspect a tally base record's ``sub_type`` and ``sub_ID`` and then select its estimator-specific record.
+These IDs replace Python object references in the runtime layer.
+
+Snapshot Lifecycle
+------------------
+
+``set_model``, ``set_sources``, and ``set_tallies`` invalidate the compiled state.
+``run`` and ``visualize_model`` call ``compile`` automatically when the simulation is not compiled, so most user inputs do not need an explicit call.
+Command-line overrides are applied before this compilation so model-wide derived state reflects the effective settings.
+
+An explicit call is useful for inspecting discovered objects and assigned IDs:
+
+.. code-block:: python
+
+ simulation.compile()
+
+ print(simulation.materials)
+ print(source_cell.ID)
+ print(source_region_material.ID)
+
+IDs can change whenever the simulation is compiled again.
+They must not be stored as durable identifiers outside the current snapshot.
+
+Directly mutating an already attached object does not notify its owning simulation.
+After such a change to an explicitly compiled or visualized model, call ``simulation.compile()`` again before inspecting or visualizing the new snapshot.
+``simulation.run()`` marks the snapshot uncompiled after execution, so a subsequent run recompiles it.
+
+Iterative Partial Updates
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+An iterative study can keep one simulation and change only part of its owned model between runs.
+For example, two sources can be reweighted while the geometry, materials, and tallies remain unchanged:
+
+.. code-block:: python
+
+ simulation.set_sources([source_left, source_right])
+
+ for iteration, left_fraction in enumerate([0.2, 0.5, 0.8]):
+ source_left.probability = left_fraction
+ source_right.probability = 1.0 - left_fraction
+ simulation.settings.output_name = f"source_mix_{iteration}"
+
+ simulation.compile()
+ simulation.run()
+
+Each call to ``compile`` assigns a new ``compile_ID`` and rebuilds a complete, consistent snapshot even though only the source probabilities changed.
+The objects remain owned by the same ``Simulation`` throughout the study.
+
+From Objects to Runtime Data
+----------------------------
+
+Model compilation finalizes the original Python objects and populates the simulation's ordered registries.
+Runtime preparation then reads that complete snapshot, derives structured dtypes from its annotations, and packs its values while allocating framework-owned execution resources.
+
+Continue with :doc:`runtime_data_layout` for that conversion.
+For the user-facing construct-to-output workflow, see :doc:`../../user_guide/simulation_lifecycle`.
diff --git a/docs/source/theory/gpu.rst b/docs/source/developer_guide/architecture/transport_execution.rst
similarity index 56%
rename from docs/source/theory/gpu.rst
rename to docs/source/developer_guide/architecture/transport_execution.rst
index d3c8db863..280d9c782 100644
--- a/docs/source/theory/gpu.rst
+++ b/docs/source/developer_guide/architecture/transport_execution.rst
@@ -1,46 +1,64 @@
-.. _theory_gpu:
+.. _transport_execution:
-=================
-GPU Functionality
-=================
+===================
+Transport Execution
+===================
+
+MC/DC uses one adaptable transport implementation with the runtime representation described in :doc:`runtime_data_layout`.
+The selected execution mode determines how that implementation runs after the common model-preparation stages.
+See :doc:`python_first_numba_accelerated_design` for the rationale behind this design.
+
+Execution Modes
+---------------
+
+In **Python mode**, MC/DC disables Numba just-in-time (JIT) compilation, and functions decorated with ``@njit`` execute as ordinary Python functions.
+This mode provides the most inspectable execution of the shared transport implementation.
+
+In **Numba-CPU mode**, Numba specializes those transport functions for the prepared runtime types and compiles them into machine code for the host CPU.
+The first call includes compilation work, while subsequent calls use the compiled functions.
+
+In **Numba-GPU mode**, MC/DC adapts the transport functions for device execution, places runtime state in GPU-accessible memory, and uses Harmonize to schedule particle work.
+The remaining sections describe this additional GPU-specific compilation machinery.
+
+Use :doc:`../extending/writing_numba_compatible_transport_code` for contributor constraints, porting guidance, and staged verification.
+Use the :doc:`../../user_guide/execution/index` for operational commands.
GPU Compilation
---------------
-When targeting GPUs, MC/DC functions are just-in-time (JIT) compiled with Harmonize.
-To JIT compile and execute on AMD or Nvidia GPUs, MC/DC users need only to append their terminal launches with a ``--target=gpu`` option.
-When considered in totality the MC/DC+Numba+Harmonize JIT compilation structure is akin to "portability framework", in that it allows dynamic targeting and developer abstraction of hardware architectures, like OpenMP target-offloading used by OpenMC.
-This JIT compilation process allows MC/DC to pair the idea of a portability framework with a high-level language in an effort to enable more rapid methods development on Exascale systems.
+When targeting GPUs, MC/DC functions are just-in-time (JIT) compiled with Numba and integrated with Harmonize.
-Monte Carlo transport functions from MC/DC are treated as device functions with global, host, and additional device functions coming from Harmonize.
-Mixing codes from various sources (Python and C++) requires the user to provide an *exacting* set of compiler options to achieve an operable executable.
-We provide in-depth descriptions of these sets of commands as we found the definition of this JIT compilation process one of the most difficult parts to get the MC/DC+Harmonize software engineering structure operable.
+Together, MC/DC, Numba, and Harmonize form a JIT portability framework that dynamically targets different hardware architectures.
+This framework combines hardware portability with high-level Python methods development for exascale systems.
-To examine the compilation strategy in-depth, a simple proxy problem is provided in Figures figcodenvcc and figcodeclang.
-The figures show a simple Python function that does integer addition on a provided value (representing MC/DC transport operations) and a C++ snippet (representing Harmonize) showing first the declaration of an extern device function (eventually coming from Python) and a global function which will act as the GPU runtime for our Python device function.
-Note that for the operability of these examples, extra functions are required in ``dep.cpp`` and ``add_one.py`` but are truncated for brevity.
+MC/DC transport functions become device functions, while Harmonize supplies the associated global, host, and additional device functions.
+Linking device code generated from Python with the C++ Harmonize runtime requires an exact set of compiler options.
+The NVIDIA and AMD proxy examples below demonstrate this process with a Python integer-addition function representing MC/DC transport and a C++ declaration and global function representing Harmonize.
+Supporting functions in ``dep.cpp`` and ``add_one.py`` are omitted from the illustrations.
--------------
Nvidia Targets
--------------
-To compile to Nvidia GPU hardware-targets at runtime, we rely entirely on the Nvidia C-Compiler (`nvcc`).
+MC/DC uses Numba to produce PTX and the NVIDIA CUDA compiler (``nvcc``) for NVIDIA device compilation and linking.
Current versions of Numba come with CUDA operability natively, but this is set to be deprecated in future releases in favor of a more modular approach where the Numba-CUDA package will be an optional separate feature.
-.. image:: ../images/theory/gpu_comp/nvcc_flow.png
+.. image:: ../../images/developer_guide/architecture/numba_gpu_nvidia_flow.png
:width: 800
- :alt: Simple proxy example describing how to compile device functions in Numba-Python with external C++ code for targeting Nvidia GPUs. In this simplified proxy, the Python function corresponds to MC/DC, and the C++ code corresponds to Harmonize.
+ :alt: Simple proxy example describing how to compile device functions in Numba-Python with external C++ code for targeting Nvidia GPUs.
+ In this simplified proxy, the Python function corresponds to MC/DC, and the C++ code corresponds to Harmonize.
-Simple proxy example describing how to compile device functions in Numba-Python with external C++ code for targeting Nvidia GPUs. In this simplified proxy, the Python function corresponds to MC/DC, and the C++ code corresponds to Harmonize
+Simple proxy example describing how to compile device functions in Numba-Python with external C++ code for targeting Nvidia GPUs.
+In this simplified proxy, the Python function corresponds to MC/DC, and the C++ code corresponds to Harmonize
-We begin by
+The NVIDIA compilation sequence is:
#. Compiling Python device code to Nvidia PTX by ``numba.cuda.compile_ptx_for_current_device`` (which requires typed function signatures), then place that output into ``add_one.ptx`` file; next
#. Compiling PTX to relocatable device code using ``nvcc -rdc=true -dc -arch= --cudart shared --compiler-options -fPIC add.ptx -o add.o`` where ``-dc`` asks the compiler for device code, ``-rdc`` asks to make that device code relocatable, ``--cudart shared`` asks for shared CUDA runtime libraries and ``-fPIC`` generates position-independent code;
#. Compiling that relocatable byte code into a library of executable device functions is done with ``nvcc -dlink add.o -arch= --cudart shared -o device.o --compiler-options -fPIC`` where ``-dlink`` asks the compiler for relocatable device code; and finally
#. Compiling the C-CUDA file containing the global function and linking with the library of device functions originating from Python with ``nvcc -shared add.o device.o -arch= --cudart shared``.
-
+
While the complexity of the functions both from MC/DC (Python) and Harmonize (C++) increases dramatically when moving toward implementation in MC/DC, this compilation strategy remains mostly the same.
The exact compilation commands Harmonize calls when compiling MC/DC functions can be viewed by setting ``VERBOSE=True`` in ``harmonize/python/config.py``.
@@ -50,10 +68,10 @@ This compilation strategy also allows for the extension of functions defined in
AMD Targets
-----------
-Just in time compilation and execution to AMD devices are enabled as of `MC/DC v0.11.0 `_.
+Just in time compilation and execution to AMD devices are enabled as of `MC/DC v0.11.0 `_.
Significant adaptations from the process of Nvidia compilation are required to target AMD GPUs.
-PTX is a proprietary Nvidia standard, so when targeting AMD GPUs, we rely on intermediate compiler representation (IR) from LLVM for an AMD GPU hardware-target (also called an LLVM target triple).
-AMD's compiler toolchain is based in the LLVM-Clang ecosystem, so we will be calling LLVM-Clang-based tools (e.g., ``hipcc`` is a wrapper function for ``clang``).
+PTX is a proprietary NVIDIA standard, so AMD targets use an LLVM intermediate representation (IR) generated for the selected AMD GPU target triple.
+AMD's compiler toolchain is based on LLVM and Clang, and MC/DC invokes tools such as ``hipcc``, which wraps ``clang``.
Note that while the LLVM-Clang commands are generic, AMD variations of compilers, linkers, etc. must be invoked.
For example, to invoke the correct Clang compiler point to the ROCm installed variation (often on LinuxOS at ``opt/rocm/llvm/bin/clang``).
@@ -63,34 +81,36 @@ As this patch is a port of AMD's Heterogeneous-computing Interface for Portabili
The Numba-HIP development team has gone as far as to provide a ``numba.hip.pose_as_cuda()`` function, which, after being called in Python script, will alias all supported Numba-CUDA functions to Numba-HIP ones and compile/run automatically.
-When moving to compile and execute full MC/DC+Harmonize, we must again enable the compilation of device functions from Numba-HIP and device, global, and host functions from C++.
-To show that process, we again explore a simple proxy application shown in figure fig:codeclang where a Numba-HIP function adds one to an integer value and a C++ function declares an extern function by the same name and runs that function for all values of an array.
+Full MC/DC+Harmonize compilation combines device functions from Numba-HIP with device, global, and host functions from C++.
+The AMD proxy example pairs a Numba-HIP integer-addition function with a C++ declaration and global function that applies it to an array.
Every GPU program is technically a bound set of two complementary applications: one that runs on the host side (CPU) and the other on the device side (GPU), with global functions linking them together.
-To link external device code together for AMD hardware-targets, we have to unbundle these two programs, link the extra device functions (coming from Python) to the device side, then re-bundle the device and host functions back together.
+Linking external device code for AMD hardware requires unbundling the host and device programs, linking the Python-generated functions into the device program, and rebundling both programs.
This process is done in LLVM-IR.
-.. image:: ../images/theory/gpu_comp/amd_flow.png
+.. image:: ../../images/developer_guide/architecture/numba_gpu_amd_flow.png
:width: 800
- :alt: Simple proxy example describing how to compile device functions in Numba-HIP with external C++ code to AMD GPU targets. In this simplified proxy, the Python function corresponds to MC/DC, and the C++ code corresponds to Harmonize.
+ :alt: Simple proxy example describing how to compile device functions in Numba-HIP with external C++ code to AMD GPU targets.
+ In this simplified proxy, the Python function corresponds to MC/DC, and the C++ code corresponds to Harmonize.
-Simple proxy example describing how to compile device functions in Numba-HIP with external C++ code to AMD GPU targets. In this simplified proxy, the Python function corresponds to MC/DC, and the C++ code corresponds to Harmonize
+Simple proxy example describing how to compile device functions in Numba-HIP with external C++ code to AMD GPU targets.
+In this simplified proxy, the Python function corresponds to MC/DC, and the C++ code corresponds to Harmonize
Figure fig:codeclang shows the compilation structure.
-We begin compilation by
+The AMD compilation sequence is:
-#. Compiling C++ source in ``dep.cpp`` to LLVM-IR with host and device code bundled together with ``hipcc -c -fgpu-rdc -S -emit-llvm -o dep.ll -x hip dep.cpp -g`` where ``-fgpu-rdc`` asks the compiler for relocatable device code ``-emit-llvm`` requests the LLVM-IR, ``-c`` only runs preprocess, compile, and assemble steps, and ``-x hip`` specifies that ``dep.cpp`` is HIP code;
+#. Compiling C++ source in ``dep.cpp`` to LLVM-IR with host and device code bundled together with ``hipcc -c -fgpu-rdc -S -emit-llvm -o dep.ll -x hip dep.cpp -g`` where ``-fgpu-rdc`` asks the compiler for relocatable device code ``-emit-llvm`` requests the LLVM-IR, ``-c`` only runs preprocess, compile, and assemble steps, and ``-x hip`` specifies that ``dep.cpp`` is HIP code;
#. Unbundling the LLVM-IR:
-
+
a. first the device half ``clang-offload-bundler --type=ll --unbundle --input=dep.ll --output=dep_gpu.ll --targets=hip-amdgcn-amd-amdhsa--gfx90a`` where ``amdgcn-amd-amdhsa`` is the LLVM target-tipple and ``gfx90a`` is compiler designation for an MI250X
b. then the host half ``clang-offload-bundler --type=ll --unbundle --input=dep.ll --output=dep_cpu.ll --targets=host-x86_64-unknown-linux-gnu``; then
#. Compiling device functions from Python source with ``numba.hip.generate_llvmir()`` and place into ``add_one.ll``;
-#. Linking the now unbundled device code in ``dep_gpu.ll`` and the device code from Python in ``add_one.ll`` together with ``llvm-link dep_gpu.ll add_one.ll -S -o dep_gpu_linked.ll``;
+#. Linking the now unbundled device code in ``dep_gpu.ll`` and the device code from Python in ``add_one.ll`` together with ``llvm-link dep_gpu.ll add_one.ll -S -o dep_gpu_linked.ll``;
#. Rebundling the now combined Python/C++ device LLVM-IR back to the host LLVM-IR with ``clang-offload-bundler --type=ll --input=dep_gpu_linked.ll --input=dep_cpu.ll --output=dep_bundled.ll --targets=hip-amdgcn-amd-amdhsa--gfx90a, host-x86_64-unknown-linux-gnu``; and finally
#. Compiling to an executable with ``hipcc -v -fgpu-rdc --hip-link dep_bundled.ll-o program`` where ``--hip-link`` links clang-offload-bundles for HIP
As in the Nvidia compilation, non-implemented functions can be brought into the final program via the C++ source.
This was required for MC/DC on AMD GPUs as vector operable atomics are not currently implemented in the Numba HIP port and thus must come from the C++ side.
-We hope that these more generic adaptations (relying on LLVM-Clang infrastructure instead of CUDA) will allow for greater extensibility as we move to target future accelerator platforms---namely, Intel GPUs.
-For compilation to Nvidia hardware-targets, we will still keep the PTX-based compilation structure.
\ No newline at end of file
+The LLVM-Clang-based path is designed to remain extensible to future accelerator platforms, including Intel GPUs.
+NVIDIA compilation continues to use the PTX-based path.
diff --git a/docs/source/developer_guide/documentation/index.rst b/docs/source/developer_guide/documentation/index.rst
new file mode 100644
index 000000000..9da4a533e
--- /dev/null
+++ b/docs/source/developer_guide/documentation/index.rst
@@ -0,0 +1,18 @@
+.. _documentation:
+
+Documentation
+=============
+
+High-quality documentation is an essential part of MC/DC.
+Clear documentation supports simulation work, the development of computational methods, and the maintenance and evolution of the software framework.
+
+This section explains both the philosophy behind MC/DC's documentation and the tools used to build it.
+
+Read :doc:`philosophy` when deciding what information belongs in a page and how to layer it for different audiences.
+Read :doc:`sphinx` for source layout, reStructuredText, API generation, and local build instructions.
+
+.. toctree::
+ :maxdepth: 1
+
+ philosophy
+ sphinx
diff --git a/docs/source/developer_guide/documentation/philosophy.rst b/docs/source/developer_guide/documentation/philosophy.rst
new file mode 100644
index 000000000..38648a159
--- /dev/null
+++ b/docs/source/developer_guide/documentation/philosophy.rst
@@ -0,0 +1,96 @@
+.. _documentation_philosophy:
+
+========================
+Documentation Philosophy
+========================
+
+Vision
+------
+
+The MC/DC documentation should serve the diverse community that develops and uses the project.
+As MC/DC continues to grow, its documentation should be as scalable and maintainable as its software architecture.
+
+MC/DC adopts a layered documentation philosophy that balances usability, technical depth, and long-term maintainability across the entire project.
+
+This philosophy applies to all forms of MC/DC documentation, including Getting Started, the User Guide, Examples, Theory and Methods, Reference, the Developer Guide, Contributing, the README, and API docstrings.
+
+The Layered Documentation Philosophy
+------------------------------------
+
+MC/DC documentation supports three complementary kinds of work.
+Individual documents should progressively layer information from high-level usage to mathematical concepts and implementation details.
+Readers can naturally stop at the level of detail appropriate for their needs.
+
+Using MC/DC
+^^^^^^^^^^^
+
+Using MC/DC involves building geometry, defining materials and sources, configuring simulations, executing transport calculations, and analyzing results.
+
+Documentation supporting this work should emphasize:
+
+- What MC/DC provides.
+- How to use the public API.
+- Tutorials, examples, and recommended workflows.
+- Best practices for building transport models.
+
+Developing Methods
+^^^^^^^^^^^^^^^^^^
+
+Developing methods in MC/DC includes creating and evaluating new transport methods and computational algorithms.
+
+Documentation supporting this work should explain:
+
+- Mathematical formulations.
+- Numerical algorithms.
+- Data representations.
+- Design rationale.
+- Extensibility points.
+- Relationships between the public API and transport algorithms.
+
+Developing the Framework
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Developing the framework includes extending and maintaining MC/DC's software infrastructure.
+
+Documentation supporting this work should describe:
+
+- Software architecture.
+- Internal APIs.
+- Preparation pipeline.
+- Memory layout.
+- Compilation workflow.
+- Parallel execution.
+- Performance considerations.
+- Implementation and design decisions.
+
+Guiding Principles
+------------------
+
+Documentation should naturally progress from high-level concepts toward implementation details.
+
+A typical progression is:
+
+#. Overview
+#. Usage
+#. Examples
+#. Mathematical concepts
+#. Implementation notes
+
+Not every document requires every section.
+However, documentation should generally present information in this order so that each audience can stop reading once they have reached the level of detail they need.
+
+Public behavior should be described before mathematical representation, and mathematical representation should be described before implementation details.
+
+API Docstrings
+--------------
+
+API docstrings should follow the same layered philosophy.
+In general:
+
+- The opening description should explain the public purpose of the object, function, or module.
+- Parameters, return values, attributes, and examples should focus on the public interface.
+- Mathematical representations, algorithms, and design rationale should be documented in the ``Notes`` section when they support methods development.
+- Framework-specific implementation details should be documented separately as implementation notes when appropriate.
+
+Not every API requires all of these sections.
+The goal is to provide each audience with the information it needs while keeping the documentation clear, progressive, and easy to navigate.
diff --git a/docs/source/contribution_guide/documentation/sphinx.rst b/docs/source/developer_guide/documentation/sphinx.rst
similarity index 58%
rename from docs/source/contribution_guide/documentation/sphinx.rst
rename to docs/source/developer_guide/documentation/sphinx.rst
index dbe8914dc..4cd05814b 100644
--- a/docs/source/contribution_guide/documentation/sphinx.rst
+++ b/docs/source/developer_guide/documentation/sphinx.rst
@@ -5,13 +5,9 @@
Sphinx and Read the Docs
========================
-MC/DC uses `Sphinx `_ to generate its
-documentation and `Read the Docs `_ to build
-and host the documentation website.
+MC/DC uses `Sphinx `_ to generate its documentation and `Read the Docs `_ to build and host the documentation website.
-This page introduces the subset of Sphinx needed to contribute to MC/DC's
-documentation, including reStructuredText, document organization, automatic API
-generation, and local documentation builds.
+This page introduces the subset of Sphinx needed to contribute to MC/DC's documentation, including reStructuredText, document organization, automatic API generation, and local documentation builds.
reStructuredText and Sphinx
@@ -19,17 +15,17 @@ reStructuredText and Sphinx
We write files for Sphinx using a plaintext markup language called reStructuredText (rst).
`Click here for a rst Primer `_.
-Sphinx builds an html file for every rst file in the documentation root directory and its subdirectories our documentation root directory is ``mcdc/docs/source/``.
-The root document, ``index.rst``, serves as the welcome page.
-The root directory also contains several subdirectories, each of which has its own ``index.rst`` file and several other rst files.
+Sphinx builds an html file for every rst file in the documentation root directory and its subdirectories our documentation root directory is ``mcdc/docs/source/``.
+The root document, ``index.rst``, serves as the welcome page.
+The root directory also contains task-oriented sections, each with an ``index.rst`` landing page and related topic pages.
It's useful to compare our rst files to their associated webpages to get a feel for how they translate.
Like any plaintext markup language, rst uses "explicit markup" for constructs that need special handling, such as including a code-block or cross-referencing other pages.
-
+
A block of explicit markup text starts with ".. " and is terminated by the next paragraph at the same level of indentation.
-Sphinx creates webpage elements using explicit markup blocks called directives.
+Sphinx creates webpage elements using explicit markup blocks called directives.
.. tip::
For example, this block was created using the `tip` directive!
@@ -39,18 +35,17 @@ Sphinx creates webpage elements using explicit markup blocks called directives.
.. tip::
For example, this block was created using the `tip` directive!
-An explicit markup block without a directive is taken as a comment that will not appear on the webpage:
-::
+An explicit markup block without a directive is taken as a comment that will not appear on the webpage: ::
.. For example, this is a comment.
-In addition to directives for blocks of explicit markup, Sphinx handles in-line explicit markup with roles.
+In addition to directives for blocks of explicit markup, Sphinx handles in-line explicit markup with roles.
For example, this equation :math:`a^2 + b^2 = c^2` was created using the `math` role.
::
For example, this equation :math:`a^2 + b^2 = c^2` was created using the `math` role.
-`Click here for a list of Sphinx directives `_ and `click here for a list of Sphinx roles `_.
+`Click here for a list of Sphinx directives `_ and `click here for a list of Sphinx roles `_.
The toctree
@@ -58,13 +53,12 @@ The toctree
Sphinx's main directive is the `toctree` directive, which generates a table of contents tree (toctree) with links to other webpages in the build.
The listed documents should be named relative to the current document and excluding the .rst extension.
-For example, the MC/DC docs root directory contains ``index.rst``, ``install.rst``, and a subdirectory ``user/`` that also contains its own ``index.rst``.
-The following on ``index.rst`` creates a table of contents on the main page with links to the install and user pages:
-::
+For example, the following on ``index.rst`` creates a table of contents on the main page with links to the main user-facing sections: ::
.. toctree::
- install
- user/index
+ user_guide/index
+ theory/index
+ examples/index
Sphinx will build an html file for all rst files in the source directory and its subdirectories.
Sphinx will issue a warning if an html file isn't referenced in any toctree because that means that the generated webpage is not reachable through standard navigation.
@@ -77,67 +71,53 @@ Within MC/DC's source code, we document functions and classes using docstrings.
`We use two Sphinx extensions `_ -- ``autodoc`` and ``autosummary`` -- to generate rst files for Sphinx using the existing docstrings in our source code.
For ``autodoc`` and ``autosummary`` to work, the docstrings within MC/DC's source code must be written in correct rst.
-The ``autodoc`` extension includes a set of directives to document different chunks of code (e.g., modules, functions, classes).
-For example, below is the entire rst file that generates the :doc:`../../pythonapi/generated/mcdc.MaterialMG` page:
+The ``autodoc`` extension includes a set of directives to document different chunks of code (e.g., modules, functions, classes).
+For example, below is the entire rst file that generates the :doc:`../../reference/python_api/generated/mcdc.NeutronMultigroupData` page:
.. code-block::
-
- mcdc.MaterialMG
- ===============
-
+
+ mcdc.NeutronMultigroupData
+ ======================
+
.. currentmodule:: mcdc
-
- .. autoclass:: MaterialMG
-(That in-line reference was created using :code:`:doc:\`../../pythonapi/generated/mcdc.MaterialMG\``, by the way).
+ .. autoclass:: NeutronMultigroupData
+
+(That in-line reference was created using :code:`:doc:\`../../reference/python_api/generated/mcdc.NeutronMultigroupData\``, by the way).
A rst file with an ``autodoc`` directive is required for each module or function that we would like to document.
Rather than create all of these rst files by hand, we use the ``autosummary`` extension to do it for us.
-For example, let's look at the first ``autosummary`` directive in ``source/pythonapi/index.rst``, the file that governs the :doc:`../../pythonapi/index` page:
+For example, consider the first ``autosummary`` directive in ``source/reference/python_api/index.rst``, the file that governs the :doc:`../../reference/python_api/index` page:
.. code-block::
.. autosummary::
-
+
mcdc.Material
- mcdc.MaterialMG
+ mcdc.NeutronMultigroupData
This directive:
- #. Generates two files in ``pythonapi/generated/``: ``mcdc.Material.rst`` and ``mcdc.MaterialMG.rst``.
+ #. Generates two files in ``reference/python_api/generated/``: ``mcdc.Material.rst`` and ``mcdc.NeutronMultigroupData.rst``.
#. Populates each file with the proper autoclass directive.
- #. Creates a table on :doc:`../../pythonapi/index` with entries mcdc.Material and mcdc.MaterialMG that link to the respective generated pages.
+ #. Creates a table on :doc:`../../reference/python_api/index` with entries mcdc.Material and mcdc.NeutronMultigroupData that link to the respective generated pages.
Building Locally
----------------
-We can check our work with a local build.
+We can check our work with a local build.
Make sure you're in ``mcdc/docs/``:
-#. Both Sphinx and furo (the package we use for website theming) should have been installed with MC/DC.
+#. Both Sphinx and the PyData Sphinx Theme should have been installed with MC/DC.
To check, type ``sphinx-build --version`` on the commandline.
- If not installed, ``pip install sphinx furo``.
+ If they are not installed, run ``pip install -e ".[docs]"`` from the repository root.
#. With Sphinx installed, run ``make html``.
This builds local html files in ``mcdc/docs/build/``.
#. To launch your local html from the commandline, ``open build/html/index.html``.
Check your work: has your content been added or changed as you expected?
#. Continue making changes to your local rst files, building locally, and launching the built html files until you're satisfied with how the website will look.
-.. warning::
- In the process of creating MC/DC's documentation, ``autodoc`` *imports every python module that MC/DC imports*.
-
- This doesn't cause any issues when you build the webpages locally, because you already have all of MC/DC's requisite packages installed.
-
- However, this *WILL* cause issues with our documentation website host, readthedocs.
- Like you just did, readthedocs will checkout our repo and use Sphinx to build html files from our rst files, attempting to import all of MC/DC's packages along the way.
- There are some python packages, like ``mpi4py``, that readthedocs is unable to import, causing the documentation build to fail.
-
- **If you've added any new package imports to MC/DC's source code, add them to the** ``MOCK_MODULES`` **list in** ``mcdc/docs/source/conf.py``.
-
- This will allow readthedocs to get past the imports without issue.
-
-
-Once you're satisfied with your changes and have added any new modules to ``conf.py``, submit a PR!
-
+The API reference imports MC/DC during the documentation build, so run a complete local build after changing package imports or dependencies.
+Once you are satisfied with the local build, submit a PR.
diff --git a/docs/source/developer_guide/extending/extending_the_object_model.rst b/docs/source/developer_guide/extending/extending_the_object_model.rst
new file mode 100644
index 000000000..68a7cc667
--- /dev/null
+++ b/docs/source/developer_guide/extending/extending_the_object_model.rst
@@ -0,0 +1,382 @@
+.. _extending_the_object_model:
+
+==========================
+Extending the Object Model
+==========================
+
+Use this page when an extension changes a model class, introduces runtime-visible state, or adds a registered or polymorphic object type.
+It assumes the hierarchy and data representation described in :doc:`../architecture/simulation_compilation` and :doc:`../architecture/runtime_data_layout` and applies those designs as implementation recipes.
+
+When the new state is consumed during particle transport, continue with :doc:`writing_numba_compatible_transport_code` for type, dispatch, allocation, CPU/GPU compatibility, and verification guidance.
+
+Choose the Extension Type
+-------------------------
+
+Choose the narrowest extension that represents the new concept:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 20 36 24
+
+ * - Change
+ - Starting point
+ - Use when
+ - Example
+ * - Add a field
+ - Existing class
+ - The concept already belongs to an existing model or configuration object.
+ - Add a new source parameter to ``Source``.
+ * - Add embedded state
+ - ``MCDCBase``
+ - The state belongs to one parent and does not need an independently addressable entry in a simulation registry.
+ - Add technique settings owned by ``Simulation``.
+ * - Add a registered category
+ - ``MCDCObject``
+ - Transport must refer to independently registered instances by simulation-local ``ID``.
+ - Add a new Surface-like category with its own collection.
+ * - Add a representation to an existing category
+ - ``MCDCPolymorphic``
+ - The extension shares a category interface but needs a distinct packed layout and dispatch code.
+ - Add a concrete ``MeshBase`` representation.
+
+Prefer adding a subtype to an existing polymorphic family over creating a new registered category when the new object has the same conceptual role.
+For example, implement a new tally estimator as a ``Tally`` subtype.
+
+The Common Class Contract
+-------------------------
+
+Every MC/DC model class must follow the conventions used by the compiler and Numba-layer generator.
+
+``label``
+ Provide a unique, stable, lower-case label such as ``structured_mesh``.
+ The label names generated structured layouts and the corresponding modules under ``mcdc_get`` and ``mcdc_set``.
+
+ .. code-block:: python
+
+ class MeshStructured(MeshBase):
+ label = "structured_mesh"
+
+``sub_type``
+ Give every concrete ``MCDCPolymorphic`` subclass a unique named integer constant within its family.
+ The shared base uses ``sub_type = -1``.
+
+ .. code-block:: python
+
+ class MeshStructured(MeshBase):
+ sub_type = MESH_STRUCTURED
+
+Type annotations
+ Annotate every field that must be represented at runtime.
+ Annotations define scalar fields, embedded structures, object-ID references, and variable-length payloads.
+
+ .. code-block:: python
+
+ active: bool
+ translation: Annotated[NDArray[float64], (3,)]
+ move_velocities: Annotated[NDArray[float64], ("N_move", 3)]
+ surfaces: list[Surface]
+
+Initialization
+ Assign every runtime-visible field a valid initial value.
+ Subclasses of ``MCDCObject`` must call ``super().__init__()`` so ``ID`` is initialized; subclasses of ``MCDCPolymorphic`` must do the same so both ``ID`` and ``sub_ID`` are initialized.
+
+ .. code-block:: python
+
+ def __init__(self, name, boundaries):
+ super().__init__()
+ self.name = name
+ self.boundaries = np.asarray(boundaries, dtype=float64)
+
+``non_numba``
+ List Python-only fields that should not be traversed or packed automatically.
+ The class must explicitly convert any required information from those fields into annotated runtime-visible fields before packing.
+
+ For example, ``Cell`` keeps its expressive ``region`` and ``fill`` objects on the Python side, then derives RPN tokens, a fill-type code, and a fill ID for transport:
+
+ .. code-block:: python
+
+ non_numba = ["region", "fill"]
+
+ region: Region
+ fill: Material | Universe | Lattice | None
+ region_RPN_tokens: list[int]
+ fill_type: int
+ fill_ID: int
+
+Compilation hook
+ Use the inherited ``_compile_into_simulation`` implementation unless the class must canonicalize objects, compile excluded references, derive fields from assigned object IDs, or otherwise finalize state owned by that object.
+
+ .. code-block:: python
+
+ def _compile_into_simulation(self, simulation):
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ self.reference._compile_into_simulation(simulation)
+ self.reference_ID = self.reference.ID
+ return True
+
+ If Python-only members must be canonicalized before ordinary traversal, guard the work with ``compile_ID`` and then call ``super`` exactly once:
+
+ .. code-block:: python
+
+ def _compile_into_simulation(self, simulation):
+ if self.compile_ID == simulation.compile_ID:
+ return False
+
+ self._resolve_python_inputs(simulation)
+
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ self._derive_post_registration_fields()
+ return True
+
+ Do not assign ``compile_ID``, ``ID``, or ``sub_ID`` manually.
+
+Model-wide finalization
+ Object hooks should not normalize or coordinate unrelated registries.
+ When a value requires the complete discovered model, coordinate it once in ``Simulation._finalize_compilation`` instead.
+ Source-probability normalization, particle-bank capacities, and settings derived from the complete material or tally collections are examples of model-wide finalization.
+ Explicitly compile any new runtime-visible object introduced during this phase because ordinary recursive discovery has already occurred.
+
+ ``compile_simulation`` orchestrates recursive discovery and calls this model-wide finalization phase.
+ Change the compiler orchestration only when adding a new compilation phase or registered category.
+ Do not place model-specific finalization in ``mcdc.main.prepare``; that function is reserved for framework-level packing, resource allocation, backend configuration, and external runtime state.
+
+Represent Fields Deliberately
+-----------------------------
+
+The layer generator interprets annotations according to the field's role:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 28 27 45
+
+ * - Example annotation
+ - Runtime representation
+ - Transport access
+ * - ``active: bool``
+ - Scalar structured field
+ - ``simulation["technique"]["new_technique"]["active"]``
+ * - ``translation: Annotated[NDArray[float64], (3,)]``
+ - Fixed-size embedded array
+ - ``cell["translation"][axis]``
+ * - ``energy: NDArray[float64]``
+ - Offset and length plus values in ``data``
+ - ``mcdc_get.tally.energy(index, tally, data)``
+ * - ``move_velocities: Annotated[NDArray[float64], ("N_move", 3)]``
+ - Offset, length, and shape metadata plus flattened values
+ - ``mcdc_get.surface.move_velocities(move, axis, surface, data)``
+ * - ``energy_pmf: DistributionPMF``
+ - Simulation-local object ID
+ - ``simulation["distributions"][source["energy_pmf_ID"]]``
+ * - ``collision_tallies: list[TallyCollision]``
+ - Count and offset to IDs stored in ``data``
+ - ``mcdc_get.cell.collision_tally_IDs(index, cell, data)``
+
+Use an integer-only shape when an array is always the same size.
+Use symbolic dimensions when a shape depends on the model.
+Do not store a Python reference in transport-visible state; annotate it as an ``MCDCObject`` or polymorphic base so the packed layer records an ID.
+
+Adding a Field to an Existing Class
+-----------------------------------
+
+#. Add the annotation to the class that owns the concept.
+#. Initialize the field for every construction path.
+#. Decide whether it is fixed-size, variable-length, an embedded ``MCDCBase``, or an ``MCDCObject`` reference.
+#. Update any compile hook that derives the field or converts a Python-only input into its runtime representation.
+#. Consume the field through direct structured access or its generated ``mcdc_get`` and ``mcdc_set`` helpers.
+#. Update the public docstring and API documentation when users can configure the field.
+
+For example, a new variable-length ``energy_bias`` field on ``Source`` requires an annotation and initialized array on the model class:
+
+.. code-block:: python
+
+ # In Source annotations
+ energy_bias: NDArray[float64]
+
+ # In Source.__init__
+ self.energy_bias = np.asarray(energy_bias, dtype=float64)
+
+Transport then reads one value through the generated accessor:
+
+.. code-block:: python
+
+ bias = mcdc_get.source.energy_bias(index, source, data)
+
+Avoid adding parallel state in several classes.
+If a value belongs to the simulation as a whole, place it in ``Simulation`` or one of its embedded configuration objects and pass or access that representation consistently.
+
+Adding Embedded ``MCDCBase`` State
+----------------------------------
+
+Use ``MCDCBase`` for configuration or runtime state that is owned by one parent.
+A minimal class has a label, annotated fields, and initialized values:
+
+.. code-block:: python
+
+ class NewTechnique(MCDCBase):
+ label = "new_technique"
+
+ active: bool
+ strength: float
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.active = False
+ self.strength = 1.0
+
+Add an annotated field for the object to its owner and instantiate it with the owner.
+Simulation-wide transport techniques belong to the ``Technique`` aggregate,
+which is itself owned by ``Simulation``.
+The embedded object participates in recursive compilation but does not require a registry branch or object ID.
+This Python ownership hierarchy is preserved under the packed
+``simulation["technique"]`` record.
+
+.. code-block:: python
+
+ class Technique(MCDCBase):
+ new_technique: NewTechnique
+
+ def __init__(self):
+ self.new_technique = NewTechnique()
+
+The corresponding user and runtime interfaces are
+``simulation.technique.new_technique(...)`` and
+``simulation["technique"]["new_technique"]``.
+
+If the embedded object refers to an ``MCDCObject``, annotate that reference.
+The default traversal will register the referenced object when compilation reaches the embedded configuration.
+
+Adding a New ``MCDCObject`` Category
+------------------------------------
+
+A genuinely new registered category requires coordinated changes:
+
+#. Define the class with a unique ``label``, annotations, initialized fields, and a call to ``super().__init__()``.
+#. Add the category collection to ``Simulation`` annotations and initialize or reset it in ``Simulation._reset_model``.
+#. Import the category in ``mcdc/code_factory/python_objects_compiler.py`` and add an ``isinstance`` branch in ``register_object`` that selects its simulation collection.
+#. Ensure the object is reachable from an existing simulation root or add an explicit root and compilation step.
+#. Ensure the class's module is imported before ``numba_layers_generator.py`` discovers the classes.
+ A public class is normally imported through ``mcdc/__init__.py``; an internal class must be imported by another module in the compilation path.
+#. Add the collection and lookup behavior required by transport.
+
+For example, the class and its simulation collection begin with:
+
+.. code-block:: python
+
+ class Detector(MCDCObject):
+ label = "detector"
+
+ name: str
+ response: NDArray[float64]
+
+ def __init__(self, name, response):
+ super().__init__()
+ self.name = name
+ self.response = np.asarray(response, dtype=float64)
+
+
+ class Simulation(MCDCBase):
+ detectors: list[Detector]
+
+The compiler then selects that collection explicitly:
+
+.. code-block:: python
+
+ elif isinstance(object_, Detector):
+ object_list = simulation.detectors
+
+Do not add a fallback registration branch that silently accepts unknown objects.
+An explicit category branch keeps registry ownership and runtime layout reviewable.
+
+Adding a Polymorphic Subtype
+----------------------------
+
+Adding a concrete subtype to an existing family is more localized than adding a category:
+
+#. Add a unique integer constant for the subtype.
+#. Inherit from the existing polymorphic base, such as ``MeshBase`` or ``Tally``.
+#. Set a unique concrete ``label`` and the new ``sub_type`` constant.
+#. Call the base initializer so shared fields, ``ID``, and ``sub_ID`` are initialized.
+#. Annotate and initialize subtype-specific fields.
+#. Import the subtype before layer generation and expose it from ``mcdc/__init__.py`` when it is public.
+#. Add transport dispatch for the new ``sub_type`` and implement the subtype-specific behavior.
+
+For example, the structural part of a mesh subtype follows this pattern:
+
+.. code-block:: python
+
+ class MeshNew(MeshBase):
+ label = "new_mesh"
+ sub_type = MESH_NEW
+
+ boundaries: NDArray[float64]
+
+ def __init__(self, boundaries, name="") -> None:
+ super().__init__(name)
+ self.boundaries = np.asarray(boundaries, dtype=float64)
+
+No new ``register_object`` branch is needed for a subtype of an already registered family.
+The existing ``isinstance(..., MeshBase)`` or corresponding category check places it in the base collection, while ``sub_type`` and ``sub_ID`` connect it to its concrete packed collection.
+
+Generated Runtime Layers and Accessors
+--------------------------------------
+
+The annotation is the source of truth for generated runtime fields and accessors.
+Do not edit ``mcdc/numba_types.py``, ``mcdc_get``, or ``mcdc_set`` to introduce a field.
+Prepare a representative simulation so ``numba_layers_generator.py`` regenerates those files, then verify the access pattern predicted by the field representation chosen above.
+
+For example, a variable-length ``Detector.response`` field produces element accessors associated with the ``detector`` label:
+
+.. code-block:: python
+
+ value = mcdc_get.detector.response(index, detector, data)
+ mcdc_set.detector.response(index, detector, data, new_value)
+
+A fixed-size field such as ``Cell.translation`` remains embedded and is accessed directly:
+
+.. code-block:: python
+
+ value = cell["translation"][axis]
+
+Public API and Documentation
+----------------------------
+
+For a user-facing class or constructor:
+
+#. Export the class from ``mcdc/__init__.py``.
+#. Add it to the appropriate autosummary group in ``docs/source/reference/python_api/index.rst``.
+#. Document parameters, units, defaults, constraints, and at least one usable example in the class docstring.
+#. Update the User Guide when the extension changes how users construct or run a model.
+
+Keep internal helper classes under ``mcdc.object_`` and import them explicitly in the compilation path.
+Export only classes that form part of the public API.
+
+For example, a public ``Detector`` is re-exported from the package and listed by its qualified name in the API autosummary:
+
+.. code-block:: python
+
+ from mcdc.object_.detector import Detector
+
+.. code-block:: rst
+
+ ~mcdc.Detector
+
+Verification Checklist
+----------------------
+
+An object-model extension should verify all affected layers:
+
+- Construction accepts valid input and rejects invalid shapes or types.
+- Compilation discovers the object from the intended root.
+- Shared references register once, and recompilation produces a valid new snapshot.
+- Packed fields, object IDs, offsets, and generated accessors contain the expected values.
+- Python and Numba-CPU modes produce equivalent behavior.
+- GPU execution is covered when the changed transport path supports GPUs.
+- Public examples compile under the example validator when the API changes.
+- API and developer documentation build without warnings.
+
+Add focused unit tests near ``test/unit/test_object_compilation.py`` for compilation behavior and near the relevant transport tests for runtime behavior.
+Use :doc:`../../contributing/example_validation` when an extension changes public examples.
diff --git a/docs/source/developer_guide/extending/index.rst b/docs/source/developer_guide/extending/index.rst
new file mode 100644
index 000000000..4240efb0a
--- /dev/null
+++ b/docs/source/developer_guide/extending/index.rst
@@ -0,0 +1,29 @@
+.. _extending_mcdc:
+
+===============
+Extending MC/DC
+===============
+
+Use this section to extend the Python model, transport implementation, public API, and tests.
+Read :doc:`../architecture/index` for the design that these recipes extend.
+
+.. tip::
+
+ :ref:`Methods development ` usually takes place in ``mcdc/object_`` and ``mcdc/transport``.
+ The object modules define model-facing state and its runtime-visible declarations, while the transport modules implement the corresponding numerical behavior.
+ ``mcdc/code_factory`` provides compilation, layer-generation, accessor-generation, and backend-adaptation services behind this interface.
+ This separation allows methods development to use those services without managing the framework machinery directly.
+ Modify ``code_factory`` when changing compilation, layer generation, accessor generation, or backend adaptation.
+
+Start with :doc:`extending_the_object_model` when adding a field to an existing model class, introducing simulation-owned configuration, creating a registered model-object category, or implementing a new polymorphic subtype.
+Continue with :doc:`writing_numba_compatible_transport_code` when the extension adds or changes code executed during particle transport.
+
+For example, a new runtime field with no transport behavior uses the object-model guide, a numerical change using existing fields starts with the transport-code guide, and a new tally subtype follows both in that order.
+
+Use the :doc:`../../contributing/index` for repository setup, test commands, continuous-integration coverage, and pull-request requirements.
+
+.. toctree::
+ :maxdepth: 1
+
+ extending_the_object_model
+ writing_numba_compatible_transport_code
diff --git a/docs/source/developer_guide/extending/writing_numba_compatible_transport_code.rst b/docs/source/developer_guide/extending/writing_numba_compatible_transport_code.rst
new file mode 100644
index 000000000..4a84436dd
--- /dev/null
+++ b/docs/source/developer_guide/extending/writing_numba_compatible_transport_code.rst
@@ -0,0 +1,411 @@
+.. _writing_numba_compatible_transport_code:
+
+=======================================
+Writing Numba-Compatible Transport Code
+=======================================
+
+Use this page when verified Python transport behavior is being prepared for Numba-CPU execution and long-term maintenance in MC/DC.
+Read :doc:`../architecture/python_first_numba_accelerated_design` for the development rationale.
+If the change introduces new model state, begin with :doc:`extending_the_object_model`.
+
+Numba-CPU Development
+---------------------
+
+Complete the Python and Numba-CPU implementation before considering additional execution targets.
+
+Choose Between Model Compilation and Particle Transport
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+First decide whether the change belongs to the Python model or to the algorithms executed during particle transport.
+
+Place model definition and model-finalization work in ``mcdc/object_/`` when it changes or completes the model before execution:
+
+- Validate and normalize user input.
+- Traverse Python objects or inspect their classes.
+- Add, remove, or resize model data.
+- Read files, construct tables, or derive run-wide configuration.
+- Define the state that must later be available to transport.
+
+Use an object's ``_compile_into_simulation`` hook when the work belongs to that object and use ``Simulation._finalize_compilation`` when it requires the complete discovered model.
+``compile_simulation`` in ``mcdc/code_factory/`` coordinates those phases and should change only when the compilation framework itself gains a new phase or registered category.
+
+``mcdc.main.prepare`` and the runtime generators in ``mcdc/code_factory/`` are framework-level machinery that pack the finalized model, allocate execution resources, and configure execution backends.
+Most scientific-method additions should not change them; extend them only when the runtime representation or execution framework cannot express the required behavior.
+See :doc:`extending_the_object_model` for the model-compilation workflow.
+
+Place event-time numerical work in ``mcdc/transport/`` when it must be performed during particle execution:
+
+- Inspect or mutate particle records.
+- Evaluate geometry or physics from prepared numerical data.
+- Sample distributions and update the random-number state.
+- Score tallies or update preallocated banks and counters.
+
+Do as much irregular work as practical during model compilation.
+A small amount of object-side finalization can turn a dynamic operation into simple indexed access inside a frequently called kernel.
+
+For example, derive a reusable coefficient in the model object's compilation hook instead of recomputing it for every particle event:
+
+.. code-block:: python
+
+ # On the model class
+ inverse_dx: float
+
+ def _compile_into_simulation(self, simulation):
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ self.inverse_dx = 1.0 / self.dx
+ return True
+
+ # In transport
+ index = int((particle["x"] - mesh["x0"]) * mesh["inverse_dx"])
+
+Port a Verified Method
+^^^^^^^^^^^^^^^^^^^^^^
+
+Keep the verified Python result as the behavioral baseline while adapting the method to MC/DC's packed runtime inputs and compiler-compatible operations.
+Decorate the maintained function with ``@njit`` like the surrounding transport functions; in Python mode, MC/DC disables JIT compilation and calls the same function as Python.
+
+.. code-block:: python
+
+ from numba import njit
+
+
+ @njit
+ def apply_weight_factor(particle_container, factor):
+ particle = particle_container[0]
+ particle["w"] *= factor
+
+Keep the function small enough that its inputs, outputs, and mutations are clear.
+Exercise the maintained function in Python mode against the baseline, then use Numba-CPU to resolve typing and compiled-runtime issues.
+
+Use Runtime Data, Not Model Objects
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+An integrated transport function should accept scalars, NumPy arrays, structured records, one-element record containers, and the packed ``simulation`` and ``data`` state.
+It should not accept an ``MCDCObject`` instance or follow Python object references.
+
+Use the representation appropriate to each field:
+
+- Read fixed-size values directly from a structured record.
+- Follow registered relationships with their integer IDs.
+- Use ``sub_type`` and ``sub_ID`` for polymorphic dispatch.
+- Use generated ``mcdc_get`` and ``mcdc_set`` helpers for variable-length fields in ``data``.
+- Mutate only state allocated during runtime preparation.
+
+If the required value is unavailable in this form, extend the object model and generated runtime layer before writing the transport behavior.
+Do not create a parallel Python-only lookup inside the kernel.
+
+For example, recover a prepared distribution by simulation-local ID instead of passing its Python model object into transport:
+
+.. code-block:: python
+
+ # Python model construction
+ source_object.energy_pmf = distribution_object
+
+ # Portable transport representation
+ distribution = simulation["distributions"][source["energy_pmf_ID"]]
+
+Keep Types Stable
+^^^^^^^^^^^^^^^^^
+
+Numba determines a compiled function's types from its arguments and control flow.
+Make those types unambiguous:
+
+- Initialize local variables on every path before use.
+- Return the same number and compatible types of values from every branch.
+- Avoid changing a variable from a scalar to an array or from an integer to an unrelated object.
+- Use explicit integer and floating-point constants when their width or signedness affects an operation.
+- Keep structured-record field names fixed; do not compute field names at run time.
+- Avoid heterogeneous Python lists, dictionaries, sets, generators, and dynamically created classes in transport.
+
+For example, initialize a scalar result before control flow so every path returns the same type:
+
+.. code-block:: python
+
+ @njit
+ def find_energy_bin(E, grid):
+ index = -1
+ for i in range(len(grid) - 1):
+ if grid[i] <= E < grid[i + 1]:
+ index = i
+ break
+ return index
+
+Represent Variable-Length Data Explicitly
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Do not recover variable-length model data by allocating a new Python container.
+Read it from ``data`` using the metadata in its owning record.
+Prefer the generated accessor because it preserves the layout convention:
+
+.. code-block:: python
+
+ value = mcdc_get.tally.energy(index, tally, data)
+
+An ``*_all`` or ``*_chunk`` helper may expose a view when an algorithm needs a range.
+Element access is preferable when the kernel only needs one value.
+Do not resize the returned view or retain it beyond the prepared run.
+
+For a new variable-length field, declare the field on the Python model class and regenerate its accessors as described in :doc:`extending_the_object_model`.
+Do not hand-maintain offset arithmetic in several transport modules.
+
+Use Named Constants and Explicit Dispatch
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The packed runtime representation expresses categories, events, and other discrete states as primitive numerical values rather than Python types.
+``mcdc.constant`` gives those values shared names and also defines common numerical limits and tolerances.
+These definitions form a static implementation contract and retain the same meaning across simulations.
+Use the named constants instead of repeating their numerical values in model, transport, scoring, or output code.
+
+Dispatch on those constants through a small interface function rather than using Python ``isinstance`` checks or methods on runtime records:
+
+.. code-block:: python
+
+ @njit
+ def get_mesh_x(index, mesh, simulation, data):
+ sub_ID = mesh["sub_ID"]
+ if mesh["sub_type"] == MESH_STRUCTURED:
+ structured_mesh = simulation["structured_meshes"][sub_ID]
+ return mcdc_get.structured_mesh.x(index, structured_mesh, data)
+ if mesh["sub_type"] == MESH_UNIFORM:
+ uniform_mesh = simulation["uniform_meshes"][sub_ID]
+ return uniform_mesh["x0"] + index * uniform_mesh["dx"]
+ return 0.0
+
+Use the actual category interface and constants already defined for that family.
+When introducing a new subtype, event, or score, give its constant a unique value within the corresponding family and update every consumer of that family.
+Test an unsupported value deliberately when the interface defines fallback behavior.
+Do not place a size or setting derived from one simulation in ``constant.py``; simulation-specific values that must be visible to Numba as compile-time values use the generated literals described in :ref:`simulation_specific_literals`.
+
+Control Allocation and Mutation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Transport functions execute repeatedly for every particle history, so they should normally operate on storage prepared before transport begins.
+Reuse particle banks, tally arrays, and other prepared runtime state instead of rebuilding them inside the particle loop.
+Update structured fields or ``data`` through explicit assignments and generated setters, and do not append model entities or change field shapes during transport.
+
+Prefer scalar state and bounded loops when an intermediate container is unnecessary.
+For example, select a minimum directly instead of building a temporary list of distances:
+
+.. code-block:: python
+
+ best_distance = INF
+ for i in range(cell["N_surface"]):
+ distance = distance_to_surface(i, particle, cell, simulation, data)
+ if distance < best_distance:
+ best_distance = distance
+
+Some transport operations genuinely require new local storage.
+Fission and scattering create secondary-particle records during transport, while geometry and physics routines may require small bounded work arrays.
+Use ``util.local_array`` with a known shape and stable dtype for these cases.
+In Numba-CPU mode, it provides a NumPy array that Numba can compile and mutate within the transport function.
+
+For example, fission allocates one reusable record for newly generated particles, initializes it for each secondary, and passes its container to a particle bank:
+
+.. code-block:: python
+
+ particle_container_new = util.local_array(1, type_.particle_data)
+ particle_new = particle_container_new[0]
+
+ for _ in range(N):
+ particle_module.copy_as_child(
+ particle_container_new, particle_container
+ )
+ particle_new["w"] = weight_product
+ particle_bank_module.bank_census_particle(
+ particle_container_new, program
+ )
+
+Use generated structured dtypes from ``mcdc.numba_types`` for local transport records.
+Keep local work arrays small and predictable, and reuse the same allocation within a loop when its contents can be overwritten.
+Make every mutation visible through function arguments rather than hidden module-level mutable state.
+The one-element container preserves the mutable record across function boundaries; the next section explains when functions should receive such a container and when they should receive the record itself.
+
+Use Runtime Arguments Deliberately
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+MC/DC uses ``particle_container``, ``simulation_container``, and ``program`` for different purposes.
+They are calling conventions that preserve mutation and backend portability, not interchangeable names for simulation state.
+
+``particle_container``
+ A one-element structured array that owns one mutable particle record.
+ Pass the container when a function must mutate the particle and recover the record locally for field access.
+ The container may be caller-owned, a one-element view into a particle bank, or local storage created with ``util.local_array``.
+
+ .. code-block:: python
+
+ @njit
+ def reduce_weight(particle_container, factor):
+ particle = particle_container[0]
+ particle["w"] *= factor
+
+``simulation_container``
+ A one-element structured array that owns the mutable top-level ``simulation`` record.
+ It establishes storage and lifetime at an execution entry point; ordinary transport functions normally receive the recovered record rather than the container.
+
+ .. code-block:: python
+
+ def fixed_source_simulation(simulation_container, data):
+ simulation = simulation_container[0]
+ settings = simulation["settings"]
+ for idx_batch in range(settings["N_batch"]):
+ simulation["idx_batch"] = idx_batch
+
+``program``
+ A backend-neutral execution handle used when an operation requires execution services such as particle banking or scheduling.
+ In Python and Numba-CPU modes, ``program`` is the ``simulation`` record and ``util.access_simulation(program)`` returns it unchanged.
+ Treat the handle as opaque even in these modes so the same transport function can later use a GPU implementation.
+
+ .. code-block:: python
+
+ @njit
+ def bank_active_particle(particle_container, program):
+ simulation = util.access_simulation(program)
+ bank = simulation["bank_active"]
+ _bank_particle(particle_container, bank)
+
+Accept ``simulation`` when a function only needs prepared simulation state.
+Accept ``program`` when it needs backend-dependent execution services, and recover ``simulation`` through ``util.access_simulation``.
+Create one-element containers only at ownership or local-storage boundaries; do not wrap every structured record passed between helpers.
+
+Cross the Python Boundary with ``objmode``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Numba's ``objmode`` temporarily returns from compiled Numba-CPU execution to the Python interpreter for one bounded block.
+MC/DC uses this boundary when compiled orchestration must invoke a Python service that Numba cannot compile and moving the complete operation outside the compiled driver would obscure its ownership or timing.
+
+Current uses include:
+
+- MPI collectives and particle-bank communication.
+- Wall-clock timing through ``MPI.Wtime``.
+- Progress and fatal diagnostics provided by ``mcdc.print_``.
+- Census-based HDF5 output performed between transport stages.
+
+Keep the ``objmode`` block as small and infrequent as possible because entering the interpreter interrupts compiled execution and adds conversion and dispatch overhead.
+Perform the surrounding numerical work in compiled code, and do not use ``objmode`` merely to avoid expressing a maintained numerical algorithm in Numba-compatible form.
+
+If a value produced in Python is used after the block, declare its Numba type on the context manager:
+
+.. code-block:: python
+
+ time_start = 0.0
+ with objmode(time_start="float64"):
+ time_start = MPI.Wtime()
+
+No output declaration is needed when the block only performs a side effect or mutates an array that was allocated before entering it:
+
+.. code-block:: python
+
+ local_total = np.array([particle_weight], dtype=np.float64)
+ global_total = np.zeros(1, dtype=np.float64)
+ with objmode():
+ MPI.COMM_WORLD.Allreduce(local_total, global_total, MPI.SUM)
+
+``objmode`` is a deliberate Numba-CPU escape hatch, not part of the GPU execution model.
+Code intended for Numba-GPU must keep the Python service outside device execution or provide a backend-specific implementation.
+
+Debug Python and Numba-CPU
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When a change fails, isolate the layer:
+
+#. **Python construction** -- confirm that the model compiles and the expected objects, IDs, offsets, and data are present.
+#. **Python transport** -- verify the algorithm and state mutation with JIT disabled.
+#. **Numba-CPU** -- resolve typing, unsupported-operation, and compiled-runtime failures.
+
+Resolve each layer before moving to the next.
+A passing Python test establishes behavior but does not establish Numba type compatibility.
+For example, investigate an unexpected physical result in Python transport and a Numba ``TypingError`` during Numba-CPU porting.
+
+Verify Numba-CPU Support
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Before considering the Numba-CPU implementation complete:
+
+- The model compiles into the expected packed representation.
+- Focused unit tests exercise the numerical behavior in Python.
+- The same tests or a representative regression case pass in Numba-CPU mode.
+- Python and Numba-CPU results agree within the test's numerical tolerance.
+- Existing examples still construct successfully when the public API or model compilation changed.
+- User and developer documentation describe the new behavior and any CPU limitation.
+
+Numba-GPU Development
+---------------------
+
+Numba-CPU is a valid final implementation when it satisfies the intended workloads and project requirements.
+Add Numba-GPU support as a later phase when those requirements call for accelerator execution.
+Begin this phase from a verified Numba-CPU implementation and retain its tests as the behavioral baseline.
+Read :doc:`../architecture/transport_execution` for MC/DC's GPU compilation and runtime architecture.
+
+Apply Additional GPU Constraints
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Numba-CPU supports Python and Numba features that may not be available in device code.
+For transport code that will execute on a GPU:
+
+- Use numerical control flow and operations established in nearby GPU-compatible transport modules.
+- Avoid Python exceptions as ordinary control flow.
+- Keep file access, printing, timing, MPI orchestration, and other host services outside device functions.
+- Do not use Numba ``objmode`` in a device path.
+- Replace CPU-only Numba features with device-compatible operations.
+- Keep target-specific atomics, memory operations, and scheduling behind the existing GPU adaptation layer.
+
+Use the GPU Program Handle
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In Numba-GPU execution, ``program`` is a Harmonize program handle rather than the ``simulation`` record itself.
+The GPU adaptation replaces ``util.access_simulation`` so shared transport functions can recover the device-resident simulation state without knowing how the handle stores it.
+Do not index ``program`` directly or assume its CPU representation.
+The same adaptation replaces ``util.local_array`` with device-local allocation, which is why shared code should use the utility consistently before GPU porting begins.
+
+For example, a GPU entry point recovers state from the handle before calling shared transport behavior:
+
+.. code-block:: python
+
+ def step(program: nb.uintp, particle_input: particle_gpu):
+ simulation = access_simulation(program)
+ data_ptr = access_data_ptr(program)
+ data = harmonize.array_from_ptr(data_ptr, shape, nb.float64)
+
+ particle_container = util.local_array(1, type_.particle)
+ particle_container[0] = particle_input
+ step_particle(particle_container, program, data)
+
+Separate Shared and GPU-Specific Code
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Keep the shared numerical operation in ``mcdc/transport`` when possible, and place only the required device adaptation under ``mcdc/code_factory/gpu``.
+Document why a GPU path differs and test its physical equivalence with Python and Numba-CPU.
+
+For example, keep reporting on the host while the numerical operation remains available to compiled transport:
+
+.. code-block:: python
+
+ @njit
+ def apply_survival_biasing(particle, survival_probability):
+ particle["w"] *= survival_probability
+
+
+ def report_survival_biasing(survival_probability):
+ print(f"Survival probability: {survival_probability}")
+
+Debug Numba-GPU
+^^^^^^^^^^^^^^^
+
+Start GPU diagnosis only after the Python and Numba-CPU tests pass.
+Then isolate device compilation, memory placement, atomics, and Harmonize scheduling.
+For example, investigate a device-link or unsupported-atomic error in this phase without reopening already verified CPU behavior.
+
+Verify Numba-GPU Support
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Before claiming Numba-GPU support:
+
+- The Python and Numba-CPU verification remains passing.
+- A supported GPU environment exercises every device path being claimed.
+- GPU results preserve the same physical behavior within appropriate numerical and statistical tolerances.
+- GPU-specific adaptations and limitations are documented.
+
+Use the :doc:`../../contributing/index` for repository commands, continuous-integration coverage, and regression-test options.
+For changes affecting public inputs, follow :doc:`../../contributing/example_validation`.
diff --git a/docs/source/developer_guide/index.rst b/docs/source/developer_guide/index.rst
new file mode 100644
index 000000000..24fb96233
--- /dev/null
+++ b/docs/source/developer_guide/index.rst
@@ -0,0 +1,36 @@
+.. _developer_guide:
+
+===============
+Developer Guide
+===============
+
+The Developer Guide explains how MC/DC works internally and how to extend or maintain its implementation.
+
+.. _development_areas:
+
+Development Areas
+-----------------
+
+MC/DC development spans two complementary areas.
+
+**Methods development** covers the implementation and evaluation of transport methods, numerical algorithms, and their model-facing data.
+This work primarily uses ``mcdc/object_`` and ``mcdc/transport``.
+
+**Framework development** covers the machinery that compiles models, generates runtime layers and accessors, configures execution, and supports CPU and GPU backends.
+This work primarily uses ``mcdc/code_factory``, ``mcdc/main.py``, and related execution infrastructure.
+
+Where to Go Next
+----------------
+
+- Read :doc:`architecture/index` to understand MC/DC's Python-first design and follow a transport model through simulation compilation, runtime preparation, shared transport algorithm, and the available execution modes.
+- Read :doc:`extending/index` when adding model fields, registered objects, polymorphic subtypes, or Numba-compatible transport behavior.
+- Read :doc:`documentation/index` when writing or reviewing project documentation.
+- Use :doc:`../contributing/index` for repository setup, development workflow, testing, and pull-request requirements.
+
+.. toctree::
+ :maxdepth: 1
+
+ architecture/index
+ extending/index
+ documentation/index
+ Contributing <../contributing/index>
diff --git a/docs/source/examples/c5g7_k_eigenvalue.rst b/docs/source/examples/c5g7_k_eigenvalue.rst
index 67eda2256..a054ef719 100644
--- a/docs/source/examples/c5g7_k_eigenvalue.rst
+++ b/docs/source/examples/c5g7_k_eigenvalue.rst
@@ -21,8 +21,8 @@ using MC/DC’s lattice and universe system.
Key concepts demonstrated:
-- **Multi-group materials** loaded from an external HDF5 library via
- ``mcdc.MaterialMG(library=...)``.
+- **Multigroup data** loaded from an external HDF5 library and used to construct
+ materials through ``mcdc.Material.multigroup(...)``.
- **Pin-cell universes** built from cylindrical fuel pins in square
moderator cells.
- **Lattice assemblies** that tile pin-cell universes into fuel
@@ -42,7 +42,7 @@ section (materials, pins, assemblies, core, source, tallies, settings).
Full Input
==========
-Click here to view the input file: `examples/c5g7/k-eigenvalue/input.py `_.
+Click here to view the input file: `examples/c5g7/k-eigenvalue/input.py `_.
The complete input used for this example is embedded below:
@@ -53,9 +53,9 @@ The complete input used for this example is embedded below:
How to Run
==========
-From the repository root run::
+From inside ``examples/c5g7/k-eigenvalue`` run::
- python examples/c5g7/k-eigenvalue/input.py
+ python input.py
Expected Output
===============
diff --git a/docs/source/examples/c5g7_transient.rst b/docs/source/examples/c5g7_transient.rst
index 5d4a5a025..d1f2a22c3 100644
--- a/docs/source/examples/c5g7_transient.rst
+++ b/docs/source/examples/c5g7_transient.rst
@@ -41,7 +41,7 @@ Refer to the embedded code below for the full implementation.
Full Input
==========
-Click here to view the input file: `examples/c5g7/transient/input.py `_.
+Click here to view the input file: `examples/c5g7/transient/input.py `_.
The complete input used for this example is embedded below:
@@ -52,9 +52,9 @@ The complete input used for this example is embedded below:
How to Run
==========
-From the repository root run::
+From inside ``examples/c5g7/transient`` run::
- python examples/c5g7/transient/input.py
+ python input.py
Expected Output
===============
diff --git a/docs/source/examples/fuel_array_packaged.rst b/docs/source/examples/fuel_array_packaged.rst
index 196b28f1a..c211ffa2f 100644
--- a/docs/source/examples/fuel_array_packaged.rst
+++ b/docs/source/examples/fuel_array_packaged.rst
@@ -91,7 +91,7 @@ Reference Solution
==================
No analytical reference. The geometry can be verified using MC/DC's
-built-in ``mcdc.visualize()`` function to render the CSG model.
+built-in ``simulation.visualize_model()`` method to render the CSG model.
Step-by-Step Walkthrough
========================
@@ -133,7 +133,7 @@ The assembly universe is placed twice using ``mcdc.Cell(..., fill=assembly)``:
- **Left** — translated to :math:`(-5, 0, 0)`.
- **Right** — translated to :math:`(+5, 0, 0)` and rotated 10° about :math:`y`.
-``set_root_universe()`` tells MC/DC these are the top-level cells.
+``simulation.set_model()`` tells MC/DC these are the top-level cells.
**4. Source, Tallies, Settings, and Run (lines 82–105)**
@@ -156,7 +156,7 @@ The ``active_bank_buffer`` accommodates fission-born particles.
:lineno-match:
Set ``visualize = True`` to render the CSG geometry with
-``mcdc.visualize()`` instead of running the transport.
+``simulation.visualize_model()`` instead of running the transport.
**What to try:**
@@ -167,7 +167,7 @@ Set ``visualize = True`` to render the CSG geometry with
Full Input
==========
-Click here to view the input file: `examples/fuel_array_packaged/input.py `_.
+Click here to view the input file: `examples/fuel_array_packaged/input.py `_.
The complete input used for this example is embedded below:
@@ -178,12 +178,12 @@ The complete input used for this example is embedded below:
How to Run
==========
-From the repository root run::
+From inside ``examples/fuel_array_packaged`` run::
- python examples/fuel_array_packaged/input.py
+ python input.py
Expected Output
===============
An HDF5 mesh tally and optional visualization images produced by the
-``mcdc.visualize()`` helper when run with visualization enabled.
+``simulation.visualize_model()`` method when run with visualization enabled.
diff --git a/docs/source/examples/hybrid_multigroup.rst b/docs/source/examples/hybrid_multigroup.rst
new file mode 100644
index 000000000..f8ba3074e
--- /dev/null
+++ b/docs/source/examples/hybrid_multigroup.rst
@@ -0,0 +1,37 @@
+.. _example_hybrid_multigroup:
+
+===========================
+Hybrid Multigroup Transport
+===========================
+
+This fixed-source problem combines native H-1 composition with one-group neutron multigroup data on the same material.
+The 1 eV source lies inside the multigroup interval from 0.1 to 10 eV, while the 1 MeV source uses native H-1 physics outside that interval.
+Multigroup scattering represents the outgoing group at its 5.05 eV midpoint.
+The tally uses physical energy boundaries because particle energy remains in eV during hybrid transport.
+The example requires ``MCDC_LIB`` to identify a native-data library containing ``H1-293.6K.h5``.
+
+Full Input
+----------
+
+.. literalinclude:: ../../../examples/hybrid_multigroup/input.py
+ :language: python
+ :linenos:
+
+Post-processing
+---------------
+
+.. literalinclude:: ../../../examples/hybrid_multigroup/process-output.py
+ :language: python
+ :linenos:
+
+How to Run
+----------
+
+From inside ``examples/hybrid_multigroup``:
+
+.. code-block:: sh
+
+ python input.py
+ python process-output.py
+
+The transport calculation writes ``hybrid_multigroup.h5`` and the post-processing script writes ``hybrid_multigroup_flux.png``.
diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst
index c5fc2feaa..99719be5d 100644
--- a/docs/source/examples/index.rst
+++ b/docs/source/examples/index.rst
@@ -18,12 +18,21 @@ the code shipped in-tree. A **Step-by-Step Walkthrough** section breaks
the input into annotated blocks, and a **What to try** box suggests
parameter changes for further exploration.
+If you are learning MC/DC for the first time, complete the
+:doc:`../user_guide/getting_started/first_simulation` before using these examples as
+templates. The :doc:`../user_guide/simulation_lifecycle` explains the workflow
+shared by every input. For individual API details, consult the
+:doc:`../reference/index`.
+
Basic Examples
--------------
.. toctree::
:maxdepth: 1
+ slab_shielding
+ hybrid_multigroup
+ iterative_source_reweighting
kobayashi_dog_leg
kobayashi_td
diff --git a/docs/source/examples/iterative_source_reweighting.rst b/docs/source/examples/iterative_source_reweighting.rst
new file mode 100644
index 000000000..c1c6c91f5
--- /dev/null
+++ b/docs/source/examples/iterative_source_reweighting.rst
@@ -0,0 +1,48 @@
+.. _example_iterative_source_reweighting:
+
+============================
+Iterative Source Reweighting
+============================
+
+This one-group fixed-source example demonstrates partial model updates across
+several runs of one :class:`mcdc.Simulation`. A homogeneous slab contains
+symmetric left and right sources. Each iteration changes only their relative
+probabilities, compiles a complete new snapshot, and writes a separate output
+file.
+
+The three source mixtures are 20/80, 50/50, and 80/20. Because the geometry and
+material are symmetric, the 20/80 and 80/20 flux profiles should approximately
+mirror one another, while the 50/50 profile should be approximately symmetric
+about the slab midpoint.
+
+Full Input
+----------
+
+.. literalinclude:: ../../../examples/iterative_source_reweighting/input.py
+ :language: python
+ :linenos:
+
+Post-processing
+---------------
+
+.. literalinclude:: ../../../examples/iterative_source_reweighting/process-output.py
+ :language: python
+ :linenos:
+
+The post-processing script overlays the three spatial flux profiles, prints
+the integrated flux in each half of the slab, and reports two symmetry
+comparisons.
+
+How to Run
+----------
+
+From inside ``examples/iterative_source_reweighting``:
+
+.. code-block:: sh
+
+ python input.py
+ python process-output.py
+
+The calculation writes ``source_mix_left_20.h5``,
+``source_mix_left_50.h5``, and ``source_mix_left_80.h5``. Post-processing
+writes ``iterative_source_comparison.png``.
diff --git a/docs/source/examples/kobayashi_dog_leg.rst b/docs/source/examples/kobayashi_dog_leg.rst
index b7869c5f0..bb0d6699f 100644
--- a/docs/source/examples/kobayashi_dog_leg.rst
+++ b/docs/source/examples/kobayashi_dog_leg.rst
@@ -102,7 +102,7 @@ This section walks through the input file block by block.
:linenos:
:lineno-match:
-Two mono-energetic multi-group materials are created:
+Two mono-energetic multigroup materials are created:
``m`` for the shield (:math:`\Sigma_c = \Sigma_s = 0.05`) and
``m_void`` for the dog-leg channel (:math:`10^{-4}` total).
@@ -156,7 +156,7 @@ An isotropic, uniformly distributed source fills the
- A uniform :math:`60 \times 100 \times 60` mesh tally records scalar flux.
- 1 000 source particles in 2 batches (increase for production).
- Implicit capture prevents particles from being absorbed prematurely.
-- ``mcdc.run()`` launches the simulation.
+- ``simulation.run()`` launches the simulation.
**What to try:**
@@ -167,7 +167,7 @@ An isotropic, uniformly distributed source fills the
Full Input
==========
-Click here to view the input file: `examples/kobayashi/input.py `_.
+Click here to view the input file: `examples/kobayashi/input.py `_.
The complete input used for this example is embedded below:
@@ -178,9 +178,9 @@ The complete input used for this example is embedded below:
How to Run
==========
-From the repository root run::
+From inside ``examples/kobayashi`` run::
- python examples/kobayashi/input.py
+ python input.py
Expected Output
===============
diff --git a/docs/source/examples/kobayashi_td.rst b/docs/source/examples/kobayashi_td.rst
index f55f216af..9f1cd0781 100644
--- a/docs/source/examples/kobayashi_td.rst
+++ b/docs/source/examples/kobayashi_td.rst
@@ -52,7 +52,7 @@ population over time.
Full Input
==========
-Click here to view the input file: `examples/kobayashi-TD/input.py `_.
+Click here to view the input file: `examples/kobayashi-TD/input.py `_.
The complete input used for this example is embedded below:
@@ -63,9 +63,9 @@ The complete input used for this example is embedded below:
How to Run
==========
-From the repository root run::
+From inside ``examples/kobayashi-TD`` run::
- python examples/kobayashi-TD/input.py
+ python input.py
Expected Output
===============
diff --git a/docs/source/examples/moving_pellet.rst b/docs/source/examples/moving_pellet.rst
index af70b5a34..56c8cab18 100644
--- a/docs/source/examples/moving_pellet.rst
+++ b/docs/source/examples/moving_pellet.rst
@@ -169,13 +169,13 @@ captures the fission rate as the pellet moves.
- Change the pellet velocities to create different trajectories.
- Set ``visualize = True`` to watch the geometry evolve with
- ``mcdc.visualize(..., time=...)``.
+ ``simulation.visualize_model(..., time=...)``.
- Compare with ``moving_source`` to see source motion vs. geometry motion.
Full Input
==========
-Click here to view the input file: `examples/moving_pellet/input.py `_.
+Click here to view the input file: `examples/moving_pellet/input.py `_.
The complete input used for this example is embedded below:
@@ -186,9 +186,9 @@ The complete input used for this example is embedded below:
How to Run
==========
-From the repository root run::
+From inside ``examples/moving_pellet`` run::
- python examples/moving_pellet/input.py
+ python input.py
Expected Output
===============
diff --git a/docs/source/examples/moving_source.rst b/docs/source/examples/moving_source.rst
index 99263cab6..b1f43dcd2 100644
--- a/docs/source/examples/moving_source.rst
+++ b/docs/source/examples/moving_source.rst
@@ -145,7 +145,7 @@ generates an animated GIF.
Full Input
==========
-Click here to view the input file: `examples/moving_source/input.py `_.
+Click here to view the input file: `examples/moving_source/input.py `_.
The complete input used for this example is embedded below:
@@ -156,9 +156,9 @@ The complete input used for this example is embedded below:
How to Run
==========
-From the repository root run::
+From inside ``examples/moving_source`` run::
- python examples/moving_source/input.py
+ python input.py
Expected Output
===============
diff --git a/docs/source/examples/slab_shielding.rst b/docs/source/examples/slab_shielding.rst
new file mode 100644
index 000000000..0973be407
--- /dev/null
+++ b/docs/source/examples/slab_shielding.rst
@@ -0,0 +1,38 @@
+.. _example_slab_shielding:
+
+==============
+Slab Shielding
+==============
+
+This one-group fixed-source problem introduces the complete MC/DC workflow
+with two materials, two slab cells, an isotropic source, and a mesh flux
+tally. The :doc:`../user_guide/getting_started/first_simulation` guide explains the input
+step by step.
+
+Full Input
+----------
+
+.. literalinclude:: ../../../examples/slab_shielding/input.py
+ :language: python
+ :linenos:
+
+Post-processing
+---------------
+
+.. literalinclude:: ../../../examples/slab_shielding/process-output.py
+ :language: python
+ :linenos:
+
+How to Run
+----------
+
+From inside ``examples/slab_shielding``:
+
+.. code-block:: sh
+
+ python input.py
+ python process-output.py
+
+The transport calculation writes ``slab_shielding.h5``. The
+post-processing script reads its flux tally and writes
+``slab_shielding_flux.png``.
diff --git a/docs/source/examples/sphere_in_cube.rst b/docs/source/examples/sphere_in_cube.rst
index 8c6754b00..f1da9876b 100644
--- a/docs/source/examples/sphere_in_cube.rst
+++ b/docs/source/examples/sphere_in_cube.rst
@@ -132,7 +132,7 @@ Implicit capture is enabled to keep particles alive longer.
Full Input
==========
-Click here to view the input file: `examples/sphere_in_cube/input.py `_.
+Click here to view the input file: `examples/sphere_in_cube/input.py `_.
The complete input used for this example is embedded below:
@@ -143,9 +143,9 @@ The complete input used for this example is embedded below:
How to Run
==========
-From the repository root run::
+From inside ``examples/sphere_in_cube`` run::
- python examples/sphere_in_cube/input.py
+ python input.py
Expected Output
===============
diff --git a/docs/source/images/developer_guide/architecture/architecture_flow.png b/docs/source/images/developer_guide/architecture/architecture_flow.png
new file mode 100644
index 000000000..7ff99d0d0
Binary files /dev/null and b/docs/source/images/developer_guide/architecture/architecture_flow.png differ
diff --git a/docs/source/images/theory/gpu_comp/amd_flow.png b/docs/source/images/developer_guide/architecture/numba_gpu_amd_flow.png
similarity index 100%
rename from docs/source/images/theory/gpu_comp/amd_flow.png
rename to docs/source/images/developer_guide/architecture/numba_gpu_amd_flow.png
diff --git a/docs/source/images/theory/gpu_comp/nvcc_flow.png b/docs/source/images/developer_guide/architecture/numba_gpu_nvidia_flow.png
similarity index 100%
rename from docs/source/images/theory/gpu_comp/nvcc_flow.png
rename to docs/source/images/developer_guide/architecture/numba_gpu_nvidia_flow.png
diff --git a/docs/source/images/developer_guide/architecture/runtime_data_layout.png b/docs/source/images/developer_guide/architecture/runtime_data_layout.png
new file mode 100644
index 000000000..549b7a800
Binary files /dev/null and b/docs/source/images/developer_guide/architecture/runtime_data_layout.png differ
diff --git a/docs/source/images/home/DOE_logo.png b/docs/source/images/home/DOE_logo.png
deleted file mode 100644
index 010e34c54..000000000
Binary files a/docs/source/images/home/DOE_logo.png and /dev/null differ
diff --git a/docs/source/images/home/NNSA_Logo.png b/docs/source/images/home/NNSA_Logo.png
deleted file mode 100644
index 9098d9c50..000000000
Binary files a/docs/source/images/home/NNSA_Logo.png and /dev/null differ
diff --git a/docs/source/images/home/SU.png b/docs/source/images/home/SU.png
deleted file mode 100644
index ffbd1ac22..000000000
Binary files a/docs/source/images/home/SU.png and /dev/null differ
diff --git a/docs/source/images/home/berkeley-logo.png b/docs/source/images/home/berkeley-logo.png
deleted file mode 100644
index fd24a4f6d..000000000
Binary files a/docs/source/images/home/berkeley-logo.png and /dev/null differ
diff --git a/docs/source/images/home/carre.png b/docs/source/images/home/carre.png
deleted file mode 100644
index 585588409..000000000
Binary files a/docs/source/images/home/carre.png and /dev/null differ
diff --git a/docs/source/images/home/carre2.png b/docs/source/images/home/carre2.png
deleted file mode 100644
index 9de26a319..000000000
Binary files a/docs/source/images/home/carre2.png and /dev/null differ
diff --git a/docs/source/images/home/cement-logo-1.png b/docs/source/images/home/cement-logo-1.png
deleted file mode 100644
index 4d74d24fa..000000000
Binary files a/docs/source/images/home/cement-logo-1.png and /dev/null differ
diff --git a/docs/source/images/home/kobayashi.gif b/docs/source/images/home/kobayashi.gif
deleted file mode 100644
index 55a6601db..000000000
Binary files a/docs/source/images/home/kobayashi.gif and /dev/null differ
diff --git a/docs/source/images/home/kobayishi-red.png b/docs/source/images/home/kobayishi-red.png
deleted file mode 100644
index b53808400..000000000
Binary files a/docs/source/images/home/kobayishi-red.png and /dev/null differ
diff --git a/docs/source/images/home/mcdc.svg b/docs/source/images/home/mcdc.svg
deleted file mode 100644
index 366ea9230..000000000
--- a/docs/source/images/home/mcdc.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/source/images/home/ncsu-logo.png b/docs/source/images/home/ncsu-logo.png
deleted file mode 100644
index 6d9014ad4..000000000
Binary files a/docs/source/images/home/ncsu-logo.png and /dev/null differ
diff --git a/docs/source/images/home/nd-logo.png b/docs/source/images/home/nd-logo.png
deleted file mode 100644
index 2cc62f262..000000000
Binary files a/docs/source/images/home/nd-logo.png and /dev/null differ
diff --git a/docs/source/images/home/osu-logo.png b/docs/source/images/home/osu-logo.png
deleted file mode 100644
index 6813dd469..000000000
Binary files a/docs/source/images/home/osu-logo.png and /dev/null differ
diff --git a/docs/source/images/home/psaapiii.png b/docs/source/images/home/psaapiii.png
deleted file mode 100644
index 3fcf650db..000000000
Binary files a/docs/source/images/home/psaapiii.png and /dev/null differ
diff --git a/docs/source/images/home/psaapiv.png b/docs/source/images/home/psaapiv.png
deleted file mode 100644
index 21518de42..000000000
Binary files a/docs/source/images/home/psaapiv.png and /dev/null differ
diff --git a/docs/source/images/home/smr-mcdc.png b/docs/source/images/home/smr-mcdc.png
deleted file mode 100644
index ea5d398e6..000000000
Binary files a/docs/source/images/home/smr-mcdc.png and /dev/null differ
diff --git a/docs/source/images/home/ucsd-logo.png b/docs/source/images/home/ucsd-logo.png
deleted file mode 100644
index 0a6ad6d4a..000000000
Binary files a/docs/source/images/home/ucsd-logo.png and /dev/null differ
diff --git a/docs/source/images/home/vanderbilt-logo.png b/docs/source/images/home/vanderbilt-logo.png
deleted file mode 100644
index 8cf0cc4ea..000000000
Binary files a/docs/source/images/home/vanderbilt-logo.png and /dev/null differ
diff --git a/docs/source/images/user/af_slab_1.png b/docs/source/images/user/af_slab_1.png
deleted file mode 100644
index 3d54fb7ad..000000000
Binary files a/docs/source/images/user/af_slab_1.png and /dev/null differ
diff --git a/docs/source/images/user/af_slab_2.png b/docs/source/images/user/af_slab_2.png
deleted file mode 100644
index e27253f56..000000000
Binary files a/docs/source/images/user/af_slab_2.png and /dev/null differ
diff --git a/docs/source/images/user/c5g7.png b/docs/source/images/user/c5g7.png
deleted file mode 100644
index 1169d1344..000000000
Binary files a/docs/source/images/user/c5g7.png and /dev/null differ
diff --git a/docs/source/images/user/dragon.gif b/docs/source/images/user/dragon.gif
deleted file mode 100644
index 0d5bf12d8..000000000
Binary files a/docs/source/images/user/dragon.gif and /dev/null differ
diff --git a/docs/source/images/user/j_slab_1.png b/docs/source/images/user/j_slab_1.png
deleted file mode 100644
index 46c730d5f..000000000
Binary files a/docs/source/images/user/j_slab_1.png and /dev/null differ
diff --git a/docs/source/images/user/j_slab_2.png b/docs/source/images/user/j_slab_2.png
deleted file mode 100644
index 065d71a1a..000000000
Binary files a/docs/source/images/user/j_slab_2.png and /dev/null differ
diff --git a/docs/source/images/user/kobayashi-white.png b/docs/source/images/user/kobayashi-white.png
deleted file mode 100644
index 29993cf68..000000000
Binary files a/docs/source/images/user/kobayashi-white.png and /dev/null differ
diff --git a/docs/source/images/user/sf_slab_1.png b/docs/source/images/user/sf_slab_1.png
deleted file mode 100644
index e420defba..000000000
Binary files a/docs/source/images/user/sf_slab_1.png and /dev/null differ
diff --git a/docs/source/images/user/sf_slab_2.png b/docs/source/images/user/sf_slab_2.png
deleted file mode 100644
index b599bf2a6..000000000
Binary files a/docs/source/images/user/sf_slab_2.png and /dev/null differ
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 8a908ff7f..e9d2d3a75 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,96 +1,96 @@
-.. MC/DC documentation master file
+:html_theme.sidebar_secondary.remove: true
======================================
MC/DC: Monte Carlo Dynamic Code
======================================
MC/DC is an open-source, Python-based Monte Carlo radiation transport software
-package that combines rapid methods development with scalable execution on modern
-high-performance computing systems. It supports execution across CPUs and GPUs
-while providing a flexible environment for developing and testing new transport
-algorithms.
-
-MC/DC is intended for researchers developing new Monte Carlo transport methods,
-including variance reduction techniques, sensitivity and uncertainty quantification
-methods, as well as high-performance computing algorithms. It also provides
-an accessible platform for students learning Monte Carlo radiation transport methods
-and modern code development.
-
-MC/DC supports continuous-energy and multi-group neutron transport calculations,
-including fixed-source and eigenvalue simulations on constructive solid geometry
-(CSG) models. For continuous-energy transport, MC/DC converts
-`ACE `_-format nuclear data libraries into its native
-`HDF5 `_ format. Photon, electron, proton,
-and other charged-particle transport capabilities are currently under development as
-part of the ongoing expansion of MC/DC into a comprehensive multi-particle radiation
-transport software package.
-
-MC/DC's Python interface enables rapid prototyping and iterative development,
-while its `Numba `_-based compilation framework delivers high
-performance without sacrificing portability.
-`Harmonize `_ provides a GPU execution
-framework, and `MPI4Py `_ enables
-distributed-memory parallelism across large HPC systems.
-In addition to desktop and workstation systems, MC/DC has been demonstrated on
-large heterogeneous supercomputers, including
-`Lassen `_
-(IBM POWER9 and NVIDIA Volta V100) and
-`Tuolumne `_ (AMD
-MI300A APU).
-
-MC/DC was initiated by the Center for Exascale Monte Carlo Neutron Transport
-(`CEMeNT `_), a Focused Investigatory Center of
-the Predictive Science Academic Alliance Program–III
-(`PSAAP-III `_). Development is now led by the Center for
-Advancing the Radiation Resilience of Electronics
-(`CARRE `_), a Predictive Simulation Center of
-`PSAAP-IV `_.
-
-MC/DC is released under the
-`BSD 3-Clause `_
-license and welcomes community contributions through
-`GitHub `_.
+package for rapid methods development and scalable execution on CPUs, GPUs,
+and modern high-performance computing systems. New to the project? Begin with
+:doc:`What is MC/DC? `.
+-------------
+Documentation
+-------------
+Choose the path that best matches what you want to accomplish.
-.. admonition:: Recommended citation
- :class: tip
+.. grid:: 1 1 2 2
+ :gutter: 3
- Morgan, Joanna Piper, et al. "Monte Carlo/Dynamic Code (MC/DC): An accelerated
- Python package for fully transient neutron transport and rapid methods development."
- Journal of Open Source Software 9.96 (2024): 6415.
- https://joss.theoj.org/papers/10.21105/joss.06415
+ .. grid-item-card:: :octicon:`book;2em` User Guide
+ :link: user_guide/index
+ :link-type: doc
+ :link-alt: Get started and learn how to use MC/DC
+ :class-card: sd-card-hover
+ :text-align: center
-------------------------------
-Contents
-------------------------------
+ Learn what MC/DC is, install it, run your first simulation, and follow
+ task-oriented guidance for everyday use.
-.. toctree::
- :maxdepth: 1
- :caption: User Documentation
+ +++
+ Start here :octicon:`arrow-right`
- install
- user/index
- pythonapi/index
- examples/index
+ .. grid-item-card:: :octicon:`beaker;2em` Theory and Methods
+ :link: theory/index
+ :link-type: doc
+ :link-alt: Study the transport theory and numerical methods in MC/DC
+ :class-card: sd-card-hover
+ :text-align: center
-.. toctree::
- :maxdepth: 1
- :caption: Developer Documentation
+ Study the transport theory, numerical algorithms, and acceleration
+ methods implemented in MC/DC.
- contribution_guide/index
- theory/index
+ +++
+ Explore the theory :octicon:`arrow-right`
-.. toctree::
- :maxdepth: 1
- :caption: References
+ .. grid-item-card:: :octicon:`code-square;2em` API Reference
+ :link: reference/python_api/index
+ :link-type: doc
+ :link-alt: Look up MC/DC Python classes and methods
+ :class-card: sd-card-hover
+ :text-align: center
+
+ Look up the classes, methods, arguments, and attributes available through
+ MC/DC's Python interface.
+
+ +++
+ Browse the API :octicon:`arrow-right`
+
+ .. grid-item-card:: :octicon:`tools;2em` Developer Guide
+ :link: developer_guide/index
+ :link-type: doc
+ :link-alt: Understand and contribute to MC/DC development
+ :class-card: sd-card-hover
+ :text-align: center
- publications
+ Understand MC/DC's architecture, extend its implementation, and prepare
+ contributions to the project.
-.. sidebar-links::
- :caption: External Links
- :pypi: mcdc
- :github:
+ +++
+ Develop MC/DC :octicon:`arrow-right`
- CARRE
- CEMeNT
+More resources
+--------------
+
+- Learn from complete input decks in :doc:`examples/index`.
+- Follow the contribution workflow in :doc:`contributing/index`.
+- Find citation and publication information in :doc:`project/index`.
+
+.. admonition:: Recommended citation
+ :class: tip
+
+ Morgan, Joanna Piper, et al. "Monte Carlo/Dynamic Code (MC/DC): An accelerated
+ Python package for fully transient neutron transport and rapid methods development."
+ *Journal of Open Source Software* 9.96 (2024): 6415.
+ https://doi.org/10.21105/joss.06415
+
+.. toctree::
+ :hidden:
+ :maxdepth: 2
+
+ user_guide/index
+ theory/index
+ reference/index
+ developer_guide/index
+ project/index
diff --git a/docs/source/install.rst b/docs/source/install.rst
deleted file mode 100644
index 9750d3733..000000000
--- a/docs/source/install.rst
+++ /dev/null
@@ -1,267 +0,0 @@
-.. _install:
-
-===================
-Installation Guide
-===================
-
-Whether installing MC/DC as a user or from source as a developer,
-we recommend doing so using an environment manager like venv or conda.
-This will avoid the need for any admin access and keep dependencies clean.
-
-In general, :ref:`creating-a-venv-environment` and :ref:`installing-with-pip` is easier and recommended.
-Creating a conda environment and :ref:`installing-with-conda` is more robust and reliable, but is also more difficult.
-A conda environment is necessary to install MC/DC on LLNL's Lassen machine.
-
-
-
-.. _creating-a-venv-environment:
-
----------------------------
-Creating a venv environment
----------------------------
-
-Python `virtual environments `_ are the easy and
-recommended way to get MC/DC operating on personal machines as well as HPCs;
-all you need is a working Python version with venv installed.
-Particularly on HPCs, using a Python virtual environment is convenient because
-system admins will have already configured venv and the pip within it to load packages and dependencies
-from the proper sources.
-HPCs often use a module system, so before doing anything else,
-``module load python/``.
-
-A python virtual environment can (usually) be created using
-
-.. code-block:: sh
-
- python -m venv
-
-Once you have created a venv, you will need to activate it
-
-.. code-block:: sh
-
- source /bin/activate
-
-and will need to do so every time a new terminal instance is launched.
-Once your environment is active, you can move on to :ref:`installing-with-pip`.
-
-
-.. _installing-with-pip:
-
--------------------
-Installing with pip
--------------------
-Assuming you have a working Python environment, you can install using pip.
-Doing so within an active venv or conda environment avoids the need for any admin access
-and keeps dependencies clean.
-
-If you would like to run MC/DC as published in the main branch *and*
-do not need to develop in MC/DC, you can install from PyPI:
-
-.. code-block:: sh
-
- pip install mcdc
-
-----------------------
-Installing from Source
-----------------------
-If you would like to execute a version of MC/DC from a specific branch or
-*do* plan to develop in MC/DC, you'll need to install from source:
-
-#. Clone the MC/DC repo: ``git clone https://github.com/CEMeNT-PSAAP/MCDC.git``
-#. Go to your new MC/DC directory: ``cd MCDC``
-#. Install the package from your MC/DC files: ``pip install -e .``
-
-This should install all needed dependencies without a hitch.
-The `-e` flag installs MC/DC as an editable package, meaning that any changes
-you make to the MC/DC source files, including checking out a different
-branch, will be immediately reflected without needing to do any re-installation.
-
-.. _installing-with-conda:
-
---------------------------
-Installing MC/DC via conda
---------------------------
-
-Conda is the most robust (works even on bespoke systems) option to install MC/DC.
-`Conda `_ is an open source package and environment management system
-that runs on Windows, macOS, and Linux. It allows for easy installing and switching between multiple
-versions of software packages and their dependencies.
-Conda is really useful on systems with non-standard hardware (e.g. not x86 CPUs) like Lassen, where
-mpi4py is often the most troublesome dependency.
-
-First, ``conda`` should be installed with `Miniconda `_
-or `Anaconda `_. HPC instructions:
-
-`Dane `_ (LLNL, x86_64),
-
-.. code-block:: sh
-
- wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
- bash Miniconda3-latest-Linux-x86_64.sh
-
-
-`Lassen `_ (LLNL, IBM Power9),
-
-.. code-block:: sh
-
- wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-ppc64le.sh
- bash Miniconda3-latest-Linux-ppc64le.sh
-
-
-Then create and activate a new conda environment called *mcdc-env* in
-which to install MC/DC. MC/DC supports Python ``>3.10``;
-we recommend Python 3.11:
-
-.. code-block:: sh
-
- conda create -n mcdc-env python=3.11
- conda activate mcdc-env
-
-Then, MC/DC can be installed from source by first cloning the MC/DC repository:
-
-.. code-block:: sh
-
- git clone https://github.com/CEMeNT-PSAAP/MCDC.git
- cd MCDC
-
-then using the the ``install.sh`` within it. The install script will
-build MC/DC and all of its dependencies and execute any necessary patches.
-This has been tested on Quartz, Dane, Tioga, Lassen, and Apple M2.
-The ``install.sh`` script **will fail outside of a conda environment**.
-
-On HPC machines, the script will install mpi4py
-`from source `_.
-This means that all appropriate modules must be loaded prior to executing.
-
-On Quartz, the default modules are sufficient (``intel-classic`` and ``mvapich2``).
-On Lassen, ``module load gcc/8 cuda/11.8``. Then,
-
-.. code-block:: sh
-
- bash install.sh --hpc
-
-
-On local machines, mpi4py will be installed using conda,
-
-.. code-block:: sh
-
- bash install.sh
-
-To confirm that everything is properly installed, execute ``pytest`` from the MCDC directory.
-
-.. _installing-via-containers:
-
---------------------------
-Installing via Containers
---------------------------
-
-For container-based installation and execution, see :doc:`user/container`.
-
-.. toctree::
- :maxdepth: 1
-
- user/container
-
-.. _install-data-library:
-
------------------------------------------
-Generating a Data Library from ACE Files
------------------------------------------
-
-MC/DC ships with a conversion tool in ``tools/data_library_generator/`` that reads
-standard ACE-format nuclear data files and writes them into MC/DC's per-nuclide
-HDF5 format. This is the primary path for creating CE libraries.
-
-**Prerequisites:**
-
-.. code-block:: sh
-
- pip install ACEtk h5py numpy tqdm
-
-You also need a set of ACE files (e.g., from `NJOY `_ or
-an ENDF/B distribution).
-
-**Environment variables:**
-
-.. list-table::
- :widths: 25 75
- :header-rows: 1
-
- * - Variable
- - Description
- * - ``MCDC_ACELIB``
- - Path to the directory containing your ACE files.
- * - ``MCDC_LIB``
- - Path to the output directory where MC/DC HDF5 files will be written.
-
-**Running the generator:**
-
-.. code-block:: sh
-
- export MCDC_ACELIB=/path/to/ace/files
- export MCDC_LIB=/path/to/mcdc/library
-
- cd tools/data_library_generator
- python generate.py
-
-By default the tool only converts nuclides that do not already have a corresponding
-HDF5 file in ``$MCDC_LIB``. Use ``--rewrite`` to regenerate all files, or
-``--verbose`` for detailed per-nuclide output:
-
-.. code-block:: sh
-
- python generate.py --rewrite --verbose
-
-The generator processes each ACE file as follows:
-
-#. Reads the ACE header to determine nuclide identity (Z, A, isomeric state)
- and temperature.
-#. Extracts the principal cross-section block (energy grid, elastic, capture,
- fission, inelastic channels) and writes them as HDF5 datasets grouped by
- reaction type (elastic scattering, capture, inelastic scattering, fission).
-#. Extracts angular distributions (tabulated cosine PDFs) and energy
- distributions (level scattering, evaporation, Maxwellian, Kalbach-Mann,
- N-body phase space, tabulated outgoing energy) for each reaction channel.
-#. For fissionable nuclides, extracts prompt/delayed :math:`\nu(E)` multiplicities,
- delayed neutron precursor fractions, decay constants, and energy spectra.
-
-The resulting HDF5 file (e.g., ``U235-293.6K.h5``) is ready for use with ``mcdc.Material()``.
-
-
----------------------------------
-GPU Operability (MC/DC+Harmonize)
----------------------------------
-
-MC/DC supports most of its Numba enabled features for GPU compilation and execution.
-When targeting GPUs, MC/DC uses the `Harmonize `_ library as its GPU runtime, a.k.a. the thing that actually executes MC/DC functions.
-How Harmonize works gets a little involved, but in short,
-Harmonize acts as MC/DC's GPU runtime by using two major scheduling schemes: an event schedular similar to those implemented in OpenMC and Shift, plus a novel scheduler.
-For more information on Harmonize and how we compile MC/DC with it, see this `TOMACs article describing the async scheduler `_ or our publications in American Nuclear Society: Math and Comp Meeting in 2025.
-
-If you encounter problems with configuration, please file `Github issues promptly `_ ,
-especially when on supported super computers (LLNL's `Tioga `_, `El Capitan `_, and `Lassen `_).
-
-.. rubric:: Nvidia GPUs
-
-To compile and execute MC/DC on Nvidia GPUs first ensure you have the `Harmonize prerecs `_ (CUDA=11.8, Numba>=0.60.0) and a working MC/DC version >=0.10.0. Then,
-
-#. Clone the harmonize repo: ``git clone https://github.com/CEMeNT-PSAAP/harmonize.git``
-#. Install into the proper Python env: ``pip install -e .``
-
-Operability should now be enabled.
-
-.. _install-amd-gpus:
-
-.. rubric:: AMD GPUs
-
-The prerequisites for AMD operability are slightly more complex and
-require a patch to Numba to allow for AMD target triple LLVM-IR.
-It is recommended that this is done within a Python venv virtual environment.
-
-To compile and execute MC/DC on AMD GPUs first ensure you have the `Harmonize prerecs `_ (ROCm=6.0.0, Numba>=0.60.0) and a working MC/DC version >=0.11.0. Then,
-
-#. Patch Numba to enable HIP (`instructions here `_)
-#. Clone harmonize and `switch to the AMD `_ branch with ``git switch amd_event_interop_revamp``
-#. Install Harmonize with ``pip install -e .`` or using `Harmonize's install script `_
-
-Operability should now be enabled.
diff --git a/docs/source/project/index.rst b/docs/source/project/index.rst
new file mode 100644
index 000000000..fbbd72b4f
--- /dev/null
+++ b/docs/source/project/index.rst
@@ -0,0 +1,18 @@
+.. _project:
+
+=======
+Project
+=======
+
+Project information collects material about MC/DC as a research software
+project rather than instructions for using or developing the code.
+
+Use :doc:`publications` to find overview and method-specific references for
+published work. Read :doc:`release_policy` for the project's release cadence
+and handling of bug-fix releases.
+
+.. toctree::
+ :maxdepth: 1
+
+ publications
+ release_policy
diff --git a/docs/source/publications.rst b/docs/source/project/publications.rst
similarity index 96%
rename from docs/source/publications.rst
rename to docs/source/project/publications.rst
index eaf986a88..2c770eac3 100644
--- a/docs/source/publications.rst
+++ b/docs/source/project/publications.rst
@@ -1,4 +1,4 @@
-.. _pubs:
+.. _publications:
=============
Publications
@@ -53,8 +53,6 @@ Hybrid Monte Carlo Transport
- Pasmann, Sam, et al. "A quasi–Monte Carlo method with Krylov linear solvers for multigroup neutron transport simulations." Nuclear Science and Engineering 197.6 (2023): 1159-1173. https://www.tandfonline.com/doi/abs/10.1080/00295639.2022.2143704
-- Pasmann, Sam, et al. "A quasi–Monte Carlo method with Krylov linear solvers for multigroup neutron transport simulations." Nuclear Science and Engineering 197.6 (2023): 1159-1173. https://www.tandfonline.com/doi/abs/10.1080/00295639.2022.2143704
-
- Pasmann, Samuel, et al. “iQMC: Iterative Quasi-Monte Carlo with Krylov Linear Solvers for k-Eigenvalue Neutron Transport Simulations.” In International Conference on Mathematics and Computational Methods Applied to Nuclear Science and Engineering. Niagara Falls, Ontario, Canada (2023). Preprint: https://arxiv.org/abs/2306.11600
- Pasmann, Samuel, Ilham Variansyah, and R. G. McClarren. "Convergent transport source iteration calculations with Quasi-Monte Carlo." Transactions of the American Nuclear Society 124 (2021): 192-195.
@@ -80,4 +78,3 @@ Miscellany
- Variansyah, Ilham, and Ryan G. McClarren. “High-fidelity treatment for object movement in time-dependent Monte Carlo transport simulations.” In International Conference on Mathematics and Computational Methods Applied to Nuclear Science and Engineering. Niagara Falls, Ontario, Canada (2023). Preprint: https://doi.org/10.48550/arXiv.2305.07641
- Variansyah, Ilham, and Ryan G. McClarren. “An effective initial particle sampling technique for Monte Carlo reactor transient simulations.” In International Conference on Mathematics and Computational Methods Applied to Nuclear Science and Engineering. Niagara Falls, Ontario, Canada (2023). Preprint: https://doi.org/10.48550/arXiv.2305.07646
-
diff --git a/docs/source/project/release_policy.rst b/docs/source/project/release_policy.rst
new file mode 100644
index 000000000..bfd7c0d2f
--- /dev/null
+++ b/docs/source/project/release_policy.rst
@@ -0,0 +1,25 @@
+.. _release_policy:
+
+==============
+Release Policy
+==============
+
+MC/DC follows `Semantic Versioning `_ and maintains a human-readable release history in `CHANGELOG.md `_.
+
+Minor Releases
+--------------
+
+MC/DC plans one minor release in each three-month seasonal cycle.
+These releases collect compatible features, improvements, and fixes that have passed the project's required review and validation.
+For planning convenience, these cycles follow the Northern Hemisphere meteorological seasons: winter (Dec-Feb), spring (Mar-May), summer (Jun-Aug), and autumn/fall (Sep-Nov).
+
+The seasonal schedule is a target rather than a reason to release unverified work.
+A minor release may be delayed when additional testing, documentation, or integration work is needed; it may also be brought forward when the accumulated changes are substantial and users would benefit from earlier availability.
+
+Patch Releases
+--------------
+
+Bug fixes are not held until the next seasonal minor release.
+Once a fix has passed review and the relevant validation and release checks, MC/DC publishes a patch release as soon as practical.
+
+Published versions and release notes are available from the `MC/DC releases page `_.
diff --git a/docs/source/pythonapi/index.rst b/docs/source/pythonapi/index.rst
deleted file mode 100644
index f3a8ed46f..000000000
--- a/docs/source/pythonapi/index.rst
+++ /dev/null
@@ -1,111 +0,0 @@
-.. _pythonapi:
-
-================
-Input Definition
-================
-
-Full API documentation.
-
-
-Defining materials
-------------------
-
-.. autosummary::
- :toctree: generated
- :nosignatures:
- :template: omcclass.rst
-
- mcdc.Material
- mcdc.MaterialMG
-
-
-Defining geometry
------------------
-
-.. autosummary::
- :toctree: generated
- :nosignatures:
- :template: omcclass.rst
-
- mcdc.Cell
- mcdc.Lattice
- mcdc.Surface
- mcdc.Universe
-
-Defining meshes
----------------
-
-.. autosummary::
- :toctree: generated
- :nosignatures:
- :template: omcclass.rst
-
- mcdc.MeshUniform
- mcdc.MeshStructured
-
-Defining sources
-----------------
-
-.. autosummary::
- :toctree: generated
- :nosignatures:
- :template: omcclass.rst
-
- mcdc.Source
-
-Defining tallies
-----------------
-
-.. autosummary::
- :toctree: generated
- :nosignatures:
- :template: omcclass.rst
-
- mcdc.Tally
-
-Defining simulation settings
------------------------------
-
-Settings are configured by assigning attributes on the ``mcdc.settings`` singleton.
-Key attributes include:
-
-- ``mcdc.settings.N_particle`` — Number of particles.
-- ``mcdc.settings.N_batch`` — Number of batches.
-- ``mcdc.settings.rng_seed`` — RNG seed.
-- ``mcdc.settings.output_name`` — Output file name (default: ``"output"``).
-- ``mcdc.settings.time_boundary`` — Time boundary.
-
-Methods:
-
-- ``mcdc.settings.set_eigenmode(N_inactive=..., N_active=..., k_init=...)`` — Enable k-eigenvalue mode.
-- ``mcdc.settings.set_time_census(time, tally_frequency=...)`` — Set time census parameters.
-- ``mcdc.settings.set_source_file(source_file_name)`` — Load source particles from file.
-
-Defining techniques
--------------------
-
-Techniques are enabled by calling methods on the ``mcdc.simulation`` singleton:
-
-- ``mcdc.simulation.implicit_capture(active=True)``
-- ``mcdc.simulation.global_weight_roulette(weight_threshold=0.0, weight_target=1.0)``
-- ``mcdc.simulation.population_control(active=True)``
-- ``mcdc.simulation.weighted_emission(active=True, weight_target=1.0)``
-- ``mcdc.simulation.weight_windows(weight_windows, mesh=None, energy=None)``
-
-Running
--------
-
-.. autosummary::
- :toctree: generated
- :nosignatures:
- :template: omcfunction.rst
-
- mcdc.run
-
-
-
-
-
-
-
-
diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst
new file mode 100644
index 000000000..023846db3
--- /dev/null
+++ b/docs/source/reference/index.rst
@@ -0,0 +1,20 @@
+.. _reference:
+
+=============
+API Reference
+=============
+
+Reference documentation provides precise descriptions of MC/DC's public
+interfaces. Use it when you know what object or operation you need and want its
+accepted arguments, attributes, or behavior.
+
+The :doc:`python_api/index` documents the classes used to construct, configure,
+compile, visualize, and run a simulation.
+
+.. toctree::
+ :maxdepth: 1
+
+ Python API
+
+For guided model construction, begin with the
+:doc:`../user_guide/getting_started/first_simulation` instead.
diff --git a/docs/source/reference/python_api/index.rst b/docs/source/reference/python_api/index.rst
new file mode 100644
index 000000000..d23984b4d
--- /dev/null
+++ b/docs/source/reference/python_api/index.rst
@@ -0,0 +1,137 @@
+.. _python_api:
+
+==========
+Python API
+==========
+
+The MC/DC public API is centered on :class:`mcdc.Simulation`.
+A simulation owns the model geometry and material, sources, tallies, settings, transport techniques, and runtime state needed for one calculation.
+
+Build the model with the public objects listed below, attach its root cells, sources, and tallies to a simulation, and then visualize or run that simulation:
+
+.. code-block:: python
+
+ simulation = mcdc.Simulation(name="Example")
+ simulation.set_model([cell])
+ simulation.set_sources([source])
+ simulation.set_tallies([tally])
+ simulation.settings.N_particle = 10_000
+ simulation.run()
+
+The complete public interfaces and additional examples are documented on each linked API page.
+For a task-oriented explanation of how these objects move through construction, compilation, execution, and output, see :doc:`../../user_guide/simulation_lifecycle`.
+
+
+Simulation
+----------
+
+.. autosummary::
+ :toctree: generated
+ :nosignatures:
+ :template: simulationclass.rst
+
+ mcdc.Simulation
+
+
+Model building blocks
+---------------------
+
+Materials
+^^^^^^^^^
+
+Materials describe the physical media that fill cells.
+A :ref:`native composition ` connects :class:`mcdc.Material` to MC/DC's data libraries, while optional particle-specific data augments native interaction data or supports specialized and reduced transport treatments.
+:class:`mcdc.NeutronMultigroupData` represents neutron energy with discrete groups and stores groupwise macroscopic cross sections and related production data.
+:meth:`mcdc.Material.multigroup` provides its convenient material interface.
+
+.. autosummary::
+ :toctree: generated
+ :nosignatures:
+ :template: omcclass.rst
+
+ mcdc.Material
+ mcdc.NeutronMultigroupData
+
+
+Geometry
+^^^^^^^^
+
+Surfaces bound spatial regions, cells pair those regions with materials or universes, and universes and lattices organize repeated geometry.
+
+.. autosummary::
+ :toctree: generated
+ :nosignatures:
+ :template: omcclass.rst
+
+ mcdc.Surface
+ mcdc.Cell
+ mcdc.Universe
+ mcdc.Lattice
+
+
+Sources
+^^^^^^^
+
+Sources describe the distribution of the initial particle population in the simulation.
+
+.. autosummary::
+ :toctree: generated
+ :nosignatures:
+ :template: omcclass.rst
+
+ mcdc.Source
+
+
+Tallies
+^^^^^^^
+
+Tallies define the quantities to score and the filters over which those scores are accumulated.
+
+.. autosummary::
+ :toctree: generated
+ :nosignatures:
+ :template: omcclass.rst
+
+ mcdc.Tally
+
+
+Meshes
+^^^^^^
+
+Meshes provide spatial bins for mesh-filtered tallies and transport techniques.
+
+.. autosummary::
+ :toctree: generated
+ :nosignatures:
+ :template: omcclass.rst
+
+ mcdc.MeshUniform
+ mcdc.MeshStructured
+
+
+Configuration and execution
+---------------------------
+
+Simulation settings
+^^^^^^^^^^^^^^^^^^^
+
+Each :class:`mcdc.Simulation` owns its settings at ``simulation.settings``.
+Settings control particle histories, batches, random-number generation, transport modes, census times, particle banks, output, and GPU execution.
+Specialized modes are configured through methods such as ``simulation.settings.set_eigenmode(...)`` and ``simulation.settings.set_time_census(...)``.
+See :class:`mcdc.Simulation` for the complete settings interface.
+
+Transport techniques
+^^^^^^^^^^^^^^^^^^^^
+
+Transport techniques are grouped under ``simulation.technique``.
+For example, enable implicit capture with ``simulation.technique.implicit_capture()`` or configure weight windows with ``simulation.technique.weight_windows(...)``.
+See :class:`mcdc.Simulation` for the ownership model and examples.
+
+Compiling and running
+^^^^^^^^^^^^^^^^^^^^^
+
+Calling ``simulation.run()`` compiles the current Python object graph when needed, executes particle transport, and writes the configured output.
+``simulation.visualize_model(...)`` similarly compiles when needed before rendering the model.
+Use ``simulation.compile()`` when an explicit compiled snapshot is required before either operation.
+
+The internal compilation and packing stages are documented in :doc:`../../developer_guide/architecture/simulation_compilation` and :doc:`../../developer_guide/architecture/runtime_data_layout`.
diff --git a/docs/source/theory/ana.rst b/docs/source/theory/ana.rst
deleted file mode 100644
index 7ed8fcc65..000000000
--- a/docs/source/theory/ana.rst
+++ /dev/null
@@ -1,53 +0,0 @@
-.. _ana:
-
-============================
-Acceleration and Abstraction
-============================
-
-MC/DC employs a layered compilation and abstraction strategy that allows the same Python source code to target CPUs (pure Python or Numba JIT) and GPUs (via Harmonize) without modification to the transport algorithms.
-
-Execution Modes
----------------
-
-MC/DC supports three execution modes, selected at runtime with the ``--mode`` flag:
-
-- **Python mode** (``--mode=python``): Transport kernels run as interpreted Python. Useful for debugging and rapid prototyping.
-- **Numba mode** (``--mode=numba``): Transport kernels are just-in-time compiled to native machine code using `Numba `_. This provides significant speedup (often 100x or more) at the cost of an initial compilation overhead of 15–80 seconds.
-- **Numba debug mode** (``--mode=numba_debug``): JIT compilation with extra debug instrumentation (bounds checking, full tracebacks, type inference logging). Slower, but produces actionable error messages.
-
-The ``--target`` flag selects the hardware target: ``cpu`` (default) or ``gpu``.
-
-Numba Object Generation
------------------------
-
-MC/DC's simulation state (materials, surfaces, cells, tallies, settings, particle banks, etc.) is defined as annotated Python classes in ``mcdc/object_/``.
-At startup, the **Numba object generator** (``mcdc/code_factory/numba_objects_generator.py``) converts these class hierarchies into NumPy structured array dtypes:
-
-#. Class annotations (type hints) are read and mapped to NumPy dtypes.
-#. Polymorphic objects (e.g., different surface types) are represented using ``parent_ID``/``child_ID`` fields that index into typed sub-arrays.
-#. All simulation data is flattened into a single contiguous NumPy buffer (``data``), enabling efficient access from JIT-compiled code.
-#. Getter and setter access functions (in ``mcdc/mcdc_get/`` and ``mcdc/mcdc_set/``) are auto-generated so that JIT-compiled transport kernels can read and write simulation state without Python object overhead.
-
-This approach allows the transport code to be written in natural, object-oriented Python while still achieving the performance of flat array access in compiled mode.
-
-GPU Portability
----------------
-
-When targeting GPUs, MC/DC uses the `Harmonize `_ library as its GPU runtime.
-The GPU program builder (``mcdc/code_factory/gpu/program_builder.py``) constructs a Harmonize ``RuntimeSpec`` that includes:
-
-- **Global state**: the simulation structured array and the flat data buffer.
-- **Device functions**: the MC/DC transport kernels, compiled from Python to device code via Numba.
-- **Scheduling strategy**: either event-based (``--gpu_strategy=event``) or asynchronous (``--gpu_strategy=async``, Nvidia only).
-
-The compilation pipeline differs by vendor:
-
-- **Nvidia**: Python → PTX (via ``numba.cuda``) → relocatable device code (via ``nvcc``) → linked shared library.
-- **AMD**: Python → LLVM-IR (via a `Numba-HIP patch `_) → relocatable device code (via ``hipcc`` / ``clang``) → linked shared library.
-
-For a detailed walkthrough of the compilation flow, see the :ref:`theory_gpu` section.
-
-For more details, see:
-
-- J. P. Morgan, I. Variansyah, B. Cuneo, T. S. Palmer, and K. E. Niemeyer. "Performance Portable Monte Carlo Neutron Transport in MCDC via Numba." Preprint DOI 10.48550/arXiv.2306.07847.
-- B. Cuneo and M. Bailey. "Divergence Reduction in Monte Carlo Neutron Transport with On-GPU Asynchronous Scheduling." *ACM TOMACS* (2023). DOI 10.1145/3626957.
diff --git a/docs/source/theory/cont_energy.rst b/docs/source/theory/continuous_energy.rst
similarity index 72%
rename from docs/source/theory/cont_energy.rst
rename to docs/source/theory/continuous_energy.rst
index 7fe8aa35f..92eecef1b 100644
--- a/docs/source/theory/cont_energy.rst
+++ b/docs/source/theory/continuous_energy.rst
@@ -1,11 +1,11 @@
-.. _cont_energy:
+.. _continuous_energy:
=================
Continuous Energy
=================
MC/DC supports continuous energy (CE) neutron transport using pointwise nuclear data libraries.
-In CE mode, cross sections are represented as energy-dependent tabulated data rather than multi-group averages, enabling higher-fidelity simulations.
+In CE mode, cross sections are represented as energy-dependent tabulated data rather than multigroup averages, enabling higher-fidelity simulations.
Data Libraries
--------------
@@ -40,7 +40,8 @@ Collision Physics
CE collision processing implements full center-of-mass (COM) kinematics:
-- **Elastic scattering** (MT-2): Thermal motion of the target nucleus is sampled from a Maxwellian distribution parameterized by :math:`\beta = \sqrt{A m / (2 k_B T)}`, where :math:`A` is the mass ratio. Rejection sampling is used for the relative speed.
+- **Elastic scattering** (MT-2): Thermal motion of the target nucleus is sampled from a Maxwellian distribution parameterized by :math:`\beta = \sqrt{A m / (2 k_B T)}`, where :math:`A` is the mass ratio.
+ Rejection sampling is used for the relative speed.
- **Inelastic scattering**: Multiple MT channels with tabulated energy-angle distributions (Kalbach-Mann, evaporation, Maxwellian, N-body, level scattering).
- **Capture**: Particle is absorbed; implicit capture can be enabled as a variance reduction technique.
- **Fission**: Secondary particles are emitted using :math:`\nu(E)/k_\text{eff}` scaling, with prompt and delayed components sampled separately.
@@ -55,9 +56,8 @@ Relativistic particle speed is computed as:
Generating a Data Library from ACE Files
-----------------------------------------
-MC/DC ships with a conversion tool in ``tools/data_library_generator/`` that reads
-standard ACE-format nuclear data files and writes them into MC/DC's per-nuclide
-HDF5 format. This is the primary path for creating CE libraries.
+MC/DC ships with a conversion tool in ``tools/data_library_generator/neutron/`` that reads standard ACE-format nuclear data files and writes them into MC/DC's per-nuclide HDF5 format.
+This is the primary path for creating CE libraries.
**Prerequisites:**
@@ -65,8 +65,7 @@ HDF5 format. This is the primary path for creating CE libraries.
pip install ACEtk h5py numpy tqdm
-You also need a set of ACE files (e.g., from `NJOY `_ or
-an ENDF/B distribution).
+You also need a set of ACE files from a source such as `NJOY `_ or an ENDF/B distribution.
**Environment variables:**
@@ -88,12 +87,11 @@ an ENDF/B distribution).
export MCDC_ACELIB=/path/to/ace/files
export MCDC_LIB=/path/to/mcdc/library
- cd tools/data_library_generator
+ cd tools/data_library_generator/neutron
python generate.py
-By default the tool only converts nuclides that do not already have a corresponding
-HDF5 file in ``$MCDC_LIB``. Use ``--rewrite`` to regenerate all files, or
-``--verbose`` for detailed per-nuclide output:
+By default, the tool converts only nuclides without a corresponding HDF5 file in ``$MCDC_LIB``.
+Use ``--rewrite`` to regenerate all files or ``--verbose`` for detailed per-nuclide output:
.. code-block:: sh
@@ -101,26 +99,18 @@ HDF5 file in ``$MCDC_LIB``. Use ``--rewrite`` to regenerate all files, or
The generator processes each ACE file as follows:
-#. Reads the ACE header to determine nuclide identity (Z, A, isomeric state)
- and temperature.
-#. Extracts the principal cross-section block (energy grid, elastic, capture,
- fission, inelastic channels) and writes them as HDF5 datasets grouped by
- reaction type (elastic scattering, capture, inelastic scattering, fission).
-#. Extracts angular distributions (tabulated cosine PDFs) and energy
- distributions (level scattering, evaporation, Maxwellian, Kalbach-Mann,
- N-body phase space, tabulated outgoing energy) for each reaction channel.
-#. For fissionable nuclides, extracts prompt/delayed :math:`\nu(E)` multiplicities,
- delayed neutron precursor fractions, decay constants, and energy spectra.
+#. Reads the ACE header to determine nuclide identity and temperature.
+#. Extracts the principal cross-section block and writes HDF5 datasets grouped by reaction type.
+#. Extracts angular and energy distributions for each reaction channel.
+#. Extracts prompt and delayed :math:`\nu(E)` data, precursor fractions, decay constants, and energy spectra for fissionable nuclides.
-The resulting HDF5 file (e.g., ``U235-293.6K.h5``) is ready for use with
-``mcdc.Material()``.
+The resulting HDF5 file, such as ``U235-293.6K.h5``, is ready for use with ``mcdc.Material()``.
Using CE Materials in an Input Deck
------------------------------------
-Once the library is generated, set the ``MCDC_LIB`` environment variable and
-define materials with ``mcdc.Material()``:
+Once the library is generated, set the ``MCDC_LIB`` environment variable and define materials with ``mcdc.Material()``:
.. code-block:: python3
@@ -141,9 +131,7 @@ Note on External Data Sources
------------------------------
MC/DC's internal HDF5 format is independent of the original data source.
-While the shipped tool converts from **ACE format**, users with data in other
-formats (e.g., OpenMC HDF5 nuclear data) can write their own converter
-following the same HDF5 schema used by ``generate.py``.
+The shipped tool converts ACE data, but users can implement converters for other formats by following the same HDF5 schema used by ``generate.py``.
The key HDF5 structure expected by MC/DC is:
@@ -175,6 +163,5 @@ The key HDF5 structure expected by MC/DC is:
├── nu_delayed/
└── delayed_neutron/ ...
-A converter from OpenMC's ``IncidentNeutron`` HDF5 format to this schema is
-a planned future addition. Contributions are welcome — see
-`Issue #333 `_.
+A converter from OpenMC's ``IncidentNeutron`` HDF5 format to this schema is a planned future addition.
+Contributions are welcome through `Issue #333 `_.
diff --git a/docs/source/theory/cont_movement.rst b/docs/source/theory/continuous_movement.rst
similarity index 99%
rename from docs/source/theory/cont_movement.rst
rename to docs/source/theory/continuous_movement.rst
index 0b484115d..c943c7113 100644
--- a/docs/source/theory/cont_movement.rst
+++ b/docs/source/theory/continuous_movement.rst
@@ -1,4 +1,4 @@
-.. _cont_movement:
+.. _continuous_movement:
===================
Continuous Movement
diff --git a/docs/source/theory/domain_decomp.rst b/docs/source/theory/domain_decomposition.rst
similarity index 98%
rename from docs/source/theory/domain_decomp.rst
rename to docs/source/theory/domain_decomposition.rst
index b0b0edb7d..be19d0f25 100644
--- a/docs/source/theory/domain_decomp.rst
+++ b/docs/source/theory/domain_decomposition.rst
@@ -1,4 +1,4 @@
-.. _dd:
+.. _domain_decomposition:
====================
Domain Decomposition
diff --git a/docs/source/theory/geometry.rst b/docs/source/theory/geometry.rst
index 062523ef8..e3e1435af 100644
--- a/docs/source/theory/geometry.rst
+++ b/docs/source/theory/geometry.rst
@@ -151,12 +151,14 @@ for a full-core lattice example.
Root Universe
^^^^^^^^^^^^^
-When using universes or lattices, the top-level cell collection is
-registered as the **root universe**:
+When using universes or lattices, attach the top-level cell collection
+to a simulation. MC/DC registers these cells in the simulation's
+**root universe**:
.. code-block:: python3
- mcdc.simulation.set_root_universe(cells=[cell_left, cell_right])
+ simulation = mcdc.Simulation()
+ simulation.set_model([cell_left, cell_right])
Geometry Visualization
@@ -167,13 +169,15 @@ geometries before running a full transport simulation:
.. code-block:: python3
- mcdc.visualize(
- "xz", # projection plane
+ simulation.visualize_model(
+ vis_plane="xz", # projection plane
y=0.0, # slice position
x=[-10.0, 10.0], # plot range
z=[-5.0, 5.0],
pixels=(400, 400),
colors={fuel: "red", water: "blue"},
+ time=[0.0],
+ save_as=None,
)
This produces a pixel map showing which material fills each pixel.
@@ -182,9 +186,18 @@ argument:
.. code-block:: python3
- mcdc.visualize(..., time=np.linspace(0, 9, 19), save_as="geo_animation")
+ simulation.visualize_model(
+ vis_plane="xz",
+ y=0.0,
+ x=[-10.0, 10.0],
+ z=[-5.0, 5.0],
+ pixels=(400, 400),
+ colors={fuel: "red", water: "blue"},
+ time=np.linspace(0, 9, 19),
+ save_as="geo_animation",
+ )
-For details on moving surfaces and sources, see :ref:`cont_movement`.
+For details on moving surfaces and sources, see :ref:`continuous_movement`.
Moving Surfaces
@@ -199,4 +212,4 @@ Any surface can be given a piecewise-constant velocity using the
MC/DC solves for the exact intersection of a particle trajectory with
the moving surface — no time-step discretization error is introduced.
-For the mathematical formulation, see :ref:`cont_movement`.
+For the mathematical formulation, see :ref:`continuous_movement`.
diff --git a/docs/source/theory/index.rst b/docs/source/theory/index.rst
index dd40c302f..15803d817 100644
--- a/docs/source/theory/index.rst
+++ b/docs/source/theory/index.rst
@@ -1,13 +1,15 @@
.. _theory:
-============
-Theory Guide
-============
+==================
+Theory and Methods
+==================
-We provided a brief theory guide into the methods, algorithms, and compilation
-schemes in MC/DC.
+Theory and Methods explains the physical models, mathematical formulations, and
+numerical algorithms implemented in MC/DC. Use these pages to understand why a
+method works; use the :doc:`../user_guide/index` to learn how to configure it
+and the :doc:`../developer_guide/index` for software implementation details.
-New to Monte Carlo transport? Start with :ref:`mc_basics` for the
+New to Monte Carlo transport? Start with :ref:`monte_carlo` for the
fundamentals and :ref:`geometry` for how MC/DC represents problem domains.
Then explore the advanced topics below.
@@ -20,7 +22,7 @@ Fundamentals
.. toctree::
:maxdepth: 1
- mc_basics
+ monte_carlo
geometry
k_eigenvalue
@@ -31,19 +33,17 @@ Advanced Methods
:maxdepth: 1
variance_reduction
- ana
iqmc
- ww
- uq
+ weight_windows
+ uncertainty_quantification
compressed_sensing
-Implementation
---------------
+Transport Models
+----------------
.. toctree::
:maxdepth: 1
- gpu
- cont_energy
- domain_decomp
- cont_movement
+ continuous_energy
+ domain_decomposition
+ continuous_movement
diff --git a/docs/source/theory/k_eigenvalue.rst b/docs/source/theory/k_eigenvalue.rst
index 521261d26..cad27a655 100644
--- a/docs/source/theory/k_eigenvalue.rst
+++ b/docs/source/theory/k_eigenvalue.rst
@@ -53,7 +53,7 @@ Users configure eigenmode via:
.. code-block:: python3
- mcdc.settings.set_eigenmode(N_inactive=50, N_active=200, k_init=1.0)
+ simulation.settings.set_eigenmode(N_inactive=50, N_active=200, k_init=1.0)
- ``N_inactive`` — Cycles discarded for fission source convergence.
- ``N_active`` — Cycles used for tally accumulation.
diff --git a/docs/source/theory/mc_basics.rst b/docs/source/theory/monte_carlo.rst
similarity index 98%
rename from docs/source/theory/mc_basics.rst
rename to docs/source/theory/monte_carlo.rst
index 1b76a680a..911cb3af0 100644
--- a/docs/source/theory/mc_basics.rst
+++ b/docs/source/theory/monte_carlo.rst
@@ -1,4 +1,4 @@
-.. _mc_basics:
+.. _monte_carlo:
============================
Monte Carlo Transport Basics
@@ -118,7 +118,7 @@ The relative standard deviation
- :ref:`variance_reduction` — implicit capture, weight roulette,
population control.
-- :ref:`ww` — weight windows.
+- :ref:`weight_windows` — weight windows.
- :ref:`iqmc` — quasi-Monte Carlo methods for :math:`O((\log N)^d / N)`
convergence.
diff --git a/docs/source/theory/uq.rst b/docs/source/theory/uncertainty_quantification.rst
similarity index 98%
rename from docs/source/theory/uq.rst
rename to docs/source/theory/uncertainty_quantification.rst
index 96e07f39d..52d7a64de 100644
--- a/docs/source/theory/uq.rst
+++ b/docs/source/theory/uncertainty_quantification.rst
@@ -1,4 +1,4 @@
-.. _uq:
+.. _uncertainty_quantification:
==========================
Uncertainty Quantification
diff --git a/docs/source/theory/variance_reduction.rst b/docs/source/theory/variance_reduction.rst
index 84a33183c..ace885bb9 100644
--- a/docs/source/theory/variance_reduction.rst
+++ b/docs/source/theory/variance_reduction.rst
@@ -10,8 +10,8 @@ MC/DC provides several **variance reduction** (VR) techniques that
reduce the statistical uncertainty per particle history without
introducing bias.
-All techniques below are activated through the ``mcdc.simulation``
-interface.
+All techniques below are configured on an explicit
+:class:`mcdc.Simulation` instance.
Implicit Capture
-----------------
@@ -33,7 +33,7 @@ effective in highly absorbing media.
.. code-block:: python3
- mcdc.simulation.implicit_capture()
+ simulation.technique.implicit_capture()
.. note::
@@ -59,7 +59,10 @@ preserving the expected weight (unbiased).
.. code-block:: python3
- mcdc.simulation.global_weight_roulette(weight_threshold=0.25, weight_target=1.0)
+ simulation.technique.global_weight_roulette(
+ weight_threshold=0.25,
+ weight_target=1.0,
+ )
``weight_threshold`` and ``weight_target`` should be chosen so that
:math:`w_{\text{thresh}} < w_{\text{target}}`; a common ratio is
@@ -85,7 +88,7 @@ This reduces the variance of the fission source weight distribution.
.. code-block:: python3
- mcdc.simulation.weighted_emission(active=True, weight_target=1.0)
+ simulation.technique.weighted_emission(active=True, weight_target=1.0)
Population Control
@@ -102,7 +105,7 @@ rouletting low-weight ones, targeting a uniform weight close to
.. code-block:: python3
- mcdc.simulation.population_control()
+ simulation.technique.population_control()
Population control is typically combined with a time census
(``set_time_census``) that checkpoints the particle population at
@@ -117,7 +120,7 @@ time-dependent) target weights and bounds. They combine splitting and
roulette to focus computational effort in regions of high importance.
MC/DC supports both user-defined and automatically generated weight
-windows. See :ref:`ww` for a full description of the available
+windows. See :ref:`weight_windows` for a full description of the available
strategies (``WW_USER`` and ``WW_PREVIOUS``) and modification schemes
(``WW_MIN`` and ``WW_WOLLABER``).
@@ -130,16 +133,19 @@ setup might use:
.. code-block:: python3
- mcdc.simulation.implicit_capture()
- mcdc.simulation.global_weight_roulette(weight_threshold=0.25, weight_target=1.0)
+ simulation.technique.implicit_capture()
+ simulation.technique.global_weight_roulette(
+ weight_threshold=0.25,
+ weight_target=1.0,
+ )
For time-dependent fission problems:
.. code-block:: python3
- mcdc.simulation.implicit_capture()
- mcdc.simulation.weighted_emission(active=True, weight_target=1.0)
- mcdc.simulation.population_control()
+ simulation.technique.implicit_capture()
+ simulation.technique.weighted_emission(active=True, weight_target=1.0)
+ simulation.technique.population_control()
The order of activation does not matter — MC/DC applies them in the
correct transport-physics order internally.
diff --git a/docs/source/theory/ww.rst b/docs/source/theory/weight_windows.rst
similarity index 98%
rename from docs/source/theory/ww.rst
rename to docs/source/theory/weight_windows.rst
index 33701163e..d61f40d12 100644
--- a/docs/source/theory/ww.rst
+++ b/docs/source/theory/weight_windows.rst
@@ -1,4 +1,4 @@
-.. _ww:
+.. _weight_windows:
===============
Weight Windows
diff --git a/docs/source/user/first_mcdc.rst b/docs/source/user/first_mcdc.rst
deleted file mode 100644
index 5c1d1ad92..000000000
--- a/docs/source/user/first_mcdc.rst
+++ /dev/null
@@ -1,409 +0,0 @@
-.. _first_mcdc:
-
-
-======================
-First MC/DC Simulation
-======================
-
-This guide presupposes you are familiar with modeling nuclear systems using a Monte Carlo method.
-If you are completely new, we suggest checking out `OpenMC's theory guide `_ as most the basic underlying algorithms and core concepts are the same.
-Our input decks and keyword phrases are designed so that if you are familiar with tools like OpenMC or MCNP, you should be able to get up and running quickly.
-
-While this guide is a great place to start, the next best place to look when getting started are our ``MCDC/examples`` or ``MCDC/test`` directories.
-Run a few problems there, change a few inputs around, and keep looking around until you get the general hang of what we are doing.
-Believe it or not, there is a method to all this madness.
-If you find yourself with errors you really don't know what to do with, take look at our `GitHub issues page `_.
-If it looks like you are the first to have a given problem feel free to submit a new ticket!
-
-A note on testing:
-Just because something seems right doesn't mean it is.
-Care must be taken to ensure that you are running the problem you think you are.
-The software only knows what you tell it.
-
-MC/DC Workflow
---------------
-
-MC/DC uses an ``input`` -> ``run`` -> ``post-process`` workflow, where users
-
-#. build input decks using scripts that import ``mcdc`` as a package and call functions to build geometries, tally meshes, and set other simulation parameters,
-#. define a runtime sequence in the terminal to execute the ``input`` script (terminal operations are required for MPI calls),
-#. export the results from ``.h5`` files and use the MC/DC visualizer or tools like ``matplotlib`` to view results.
-
-Building an Input Script
-------------------------
-
-Building an input deck can be a complicated and nuanced process. Depending on the type of simulation you need to build, you could end up touching most of the functions in MC/DC, or very few.
-Again, the best way to start building input decks is to look at what we have already done in the ``MCDC/examples`` or ``MCDC/test`` directories.
-To see more on the available input functions, look through the :doc:`../pythonapi/index` section.
-
-As an example, we walk through building the input for the ``MCDC/test/regression/slab_absorbium`` problem, which simulates a three-region, purely absorbing, mono-energetic slab wall.
-
-We start with our imports:
-
-.. code-block:: python3
-
- import numpy as np
-
- import mcdc
-
-You may require more packages depending on the methods you are constructing, but most of what you need will be in these two.
-Now, we define the materials for the problem:
-
-.. code-block:: python3
-
- # Set materials
- m1 = mcdc.MaterialMG(capture=np.array([1.0]))
- m2 = mcdc.MaterialMG(capture=np.array([1.5]))
- m3 = mcdc.MaterialMG(capture=np.array([2.0]))
-
-In this problem we only have mono-energetic capture, but MC/DC has support for multi-group (capture, scatter, fission) and continuous energy (capture, scatter, fission).
-Multi-group materials are created with ``mcdc.MaterialMG``; for example, a 3-group capture cross section would be ``capture=np.array([1.0, 1.1, 0.8])``.
-Continuous-energy materials are created with ``mcdc.Material``.
-
-If you are a member of CEMeNT, we have internal repositories containing the data required for continuous-energy simulation.
-Unfortunately due to export controls we can not publicly distribute this data.
-If you are looking for cross-section data to plug into MC/DC, we recommend you look at OpenMC or `NJOY `_.
-
-After setting material data, we define the problem space by setting up surfaces with their boundary conditions.
-If no boundary condition is defined, the surface is assumed to be internal (``boundary_condition="none"``).
-Surfaces are created using class methods on ``mcdc.Surface`` (e.g., ``PlaneX``, ``PlaneY``, ``PlaneZ``, ``Sphere``, ``CylinderZ``).
-
-.. code-block:: python3
-
- # Set surfaces
- s1 = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
- s2 = mcdc.Surface.PlaneZ(z=2.0)
- s3 = mcdc.Surface.PlaneZ(z=4.0)
- s4 = mcdc.Surface.PlaneZ(z=6.0, boundary_condition="vacuum")
-
-Remember that the radiation transport equation is a 7-dimensional integro-differential equation,
-so it's possible your problem will need both initial and boundary conditions.
-While we have tried to include warnings and errors if an ill-posed problem is detected,
-we cannot forecast all the ways in which things might go haywire.
-For transient simulations, initial conditions are assumed to be 0 everywhere.
-
-We create problem geometry using cells, which are defined by the surfaces that constrain them and the material that fills them.
-The ``+/-`` convention is used to indicate whether the cell volume is outside (+) or inside (-) a given surface.
-For example, below, the first cell is filled with material m2 and is positive with respect to s1, negative with respect to s2.
-This corresponds to being bound on the left by s1 and on the right by s2.
-Cells are created with ``mcdc.Cell``, using the ``region`` and ``fill`` keyword arguments.
-
-.. code-block:: python3
-
- mcdc.Cell(region=+s1 & -s2, fill=m2)
- mcdc.Cell(region=+s2 & -s3, fill=m3)
- mcdc.Cell(region=+s3 & -s4, fill=m1)
-
-We define a uniform isotropic source throughout the domain:
-
-.. code-block:: python3
-
- mcdc.Source(z=[0.0, 6.0], isotropic=True, energy_group=0)
-
-Next we set tallies and specify the specific parameters of interest. Here, we're interested in the space-averaged flux
-and collision rate. A mesh is created first, then a mesh-filtered ``Tally`` is constructed on that mesh.
-Direction bins can also be specified on the tally.
-Regardless of problem specifics, particles are simulated through all space, direction, and time;
-the tally definitions are used to indicate in which dimensions a record of particle behavior should be kept.
-Available tracklength scores include ``"flux"``, ``"density"``, ``"collision"``, ``"capture"``, and ``"fission"``.
-Current tallies can be attached to either a surface or a cell.
-Current scoring uses the surface-crossing estimator in both cases.
-For explicit surface filters, supported score is ``"current-net"``.
-For cell filters, supported scores are ``"current-net"``, ``"current-in"``, and ``"current-out"``.
-The ``"current-in"`` and ``"current-out"`` scores are positive partial currents; ``"current-net"`` keeps the sign
-of the crossing direction.
-
-.. code-block:: python3
-
- # Tally: cell-average fluxes and collisions
- mesh = mcdc.MeshStructured(z=np.linspace(0.0, 6.0, 61))
- mcdc.Tally(
- mesh=mesh,
- scores=["flux", "collision"],
- mu=np.linspace(-1.0, 1.0, 32 + 1),
- )
-
- # Tally: current crossing a cell boundary
- mcdc.Tally(
- cell=my_cell,
- scores=["current-net", "current-in", "current-out"],
- )
-
-Next we set simulation settings. The only required setting is the number of particles.
-Settings are configured by assigning attributes on the ``mcdc.settings`` singleton.
-Additional settings include, for example, the cycles to use for a k-eigenvalue problem
-(via ``mcdc.settings.set_eigenmode(...)``) or the output file name.
-
-.. code-block:: python3
-
- mcdc.settings.N_particle = 1000
-
-Finally, execute the problem.
-
-.. code-block:: python3
-
- mcdc.run()
-
-Put together, our example ``input.py`` file:
-
-.. code-block:: python3
-
- import numpy as np
- import mcdc
-
- # =============================================================================
- # Set model
- # =============================================================================
- # Three slab layers with different purely-absorbing materials
-
- # Set materials
- m1 = mcdc.MaterialMG(capture=np.array([1.0]))
- m2 = mcdc.MaterialMG(capture=np.array([1.5]))
- m3 = mcdc.MaterialMG(capture=np.array([2.0]))
-
- # Set surfaces
- s1 = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
- s2 = mcdc.Surface.PlaneZ(z=2.0)
- s3 = mcdc.Surface.PlaneZ(z=4.0)
- s4 = mcdc.Surface.PlaneZ(z=6.0, boundary_condition="vacuum")
-
- # Set cells
- mcdc.Cell(region=+s1 & -s2, fill=m2)
- mcdc.Cell(region=+s2 & -s3, fill=m3)
- mcdc.Cell(region=+s3 & -s4, fill=m1)
-
- # =============================================================================
- # Set source
- # =============================================================================
- # Uniform isotropic source throughout the domain
-
- mcdc.Source(z=[0.0, 6.0], isotropic=True, energy_group=0)
-
- # =============================================================================
- # Set tally, setting, and run mcdc
- # =============================================================================
-
- # Tally: cell-average fluxes and collisions
- mesh = mcdc.MeshStructured(z=np.linspace(0.0, 6.0, 61))
- mcdc.Tally(
- mesh=mesh,
- scores=["flux", "collision"],
- mu=np.linspace(-1.0, 1.0, 32 + 1),
- )
-
- # Setting
- mcdc.settings.N_particle = 1000
-
- # Run
- mcdc.run()
-
-Now that we have a script to run, how do we actually run it?
-
-Running a Simulation
---------------------
-
-MC/DC supports execution purely in the Python interpreter, compiled to CPUs (x86, ARM64 and Power9-64),
-and GPUs (AMD and Nvidia) and supports threading with MPI (Python or compiled modes).
-Other guides are included to execute in these modes but for the sake of this first
-MC/DC simulation we will simply execute in Python mode (slower, no acceleration) simply with
-
-.. code-block:: python3
-
- python input.py
-
-from a command line.
-For more performance see how to execute MC/DC on CPUs and GPUs
-
-Postprocessing Results
-----------------------
-
-While the entire workflow of running and post-processing MC/DC could be done in one script,
-unless the problem is very small (or you're an expert),
-we recommend using separate simulation and post-processing/visualization scripts.
-
-When a problem is executed tallied results are compiled, compressed, and saved in ``.h5`` files.
-The size of these files can vary widely depending on your tally settings,
-the geometric size of the problem (e.g. number of surfaces), and the number of particles tracked.
-Expect sizes as small as ``kB`` or as large as ``TB``.
-
-These result files can be exported, manipulated, and visualized.
-Data can be pulled from an ``.h5`` file using something like,
-
-.. code-block:: python3
-
- import h5py
- import numpy as np
- # Load results
- with h5py.File("output.h5", "r") as f:
- # The tally name matches the auto-generated name (e.g., "mesh_tally_0")
- tally_name = list(f["tallies"].keys())[0]
- tally = f[f"tallies/{tally_name}"]
-
- z = tally["grid/z"][:]
- dz = z[1:] - z[:-1]
- z_mid = 0.5 * (z[:-1] + z[1:])
-
- mu = tally["grid/mu"][:]
- dmu = mu[1:] - mu[:-1]
- mu_mid = 0.5 * (mu[:-1] + mu[1:])
-
- psi = tally["flux/mean"][:]
- psi_sd = tally["flux/sdev"][:]
-
-While there can be some nuance to the dimensions of these data arrays, the folder structures should be evident from your tally settings.
-You can see the structure of the file layer-by-layer using the ``keys`` attribute of an h5 group.
-For example, ``f.keys()`` will return
-
-.. code-block:: bash
-
-
-
-and ``f['tallies'].keys()`` will list all tally names.
-
-If needed, you can look around a ``.h5`` file using something like `h5Viewer `_ (which on linux can be installed with ``sudo apt-get install hdfview``).
-Otherwise these arrays can then be manipulated and modified like any other.
-Results are stored as NumPy arrays, so any tool that works with NumPy arrays (*e.g.*, SciPy and Pandas)
-can be used to analyze the data from your simulations.
-
-A tool like ``matplotlib`` will work great for plotting results.
-For more complex simulations, open source professional visualization software like
-`Paraview `_ or `Visit `_ are available.
-
-As the problem we ran above is pretty simple and has no scattering or fission, we have an `analytic solution we can import `_:
-
-.. code-block:: python3
-
- from reference import reference
-
-In the script below, we plot the space-averaged flux and space-averaged current, including their statistical noise.
-We also use the space-averaged flux and current to compute a new quantity, the space-averaged angular flux, and
-plot it over space and angle in a heat map.
-Remember that when reporting results from a Monte Carlo solver, you should **always include the statistical error!**
-
-
-.. code-block:: python3
-
- import matplotlib.pyplot as plt
- import numpy as np
-
- I = len(z) - 1
- N = len(mu) - 1
-
- # Scalar flux
- phi = np.zeros(I)
- phi_sd = np.zeros(I)
- for i in range(I):
- phi[i] += np.sum(psi[i, :])
- phi_sd[i] += np.linalg.norm(psi_sd[i, :])
-
- # Normalize
- phi /= dz
- phi_sd /= dz
- J /= dz
- J_sd /= dz
- for n in range(N):
- psi[:, n] = psi[:, n] / dz / dmu[n]
- psi_sd[:, n] = psi_sd[:, n] / dz / dmu[n]
-
- # Reference solution
- phi_ref, J_ref, psi_ref = reference(z, mu)
-
- # Flux - spatial average
- plt.plot(z_mid, phi, "-b", label="MC")
- plt.fill_between(z_mid, phi - phi_sd, phi + phi_sd, alpha=0.2, color="b")
- plt.plot(z_mid, phi_ref, "--r", label="Ref.")
- plt.xlabel(r"$z$, cm")
- plt.ylabel("Flux")
- plt.ylim([0.06, 0.16])
- plt.grid()
- plt.legend()
- plt.title(r"$\bar{\phi}_i$")
- plt.show()
-
- # Current - spatial average
- plt.plot(z_mid, J, "-b", label="MC")
- plt.fill_between(z_mid, J - J_sd, J + J_sd, alpha=0.2, color="b")
- plt.plot(z_mid, J_ref, "--r", label="Ref.")
- plt.xlabel(r"$z$, cm")
- plt.ylabel("Current")
- plt.ylim([-0.03, 0.045])
- plt.grid()
- plt.legend()
- plt.title(r"$\bar{J}_i$")
- plt.show()
-
- # Angular flux - spatial average
- vmin = min(np.min(psi_ref), np.min(psi))
- vmax = max(np.max(psi_ref), np.max(psi))
- fig, ax = plt.subplots(1, 2, sharey=True)
- Z, MU = np.meshgrid(z_mid, mu_mid)
- im = ax[0].pcolormesh(MU.T, Z.T, psi_ref, vmin=vmin, vmax=vmax)
- ax[0].set_xlabel(r"Polar cosine, $\mu$")
- ax[0].set_ylabel(r"$z$")
- ax[0].set_title(r"\psi")
- ax[0].set_title(r"$\bar{\psi}_i(\mu)$ [Ref.]")
- ax[1].pcolormesh(MU.T, Z.T, psi, vmin=vmin, vmax=vmax)
- ax[1].set_xlabel(r"Polar cosine, $\mu$")
- ax[1].set_ylabel(r"$z$")
- ax[1].set_title(r"$\bar{\psi}_i(\mu)$ [MC]")
- fig.subplots_adjust(right=0.8)
- cbar_ax = fig.add_axes([0.85, 0.15, 0.05, 0.7])
- cbar = fig.colorbar(im, cax=cbar_ax)
- cbar.set_label("Angular flux")
- plt.show()
-
-While this script does look rather long, most of these commands are controlling things like axis labels and whatnot.
-But at the end we have something like this.
-
-.. image:: ../images/user/sf_slab_1.png
- :width: 266
- :alt: Reference v computed scalar flux, 1e3 particles
-.. image:: ../images/user/j_slab_1.png
- :width: 266
- :alt: Reference v computed current, 1e3 particles
-.. image:: ../images/user/af_slab_1.png
- :width: 266
- :alt: Reference v computed angular flux, 1e3 particles
-
-Notice how noisy these solutions are? We only ran 1e3 particles.
-We need more particles to get a less statistically noisy, more converged solution.
-Here's results from the same simulation run with 1e6 particles:
-
-.. image:: ../images/user/sf_slab_2.png
- :width: 266
- :alt: Reference v computed scalar flux, 1e6 particles
-.. image:: ../images/user/j_slab_2.png
- :width: 266
- :alt: Reference v computed current, 1e6 particles
-.. image:: ../images/user/af_slab_2.png
- :width: 266
- :alt: Reference v computed angular flux, 1e6 particles
-
-This is much better converged around the analytic solution.
-As with everything else, the best way to see what you can do is sniff around the examples.
-We have examples with animated solutions, subplots, moving regions and more!
-
-Additional Simulation Results
------------------------------
-
-- Neutron flux distribution on a shielded dog-leg vacuum channel after a neutron pulse is completed
-
-.. image:: ../images/user/kobayashi-white.png
- :width: 266
- :alt: Neutron flux distribution on a shielded dog-leg vacuum channel after a neutron pulse is completed
-
-- Bottom-view of a micro reactor fission rate distribution when a control rod-driven runaway prompt supercritical occurs
-
-.. image:: ../images/user/c5g7.png
- :width: 266
- :alt: Bottom-view of a micro reactor fission rate distribution when a control rod-driven runaway prompt supercritical occurs
-
-- Fission and flux bursts of a neutron excursion driven by a drop of highly-enriched uranium.
-
-.. image:: ../images/user/dragon.gif
- :width: 266
- :alt: Fission and flux bursts of a neutron excursion driven by a drop of highly-enriched uranium
-
--------------------------------------
-MC/DC's built in model ``visualizer``
--------------------------------------
diff --git a/docs/source/user/index.rst b/docs/source/user/index.rst
deleted file mode 100644
index a67d3fc2e..000000000
--- a/docs/source/user/index.rst
+++ /dev/null
@@ -1,36 +0,0 @@
-.. _users:
-
-============
-User's Guide
-============
-
-We include a simple "first simulation guide" as well as more in-depth descriptions on how to execute MC/DC in compiled modes to CPUs and GPUs with or without MPI.
-
-These instructions all assume you have an operable and working version of MC/DC installed in an appropriate environment for your system.
-
-Getting Started
----------------
-
-.. toctree::
- :maxdepth: 1
-
- first_mcdc
-
-Execution Modes
----------------
-
-.. toctree::
- :maxdepth: 1
-
- cpu
- gpu
- batch_scripts
-
-Help & Support
---------------
-
-.. toctree::
- :maxdepth: 1
-
- faq
- troubleshooting
diff --git a/docs/source/user/container.rst b/docs/source/user_guide/container.rst
similarity index 64%
rename from docs/source/user/container.rst
rename to docs/source/user_guide/container.rst
index b74af97c3..a5b7aef4c 100644
--- a/docs/source/user/container.rst
+++ b/docs/source/user_guide/container.rst
@@ -4,17 +4,14 @@ MC/DC Container Guide
What Are Containers?
--------------------
-A container is a lightweight, portable package that bundles an application
-together with everything it needs to run: code, libraries, system tools,
-and settings. Think of it like a shipping container — no matter what ship
-(computer) carries it, the contents inside stay the same.
+A container is a lightweight, portable package that bundles an application with its code, libraries, system tools, and settings.
+Like a shipping container, it keeps its contents consistent across host systems.
**Why does this matter for MC/DC?**
-Installing MC/DC requires Python, MPI, Numba, and many other dependencies.
-Getting all of these working together — especially on HPC systems where you
-don't have admin access — can be painful. A container solves this by giving
-you a pre-built environment where everything is already installed and tested.
+Installing MC/DC requires Python, MPI, Numba, and other dependencies.
+Coordinating these dependencies can be difficult on HPC systems without administrator access.
+A container provides a pre-built environment where the dependencies are already installed and tested.
Tested Platforms
----------------
@@ -33,32 +30,30 @@ Tested Platforms
| COE (OSU) | Rocky 8.10 | x86_64 | Apptainer 1.4.5 | ✓ |
+-------------+------------+--------+--------------------+--------+
-All platforms produce identical containers: Debian 13, Python 3.11,
-MPICH 4.2.1, MC/DC 0.12.0.
+The published CPU image contains the current MC/DC branch build, Python 3.13, MPICH, and the development tools needed to run the test suites.
Getting Started (New Users)
---------------------------
-This section is for anyone who just wants to **run MC/DC** in a container.
+This section is for anyone who wants to **run MC/DC** in a container.
No prior container experience needed.
Step 1: Pull the Pre-Built Image
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-You don't need to build anything. A ready-to-use image is available on
-the GitHub Container Registry.
+You do not need to build the image because a ready-to-use image is available on the GitHub Container Registry.
.. rubric:: Local Machine (Docker)
-First, install Docker Desktop if you haven't already. Then open a terminal
-and run:
+Install Docker Desktop, open a terminal, and run:
.. code-block:: bash
- docker pull ghcr.io/cement-psaap/mcdc:dev
- docker run --rm -it ghcr.io/cement-psaap/mcdc:dev
+ docker pull ghcr.io/mcdc-project/mcdc:dev
+ docker run --rm -it ghcr.io/mcdc-project/mcdc:dev
-You are now inside the container. Try:
+You are now inside the container.
+Try importing MC/DC:
.. code-block:: bash
@@ -68,17 +63,17 @@ Type ``exit`` to leave the container.
.. rubric:: LLNL Systems — Tuolumne, Tioga, Dane (Podman)
-Podman is already installed on LLNL systems. It works just like Docker.
+Podman is already installed on LLNL systems.
+It uses the same commands as Docker in these examples.
.. code-block:: bash
- podman pull ghcr.io/cement-psaap/mcdc:dev
- podman run --rm -it ghcr.io/cement-psaap/mcdc:dev
+ podman pull ghcr.io/mcdc-project/mcdc:dev
+ podman run --rm -it ghcr.io/mcdc-project/mcdc:dev
.. note::
- If you see ``lsetxattr: operation not supported``,
- see *LLNL Storage Setup* in Part 2.
+ If you see ``lsetxattr: operation not supported``, see *LLNL Storage Setup* in Part 2.
.. rubric:: OSU Systems — COE (Apptainer)
@@ -86,7 +81,7 @@ Apptainer is already installed on COE.
.. code-block:: bash
- apptainer build --sandbox mcdc_sandbox docker://ghcr.io/cement-psaap/mcdc:dev
+ apptainer build --sandbox mcdc_sandbox docker://ghcr.io/mcdc-project/mcdc:dev
apptainer exec mcdc_sandbox python -c "import mcdc; print('MC/DC OK')"
.. note::
@@ -100,8 +95,8 @@ Step 2: Run Your Simulation
.. code-block:: bash
- docker run --rm -v $(pwd):/work -w /work mcdc:dev python input.py
- docker run --rm mcdc:dev mpirun -n 4 python input.py
+ docker run --rm -v $(pwd):/work -w /work ghcr.io/mcdc-project/mcdc:dev python input.py
+ docker run --rm ghcr.io/mcdc-project/mcdc:dev mpirun -n 4 python input.py
For Podman, replace ``docker`` with ``podman``.
diff --git a/docs/source/user/batch_scripts.rst b/docs/source/user_guide/execution/batch_systems.rst
similarity index 95%
rename from docs/source/user/batch_scripts.rst
rename to docs/source/user_guide/execution/batch_systems.rst
index 53b18538b..61daa5aed 100644
--- a/docs/source/user/batch_scripts.rst
+++ b/docs/source/user_guide/execution/batch_systems.rst
@@ -1,4 +1,4 @@
-.. _batch_scripts:
+.. _batch_systems:
=================
Batch Job Scripts
@@ -34,7 +34,7 @@ as well as many university and national lab clusters.
#SBATCH --time=01:00:00
#SBATCH --partition=pbatch
- module load python/3.11
+ module load python/3.13
source /path/to/your/venv/bin/activate
srun python input.py --mode=numba --caching
@@ -51,7 +51,7 @@ as well as many university and national lab clusters.
#SBATCH --time=00:30:00
#SBATCH --partition=gpu
- module load python/3.11 cuda/11.8
+ module load python/3.13 cuda/11.8
source /path/to/your/venv/bin/activate
srun python input.py --mode=numba --target=gpu --gpu_strategy=event
@@ -69,7 +69,7 @@ El Capitan systems (AMD MI250X / MI300A GPUs).
#!/bin/bash
- module load cray-mpich python/3.11
+ module load cray-mpich python/3.13
source /path/to/your/venv/bin/activate
flux run -N 2 -n 72 python input.py --mode=numba --caching
@@ -80,7 +80,7 @@ El Capitan systems (AMD MI250X / MI300A GPUs).
#!/bin/bash
- module load cray-mpich rocm/6.0.0 python/3.11
+ module load cray-mpich rocm/6.0.0 python/3.13
source /path/to/your/venv/bin/activate
flux run -N 2 -n 8 -g 1 --queue=mi300a \
diff --git a/docs/source/user/cpu.rst b/docs/source/user_guide/execution/cpu.rst
similarity index 74%
rename from docs/source/user/cpu.rst
rename to docs/source/user_guide/execution/cpu.rst
index 41a3f4436..0522741b9 100644
--- a/docs/source/user/cpu.rst
+++ b/docs/source/user_guide/execution/cpu.rst
@@ -1,5 +1,5 @@
-.. _cpu:
+.. _cpu_execution:
=====================
Running MC/DC on CPUs
@@ -9,7 +9,10 @@ Executing MC/DC in something like a jupyter notebook is possible but not recomme
especially when using MPI and/or Numba.
The instructions below assume you have an existing MC/DC installation.
MPI can be quite tricky to configure if on an HPC; if you're having trouble,
-consult our :ref:`install`, your HPC admin, or our `GitHub issues page `_.
+consult our :ref:`installation`, your HPC admin, or our `GitHub issues page `_.
+
+For the architectural relationship between Python and Numba-CPU execution, see
+:doc:`../../developer_guide/architecture/transport_execution`.
Pure Python Mode
----------------
@@ -29,9 +32,11 @@ Numba Mode
When running in Numba mode a significant amount of time is taken compiling Python functions to performant binaries.
Only the functions used in a specific simulation will be compiled.
-These binaries will be cached, meaning that in subsequent runs of the same simulation the compilation step can be avoided.
+When ``--caching`` is enabled, compiled binaries can be reused by subsequent
+runs of a compatible simulation.
The cache can be used as an effective ahead-of-time compilation scheme where binaries can be compiled once and shared between machines.
-For more information on caching see :ref:`contribution_guide/index:Caching ` and `Numba Caching `_.
+For more information on caching, see :ref:`contributing/index:Caching` and
+`Numba Caching `_.
MC/DC also has the ability to run Numba in a debugging mode.
This will result in less performant code and longer compile times but will allow for better error messages from Numba and other packages.
@@ -41,7 +46,8 @@ This will result in less performant code and longer compile times but will allow
python input.py --mode=numba_debug
-For more information on the exact behavior of this option see :ref:`contribution_guide/index:Debugging `
+For more information on the exact behavior of this option, see
+:ref:`contributing/index:Debugging`.
Using MPI
---------
diff --git a/docs/source/user/gpu.rst b/docs/source/user_guide/execution/gpu.rst
similarity index 96%
rename from docs/source/user/gpu.rst
rename to docs/source/user_guide/execution/gpu.rst
index ab0d1234c..a53baae2b 100644
--- a/docs/source/user/gpu.rst
+++ b/docs/source/user_guide/execution/gpu.rst
@@ -1,5 +1,5 @@
-.. _gpu:
+.. _gpu_execution:
=====================
Running MC/DC on GPUs
@@ -10,6 +10,9 @@ When targeting GPUs execution MC/DC uses the Harmonize library to schedule event
Harmonize acts as the GPU runtime for MC/DC and has two major scheduling schemes including a novel asynchronous event scheduler.
For more information on Harmonize and how we compile MC/DC with it see our publications in M&C 2025.
+For the developer-level compilation and runtime design, see
+:doc:`../../developer_guide/architecture/transport_execution`.
+
Single GPU Launches
-------------------
diff --git a/docs/source/user_guide/execution/index.rst b/docs/source/user_guide/execution/index.rst
new file mode 100644
index 000000000..b5958ad74
--- /dev/null
+++ b/docs/source/user_guide/execution/index.rst
@@ -0,0 +1,20 @@
+.. _execution:
+
+=========
+Execution
+=========
+
+These guides explain how to run the same MC/DC input on different hardware and
+computing environments.
+
+- Start with :doc:`cpu` for local execution, Numba modes, and MPI.
+- Use :doc:`gpu` when targeting supported NVIDIA or AMD accelerators.
+- Use :doc:`batch_systems` when submitting calculations through a scheduler on
+ an HPC system.
+
+.. toctree::
+ :maxdepth: 1
+
+ cpu
+ gpu
+ batch_systems
diff --git a/docs/source/user/faq.rst b/docs/source/user_guide/faq.rst
similarity index 62%
rename from docs/source/user/faq.rst
rename to docs/source/user_guide/faq.rst
index 4bb2af19b..57d2aa029 100644
--- a/docs/source/user/faq.rst
+++ b/docs/source/user_guide/faq.rst
@@ -9,21 +9,16 @@ General
**What Python versions does MC/DC support?**
-MC/DC supports Python ``>3.10``.
-We recommend Python 3.11 for the best performance and compatibility with Numba.
+MC/DC supports Python 3.11 and newer.
**What platforms are supported?**
-MC/DC is validated on linux-64 (x86), win-64, osx-64 (Intel), osx-arm64 (Apple Silicon),
-linux-ppc64 (IBM POWER9), linux-nvidia-cuda, and linux-amd-rocm.
+MC/DC is validated on linux-64 (x86), win-64, osx-64 (Intel), osx-arm64 (Apple Silicon), linux-ppc64 (IBM POWER9), linux-nvidia-cuda, and linux-amd-rocm.
**Should I use pip or conda to install MC/DC?**
-For **personal machines and simple setups**, ``pip`` inside a ``venv`` is the easiest route
-(see :ref:`install:Installing with pip`).
-For **HPCs or non-standard hardware** (e.g., POWER9 on Lassen, or when mpi4py is
-troublesome), a **conda environment** with the ``install.sh`` script is more robust
-(see :ref:`install:Installing MC/DC via conda`).
+For **personal machines and simple setups**, ``pip`` inside a ``venv`` is the easiest route (see :ref:`user_guide/getting_started/installation:Installing with pip`).
+For **HPCs or non-standard hardware**, a **conda environment** can provide more robust environment management while pip installs MC/DC (see :ref:`user_guide/getting_started/installation:Installing MC/DC via conda`).
.. list-table:: pip vs. conda at a glance
:widths: 30 35 35
@@ -43,15 +38,13 @@ troublesome), a **conda environment** with the ``install.sh`` script is more rob
- Excellent
* - MPI support
- Needs system MPI
- - Can build mpi4py from source via ``install.sh``
+ - Can isolate an ``mpi4py`` build matched to the system MPI
**Where can I find cross-section data for continuous-energy simulations?**
CE data libraries are provided to CEMeNT members via an internal repository.
Due to export controls they cannot be publicly distributed.
-If you need cross-section data, we recommend using
-`OpenMC `_ or `NJOY `_ to generate it,
-then converting to MC/DC format with the tool in ``tools/data_library_generator/``.
+If you need cross-section data, we recommend using `OpenMC `_ or `NJOY `_ to generate it, then converting it to MC/DC format with the tool in ``tools/data_library_generator/neutron/``.
See :ref:`install-data-library` for setup instructions.
Installation
@@ -80,20 +73,18 @@ Load the correct MPI module first, then install from source:
module load # e.g., mvapich2, openmpi, spectrum-mpi
CC=mpicc pip install --no-binary mpi4py mpi4py
-Or use the conda path with ``bash install.sh --hpc``, which handles this automatically.
-See :ref:`user/troubleshooting:Building mpi4py from Source` for more details.
+See :ref:`user_guide/troubleshooting:Building mpi4py from Source` for more details.
**I get Numba version errors or** ``TypingError`` **on older Numba versions.**
-MC/DC requires **Numba >= 0.60.0**.
+MC/DC requires **Numba >= 0.61.0**.
If you are on an older version, upgrade:
.. code-block:: sh
- pip install 'numba>=0.60.0'
+ pip install 'numba>=0.61.0'
-If your system constrains the Numba version (e.g., due to CUDA toolkit compatibility),
-see :ref:`user/troubleshooting:Numba Version Compatibility` for patching guidance.
+If your system constrains the Numba version, see :ref:`user_guide/troubleshooting:Numba Version Compatibility` for compatibility guidance.
Running Simulations
-------------------
@@ -104,19 +95,20 @@ Running Simulations
mpiexec -n python input.py --mode=numba
-On HPCs, use the appropriate launcher (``srun``, ``jsrun``, ``flux run``).
-See :ref:`user/batch_scripts:Batch Job Scripts` for ready-to-use templates.
+On HPCs, use the appropriate launcher, such as ``srun``, ``jsrun``, or ``flux run``.
+See :ref:`user_guide/execution/batch_systems:Batch Job Scripts` for ready-to-use templates.
**My simulation is very slow — what should I check?**
-#. Are you running in ``--mode=numba``? Python mode is orders of magnitude slower.
-#. First Numba run incurs JIT compilation overhead (15–80 s).
- Subsequent runs with ``--caching`` are much faster.
+#. Are you running in ``--mode=numba``?
+ Python mode is orders of magnitude slower.
+#. The first Numba run incurs JIT compilation overhead of approximately 15–80 seconds.
+#. Subsequent runs with ``--caching`` are much faster.
#. Check your particle count — start small and scale up.
**I see** ``SyntaxWarning: invalid escape sequence`` **on import.**
-This is a known cosmetic warning in some older releases (see `#211 `_).
+This is a known cosmetic warning in some older releases (see `#211 `_).
It does not affect simulation results.
Updating to the latest MC/DC version resolves it.
@@ -135,10 +127,10 @@ Use ``h5py`` to read them:
print(list(f.keys())) # ['runtime', 'tallies']
print(list(f["tallies"].keys())) # list of tally names
-See the post-processing section in :ref:`user/first_mcdc:First MC/DC Simulation` for a complete example.
+See the post-processing section in :ref:`user_guide/getting_started/first_simulation:First MC/DC Simulation` for a complete example.
**What visualization tools work with MC/DC output?**
- ``matplotlib`` for quick 1-D / 2-D plots.
-- MC/DC's built-in ``mcdc.visualize()`` for geometry inspection.
+- ``simulation.visualize_model(...)`` for built-in geometry inspection.
- `ParaView `_ or `VisIt `_ for 3-D data.
diff --git a/docs/source/user_guide/getting_started/first_simulation.rst b/docs/source/user_guide/getting_started/first_simulation.rst
new file mode 100644
index 000000000..d68511c10
--- /dev/null
+++ b/docs/source/user_guide/getting_started/first_simulation.rst
@@ -0,0 +1,256 @@
+.. _first_simulation:
+
+======================
+First MC/DC Simulation
+======================
+
+This tutorial constructs and runs a one-group shielding calculation.
+It assumes familiarity with the basic concepts of Monte Carlo radiation transport.
+
+Monte Carlo transport inputs are generally assembled from the same core components: materials, geometry, particle sources, tallies, and simulation settings.
+In MC/DC, these components are represented by Python objects and assembled into a :class:`mcdc.Simulation`.
+
+This tutorial applies the general :doc:`../simulation_lifecycle` to one complete problem.
+
+The example uses multigroup data defined directly in the input, so it does not require an external nuclear-data library.
+Its complete, executable source is available in ``examples/slab_shielding``.
+
+MC/DC Workflow
+--------------
+
+An MC/DC calculation follows five main steps:
+
+#. Construct the materials, geometry, sources, and tallies.
+#. Create a simulation and attach the model objects to it.
+#. Configure settings and compile the connected object graph.
+#. Visualize or run the prepared model.
+#. Generate and post-process the output.
+
+.. important::
+
+ Validate the geometry, source distribution, tally definitions, and settings before interpreting simulation results.
+ A successfully executed calculation is not necessarily a correctly specified physical model.
+
+Problem Description
+-------------------
+
+The model contains two adjacent slab regions:
+
+- A mostly scattering source region over :math:`0 < z < 2` cm.
+- A more strongly absorbing shield over :math:`2 < z < 6` cm.
+
+Both outer boundaries are vacuum.
+Particles are emitted isotropically throughout the source region, and a mesh tally records the flux across the entire domain.
+
+.. list-table:: One-group material data
+ :header-rows: 1
+ :widths: 30 20 20 20
+
+ * - Region
+ - Range (cm)
+ - :math:`\Sigma_c` (cm\ :sup:`-1`)
+ - :math:`\Sigma_s` (cm\ :sup:`-1`)
+ * - Source region
+ - :math:`0 < z < 2`
+ - 0.1
+ - 0.9
+ * - Shield
+ - :math:`2 < z < 6`
+ - 0.7
+ - 0.3
+
+The total cross section is :math:`1.0\ \text{cm}^{-1}` in both regions.
+Changing the capture-to-scatter ratio isolates the effect of the shield on the flux distribution.
+
+Building the Input
+------------------
+
+Imports and Simulation
+~~~~~~~~~~~~~~~~~~~~~~
+
+NumPy provides the numerical arrays used for cross sections and tally grids.
+The ``Simulation`` instance collects the model and controls its execution:
+
+.. code-block:: python3
+
+ import numpy as np
+
+ import mcdc
+
+
+ simulation = mcdc.Simulation("One-group slab shielding")
+
+Materials
+~~~~~~~~~
+
+``Material.multigroup()`` creates a material for neutron multigroup transport and attaches the underlying ``NeutronMultigroupData``.
+A one-element capture array and a :math:`1 \times 1` scattering matrix define one-group neutron data:
+
+.. code-block:: python3
+
+ source_region_material = mcdc.Material.multigroup(
+ capture=np.array([0.1]),
+ scatter=np.array([[0.9]]),
+ )
+ shield_material = mcdc.Material.multigroup(
+ capture=np.array([0.7]),
+ scatter=np.array([[0.3]]),
+ )
+
+:ref:`Native compositions ` use the same :class:`mcdc.Material` interface and require an MC/DC nuclear-data library.
+See :ref:`install-data-library` for configuration instructions.
+
+Geometry
+~~~~~~~~
+
+Three z-planes define the two slab regions.
+The outer planes use vacuum boundary conditions; the plane at :math:`z=2` cm is an internal interface:
+
+.. code-block:: python3
+
+ left = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
+ interface = mcdc.Surface.PlaneZ(z=2.0)
+ right = mcdc.Surface.PlaneZ(z=6.0, boundary_condition="vacuum")
+
+A cell combines a region with the material that fills it.
+A positive half-space selects points above a ``PlaneZ``, while a negative half-space selects points below it:
+
+.. code-block:: python3
+
+ source_cell = mcdc.Cell(
+ region=+left & -interface,
+ fill=source_region_material,
+ )
+ shield_cell = mcdc.Cell(
+ region=+interface & -right,
+ fill=shield_material,
+ )
+ simulation.set_model([source_cell, shield_cell])
+
+The cells are the roots of the geometry.
+MC/DC reaches their materials and surfaces when it compiles the simulation, so those objects do not require separate setter calls.
+
+Source
+~~~~~~
+
+In standard multigroup transport, ``energy=0`` emits particles at group 0 isotropically and uniformly throughout the first cell:
+
+.. code-block:: python3
+
+ source = mcdc.Source(
+ z=[0.0, 2.0],
+ isotropic=True,
+ energy=0,
+ )
+ simulation.set_sources([source])
+
+Tallies
+~~~~~~~
+
+The structured mesh divides the 6 cm domain into 60 equal spatial bins.
+The tally scores the track-length estimate of flux in each bin:
+
+.. code-block:: python3
+
+ mesh = mcdc.MeshStructured(z=np.linspace(0.0, 6.0, 61))
+ flux_tally = mcdc.Tally(
+ name="slab_flux",
+ mesh=mesh,
+ scores=["flux"],
+ )
+ simulation.set_tallies([flux_tally])
+
+Naming the tally makes its location in the output file predictable: ``tallies/slab_flux``.
+
+Settings and Execution
+~~~~~~~~~~~~~~~~~~~~~~
+
+This example runs 1,000 particle histories in each of 10 statistically independent batches.
+Multiple batches allow MC/DC to estimate the standard deviation of each tally bin:
+
+.. code-block:: python3
+
+ simulation.settings.N_particle = 1_000
+ simulation.settings.N_batch = 10
+ simulation.settings.output_name = "slab_shielding"
+
+ simulation.run()
+
+The calculation writes its results to ``slab_shielding.h5``.
+
+Complete Input
+--------------
+
+The complete runnable input is embedded directly from ``examples/slab_shielding/input.py``:
+
+.. literalinclude:: ../../../../examples/slab_shielding/input.py
+ :language: python
+ :linenos:
+
+Visualizing the Model
+---------------------
+
+Before running transport, insert the following call immediately before ``simulation.run()`` to render an x-z slice of the material geometry.
+Spatial coordinates are in cm and snapshot times are in seconds:
+
+.. code-block:: python3
+
+ simulation.visualize_model(
+ vis_plane="xz",
+ x=[-1.0, 1.0],
+ y=0.0,
+ z=[0.0, 6.0],
+ pixels=(100, 300),
+ colors=None,
+ time=[0.0],
+ save_as="slab_shielding_geometry",
+ )
+
+The image is saved as ``slab_shielding_geometry.png``.
+Visualization compiles the current model when necessary.
+
+Running the Example
+-------------------
+
+Enter the problem directory, then run the input in pure Python mode:
+
+.. code-block:: sh
+
+ cd examples/slab_shielding
+ python input.py
+
+Pure Python mode avoids compilation overhead and is suitable for checking a small model.
+For accelerated or parallel calculations, see :doc:`../execution/cpu`, :doc:`../execution/gpu`, and :doc:`../execution/batch_systems`.
+
+Post-processing
+---------------
+
+MC/DC writes tally results and runtime information to HDF5.
+The companion script reads the spatial grid, normalizes the flux and standard deviation by the mesh-bin widths, and plots the result:
+
+.. literalinclude:: ../../../../examples/slab_shielding/process-output.py
+ :language: python
+ :linenos:
+
+After the transport calculation finishes, run the companion script from the same problem directory:
+
+.. code-block:: sh
+
+ python process-output.py
+
+The script writes ``slab_shielding_flux.png``.
+The dashed line marks the material interface at :math:`z=2` cm.
+The flux is expected to decrease more rapidly in the shield because capture accounts for a larger fraction of its total cross section.
+
+Next Steps
+----------
+
+After running the original problem, useful variations include:
+
+- Increase ``N_particle`` and compare the reported standard deviation.
+- Change the shield capture and scattering cross sections.
+- Move the material interface and observe the change in attenuation.
+- Add an energy group or another spatial region.
+- Add a surface-crossing tally at the material interface.
+
+See :doc:`../../examples/index` for examples involving lattices, moving geometry, time-dependent transport, and reactor benchmarks.
diff --git a/docs/source/user_guide/getting_started/index.rst b/docs/source/user_guide/getting_started/index.rst
new file mode 100644
index 000000000..97e5e7bb3
--- /dev/null
+++ b/docs/source/user_guide/getting_started/index.rst
@@ -0,0 +1,25 @@
+.. _getting_started:
+
+===============
+Getting Started
+===============
+
+Getting Started is the shortest path from learning what MC/DC is to completing
+a working transport calculation.
+
+Begin with :doc:`what_is_mcdc` for an overview of the project and its intended
+uses. Then install MC/DC and follow the first-simulation tutorial to build,
+visualize, run, and post-process a complete transport problem.
+
+.. toctree::
+ :maxdepth: 1
+
+ what_is_mcdc
+ installation
+ first_simulation
+
+After completing these pages:
+
+- Continue through the :doc:`../index` for task-oriented guidance.
+- Browse :doc:`../../examples/index` for complete models.
+- Consult the :doc:`../../reference/index` for exact API behavior.
diff --git a/docs/source/user_guide/getting_started/installation.rst b/docs/source/user_guide/getting_started/installation.rst
new file mode 100644
index 000000000..12cfcecc1
--- /dev/null
+++ b/docs/source/user_guide/getting_started/installation.rst
@@ -0,0 +1,223 @@
+.. _installation:
+
+============
+Installation
+============
+
+Use an environment manager such as venv or conda when installing MC/DC as a user or developer.
+An isolated environment avoids administrator access and keeps dependencies clean.
+
+In general, :ref:`creating-a-venv-environment` and :ref:`installing-with-pip` are the easiest options.
+Creating a conda environment and :ref:`installing-with-conda` requires more steps but can be more robust on specialized systems.
+A conda environment is necessary to install MC/DC on LLNL's Lassen machine.
+
+
+
+.. _creating-a-venv-environment:
+
+---------------------------
+Creating a venv environment
+---------------------------
+
+Python `virtual environments `_ are the recommended way to run MC/DC on personal machines and HPC systems.
+MC/DC supports Python 3.11 and newer, and the selected Python installation must provide ``venv``.
+On HPC systems, administrators often provide Python and its package sources through a module system, so load a supported module such as ``python/3.13`` before creating the environment.
+
+Create a Python virtual environment with:
+
+.. code-block:: sh
+
+ python -m venv
+
+Activate the environment after creating it:
+
+.. code-block:: sh
+
+ source /bin/activate
+
+Activate the environment in each new terminal session before using MC/DC.
+Once the environment is active, continue to :ref:`installing-with-pip`.
+
+
+.. _installing-with-pip:
+
+-------------------
+Installing with pip
+-------------------
+Install MC/DC with pip inside an active venv or conda environment to avoid administrator access and keep dependencies isolated.
+
+Install the latest stable release from PyPI when you do not need to modify MC/DC:
+
+.. code-block:: sh
+
+ pip install mcdc
+
+----------------------
+Installing from Source
+----------------------
+Install MC/DC from source when you need a specific branch or plan to contribute changes:
+Use Python 3.14 for a contributor environment that will run Black locally; runtime-only source installations may use any supported Python version.
+
+#. Clone the MC/DC repository: ``git clone https://github.com/mcdc-project/mcdc.git``
+#. Enter the repository: ``cd mcdc``
+#. Install MC/DC and its development tools: ``python -m pip install -e ".[dev]"``
+
+The ``-e`` flag installs MC/DC as an editable package, so source changes and branch switches take effect without reinstalling the package.
+
+.. _installing-with-conda:
+
+--------------------------
+Installing MC/DC via conda
+--------------------------
+
+`Conda `_ provides robust environment management on systems with non-standard hardware or constrained software stacks.
+It is particularly useful on HPC systems such as Lassen, where ``mpi4py`` must match the system MPI implementation.
+
+Install conda with `Miniconda `_ or `Anaconda `_.
+The following commands install Miniconda on selected HPC architectures.
+
+`Dane `_ (LLNL, x86_64),
+
+.. code-block:: sh
+
+ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
+ bash Miniconda3-latest-Linux-x86_64.sh
+
+
+`Lassen `_ (LLNL, IBM Power9),
+
+.. code-block:: sh
+
+ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-ppc64le.sh
+ bash Miniconda3-latest-Linux-ppc64le.sh
+
+
+The following example creates and activates a Python 3.13 conda environment named ``mcdc-env``:
+
+.. code-block:: sh
+
+ conda create -n mcdc-env python=3.13
+ conda activate mcdc-env
+
+Clone MC/DC and enter the repository:
+
+.. code-block:: sh
+
+ git clone https://github.com/mcdc-project/mcdc.git
+ cd mcdc
+
+On an HPC system, load the appropriate MPI module and build ``mpi4py`` against that implementation before installing MC/DC:
+
+.. code-block:: sh
+
+ module load
+ CC=mpicc python -m pip install --no-cache-dir --no-binary=mpi4py mpi4py
+ python -m pip install -e ".[dev]"
+
+On a local machine where pip can provide a compatible ``mpi4py`` installation, install MC/DC directly:
+
+.. code-block:: sh
+
+ python -m pip install -e ".[dev]"
+
+Run ``python -m pytest`` from the repository root to verify the installation.
+
+.. _installing-via-containers:
+
+--------------------------
+Installing via Containers
+--------------------------
+
+For container-based installation and execution, see :doc:`../container`.
+
+.. _install-data-library:
+
+-------------------------------------------------
+Generating a Neutron Data Library from ACE Files
+-------------------------------------------------
+
+MC/DC ships with a neutron conversion tool in ``tools/data_library_generator/neutron/`` that reads standard ACE-format nuclear data files and writes them into MC/DC's per-nuclide HDF5 format.
+This is the primary path for creating continuous-energy neutron libraries.
+
+**Prerequisites:**
+
+.. code-block:: sh
+
+ pip install ACEtk h5py numpy tqdm
+
+You also need a set of ACE files from a source such as `NJOY `_ or an ENDF/B distribution.
+
+**Environment variables:**
+
+.. list-table::
+ :widths: 25 75
+ :header-rows: 1
+
+ * - Variable
+ - Description
+ * - ``MCDC_ACELIB``
+ - Path to the directory containing your ACE files.
+ * - ``MCDC_LIB``
+ - Path to the output directory where MC/DC HDF5 files will be written.
+
+**Running the generator:**
+
+.. code-block:: sh
+
+ export MCDC_ACELIB=/path/to/ace/files
+ export MCDC_LIB=/path/to/mcdc/library
+
+ cd tools/data_library_generator/neutron
+ python generate.py
+
+By default, the tool converts only nuclides without a corresponding HDF5 file in ``$MCDC_LIB``.
+Use ``--rewrite`` to regenerate all files or ``--verbose`` for detailed per-nuclide output:
+
+.. code-block:: sh
+
+ python generate.py --rewrite --verbose
+
+The generator processes each ACE file as follows:
+
+#. Reads the ACE header to determine nuclide identity (Z, A, isomeric state) and temperature.
+#. Extracts the principal cross-section block and writes HDF5 datasets grouped by reaction type.
+#. Extracts angular and energy distributions for each reaction channel.
+#. Extracts prompt and delayed :math:`\nu(E)` data, precursor fractions, decay constants, and energy spectra for fissionable nuclides.
+
+The resulting HDF5 file (e.g., ``U235-293.6K.h5``) is ready for use with ``mcdc.Material()``.
+
+
+---------------------------------
+GPU Operability (MC/DC+Harmonize)
+---------------------------------
+
+MC/DC supports most of its Numba enabled features for GPU compilation and execution.
+When targeting GPUs, MC/DC uses the `Harmonize `_ library as its GPU runtime, a.k.a. the thing that actually executes MC/DC functions.
+Harmonize provides an event scheduler similar to those implemented in OpenMC and Shift, along with a novel asynchronous scheduler.
+For more information on Harmonize and how we compile MC/DC with it, see this `TOMACs article describing the async scheduler `_ or our publications in American Nuclear Society: Math and Comp Meeting in 2025.
+
+If you encounter configuration problems, please file a `GitHub issue `_, especially when using supported supercomputers such as LLNL's `Tioga `_, `El Capitan `_, or `Lassen `_.
+
+.. rubric:: Nvidia GPUs
+
+To compile and execute MC/DC on Nvidia GPUs, first satisfy the `Harmonize prerequisites `_ (CUDA 11.8 and Numba 0.61 or newer).
+
+#. Clone the harmonize repo: ``git clone https://github.com/CEMeNT-PSAAP/harmonize.git``
+#. Install into the proper Python env: ``pip install -e .``
+
+Operability should now be enabled.
+
+.. _install-amd-gpus:
+
+.. rubric:: AMD GPUs
+
+The prerequisites for AMD operability require a Numba patch that enables the AMD target triple in LLVM IR.
+It is recommended that this is done within a Python venv virtual environment.
+
+To compile and execute MC/DC on AMD GPUs, first satisfy the `Harmonize prerequisites `_ (ROCm 6.0.0 and Numba 0.61 or newer).
+
+#. Patch Numba to enable HIP (`instructions here `_)
+#. Clone harmonize and `switch to the AMD `_ branch with ``git switch amd_event_interop_revamp``
+#. Install Harmonize with ``pip install -e .`` or using `Harmonize's install script `_
+
+Operability should now be enabled.
diff --git a/docs/source/user_guide/getting_started/what_is_mcdc.rst b/docs/source/user_guide/getting_started/what_is_mcdc.rst
new file mode 100644
index 000000000..8620d4d65
--- /dev/null
+++ b/docs/source/user_guide/getting_started/what_is_mcdc.rst
@@ -0,0 +1,57 @@
+.. _what_is_mcdc:
+
+================
+What is MC/DC?
+================
+
+MC/DC is an open-source, Python-based Monte Carlo radiation transport software
+package that combines rapid methods development with scalable execution on
+modern high-performance computing systems. It supports execution across CPUs
+and GPUs while providing a flexible environment for developing and testing new
+transport algorithms.
+
+**Audience** — MC/DC is intended for researchers developing new Monte Carlo
+transport methods, including variance reduction techniques, sensitivity and
+uncertainty quantification methods, and high-performance computing algorithms.
+It also provides an accessible platform for students learning Monte Carlo
+radiation transport methods and modern code development.
+
+**Capabilities** — MC/DC supports continuous-energy and multigroup neutron
+transport calculations, including fixed-source and eigenvalue simulations on
+constructive solid geometry (CSG) models. For continuous-energy transport,
+MC/DC converts `ACE `_-format nuclear data
+libraries into its native `HDF5 `_
+format. Photon, electron, proton, and other charged-particle transport
+capabilities are currently under development as part of the ongoing expansion
+of MC/DC into a comprehensive multi-particle radiation transport software
+package.
+
+**Performance and portability** — MC/DC's Python interface enables rapid
+prototyping and iterative development, while its
+`Numba `_-based compilation framework delivers high
+performance without sacrificing portability.
+`Harmonize `_ provides a GPU
+execution framework, and `MPI4Py `_
+enables distributed-memory parallelism across large HPC systems. In addition
+to desktop and workstation systems, MC/DC has been demonstrated on large
+heterogeneous supercomputers, including
+`Lassen `_
+(IBM POWER9 and NVIDIA Volta V100) and
+`Tuolumne `_
+(AMD MI300A APU).
+
+**Origins** — MC/DC was initiated by the Center for Exascale Monte Carlo
+Neutron Transport (`CEMeNT `_), a Focused
+Investigatory Center of the Predictive Science Academic Alliance Program–III
+(`PSAAP-III `_). Development is now led by the Center
+for Advancing the Radiation Resilience of Electronics
+(`CARRE `_), a Predictive Simulation Center of
+`PSAAP-IV `_.
+
+**Open source** — MC/DC is released under the
+`BSD 3-Clause `_
+license and welcomes community contributions through
+`GitHub `_.
+
+Next, :doc:`install MC/DC ` or proceed to the
+:doc:`first simulation ` if it is already installed.
diff --git a/docs/source/user_guide/index.rst b/docs/source/user_guide/index.rst
new file mode 100644
index 000000000..99d864677
--- /dev/null
+++ b/docs/source/user_guide/index.rst
@@ -0,0 +1,68 @@
+.. _user_guide:
+
+==========
+User Guide
+==========
+
+The User Guide provides task-oriented instructions for building, running, and
+analyzing MC/DC simulations. It begins with an introduction for new users and
+then covers modeling, execution, examples, and troubleshooting.
+
+For exact class and method signatures, use the :doc:`../reference/index`.
+
+Getting Started
+---------------
+
+New to MC/DC? Begin here to understand the project, install the package, and
+complete your first transport calculation.
+
+.. toctree::
+ :maxdepth: 2
+
+ getting_started/index
+
+Modeling and Results
+--------------------
+
+Follow :doc:`simulation_lifecycle` for the complete construct, compile,
+visualize, execute, and post-process workflow. Use
+:doc:`iterative_simulations` when several runs reuse most of a model while
+changing selected inputs.
+
+.. toctree::
+ :maxdepth: 1
+
+ simulation_lifecycle
+ materials_and_multigroup
+ sources
+ iterative_simulations
+ tallies
+
+Execution
+---------
+
+.. toctree::
+ :maxdepth: 1
+
+ execution/index
+
+Learning by Example
+-------------------
+
+Use the :doc:`../examples/index` to learn from complete, runnable input decks
+that progress from basic models to advanced benchmarks.
+
+.. toctree::
+ :maxdepth: 2
+
+ Example Problems <../examples/index>
+
+Help & Support
+--------------
+
+.. toctree::
+ :maxdepth: 1
+
+ container
+ faq
+ troubleshooting
diff --git a/docs/source/user_guide/iterative_simulations.rst b/docs/source/user_guide/iterative_simulations.rst
new file mode 100644
index 000000000..2c423d851
--- /dev/null
+++ b/docs/source/user_guide/iterative_simulations.rst
@@ -0,0 +1,75 @@
+.. _iterative_simulations:
+
+=====================
+Iterative Simulations
+=====================
+
+Use an iterative simulation when several calculations share most of a model
+but change selected inputs between runs. Common applications include parameter
+sweeps, source updates, feedback iterations, and optimization studies.
+
+The workflow reuses one :class:`mcdc.Simulation` and its Python model objects.
+Each iteration still prepares a complete, consistent simulation for transport;
+MC/DC does not patch only the changed value into a previous execution.
+
+Build the Shared Model Once
+---------------------------
+
+Construct the geometry, materials, sources, tallies, settings, and techniques
+as usual. Attach them to one simulation before beginning the iteration. Objects
+that do not change can remain attached throughout the study.
+
+Use separate simulations only when the calculations require independent object
+graphs. See :doc:`simulation_lifecycle` for the normal construction workflow
+and process-ownership rules.
+
+Update, Compile, and Run
+------------------------
+
+Change the relevant Python objects at the beginning of each iteration. For
+example, the relative probabilities of two sources can be varied while the
+geometry, material, and tally remain unchanged:
+
+.. code-block:: python
+
+ source_mixes = (
+ ("left_20", 0.2),
+ ("left_50", 0.5),
+ ("left_80", 0.8),
+ )
+
+ for case_name, left_fraction in source_mixes:
+ source_left.probability = left_fraction
+ source_right.probability = 1.0 - left_fraction
+ simulation.settings.output_name = f"source_mix_{case_name}"
+
+ simulation.compile()
+ simulation.run()
+
+The explicit ``compile`` call establishes a fresh model snapshot after each
+partial update. It is particularly useful when the updated model will be
+inspected or visualized before execution.
+
+``simulation.run()`` automatically compiles an unprepared simulation and marks
+it unprepared again after execution. A simple loop that only updates and runs
+may therefore omit the explicit ``compile`` call. If an attached object is
+modified after an explicit compilation or visualization, compile again before
+inspecting, visualizing, or running the updated model.
+
+Keep Outputs Distinct
+---------------------
+
+Assign a unique ``simulation.settings.output_name`` before every run so that a
+later iteration does not replace an earlier result. Keep tally names stable
+when the same post-processing operation will be applied to every output.
+
+The driver or post-processing script can record the iteration parameters next
+to each output name. This makes plots and comparisons reproducible without
+depending on internal object identifiers.
+
+Complete Example
+----------------
+
+See :ref:`example_iterative_source_reweighting` for a complete runnable input.
+Its post-processing script overlays the flux profiles from three source
+mixtures and checks their expected symmetry.
diff --git a/docs/source/user_guide/materials_and_multigroup.rst b/docs/source/user_guide/materials_and_multigroup.rst
new file mode 100644
index 000000000..e7ad37fe3
--- /dev/null
+++ b/docs/source/user_guide/materials_and_multigroup.rst
@@ -0,0 +1,112 @@
+.. _user_materials_and_multigroup:
+
+============================
+Materials and Transport Data
+============================
+
+A :class:`mcdc.Material` describes a physical medium.
+A nuclide or element composition establishes its :ref:`native transport data `.
+Materials can also carry particle-specific data that augments native interaction data, for example with semi-empirical information, or supports specialized and reduced transport treatments.
+
+.. _user_native_transport:
+
+Native Composition and Data
+---------------------------
+
+In MC/DC, *native* refers to data-library-backed transport physics derived from a material's nuclide or element composition.
+The composition identifies the physical constituents whose library records provide the interaction data.
+
+Define a native material with either nuclide or element atomic densities in atoms/(barn cm).
+Material temperature is specified in K:
+
+.. code-block:: python
+
+ fuel = mcdc.Material(
+ name="UO2",
+ nuclide_composition={
+ "U235": 5.0e-4,
+ "U238": 2.2e-2,
+ "O16": 4.5e-2,
+ },
+ temperature=293.6,
+ )
+
+Particle-specific Transport Data
+--------------------------------
+
+Particle-specific transport data is attached to the material that uses it.
+The transport physics determines when and how each dataset contributes to a particle interaction.
+
+Neutron Multigroup Transport
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Neutron multigroup transport represents neutron energy with discrete groups and describes interactions using groupwise macroscopic data.
+It is widely used in general transport pedagogy and in nuclear engineering applications.
+:meth:`mcdc.Material.multigroup` creates a material with :class:`mcdc.NeutronMultigroupData`, which holds the cross sections, group speeds, production spectra, and delayed-precursor data.
+Macroscopic cross sections use cm\ :sup:`-1`, group speeds use cm/s, and precursor decay rates use s\ :sup:`-1`:
+
+.. code-block:: python
+
+ moderator = mcdc.Material.multigroup(
+ name="Moderator",
+ capture=np.array([0.1, 0.2]),
+ scatter=np.array([
+ [0.7, 0.1],
+ [0.2, 0.5],
+ ]),
+ energy_grid=np.array([1.0e-5, 1.0, 20.0e6]),
+ )
+
+Multigroup Energy Grids and Representation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+An explicit ``energy_grid`` contains ``G + 1`` physical energy boundaries in eV.
+Group ``g`` covers ``energy_grid[g] <= E < energy_grid[g + 1]``.
+The grid both maps continuous energy to a group and bounds continuous energy reconstructed from a group.
+
+The grid may be omitted for standard multigroup transport when every material omits ``energy_grid``.
+
+For lower and upper group boundaries :math:`E_g` and :math:`E_{g+1}`, the energy-representation policies are:
+
+- ``"midpoint"``: use the arithmetic midpoint, :math:`E=(E_g+E_{g+1})/2`;
+- ``"log_midpoint"``: use the geometric midpoint, :math:`E=\sqrt{E_g E_{g+1}}`;
+- ``"uniform"``: sample :math:`E` uniformly between the two boundaries; and
+- ``"log_uniform"``: sample :math:`\log E` uniformly between their logarithms, equivalently :math:`E=E_g(E_{g+1}/E_g)^\xi` for :math:`\xi\sim\mathcal{U}(0,1)`.
+
+The policies apply when transport reconstructs physical energy from a group, including after a hybrid multigroup interaction.
+The logarithmic policies require strictly positive energy boundaries.
+Midpoint policies reconstruct one deterministic value per group, while uniform policies sample a new value when continuous energy is reconstructed.
+Standard multigroup transport retains the group coordinate instead and does not apply a physical-energy reconstruction policy during particle transport.
+
+MC/DC determines the neutron multigroup transport organization when the simulation is compiled.
+Standard neutron multigroup transport applies when every material uses neutron multigroup data without native composition and all materials either omit ``energy_grid`` or share the same explicit grid.
+In standard multigroup transport, particle energy uses a dimensionless group coordinate: energy ``0.0`` represents group 0, energy ``1.0`` represents group 1, and so forth.
+
+Transport is hybrid when native composition data is present or multigroup materials use different grids.
+Every multigroup dataset participating in hybrid transport requires an explicit physical energy grid because continuous energy selects the applicable material-local group.
+In hybrid transport, particle energy remains physical energy in eV, and ``energy_grid`` maps it to a material-local group.
+The material's ``energy_representation`` policy maps an outgoing group back to physical energy.
+If a particle lies outside that grid, MC/DC uses the material's native neutron data when present; without a native composition, the material has zero interaction cross section at that energy.
+
+Combining Native and Multigroup Data
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Construct :class:`mcdc.NeutronMultigroupData` directly when attaching it to a material with a :ref:`native composition `:
+
+.. code-block:: python
+
+ hybrid_fuel = mcdc.Material(
+ name="Hybrid fuel",
+ nuclide_composition={"U235": 5.0e-4, "U238": 2.2e-2},
+ neutron_multigroup=mcdc.NeutronMultigroupData(
+ capture=np.array([0.10]),
+ fission=np.array([0.20]),
+ nu_p=np.array([2.50]),
+ energy_grid=np.array([1.0e-5, 20.0e6]),
+ ),
+ )
+
+The explicit energy grid is required whenever a native composition and neutron multigroup data are combined.
+Its bounds identify the energy interval where the multigroup transport model is available.
+
+See :ref:`user_standard_multigroup_sources` for the corresponding source-energy convention.
diff --git a/docs/source/user_guide/simulation_lifecycle.rst b/docs/source/user_guide/simulation_lifecycle.rst
new file mode 100644
index 000000000..a1b342086
--- /dev/null
+++ b/docs/source/user_guide/simulation_lifecycle.rst
@@ -0,0 +1,181 @@
+.. _simulation_lifecycle:
+
+====================
+Simulation Lifecycle
+====================
+
+An MC/DC calculation is owned by a :class:`mcdc.Simulation`.
+The simulation collects one model, its sources and tallies, its settings and techniques, and the runtime state needed to visualize or execute it.
+
+The normal workflow has five stages:
+
+#. Construct model objects.
+#. Attach the model roots to a simulation.
+#. Compile the connected object graph.
+#. Visualize or run the prepared model.
+#. Generate and post-process output.
+
+Most inputs call only ``visualize_model`` or ``run`` explicitly.
+MC/DC performs the required compilation and runtime preparation automatically.
+
+1. Construct Model Objects
+--------------------------
+
+Materials, surfaces, cells, sources, meshes, and tallies are ordinary Python objects.
+Here, the macroscopic cross sections are in cm\ :sup:`-1` and surface positions are in cm:
+
+.. code-block:: python
+
+ material = mcdc.Material.multigroup(
+ capture=np.array([0.1]),
+ scatter=np.array([[0.9]]),
+ )
+ left = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
+ right = mcdc.Surface.PlaneZ(z=2.0, boundary_condition="vacuum")
+ cell = mcdc.Cell(region=+left & -right, fill=material)
+
+Objects can refer to other objects.
+In this example the cell retains its region, the region retains its surfaces, and the cell retains its material.
+
+2. Attach Roots to a Simulation
+-------------------------------
+
+Create a simulation and attach the roots from which MC/DC can discover the complete model:
+
+.. code-block:: python
+
+ simulation = mcdc.Simulation("Slab")
+ simulation.set_model([cell])
+ simulation.set_sources([source])
+ simulation.set_tallies([tally])
+
+``set_model`` accepts the cells in the root universe.
+Materials, surfaces, child universes, lattices, and meshes that are reachable from those cells do not need separate registration calls.
+
+Sources and tallies are explicit roots because they are not necessarily reachable from the geometry.
+Settings and techniques belong directly to the simulation:
+
+.. code-block:: python
+
+ simulation.settings.N_particle = 10_000
+ simulation.settings.N_batch = 20
+ simulation.technique.implicit_capture()
+
+This explicit ownership replaces the former process-wide singleton workflow.
+
+Process Ownership
+^^^^^^^^^^^^^^^^^
+
+MC/DC runs one active simulation context at a time within a Python process.
+Objects may be shared within one model—for example, several cells may use the same material—but the same object instance should not be attached to different ``Simulation`` instances.
+
+Multiple simulations can be constructed in one process when they have independent object graphs and are run serially.
+For concurrent calculations, construct each model and run its simulation in a separate process.
+A Python driver, workflow system, or batch scheduler can manage those processes.
+
+3. Compile the Object Graph
+---------------------------
+
+Compilation discovers every object reachable from the simulation, registers shared objects once, and assigns simulation-local IDs.
+Normally it is automatic:
+
+- ``simulation.visualize_model(...)`` compiles when needed.
+- ``simulation.run()`` compiles when needed.
+
+Call ``simulation.compile()`` directly only when you need to inspect the compiled object lists or IDs before visualization or execution:
+
+.. code-block:: python
+
+ simulation.compile()
+ print(simulation.cells)
+ print(material.ID)
+
+Compiled IDs describe one snapshot and may change after recompilation.
+Do not use them as persistent identifiers in an input or post-processing workflow.
+
+The three setter methods invalidate the current snapshot.
+If you directly modify an attached object after explicitly compiling or visualizing the model, call ``simulation.compile()`` again before inspecting or visualizing the change.
+
+Compilation finalizes some user-facing values in place.
+In particular, source probabilities are normalized, tally limits may reduce ``settings.time_boundary``, and particle-bank buffer ratios may be adjusted for the selected run mode.
+Set new raw values explicitly before recompiling when an iterative workflow changes one of these inputs.
+
+4. Visualize or Run
+-------------------
+
+Visualization is a useful geometry check before transport.
+Spatial coordinates are in cm and snapshot times are in seconds:
+
+.. code-block:: python
+
+ simulation.visualize_model(
+ vis_plane="xz",
+ x=[-1.0, 1.0],
+ y=0.0,
+ z=[0.0, 2.0],
+ pixels=(100, 200),
+ colors=None,
+ time=[0.0],
+ save_as="slab_geometry",
+ )
+
+Run transport after validating the model:
+
+.. code-block:: python
+
+ simulation.run()
+
+Begin in Python mode:
+
+.. code-block:: sh
+
+ python input.py --mode=python
+
+After the model and workflow are correct, enable Numba-CPU:
+
+.. code-block:: sh
+
+ python input.py --mode=numba
+
+GPU execution adds another acceleration and runtime layer:
+
+.. code-block:: sh
+
+ python input.py --mode=numba --target=gpu
+
+See :doc:`execution/cpu` and :doc:`execution/gpu` for operational guidance.
+
+5. Process Output
+-----------------
+
+``simulation.run()`` writes the configured HDF5 output.
+Use stable tally names and a companion post-processing script so the relationship between an input and its analysis remains clear:
+
+.. code-block:: python
+
+ tally = mcdc.Tally(
+ name="slab_flux",
+ mesh=mesh,
+ scores=["flux"],
+ )
+ simulation.set_tallies([tally])
+ simulation.settings.output_name = "slab"
+
+The tally is then available under ``tallies/slab_flux`` in ``slab.h5``.
+
+For workflows that reuse most of a model while changing selected inputs between runs, continue with :doc:`iterative_simulations`.
+
+Complete Workflows
+------------------
+
+The example suite demonstrates the lifecycle in complete inputs:
+
+- :ref:`example_slab_shielding` — fixed-source execution, model visualization, and output processing.
+- :ref:`example_iterative_source_reweighting` — partial model updates and
+ repeated compilation within one simulation.
+- :ref:`example_moving_source` — transient fixed-source transport.
+- :ref:`example_c5g7_k_eigenvalue` — k-eigenvalue execution.
+- :ref:`example_c5g7_transient` — a larger reactor transient.
+
+For exact public signatures, use the :doc:`../reference/python_api/index`.
+For framework implementation details, continue with :doc:`../developer_guide/architecture/simulation_compilation` and :doc:`../developer_guide/architecture/runtime_data_layout`.
diff --git a/docs/source/user_guide/sources.rst b/docs/source/user_guide/sources.rst
new file mode 100644
index 000000000..8a98890be
--- /dev/null
+++ b/docs/source/user_guide/sources.rst
@@ -0,0 +1,61 @@
+.. _user_sources:
+
+================
+Particle Sources
+================
+
+A :class:`mcdc.Source` describes the position, direction, energy, time, particle type, and relative probability of particles introduced into a simulation.
+Position and spatial bounds use cm, physical energy uses eV, time uses seconds, and direction vectors are dimensionless.
+Only sources passed to :meth:`mcdc.Simulation.set_sources` participate in transport.
+
+Energy Distributions
+--------------------
+
+A scalar ``energy`` defines a mono-energetic source:
+
+.. code-block:: python
+
+ physical_source = mcdc.Source(energy=1.0e6)
+
+An ``energy`` array with shape ``(2, N)`` defines a continuous probability density with energy values in the first row in eV and density in the second row in eV\ :sup:`-1`.
+Use ``discrete_energy`` for a probability mass function over physical emission lines:
+
+.. code-block:: python
+
+ emission_lines = mcdc.Source(
+ particle_type="electron",
+ discrete_energy=(
+ [1.0e5, 2.0e5],
+ [0.8, 0.2],
+ ),
+ )
+
+The values in the second row are dimensionless relative probabilities.
+The ``energy`` and ``discrete_energy`` inputs are alternative source-energy specifications and cannot be combined.
+
+.. _user_standard_multigroup_sources:
+
+Standard Neutron Multigroup Sources
+-----------------------------------
+
+Standard neutron multigroup transport uses a dimensionless group coordinate in place of physical source energy.
+A scalar integer selects one group:
+
+.. code-block:: python
+
+ group_zero_source = mcdc.Source(energy=0)
+
+Use ``discrete_energy`` to sample among groups:
+
+.. code-block:: python
+
+ group_mixture = mcdc.Source(
+ discrete_energy=(
+ [0, 1],
+ [0.25, 0.75],
+ ),
+ )
+
+Group coordinates must be finite, integer-valued, and satisfy ``0 <= energy < G``.
+Continuous ``energy`` distributions are not valid in standard neutron multigroup transport.
+Native and hybrid transport use physical source energy in eV, as described in :doc:`materials_and_multigroup`.
diff --git a/docs/source/user_guide/tallies.rst b/docs/source/user_guide/tallies.rst
new file mode 100644
index 000000000..9059b4ea0
--- /dev/null
+++ b/docs/source/user_guide/tallies.rst
@@ -0,0 +1,242 @@
+.. _user_tallies:
+
+=============================
+Tallies and Post-processing
+=============================
+
+Tallies specify which transport quantities MC/DC records and how those quantities are divided into bins.
+This page extends the model from :doc:`getting_started/first_simulation`; add the examples below before ``simulation.run()``.
+The code snippets assume NumPy has been imported as ``np``.
+
+Each tally combines:
+
+- one or more scores, such as flux, collision rate, or current;
+- optional particle, spatial, angular, energy, time, surface, or cell filters; and
+- a name used to identify the tally in the output file.
+
+Only tallies passed to ``simulation.set_tallies(...)`` are scored.
+
+Particle and Energy Filters
+---------------------------
+
+The ``particle_type`` and ``energy`` filters are independent.
+If ``particle_type`` is omitted, a tally accepts any transported particle type.
+Set it explicitly when one tally should score only neutrons, electrons, or protons:
+
+.. code-block:: python3
+
+ neutron_flux = mcdc.Tally(
+ name="neutron_flux",
+ scores=["flux"],
+ particle_type="neutron",
+ energy=[0.0, 1.0e6, 20.0e6],
+ )
+
+For native and hybrid transport, ``energy`` contains physical energy-bin boundaries in eV.
+For standard neutron multigroup transport, ``energy`` instead contains boundaries on the dimensionless group coordinate.
+Half-integer boundaries can retain individual groups or collapse several adjacent groups into one tally bin:
+
+.. code-block:: python3
+
+ collapsed_group_flux = mcdc.Tally(
+ name="collapsed_group_flux",
+ scores=["flux"],
+ particle_type="neutron",
+ energy=[-0.5, 1.5, 3.5],
+ )
+
+Use ``energy="all"`` to create one tally bin per neutron energy group during simulation compilation:
+
+.. code-block:: python3
+
+ group_flux = mcdc.Tally(
+ name="group_flux",
+ scores=["flux"],
+ energy="all",
+ )
+
+The ``"all"`` shortcut is valid only when compilation selects standard neutron multigroup transport.
+For this shortcut, omit ``particle_type`` or set it to ``"neutron"``.
+Compilation replaces it with ``[-0.5, 0.5, ..., G - 0.5]`` for the shared ``G``-group structure.
+
+Mesh Tallies
+------------
+
+The introductory example scores flux over a structured z mesh:
+
+.. code-block:: python3
+
+ mesh = mcdc.MeshStructured(z=np.linspace(0.0, 6.0, 61))
+ flux_tally = mcdc.Tally(
+ name="slab_flux",
+ mesh=mesh,
+ scores=["flux"],
+ )
+ simulation.set_tallies([flux_tally])
+
+The grid points are positions in cm; these 61 points define 60 spatial bins.
+Track-length scores include ``"flux"``, ``"density"``, ``"collision"``, ``"capture"``, and ``"fission"``.
+
+Angular Filters
+---------------
+
+Add polar-cosine boundaries to retain angular information:
+
+.. code-block:: python3
+
+ angular_flux_tally = mcdc.Tally(
+ name="angular_flux",
+ mesh=mesh,
+ mu=np.linspace(-1.0, 1.0, 33),
+ scores=["flux"],
+ )
+ simulation.set_tallies([angular_flux_tally])
+
+The dimensionless polar-cosine boundaries produce 32 angular bins in each spatial bin.
+Azimuthal boundaries, when supplied with ``azi``, are in radians.
+A reference direction can be supplied with ``polar_reference``; its default is the positive z direction.
+Time-filter boundaries are in seconds.
+
+Surface-crossing Tallies
+------------------------
+
+A surface filter scores net current across a particular surface:
+
+.. code-block:: python3
+
+ interface_current = mcdc.Tally(
+ name="interface_current",
+ surface=interface,
+ scores=["current-net"],
+ )
+
+The current sign follows the orientation of the surface normal.
+For the ``PlaneZ`` at the material interface, crossings toward increasing z contribute positively and crossings toward decreasing z contribute negatively.
+
+A cell filter can score current across every boundary of a cell:
+
+.. code-block:: python3
+
+ shield_cell_current = mcdc.Tally(
+ name="shield_cell_current",
+ cell=shield_cell,
+ scores=["current-net", "current-in", "current-out"],
+ )
+
+``"current-in"`` and ``"current-out"`` are positive partial currents; ``"current-net"`` retains the crossing sign.
+
+Combining cell and surface filters restricts the tally to one surface while using the cell to classify incoming and outgoing crossings:
+
+.. code-block:: python3
+
+ shield_interface_current = mcdc.Tally(
+ name="shield_interface_current",
+ surface=interface,
+ cell=shield_cell,
+ scores=["current-net", "current-in", "current-out"],
+ )
+
+Attach every requested tally in one call:
+
+.. code-block:: python3
+
+ simulation.set_tallies(
+ [
+ flux_tally,
+ interface_current,
+ shield_cell_current,
+ shield_interface_current,
+ ]
+ )
+
+Statistical Uncertainty
+-----------------------
+
+MC/DC estimates tally uncertainty from statistically independent batches:
+
+.. code-block:: python3
+
+ simulation.settings.N_particle = 1_000
+ simulation.settings.N_batch = 10
+
+``N_particle`` is the number of histories per batch.
+Increasing ``N_particle`` reduces the noise within each batch, while ``N_batch`` controls how many independent batch results contribute to the reported standard deviation.
+At least two batches are required for a nonzero estimate.
+
+Reading Tally Output
+--------------------
+
+Named tallies are stored under ``tallies/`` in the output HDF5 file.
+The filter grids and score results are stored below that group:
+
+.. code-block:: text
+
+ tallies/
+ slab_flux/
+ grid/
+ z
+ flux/
+ mean
+ sdev
+
+Load and normalize the mesh flux with h5py:
+
+.. code-block:: python3
+
+ import h5py
+
+ with h5py.File("slab_shielding.h5", "r") as output:
+ tally = output["tallies/slab_flux"]
+ z = tally["grid/z"][:]
+ flux = tally["flux/mean"][:]
+ flux_sdev = tally["flux/sdev"][:]
+
+ dz = z[1:] - z[:-1]
+ flux /= dz
+ flux_sdev /= dz
+
+The score arrays contain values integrated over their bins.
+Spatial grids in the output retain cm, angular grids use radians or dimensionless polar cosine, physical energy grids use eV, standard-multigroup group-coordinate grids are dimensionless, and time grids use seconds.
+Divide by the applicable bin widths when a differential result is required.
+
+Reducing an Angular Tally
+-------------------------
+
+For the angular tally above, sum the angle-bin contributions to recover scalar flux.
+Weighting by the polar-cosine midpoint gives a midpoint approximation of the z-directed current:
+
+.. code-block:: python3
+
+ with h5py.File("slab_shielding.h5", "r") as output:
+ tally = output["tallies/angular_flux"]
+ z = tally["grid/z"][:]
+ mu = tally["grid/mu"][:]
+ angular_flux = tally["flux/mean"][:]
+ angular_flux_sdev = tally["flux/sdev"][:]
+
+ dz = z[1:] - z[:-1]
+ dmu = mu[1:] - mu[:-1]
+ mu_mid = 0.5 * (mu[:-1] + mu[1:])
+
+ scalar_flux = np.sum(angular_flux, axis=0) / dz
+ scalar_flux_sdev = np.linalg.norm(angular_flux_sdev, axis=0) / dz
+ current = np.sum(
+ angular_flux * mu_mid[:, np.newaxis],
+ axis=0,
+ ) / dz
+
+The exact array-axis order follows the active filters and is recorded by the corresponding grids in the tally group.
+Inspect the output shapes before performing reductions.
+
+Verification
+------------
+
+Before drawing conclusions from a tally:
+
+- Confirm that its filters cover the intended phase-space region.
+- Check that the result changes consistently when the mesh is refined.
+- Increase the particle population and verify that uncertainty decreases.
+- Use current tallies to check particle balance where appropriate.
+- Compare against an analytic or benchmark solution when one is available.
+
+The :class:`mcdc.Tally` API reference documents all supported scores and filter combinations.
diff --git a/docs/source/user/troubleshooting.rst b/docs/source/user_guide/troubleshooting.rst
similarity index 73%
rename from docs/source/user/troubleshooting.rst
rename to docs/source/user_guide/troubleshooting.rst
index 1e3595fbe..7c7fc75c4 100644
--- a/docs/source/user/troubleshooting.rst
+++ b/docs/source/user_guide/troubleshooting.rst
@@ -5,16 +5,13 @@ Troubleshooting
===============
This page collects solutions to common installation and runtime problems.
-If your issue is not listed here, please check our
-`GitHub issues `_
-or open a new one.
+If your issue is not listed here, check the `GitHub issues `_ or open a new one.
Numba Version Compatibility
----------------------------
-MC/DC requires **Numba >= 0.60.0**.
-Symptoms of version mismatch include ``TypingError``, unexpected ``LoweringError``,
-or missing ``@njit`` features.
+MC/DC requires **Numba >= 0.61.0**.
+Symptoms of version mismatch include ``TypingError``, unexpected ``LoweringError``, or missing ``@njit`` features.
Check your version:
@@ -27,26 +24,24 @@ Check your version:
.. code-block:: sh
# pip
- pip install --upgrade 'numba>=0.60.0'
+ pip install --upgrade 'numba>=0.61.0'
# conda
- conda install numba>=0.60.0 -c conda-forge
+ conda install 'numba>=0.61.0' -c conda-forge
**Pinning Numba for CUDA compatibility:**
-If your system requires a specific CUDA toolkit,
-Numba and ``cuda-toolkit`` versions must match.
-For example, CUDA 11.8 works best with Numba 0.60.x:
+If your system requires a specific CUDA toolkit, Numba and ``cuda-toolkit`` versions must match.
+Use a Numba release compatible with both the installed Python version and the selected CUDA toolkit:
.. code-block:: sh
- conda install numba=0.60 cudatoolkit=11.8 -c conda-forge
+ conda install 'numba>=0.61.0' cuda-toolkit=11.8 -c conda-forge
**Patching Numba for AMD GPUs (HIP):**
AMD ROCm GPU support requires a patched Numba build.
-Follow the `numba-hip instructions `_
-to apply the HIP target triple patch.
+Follow the `numba-hip instructions `_ to apply the HIP target triple patch.
This is required before installing Harmonize for AMD targets.
See also :ref:`install-amd-gpus`.
@@ -55,8 +50,7 @@ Building mpi4py from Source
----------------------------
On most HPCs, prebuilt mpi4py wheels are incompatible with the system MPI library.
-Symptoms include ``MPI_Init`` failures, segfaults at launch, or
-``ImportError: libmpi.so: cannot open shared object file``.
+Symptoms include ``MPI_Init`` failures, segfaults at launch, or ``ImportError: libmpi.so: cannot open shared object file``.
**Step 1 — Load the correct MPI module:**
@@ -73,15 +67,6 @@ Symptoms include ``MPI_Init`` failures, segfaults at launch, or
CC=mpicc pip install --no-cache-dir --no-binary mpi4py mpi4py
-Or, if using conda with the ``install.sh`` script:
-
-.. code-block:: sh
-
- bash install.sh --hpc
-
-The ``--hpc`` flag instructs the install script to build mpi4py from source
-using the currently loaded MPI module.
-
**Verifying the installation:**
.. code-block:: sh
@@ -105,10 +90,10 @@ Incorrect or missing modules are the most common source of build failures.
- **Module loads**
- **Notes**
* - Quartz (LLNL)
- - ``module load python/3.11``
+ - ``module load python/3.13``
- Default ``intel-classic`` + ``mvapich2`` are sufficient
* - Dane (LLNL)
- - ``module load python/3.11``
+ - ``module load python/3.13``
- x86_64, similar to Quartz
* - Lassen (LLNL)
- ``module load gcc/8 cuda/11.8``
@@ -120,8 +105,7 @@ Incorrect or missing modules are the most common source of build failures.
- ``module load cray-mpich rocm/6.0.0``
- AMD MI300A GPUs
-After loading modules, activate your Python environment (venv or conda)
-before running ``pip install`` or ``bash install.sh``.
+After loading modules, activate your Python environment before running ``pip install``.
Container Errors
----------------
@@ -140,7 +124,7 @@ Fix:
.. code-block:: bash
- podman run --rm -it --user root mcdc:dev
+ podman run --rm -it --user root ghcr.io/mcdc-project/mcdc:dev
``Out of memory`` (Apptainer)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -189,8 +173,7 @@ Use ``--caching`` to save compiled binaries:
python input.py --mode=numba --caching
Subsequent runs will skip compilation.
-If compilation seems stuck, check that you are not running
-on a login node with limited resources.
+If compilation seems stuck, check that you are not running on a login node with limited resources.
**Segmentation fault during MPI runs**
@@ -202,10 +185,9 @@ Also ensure that the number of MPI ranks does not exceed available cores:
srun -n python input.py --mode=numba
-**"AttributeError: 'list' object has no attribute 'ID'" in** ``mcdc.cell()``
+**"AttributeError: 'list' object has no attribute 'ID'" in** ``mcdc.Cell()``
-This error occurs when passing a Python list instead of using the ``&`` (intersection)
-and ``|`` (union) region operators.
+This error occurs when passing a Python list instead of using the ``&`` (intersection) and ``|`` (union) region operators.
Use the operator syntax:
.. code-block:: python3
@@ -216,22 +198,21 @@ Use the operator syntax:
# Wrong — do NOT use a list
mcdc.Cell(region=[+s1, -s2], fill=material)
-See `#348 `_.
+See `#348 `_.
Bugs and Issues
---------------
-Our documentation is in the early stages of development, so thank you for bearing with us
-while we bring it up to snuff. If you find a novel bug or anything else you feel we should
-be aware of, feel free to `open an issue `_.
+Our documentation remains under development, so thank you for bearing with us while we improve it.
+If you find a bug or another problem, please `open an issue `_.
Getting More Help
~~~~~~~~~~~~~~~~~
If you are still stuck after reviewing this troubleshooting guide:
-#. Search the `GitHub issues `_ for similar problems.
+#. Search the `GitHub issues `_ for similar problems.
#. Run in debug mode for more informative error messages:
.. code-block:: sh
diff --git a/examples/c5g7/k-eigenvalue/input.py b/examples/c5g7/k-eigenvalue/input.py
index 15ecfdc2e..990afc624 100644
--- a/examples/c5g7/k-eigenvalue/input.py
+++ b/examples/c5g7/k-eigenvalue/input.py
@@ -3,6 +3,8 @@
import mcdc
+simulation = mcdc.Simulation("C5G7 k-eigenvalue")
+
# =============================================================================
# Materials
# =============================================================================
@@ -13,7 +15,7 @@
# Setter
def set_mat(mat):
- return mcdc.MaterialMG(
+ return mcdc.Material.multigroup(
capture=mat["capture"][:],
scatter=mat["scatter"][:],
fission=mat["fission"][:],
@@ -255,9 +257,9 @@ def set_mat(mat):
reflector_south = mcdc.Cell(+x0 & -x3 & +y0 & -y1 & +z1 & -z2, mat_mod)
reflector_east = mcdc.Cell(+x2 & -x3 & +y1 & -y3 & +z1 & -z2, mat_mod)
-# Root universe
-mcdc.simulation.set_root_universe(
- cells=[
+# Set model
+simulation.set_model(
+ [
assembly_1,
assembly_2,
assembly_3,
@@ -265,20 +267,21 @@ def set_mat(mat):
reflector_bottom,
reflector_south,
reflector_east,
- ],
+ ]
)
# =============================================================================
# Set source
# =============================================================================
-mcdc.Source(
+source = mcdc.Source(
x=[0.0, pitch * 17 * 2],
y=[-pitch * 17 * 2, 0.0],
z=[-core_height / 2, core_height / 2],
isotropic=True,
- energy_group=0, # Highest energy
+ energy=0, # Highest energy
)
+simulation.set_sources([source])
# =============================================================================
# Set tallies, settings, techniques and run MC/DC
@@ -292,15 +295,16 @@ def set_mat(mat):
)
g_grid = np.array([-0.5, 3.5, 6.5]) # Collapsing to fast (1-4) and slow (5-7)
mesh = mcdc.MeshStructured(x=x_grid, y=y_grid, z=z_grid)
-mcdc.Tally(mesh=mesh, scores=["flux"], energy=g_grid)
+tally = mcdc.Tally(mesh=mesh, scores=["flux"], energy=g_grid)
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 50
-mcdc.settings.census_bank_buffer_ratio = 4.0
-mcdc.settings.set_eigenmode(N_inactive=5, N_active=10, gyration_radius="all")
+simulation.settings.N_particle = 50
+simulation.settings.census_bank_buffer_ratio = 4.0
+simulation.settings.set_eigenmode(N_inactive=5, N_active=10, gyration_radius="all")
# Techniques
-mcdc.simulation.population_control()
+simulation.technique.population_control()
# Run
-mcdc.run()
+simulation.run()
diff --git a/examples/c5g7/transient/input.py b/examples/c5g7/transient/input.py
index 2381f8c00..2ccd8661f 100644
--- a/examples/c5g7/transient/input.py
+++ b/examples/c5g7/transient/input.py
@@ -3,6 +3,8 @@
import mcdc
+simulation = mcdc.Simulation("C5G7 transient")
+
# =============================================================================
# Materials
# =============================================================================
@@ -13,7 +15,7 @@
# Setter
def set_mat(mat):
- return mcdc.MaterialMG(
+ return mcdc.Material.multigroup(
capture=mat["capture"][:],
scatter=mat["scatter"][:],
fission=mat["fission"][:],
@@ -314,9 +316,9 @@ def set_mat(mat):
reflector_south = mcdc.Cell(+x0 & -x3 & +y0 & -y1 & +z1 & -z2, mat_mod)
reflector_east = mcdc.Cell(+x2 & -x3 & +y1 & -y3 & +z1 & -z2, mat_mod)
-# Root universe
-mcdc.simulation.set_root_universe(
- cells=[
+# Set model
+simulation.set_model(
+ [
assembly_1,
assembly_2,
assembly_3,
@@ -324,7 +326,7 @@ def set_mat(mat):
reflector_bottom,
reflector_south,
reflector_east,
- ],
+ ]
)
# =============================================================================
@@ -338,9 +340,10 @@ def set_mat(mat):
y=np.array([-pitch * 17 * 3 / 2] * 2) + np.array([-pitch / 2, +pitch / 2]),
z=[-core_height / 2, core_height / 2],
isotropic=True,
- energy_group=0, # Highest energy
+ energy=0, # Highest energy
time=[0.0, 15.0],
)
+simulation.set_sources([source])
# =============================================================================
# Set tallies, settings, techniques and run MC/DC
@@ -356,12 +359,13 @@ def set_mat(mat):
y = np.linspace(-pitch * 17 * 2, 0.0, Ny + 1)
z = np.linspace(-core_height / 2, core_height / 2, Nz + 1)
mesh = mcdc.MeshStructured(x=x, y=y, z=z)
-mcdc.Tally(mesh=mesh, scores=["fission"], time=t)
+tally = mcdc.Tally(mesh=mesh, scores=["fission"], time=t)
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 10000
-mcdc.settings.N_batch = 2
-mcdc.settings.active_bank_buffer = 1000
+simulation.settings.N_particle = 10000
+simulation.settings.N_batch = 2
+simulation.settings.active_bank_buffer = 1000
# Run
-mcdc.run()
+simulation.run()
diff --git a/examples/fuel_array_packaged/input.py b/examples/fuel_array_packaged/input.py
index c507a0096..6810fba3e 100644
--- a/examples/fuel_array_packaged/input.py
+++ b/examples/fuel_array_packaged/input.py
@@ -1,22 +1,24 @@
import numpy as np
import mcdc
+simulation = mcdc.Simulation("Packaged fuel array")
+
# ======================================================================================
# Materials
# ======================================================================================
-fuel = mcdc.MaterialMG(
+fuel = mcdc.Material.multigroup(
capture=np.array([0.45]),
fission=np.array([0.55]),
nu_p=np.array([2.5]),
)
-cover = mcdc.MaterialMG(
+cover = mcdc.Material.multigroup(
capture=np.array([0.05]),
scatter=np.array([[0.95]]),
)
-water = mcdc.MaterialMG(
+water = mcdc.Material.multigroup(
capture=np.array([0.02]),
scatter=np.array([[0.08]]),
)
@@ -68,14 +70,15 @@
region=container_right, fill=assembly, translation=[+5, 0, 0], rotation=[0, 10, 0]
)
-# Root universe
-mcdc.simulation.set_root_universe(cells=[assembly_left, assembly_right])
+# Set model
+simulation.set_model([assembly_left, assembly_right])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(x=[-0.1, 0.1], isotropic=True, energy_group=0)
+source = mcdc.Source(x=[-0.1, 0.1], isotropic=True, energy=0)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -86,23 +89,31 @@
x=np.linspace(-10, 10, 201),
z=np.linspace(-5, 5, 101),
)
-mcdc.Tally(mesh=mesh, scores=["fission"])
+tally = mcdc.Tally(mesh=mesh, scores=["fission"])
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 1000
-mcdc.settings.N_batch = 2
-mcdc.settings.active_bank_buffer = 1000
+simulation.settings.N_particle = 1000
+simulation.settings.N_batch = 2
+simulation.settings.active_bank_buffer = 1000
# Run (or visualize)
visualize = False
if not visualize:
- mcdc.run()
+ simulation.run()
else:
colors = {
fuel: "red",
cover: "gray",
water: "blue",
}
- mcdc.visualize(
- "xz", y=0.0, x=[-11.0, 11.0], z=[-6, 6], pixels=(400, 400), colors=colors
+ simulation.visualize_model(
+ vis_plane="xz",
+ y=0.0,
+ x=[-11.0, 11.0],
+ z=[-6, 6],
+ pixels=(400, 400),
+ colors=colors,
+ time=[0.0],
+ save_as=None,
)
diff --git a/examples/hybrid_multigroup/hybrid_multigroup.h5 b/examples/hybrid_multigroup/hybrid_multigroup.h5
new file mode 100644
index 000000000..b391f0238
Binary files /dev/null and b/examples/hybrid_multigroup/hybrid_multigroup.h5 differ
diff --git a/examples/hybrid_multigroup/input.py b/examples/hybrid_multigroup/input.py
new file mode 100644
index 000000000..58dc2db47
--- /dev/null
+++ b/examples/hybrid_multigroup/input.py
@@ -0,0 +1,57 @@
+import numpy as np
+
+import mcdc
+
+simulation = mcdc.Simulation("Hybrid multigroup sphere")
+
+# Material with native H-1 data and a low-energy multigroup treatment
+# MCDC_LIB must contain H1-293.6K.h5.
+material = mcdc.Material(
+ nuclide_composition={"H1": 5.0e-2},
+ neutron_multigroup=mcdc.NeutronMultigroupData(
+ capture=np.array([0.2]),
+ scatter=np.array([[0.8]]),
+ speed=np.array([1.383e6]),
+ energy_grid=np.array([0.1, 10.0]),
+ energy_representation="midpoint",
+ ),
+)
+
+# Spherical domain
+boundary = mcdc.Surface.Sphere(radius=5.0, boundary_condition="vacuum")
+cell = mcdc.Cell(region=-boundary, fill=material)
+simulation.set_model([cell])
+
+# The low-energy source uses multigroup physics
+multigroup_source = mcdc.Source(
+ position=[0.0, 0.0, 1.0],
+ isotropic=True,
+ energy=1.0,
+ probability=0.5,
+)
+
+# The high-energy source lies outside the multigroup grid and uses native physics
+native_source = mcdc.Source(
+ position=[0.0, 0.0, -1.0],
+ isotropic=True,
+ energy=1.0e6,
+ probability=0.5,
+)
+simulation.set_sources([multigroup_source, native_source])
+
+# Hybrid tallies use physical energy boundaries in eV
+mesh = mcdc.MeshStructured(z=np.array([-5.0, 0.0, 5.0]))
+tally = mcdc.Tally(
+ name="hybrid_flux",
+ mesh=mesh,
+ particle_type="neutron",
+ energy=np.array([0.0, 0.1, 10.0, 1.0e5, 20.0e6]),
+ scores=["flux", "collision"],
+)
+simulation.set_tallies([tally])
+
+simulation.settings.N_particle = 1_000
+simulation.settings.N_batch = 5
+simulation.settings.output_name = "hybrid_multigroup"
+
+simulation.run()
diff --git a/examples/hybrid_multigroup/process-output.py b/examples/hybrid_multigroup/process-output.py
new file mode 100644
index 000000000..0b5316645
--- /dev/null
+++ b/examples/hybrid_multigroup/process-output.py
@@ -0,0 +1,29 @@
+import h5py
+import matplotlib.pyplot as plt
+import numpy as np
+
+with h5py.File("hybrid_multigroup.h5", "r") as output:
+ tally = output["tallies/hybrid_flux"]
+ z = tally["grid/z"][:]
+ energy = tally["grid/energy"][:]
+ flux = tally["flux/mean"][:]
+
+dz = z[1:] - z[:-1]
+z_midpoint = 0.5 * (z[:-1] + z[1:])
+flux = np.reshape(flux, (len(energy) - 1, len(z) - 1)) / dz
+
+figure, axis = plt.subplots()
+for index in range(len(energy) - 1):
+ axis.plot(
+ z_midpoint,
+ flux[index],
+ marker="o",
+ label=f"{energy[index]:.1e}–{energy[index + 1]:.1e} eV",
+ )
+
+axis.set_xlabel("z [cm]")
+axis.set_ylabel("Flux")
+axis.grid()
+axis.legend()
+figure.tight_layout()
+figure.savefig("hybrid_multigroup_flux.png", dpi=150)
diff --git a/examples/iterative_source_reweighting/input.py b/examples/iterative_source_reweighting/input.py
new file mode 100644
index 000000000..a458640a0
--- /dev/null
+++ b/examples/iterative_source_reweighting/input.py
@@ -0,0 +1,60 @@
+import numpy as np
+
+import mcdc
+
+simulation = mcdc.Simulation("Iterative source reweighting")
+
+# Homogeneous one-group slab
+material = mcdc.Material.multigroup(
+ capture=np.array([0.2]),
+ scatter=np.array([[0.8]]),
+)
+left_boundary = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
+right_boundary = mcdc.Surface.PlaneZ(z=10.0, boundary_condition="vacuum")
+slab = mcdc.Cell(
+ region=+left_boundary & -right_boundary,
+ fill=material,
+)
+simulation.set_model([slab])
+
+# Symmetric source regions
+source_left = mcdc.Source(
+ name="Left source",
+ z=[1.0, 2.0],
+ isotropic=True,
+ energy=0,
+)
+source_right = mcdc.Source(
+ name="Right source",
+ z=[8.0, 9.0],
+ isotropic=True,
+ energy=0,
+)
+simulation.set_sources([source_left, source_right])
+
+# One tally is reused by every iteration
+mesh = mcdc.MeshStructured(z=np.linspace(0.0, 10.0, 51))
+flux_tally = mcdc.Tally(
+ name="source_mix_flux",
+ mesh=mesh,
+ scores=["flux"],
+)
+simulation.set_tallies([flux_tally])
+
+simulation.settings.N_particle = 20_000
+simulation.settings.N_batch = 10
+
+# Partially update the same owned model and compile a fresh snapshot each time.
+source_mixes = (
+ ("left_20", 0.2),
+ ("left_50", 0.5),
+ ("left_80", 0.8),
+)
+
+for case_name, left_fraction in source_mixes:
+ source_left.probability = left_fraction
+ source_right.probability = 1.0 - left_fraction
+ simulation.settings.output_name = f"source_mix_{case_name}"
+
+ simulation.compile()
+ simulation.run()
diff --git a/examples/iterative_source_reweighting/process-output.py b/examples/iterative_source_reweighting/process-output.py
new file mode 100644
index 000000000..4fd1a75ec
--- /dev/null
+++ b/examples/iterative_source_reweighting/process-output.py
@@ -0,0 +1,84 @@
+from pathlib import Path
+
+import h5py
+import matplotlib.pyplot as plt
+import numpy as np
+
+cases = (
+ ("left_20", "20% left / 80% right"),
+ ("left_50", "50% left / 50% right"),
+ ("left_80", "80% left / 20% right"),
+)
+
+profiles = {}
+uncertainties = {}
+z = None
+
+for case_name, _ in cases:
+ output_path = Path(f"source_mix_{case_name}.h5")
+ with h5py.File(output_path, "r") as output:
+ tally = output["tallies/source_mix_flux"]
+ case_z = tally["grid/z"][:]
+ flux = tally["flux/mean"][:]
+ flux_sdev = tally["flux/sdev"][:]
+
+ if z is None:
+ z = case_z
+ elif not np.array_equal(z, case_z):
+ raise ValueError(f"Inconsistent tally grid in {output_path}")
+
+ dz = case_z[1:] - case_z[:-1]
+ profiles[case_name] = flux / dz
+ uncertainties[case_name] = flux_sdev / dz
+
+z_mid = 0.5 * (z[:-1] + z[1:])
+dz = z[1:] - z[:-1]
+left_half = z_mid < 5.0
+right_half = ~left_half
+
+print("Integrated flux comparison")
+print("--------------------------")
+for case_name, label in cases:
+ profile = profiles[case_name]
+ left_flux = np.sum(profile[left_half] * dz[left_half])
+ right_flux = np.sum(profile[right_half] * dz[right_half])
+ print(
+ f"{label:24s} left={left_flux:.6e} right={right_flux:.6e} "
+ f"left/right={left_flux / right_flux:.4f}"
+ )
+
+mirror_difference = np.linalg.norm(profiles["left_20"] - profiles["left_80"][::-1])
+mirror_scale = np.linalg.norm(0.5 * (profiles["left_20"] + profiles["left_80"][::-1]))
+balanced_difference = np.linalg.norm(profiles["left_50"] - profiles["left_50"][::-1])
+balanced_scale = np.linalg.norm(profiles["left_50"])
+
+print()
+print(
+ "20/80 versus mirrored 80/20 relative RMS difference: "
+ f"{mirror_difference / mirror_scale:.4e}"
+)
+print(
+ "50/50 profile relative left-right asymmetry: "
+ f"{balanced_difference / balanced_scale:.4e}"
+)
+
+figure, axis = plt.subplots()
+for case_name, label in cases:
+ profile = profiles[case_name]
+ uncertainty = uncertainties[case_name]
+ axis.plot(z_mid, profile, label=label)
+ axis.fill_between(
+ z_mid,
+ profile - uncertainty,
+ profile + uncertainty,
+ alpha=0.15,
+ )
+
+axis.axvline(5.0, color="black", linestyle="--", linewidth=1.0)
+axis.set_xlabel("z [cm]")
+axis.set_ylabel("Flux")
+axis.grid()
+axis.legend()
+figure.tight_layout()
+figure.savefig("iterative_source_comparison.png", dpi=150)
+plt.close(figure)
diff --git a/examples/kobayashi-TD/input.py b/examples/kobayashi-TD/input.py
index 05e4808aa..26b6bdb68 100644
--- a/examples/kobayashi-TD/input.py
+++ b/examples/kobayashi-TD/input.py
@@ -1,6 +1,8 @@
import numpy as np
import mcdc
+simulation = mcdc.Simulation("Time-dependent Kobayashi dog-leg benchmark")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -8,8 +10,8 @@
# (PNE 2001, https://doi.org/10.1016/S0149-1970(01)00007-5)
# Set materials
-m = mcdc.MaterialMG(capture=np.array([0.05]), scatter=np.array([[0.05]]))
-m_void = mcdc.MaterialMG(capture=np.array([5e-5]), scatter=np.array([[5e-5]]))
+m = mcdc.Material.multigroup(capture=np.array([0.05]), scatter=np.array([[0.05]]))
+m_void = mcdc.Material.multigroup(capture=np.array([5e-5]), scatter=np.array([[5e-5]]))
# Set surfaces
sx1 = mcdc.Surface.PlaneX(x=0.0, boundary_condition="reflective")
@@ -30,31 +32,33 @@
# Set cells
# Source
-mcdc.Cell(region=+sx1 & -sx2 & +sy1 & -sy2 & +sz1 & -sz2, fill=m)
+source_cell = mcdc.Cell(region=+sx1 & -sx2 & +sy1 & -sy2 & +sz1 & -sz2, fill=m)
# Voids
channel_1 = +sx1 & -sx2 & +sy2 & -sy3 & +sz1 & -sz2
channel_2 = +sx1 & -sx3 & +sy3 & -sy4 & +sz1 & -sz2
channel_3 = +sx3 & -sx4 & +sy3 & -sy4 & +sz1 & -sz3
channel_4 = +sx3 & -sx4 & +sy3 & -sy5 & +sz3 & -sz4
void_channel = channel_1 | channel_2 | channel_3 | channel_4
-mcdc.Cell(region=void_channel, fill=m_void)
+void_cell = mcdc.Cell(region=void_channel, fill=m_void)
# Shield
box = +sx1 & -sx5 & +sy1 & -sy5 & +sz1 & -sz5
-mcdc.Cell(region=box & ~void_channel, fill=m)
+shield_cell = mcdc.Cell(region=box & ~void_channel, fill=m)
+simulation.set_model([source_cell, void_cell, shield_cell])
# ======================================================================================
# Set source
# ======================================================================================
-# The source pulses in t=[0,5]
+# The source pulses in t=[0, 50]
-mcdc.Source(
+source = mcdc.Source(
x=[0.0, 10.0],
y=[0.0, 10.0],
z=[0.0, 10.0],
isotropic=True,
- energy_group=0,
+ energy=0,
time=[0.0, 50.0],
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -63,15 +67,16 @@
# Tallies
time_grid = np.linspace(0.0, 200.0, 21)
mesh = mcdc.MeshUniform(x=(0.0, 1.0, 60), y=(0.0, 1.0, 100))
-mcdc.Tally(mesh=mesh, scores=["flux"], time=time_grid)
-mcdc.Tally(scores=["density"], time=time_grid)
+flux_tally = mcdc.Tally(mesh=mesh, scores=["flux"], time=time_grid)
+density_tally = mcdc.Tally(scores=["density"], time=time_grid)
+simulation.set_tallies([flux_tally, density_tally])
# Settings
-mcdc.settings.N_particle = 100
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 100
+simulation.settings.N_batch = 2
# Techniques
-mcdc.simulation.implicit_capture()
+simulation.technique.implicit_capture()
# Run
-mcdc.run()
+simulation.run()
diff --git a/examples/kobayashi/input.py b/examples/kobayashi/input.py
index 3f6f11929..cfec37d95 100644
--- a/examples/kobayashi/input.py
+++ b/examples/kobayashi/input.py
@@ -1,6 +1,8 @@
import numpy as np
import mcdc
+simulation = mcdc.Simulation("Kobayashi dog-leg benchmark")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -8,8 +10,8 @@
# (PNE 2001, https://doi.org/10.1016/S0149-1970(01)00007-5)
# Set materials
-m = mcdc.MaterialMG(capture=np.array([0.05]), scatter=np.array([[0.05]]))
-m_void = mcdc.MaterialMG(capture=np.array([5e-5]), scatter=np.array([[5e-5]]))
+m = mcdc.Material.multigroup(capture=np.array([0.05]), scatter=np.array([[0.05]]))
+m_void = mcdc.Material.multigroup(capture=np.array([5e-5]), scatter=np.array([[5e-5]]))
# Set surfaces
sx1 = mcdc.Surface.PlaneX(x=0.0, boundary_condition="reflective")
@@ -30,30 +32,30 @@
# Set cells
# Source
-mcdc.Cell(region=+sx1 & -sx2 & +sy1 & -sy2 & +sz1 & -sz2, fill=m)
+source_cell = mcdc.Cell(region=+sx1 & -sx2 & +sy1 & -sy2 & +sz1 & -sz2, fill=m)
# Voids
channel_1 = +sx1 & -sx2 & +sy2 & -sy3 & +sz1 & -sz2
channel_2 = +sx1 & -sx3 & +sy3 & -sy4 & +sz1 & -sz2
channel_3 = +sx3 & -sx4 & +sy3 & -sy4 & +sz1 & -sz3
channel_4 = +sx3 & -sx4 & +sy3 & -sy5 & +sz3 & -sz4
void_channel = channel_1 | channel_2 | channel_3 | channel_4
-mcdc.Cell(region=void_channel, fill=m_void)
+void_cell = mcdc.Cell(region=void_channel, fill=m_void)
# Shield
box = +sx1 & -sx5 & +sy1 & -sy5 & +sz1 & -sz5
-mcdc.Cell(region=box & ~void_channel, fill=m)
+shield_cell = mcdc.Cell(region=box & ~void_channel, fill=m)
+simulation.set_model([source_cell, void_cell, shield_cell])
# ======================================================================================
# Set source
# ======================================================================================
-# The source pulses in t=[0,5]
-
-mcdc.Source(
+source = mcdc.Source(
x=[0.0, 10.0],
y=[0.0, 10.0],
z=[0.0, 10.0],
isotropic=True,
- energy_group=0,
+ energy=0,
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -61,14 +63,15 @@
# Tallies
mesh = mcdc.MeshUniform(x=(0.0, 1.0, 60), y=(0.0, 1.0, 100), z=(0.0, 1.0, 60))
-mcdc.Tally(mesh=mesh, scores=["flux"])
+tally = mcdc.Tally(mesh=mesh, scores=["flux"])
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 1000
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 1000
+simulation.settings.N_batch = 2
# Techniques
-mcdc.simulation.implicit_capture()
+simulation.technique.implicit_capture()
# Run
-mcdc.run()
+simulation.run()
diff --git a/examples/moving_pellet/input.py b/examples/moving_pellet/input.py
index addcb050e..ddd5c165e 100644
--- a/examples/moving_pellet/input.py
+++ b/examples/moving_pellet/input.py
@@ -2,18 +2,20 @@
import mcdc
+simulation = mcdc.Simulation("Moving pellet")
+
# ======================================================================================
# Set model
# ======================================================================================
# Set materials
-fuel = mcdc.MaterialMG(
+fuel = mcdc.Material.multigroup(
capture=np.array([0.5]),
fission=np.array([0.25]),
nu_p=np.array([1.5]),
speed=np.array([200000.0]),
)
-air = mcdc.MaterialMG(
+air = mcdc.Material.multigroup(
capture=np.array([0.002]),
scatter=np.array([[0.008]]),
speed=np.array([200000.0]),
@@ -39,24 +41,26 @@
# Make cells
fuel_pellet_region = +bot_z & -top_z & -cylinder_z
-mcdc.Cell(region=fuel_pellet_region, fill=fuel)
-mcdc.Cell(
+fuel_cell = mcdc.Cell(region=fuel_pellet_region, fill=fuel)
+air_cell = mcdc.Cell(
region=~fuel_pellet_region & +min_x & -max_x & +min_y & -max_y & +min_z & -max_z,
fill=air,
)
+simulation.set_model([fuel_cell, air_cell])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(
+source = mcdc.Source(
x=[2.0, 3.0],
y=[-0.5, 0.5],
z=[-0.5, 0.5],
isotropic=True,
- energy_group=0,
+ energy=0,
time=[0.0, 9.0],
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -67,24 +71,25 @@
x=np.linspace(-5, 5, 201),
z=np.linspace(-10, 10, 201),
)
-mcdc.Tally(mesh=mesh, scores=["fission"], time=np.linspace(0, 9, 46))
+tally = mcdc.Tally(mesh=mesh, scores=["fission"], time=np.linspace(0, 9, 46))
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 100000
-mcdc.settings.N_batch = 2
-mcdc.settings.active_bank_buffer = 1000
+simulation.settings.N_particle = 100000
+simulation.settings.N_batch = 2
+simulation.settings.active_bank_buffer = 1000
# Run (or visualize)
visualize = False
if not visualize:
- mcdc.run()
+ simulation.run()
else:
colors = {
fuel: "red",
air: "blue",
}
- mcdc.visualize(
- "xz",
+ simulation.visualize_model(
+ vis_plane="xz",
y=0.0,
x=[-5.0, 5.0],
z=[-10, 10],
diff --git a/examples/moving_source/input.py b/examples/moving_source/input.py
index c11ae3e8e..60cc23d06 100644
--- a/examples/moving_source/input.py
+++ b/examples/moving_source/input.py
@@ -2,12 +2,14 @@
import mcdc
+simulation = mcdc.Simulation("Moving source")
+
# ======================================================================================
# Set model
# ======================================================================================
# Set materials
-air = mcdc.MaterialMG(
+air = mcdc.Material.multigroup(
capture=np.array([0.002]),
scatter=np.array([[0.008]]),
speed=np.array([200000.0]),
@@ -22,7 +24,8 @@
max_z = mcdc.Surface.PlaneZ(z=10.0, boundary_condition="vacuum")
# Make cells
-mcdc.Cell(region=+min_x & -max_x & +min_y & -max_y & +min_z & -max_z, fill=air)
+cell = mcdc.Cell(region=+min_x & -max_x & +min_y & -max_y & +min_z & -max_z, fill=air)
+simulation.set_model([cell])
# ======================================================================================
# Set source
@@ -34,7 +37,7 @@
z=[-0.5, 0.5],
direction=[1.0, 1.0, 0.0],
polar_cosine=[-1.0, -0.9],
- energy_group=0,
+ energy=0,
time=[0.0, 10.0],
)
src.move(
@@ -45,6 +48,7 @@
],
durations=[7.0, 2.0, 1.0],
)
+simulation.set_sources([src])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -55,11 +59,12 @@
x=np.linspace(-5.0, 5.0, 201),
y=np.linspace(-5.0, 5.0, 201),
)
-mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0, 10, 46))
+tally = mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0, 10, 46))
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 100000
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 100000
+simulation.settings.N_batch = 2
# Run
-mcdc.run()
+simulation.run()
diff --git a/examples/slab_shielding/input.py b/examples/slab_shielding/input.py
new file mode 100644
index 000000000..503fd40d5
--- /dev/null
+++ b/examples/slab_shielding/input.py
@@ -0,0 +1,69 @@
+import numpy as np
+
+import mcdc
+
+simulation = mcdc.Simulation("One-group slab shielding")
+
+# Materials
+source_region_material = mcdc.Material.multigroup(
+ capture=np.array([0.1]),
+ scatter=np.array([[0.9]]),
+)
+shield_material = mcdc.Material.multigroup(
+ capture=np.array([0.7]),
+ scatter=np.array([[0.3]]),
+)
+
+# Geometry
+left = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
+interface = mcdc.Surface.PlaneZ(z=2.0)
+right = mcdc.Surface.PlaneZ(z=6.0, boundary_condition="vacuum")
+
+source_cell = mcdc.Cell(
+ region=+left & -interface,
+ fill=source_region_material,
+)
+shield_cell = mcdc.Cell(
+ region=+interface & -right,
+ fill=shield_material,
+)
+simulation.set_model([source_cell, shield_cell])
+
+# Source
+source = mcdc.Source(
+ z=[0.0, 2.0],
+ isotropic=True,
+ energy=0,
+)
+simulation.set_sources([source])
+
+# Tally
+mesh = mcdc.MeshStructured(z=np.linspace(0.0, 6.0, 61))
+flux_tally = mcdc.Tally(
+ name="slab_flux",
+ mesh=mesh,
+ scores=["flux"],
+)
+simulation.set_tallies([flux_tally])
+
+# Settings
+simulation.settings.N_particle = 1_000
+simulation.settings.N_batch = 10
+simulation.settings.output_name = "slab_shielding"
+
+# Visualize model
+"""
+simulation.visualize_model(
+ vis_plane="xz",
+ x=[-1.0, 1.0],
+ y=0.0,
+ z=[0.0, 6.0],
+ pixels=(100, 300),
+ colors=None,
+ time=[0.0],
+ save_as="slab_shielding_geometry",
+)
+"""
+
+# Run simulation
+simulation.run()
diff --git a/examples/slab_shielding/process-output.py b/examples/slab_shielding/process-output.py
new file mode 100644
index 000000000..dca8214ed
--- /dev/null
+++ b/examples/slab_shielding/process-output.py
@@ -0,0 +1,30 @@
+import h5py
+import matplotlib.pyplot as plt
+
+with h5py.File("slab_shielding.h5", "r") as output:
+ tally = output["tallies/slab_flux"]
+ z = tally["grid/z"][:]
+ flux = tally["flux/mean"][:]
+ flux_sdev = tally["flux/sdev"][:]
+
+dz = z[1:] - z[:-1]
+z_mid = 0.5 * (z[:-1] + z[1:])
+flux /= dz
+flux_sdev /= dz
+
+figure, axis = plt.subplots()
+axis.plot(z_mid, flux, label="Flux")
+axis.fill_between(
+ z_mid,
+ flux - flux_sdev,
+ flux + flux_sdev,
+ alpha=0.25,
+ label="Standard deviation",
+)
+axis.axvline(2.0, color="black", linestyle="--", label="Material interface")
+axis.set_xlabel("z [cm]")
+axis.set_ylabel("Flux")
+axis.grid()
+axis.legend()
+figure.tight_layout()
+figure.savefig("slab_shielding_flux.png", dpi=150)
diff --git a/examples/slab_shielding/slab_shielding.h5 b/examples/slab_shielding/slab_shielding.h5
new file mode 100644
index 000000000..e655468d7
Binary files /dev/null and b/examples/slab_shielding/slab_shielding.h5 differ
diff --git a/examples/sphere_in_cube/input.py b/examples/sphere_in_cube/input.py
index 6b9b4da2e..ca51baade 100644
--- a/examples/sphere_in_cube/input.py
+++ b/examples/sphere_in_cube/input.py
@@ -1,14 +1,16 @@
import numpy as np
import mcdc
+simulation = mcdc.Simulation("Sphere in cube")
+
# ======================================================================================
# Set model
# ======================================================================================
# Homogeneous pure-fission sphere inside a pure-scattering cube
# Set materials
-pure_f = mcdc.MaterialMG(fission=np.array([1.0]), nu_p=np.array([1.2]))
-pure_s = mcdc.MaterialMG(scatter=np.array([[1.0]]))
+pure_f = mcdc.Material.multigroup(fission=np.array([1.0]), nu_p=np.array([1.2]))
+pure_s = mcdc.Material.multigroup(scatter=np.array([[1.0]]))
# Set surfaces
sx1 = mcdc.Surface.PlaneX(x=0.0, boundary_condition="vacuum")
@@ -22,35 +24,40 @@
inside_box = +sx1 & -sx2 & +sy1 & -sy2 & +sz1 & -sz2
# Set cells
-mcdc.Cell(name="Box cover", region=inside_box & ~inside_sphere, fill=pure_s)
+box_cell = mcdc.Cell(name="Box cover", region=inside_box & ~inside_sphere, fill=pure_s)
sphere_cell = mcdc.Cell(name="The sphere", region=inside_sphere, fill=pure_f)
+simulation.set_model([box_cell, sphere_cell])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(
+source = mcdc.Source(
x=[0.0, 4.0],
y=[0.0, 4.0],
z=[0.0, 4.0],
isotropic=True,
- energy_group=0,
+ energy=0,
time=[0.0, 50.0],
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, techniques, and run MC/DC
# ======================================================================================
# Tallies
-mcdc.Tally(name="Spherical fission detector", cell=sphere_cell, scores=["fission"])
+tally = mcdc.Tally(
+ name="Spherical fission detector", cell=sphere_cell, scores=["fission"]
+)
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 1000
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 1000
+simulation.settings.N_batch = 2
# Techniques
-mcdc.simulation.implicit_capture()
+simulation.technique.implicit_capture()
# Run
-mcdc.run()
+simulation.run()
diff --git a/install.sh b/install.sh
deleted file mode 100755
index 23ae2f8ed..000000000
--- a/install.sh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/bin/bash
-
-# Check python version
-if ! { python3 -c 'import sys; assert sys.version_info < (3,12)' > /dev/null 2>&1 && python3 -c 'import sys; assert sys.version_info >= (3,9)' > /dev/null 2>&1; }; then
- v=$(python3 --version)
- p=$(which python)
- echo "ERROR: Python version must be < 3.12 and >= 3.9."
- echo " Found $v at $p."
- echo "ERROR: Installation failed."
- exit 1
-fi
-
-# Install or build mpi4py
-if [ $# -eq 0 ]; then
- conda install mpi4py <<< "y"
-fi
-while [ $# -gt 0 ]; do
- case $1 in
- --hpc)
- # Rename legacy compiler option in conda
- s=$(which python)
- s=${s//bin\/python/compiler_compat}
-
- if [ ! -f $s/ld.bak ] && [ -f $s/ld ]; then
- mv $s/ld $s/ld.bak
- fi
-
- mkdir installs; cd installs
- wget https://github.com/mpi4py/mpi4py/releases/download/3.1.4/mpi4py-3.1.4.tar.gz -q
- tar -zxf mpi4py-3.1.4.tar.gz
- cd mpi4py-3.1.4
- python setup.py install
- cd ../../
- rm -rf installs/
- ;;
-
- esac
- shift
-done
-
-# Install MC/DC module (and the dependencies)
-pip install -e .
-
-# Install pre-commit hook
-pre-commit install
diff --git a/mcdc/README.md b/mcdc/README.md
new file mode 100644
index 000000000..f9754e2f2
--- /dev/null
+++ b/mcdc/README.md
@@ -0,0 +1,14 @@
+# MC/DC Package Map
+
+This directory contains the main MC/DC Python package.
+
+- `__init__.py` defines the public Python interface.
+- `object_/` defines the user-facing model objects and is the model-side home for methods development.
+- `config.py`, `constant.py`, and `literals.py` provide shared execution configuration and values.
+- `main.py` coordinates runtime preparation, transport execution, and output.
+- `code_factory/` compiles Python models into runtime data and backend-specific code.
+- `mcdc_get/` and `mcdc_set/` contain runtime-data accessors generated during preparation and used during transport.
+- `transport/` implements the shared Monte Carlo algorithms and is the primary place to study or extend particle transport.
+- `output.py` processes and serializes simulation results.
+
+See the [architecture guide](../docs/source/developer_guide/architecture/index.rst) for the detailed design and component responsibilities.
diff --git a/mcdc/__init__.py b/mcdc/__init__.py
index e3147dd75..603e69feb 100644
--- a/mcdc/__init__.py
+++ b/mcdc/__init__.py
@@ -1,38 +1,34 @@
-from importlib.metadata import PackageNotFoundError, version
+"""Public Python interface for MC/DC."""
-try:
- __version__ = version("mcdc")
-except PackageNotFoundError:
- __version__ = "unknown"
-
-# ======================================================================================
-# Simulation building blocks
-# ======================================================================================
-
-# The simulation
-from mcdc.object_.simulation import simulation
-
-# The settings
-settings = simulation.settings
+from importlib.metadata import PackageNotFoundError as _PackageNotFoundError
+from importlib.metadata import version as _version
-# The objects
-from mcdc.object_.cell import Cell, Universe, Lattice
-from mcdc.object_.material import Material, MaterialMG
-from mcdc.object_.mesh import MeshUniform, MeshStructured
+from mcdc.object_.cell import Cell
+from mcdc.object_.material import Material
+from mcdc.object_.mesh import MeshStructured, MeshUniform
+from mcdc.object_.simulation import Simulation
from mcdc.object_.source import Source
from mcdc.object_.surface import Surface
from mcdc.object_.tally import Tally
+from mcdc.object_.transport_model_data import NeutronMultigroupData
+from mcdc.object_.universe import Lattice, Universe
+
+__all__ = [
+ "__version__",
+ "Cell",
+ "Lattice",
+ "Material",
+ "MeshStructured",
+ "MeshUniform",
+ "NeutronMultigroupData",
+ "Simulation",
+ "Source",
+ "Surface",
+ "Tally",
+ "Universe",
+]
-# ======================================================================================
-# Runners
-# ======================================================================================
-
-from mcdc.main import run
-from mcdc.visualize import visualize
-
-# ======================================================================================
-# Misc.
-# ======================================================================================
-
-import mcdc.config
-from mcdc.output import recombine_tallies
+try:
+ __version__: str = _version("mcdc")
+except _PackageNotFoundError:
+ __version__ = "unknown"
diff --git a/mcdc/code_factory/numba_objects_generator.py b/mcdc/code_factory/numba_layers_generator.py
similarity index 77%
rename from mcdc/code_factory/numba_objects_generator.py
rename to mcdc/code_factory/numba_layers_generator.py
index 56a276133..47dac9ccd 100644
--- a/mcdc/code_factory/numba_objects_generator.py
+++ b/mcdc/code_factory/numba_layers_generator.py
@@ -20,13 +20,13 @@
import mcdc.object_.base as base
from mcdc.object_.base import (
- ObjectBase,
- ObjectNonSingleton,
- ObjectPolymorphic,
- ObjectSingleton,
+ MCDCBase,
+ MCDCObject,
+ MCDCPolymorphic,
)
from mcdc.object_.particle import Particle, ParticleBank, ParticleData
from mcdc.object_.tally import Tally
+from mcdc.object_.util import parse_dimension_expression
from mcdc.print_ import print_error
from mcdc.util import flatten
@@ -45,6 +45,20 @@
bank_names = ["bank_active", "bank_census", "bank_source", "bank_future"]
+
+def validate_unique_class_labels(classes):
+ """Reject distinct runtime classes that use the same structure label."""
+ classes_by_label = {}
+ for class_ in classes:
+ existing = classes_by_label.get(class_.label)
+ if existing is not None and existing is not class_:
+ print_error(
+ f"Duplicate MC/DC class label '{class_.label}' used by "
+ f"{existing.__name__} and {class_.__name__}."
+ )
+ classes_by_label[class_.label] = class_
+
+
# ======================================================================================
# Gather and group the classes
# ======================================================================================
@@ -52,7 +66,7 @@
base_classes = [
getattr(base, x)
for x in dir(base)
- if isinstance(getattr(base, x), type) and issubclass(getattr(base, x), ObjectBase)
+ if isinstance(getattr(base, x), type) and issubclass(getattr(base, x), MCDCBase)
]
all_classes = [ParticleData, Particle]
@@ -67,7 +81,7 @@
item = getattr(file, item_name)
if (
isinstance(item, type)
- and issubclass(item, ObjectBase)
+ and issubclass(item, MCDCBase)
and item not in all_classes
):
all_classes.append(item)
@@ -79,20 +93,21 @@
):
mcdc_classes.append(item)
+validate_unique_class_labels(mcdc_classes)
+
polymorphic_bases = [
- x
- for x in all_classes
- if (x.__name__[-4:] == "Base" or x.__name__ == "Tally") and "label" in dir(x)
+ x for x in mcdc_classes if issubclass(x, MCDCPolymorphic) and x.sub_type == -1
]
# ======================================================================================
-# Numba object creation
+# Numba layer creation
# ======================================================================================
-def generate_numba_objects(simulation):
+def generate_numba_layers(simulation):
+ """Pack a finalized Python model into the shared runtime data layers."""
# ==================================================================================
- # Allocate key items for the Numba object:
+ # Allocate key items for the Numba runtime layers:
# - Python annotations
# - Numba structures
# - Records
@@ -110,12 +125,12 @@ def generate_numba_objects(simulation):
annotations[mcdc_class.label] = {}
structures[mcdc_class.label] = []
accessor_targets[mcdc_class.label] = []
- if issubclass(mcdc_class, ObjectNonSingleton):
+ if issubclass(mcdc_class, MCDCObject):
records[mcdc_class.label] = []
else:
records[mcdc_class.label] = {}
- # Particle banks
+ # Allocate particle banks from capacities finalized during simulation compilation
for name in bank_names:
annotations[name] = {}
structures[name] = []
@@ -140,7 +155,7 @@ def generate_numba_objects(simulation):
classes.append(item)
# If polymorphic, don't include the polymorphic base
- if issubclass(mcdc_class, ObjectPolymorphic):
+ if issubclass(mcdc_class, MCDCPolymorphic):
classes = [mcdc_class]
# Get the annotations
@@ -183,6 +198,7 @@ def generate_numba_objects(simulation):
# Temporary simulation object structure
simulation_object_structure = []
+ included_classes = []
for field in annotations["simulation"]:
hint = annotations["simulation"][field]
hint_origin = get_origin(hint)
@@ -190,14 +206,27 @@ def generate_numba_objects(simulation):
if hint in all_classes:
simulation_object_structure.append((field, hint))
+ included_classes.append(hint)
continue
if hint_origin == list and hint_args[0] in all_classes:
simulation_object_structure.append((field, list, hint_args[0]))
+ included_classes.append(hint_args[0])
continue
- # Set the structures and accessor targets
+ # Build child structures before structures that embed them
+ completed_structures = set()
+ active_structures = set()
+ structure_order = []
for label in annotations.keys():
- set_structure(label, structures, accessor_targets, annotations)
+ set_structure(
+ label,
+ structures,
+ accessor_targets,
+ annotations,
+ completed_structures,
+ active_structures,
+ structure_order,
+ )
# Generate the accessor helper
if MPI.COMM_WORLD.Get_rank() == 0:
@@ -205,15 +234,15 @@ def generate_numba_objects(simulation):
# Add ID for non-singleton
for class_ in mcdc_classes:
- if issubclass(class_, ObjectNonSingleton):
+ if issubclass(class_, MCDCObject):
structures[class_.label].append(("ID", type_map[int]))
# Set parent and child ID and type if polymorphic
- if issubclass(class_, ObjectPolymorphic):
- if class_.__name__[-4:] == "Base" or class_.__name__ == "Tally":
- structures[class_.label].append(("child_type", type_map[int]))
- structures[class_.label].append(("child_ID", type_map[int]))
+ if issubclass(class_, MCDCPolymorphic):
+ if class_ in polymorphic_bases:
+ structures[class_.label].append(("sub_type", type_map[int]))
+ structures[class_.label].append(("sub_ID", type_map[int]))
else:
- structures[class_.label].append(("parent_ID", type_map[int]))
+ structures[class_.label].append(("base_ID", type_map[int]))
# Add particle data to particle banks and add particle banks to the simulation
for name in bank_names:
@@ -241,7 +270,7 @@ def generate_numba_objects(simulation):
if (
not x.startswith("__")
and (
- isinstance(getattr(simulation, x), ObjectBase)
+ isinstance(getattr(simulation, x), MCDCBase)
or not callable(getattr(simulation, x))
)
and x not in simulation.non_numba
@@ -300,7 +329,7 @@ def generate_numba_objects(simulation):
record[f"N_{class_.label}"] = N
# Singleton
- elif item[1] in mcdc_classes and issubclass(item[1], ObjectSingleton):
+ elif item[1] in mcdc_classes and issubclass(item[1], MCDCBase):
new_structure.append((field, into_dtype(structures[item[1].label])))
else:
@@ -311,7 +340,10 @@ def generate_numba_objects(simulation):
# Print the fields
if MPI.COMM_WORLD.Get_rank() == 0:
with open(f"{Path(mcdc.__file__).parent}/numba_types.py", "w") as f:
- text = "# The following is automatically generated by code_factory.py\n\n"
+ text = (
+ "# The following is automatically generated by "
+ "numba_layers_generator.py\n\n"
+ )
text += "from numpy import bool_\n"
text += "from numpy import float64\n"
text += "from numpy import int64\n"
@@ -319,17 +351,16 @@ def generate_numba_objects(simulation):
text += "from numpy import uintp\n"
text += "\n###\n\n"
text += (
- "from mcdc.code_factory.numba_objects_generator import into_dtype\n\n"
+ "from mcdc.code_factory.numba_layers_generator import into_dtype\n\n"
)
- for label in structures.keys():
+ for label in structure_order:
# Skip special types
if label in ["gpu_meta"] + bank_names + ["simulation"]:
continue
text += f"{label} = into_dtype([\n"
structure = structures[label]
-
for item in structure:
text += decode_structure_item(item)
text += "])\n\n"
@@ -469,7 +500,37 @@ def generate_numba_objects(simulation):
return mcdc_simulation_container, data["array"]
-def set_structure(label, structures, accessor_targets, annotations):
+def is_embedded_mcdc_base(hint):
+ """Return whether a field is an inline, simulation-owned MC/DC object."""
+ return (
+ isinstance(hint, type)
+ and issubclass(hint, MCDCBase)
+ and not issubclass(hint, MCDCObject)
+ )
+
+
+def set_structure(
+ label,
+ structures,
+ accessor_targets,
+ annotations,
+ completed=None,
+ active=None,
+ order=None,
+):
+ # Track dependencies so embedded structures are complete before their owners
+ if completed is None:
+ completed = set()
+ if active is None:
+ active = set()
+ if order is None:
+ order = []
+ if label in completed:
+ return
+ if label in active:
+ print_error(f"Cyclic embedded MCDCBase structure involving '{label}'.")
+ active.add(label)
+
structure = structures[label]
annotation = annotations[label]
accessor_target = accessor_targets[label]
@@ -478,10 +539,18 @@ def set_structure(label, structures, accessor_targets, annotations):
hint = annotation[field]
hint_origin = get_origin(hint)
hint_args = get_args(hint)
+ embedded_mcdc_base = is_embedded_mcdc_base(hint)
hint_origin_shape = None
hint_inner_dtype = None
fixed_size_array = False
+ # Inline runtime fields use their class label as their field name
+ if embedded_mcdc_base and field != hint.label:
+ print_error(
+ f"Embedded MCDCBase field '{label}.{field}' must match the "
+ f"class label '{hint.label}'."
+ )
+
# Process annotation
if hint_origin is Annotated:
hint_decoded = decode_annotated_ndarray(hint)
@@ -496,6 +565,10 @@ def set_structure(label, structures, accessor_targets, annotations):
fixed_size_array = False
break
+ # Get the element dtype from an NDArray without shape metadata
+ if hint_origin is np.ndarray and hint_inner_dtype is None:
+ hint_inner_dtype = get_ndarray_dtype(hint)
+
# Skip simulation object structure
if label == "simulation":
if hint in all_classes:
@@ -515,10 +588,10 @@ def set_structure(label, structures, accessor_targets, annotations):
# MC/DC class
def non_polymorphic(x):
- # Only treat real classes that inherit from ObjectNonSingleton
+ # Only treat real classes that inherit from MCDCObject
return (
isinstance(x, type)
- and issubclass(x, ObjectNonSingleton)
+ and issubclass(x, MCDCObject)
and x not in polymorphic_bases
)
@@ -537,17 +610,40 @@ def polymorphic_base(x):
# ==========================================================================
# Basics
- if fixed_size_array:
+ if embedded_mcdc_base:
+ child_label = hint.label
+ if child_label not in annotations:
+ print_error(
+ f"Missing annotations for embedded MCDCBase {label}/{field}: "
+ f"{child_label}"
+ )
+ set_structure(
+ child_label,
+ structures,
+ accessor_targets,
+ annotations,
+ completed,
+ active,
+ order,
+ )
+ structure.append((field, into_dtype(structures[child_label])))
+ elif fixed_size_array:
structure.append((field, type_map[hint_inner_dtype], hint_origin_shape))
elif simple_scalar:
structure.append((field, type_map[hint]))
elif simple_list or numpy_array:
structure.append((f"{field}_offset", type_map[int]))
structure.append((f"{field}_length", type_map[int]))
+ logical_type = hint_args[0] if simple_list else hint_inner_dtype
+ cast_to_int = is_integer_type(logical_type)
if hint_origin_shape is not None:
- accessor_target.append((f"{field}", hint_origin_shape))
+ accessor_target.append(
+ AccessorTarget(f"{field}", hint_origin_shape, cast_to_int)
+ )
else:
- accessor_target.append((f"{field}", (f"{field}_length",)))
+ accessor_target.append(
+ AccessorTarget(f"{field}", (f"{field}_length",), cast_to_int)
+ )
# MC/DC classes
elif non_polymorphic(hint) or polymorphic_base(hint):
@@ -559,14 +655,23 @@ def polymorphic_base(x):
structure.append((f"N_{singular}", type_map[int]))
structure.append((f"{singular}_IDs_offset", type_map[int]))
if hint_origin_shape is not None:
- accessor_target.append((f"{singular}_IDs", hint_origin_shape))
+ accessor_target.append(
+ AccessorTarget(f"{singular}_IDs", hint_origin_shape, True)
+ )
else:
- accessor_target.append((f"{singular}_IDs", (f"N_{singular}",)))
+ accessor_target.append(
+ AccessorTarget(f"{singular}_IDs", (f"N_{singular}",), True)
+ )
# Unknown type
else:
print_error(f"Unknown type hint for {label}/{field}: {hint}")
+ # Record a dependency-safe declaration order for generated Numba types
+ active.remove(label)
+ completed.add(label)
+ order.append(label)
+
def set_object(
object_, annotations, structures, records, data, class_=None, set_data=False
@@ -575,7 +680,7 @@ def set_object(
class_ = object_.__class__
# Set the parent first if polymorphics
- if isinstance(object_, ObjectPolymorphic) and class_ not in polymorphic_bases:
+ if isinstance(object_, MCDCPolymorphic) and class_ not in polymorphic_bases:
for parent_class in polymorphic_bases:
if issubclass(class_, parent_class):
set_object(
@@ -595,6 +700,28 @@ def set_object(
if class_.label == "simulation":
record = records["simulation"]
+ # Recursively pack inline MCDCBase members before packing their owner
+ if class_.label != "simulation":
+ for field, child_class in annotation.items():
+ if not is_embedded_mcdc_base(child_class):
+ continue
+ child = getattr(object_, field)
+ set_object(
+ child,
+ annotations,
+ structures,
+ records,
+ data,
+ set_data=set_data,
+ )
+ child_structure = structures[child_class.label]
+ child_container = np.zeros(1, dtype=into_dtype(child_structure))
+ child_record = child_container[0]
+ for child_item in child_structure:
+ child_field = child_item[0]
+ child_record[child_field] = records[child_class.label][child_field]
+ record[field] = child_record
+
# Straightforwardly set up attributes
for key in [x[0] for x in structure]:
if key in dir(object_):
@@ -636,14 +763,8 @@ def set_object(
data["size"] += len(attribute_flatten)
# Non-singleton object
- elif isinstance(attribute, ObjectNonSingleton):
- if (
- not isinstance(attribute, ObjectPolymorphic)
- or annotation[attribute_name] in polymorphic_bases
- ):
- record[f"{attribute_name}_ID"] = attribute.ID
- else:
- record[f"{attribute_name}_ID"] = attribute.child_ID
+ elif isinstance(attribute, MCDCObject):
+ record[f"{attribute_name}_ID"] = attribute.ID
# List of Non-singleton objects
elif type(attribute) == list:
@@ -653,7 +774,7 @@ def set_object(
attribute_flatten = list(flatten(attribute))
singular_name = plural_to_singular(attribute_name)
- if not issubclass(inner_type, ObjectNonSingleton):
+ if not issubclass(inner_type, MCDCObject):
print_error(
f"[ERROR] Get a list of non-object for {attribute_name}: {attribute}"
)
@@ -661,17 +782,9 @@ def set_object(
record[f"N_{singular_name}"] = len(attribute_flatten)
record[f"{singular_name}_IDs_offset"] = data["size"]
if set_data:
- if (
- not issubclass(inner_type, ObjectPolymorphic)
- or inner_type in polymorphic_bases
- ):
- data["array"][
- data["size"] : data["size"] + len(attribute_flatten)
- ] = [x.ID for x in attribute_flatten]
- else:
- data["array"][
- data["size"] : data["size"] + len(attribute_flatten)
- ] = [x.child_ID for x in attribute_flatten]
+ data["array"][data["size"] : data["size"] + len(attribute_flatten)] = [
+ x.ID for x in attribute_flatten
+ ]
data["size"] += len(attribute_flatten)
# Complete for simulation object
@@ -679,8 +792,8 @@ def set_object(
return
# Set ID of non-singleton
- if isinstance(object_, ObjectNonSingleton):
- if not isinstance(object_, ObjectPolymorphic):
+ if isinstance(object_, MCDCObject):
+ if not isinstance(object_, MCDCPolymorphic):
record["ID"] = object_.ID
# Set parent and child ID and type if polymorphic
@@ -688,12 +801,12 @@ def set_object(
# Parent
if class_ in polymorphic_bases:
record["ID"] = object_.ID
- record["child_ID"] = object_.child_ID
- record["child_type"] = object_.type
+ record["sub_ID"] = object_.sub_ID
+ record["sub_type"] = object_.sub_type
# Child
else:
- record["ID"] = object_.child_ID
- record["parent_ID"] = object_.ID
+ record["ID"] = object_.sub_ID
+ record["base_ID"] = object_.ID
# Set tally bins
if class_ == Tally:
@@ -712,10 +825,10 @@ def set_object(
print_error(f"Missing structure keys in record for {class_.label}: {missing}")
# Register the record
- if isinstance(object_, ObjectSingleton):
- records[class_.label] = record
- elif isinstance(object_, ObjectNonSingleton):
+ if isinstance(object_, MCDCObject):
records[class_.label].append(record)
+ elif isinstance(object_, MCDCBase):
+ records[class_.label] = record
# =============================================================================
@@ -876,11 +989,26 @@ def into_dtype(field_list):
# Type parser
# ======================================================================================
-from typing import Annotated, Any, ForwardRef, Optional, Union, get_args, get_origin
+from typing import (
+ Annotated,
+ Any,
+ ForwardRef,
+ NamedTuple,
+ Optional,
+ Union,
+ get_args,
+ get_origin,
+)
import numpy as np
from numpy.typing import NDArray
+class AccessorTarget(NamedTuple):
+ name: str
+ shape: tuple[int | str, ...]
+ cast_to_int: bool
+
+
# --- Safe locals for eval + ForwardRef fallback ---
class _FwdRefDict(dict):
"""If a symbol isn't in the whitelist, treat it as a ForwardRef('Symbol')."""
@@ -951,12 +1079,44 @@ def decode_annotated_ndarray(hint):
}
+def get_ndarray_dtype(hint):
+ hint_args = get_args(hint)
+ if len(hint_args) < 2:
+ return None
+ dtype_args = get_args(hint_args[1])
+ if len(dtype_args) == 0:
+ return None
+ return dtype_args[0]
+
+
+def is_integer_type(type_):
+ if type_ is None:
+ return False
+ try:
+ return np.issubdtype(np.dtype(type_), np.integer)
+ except TypeError:
+ return False
+
+
# ======================================================================================
# Helpers for mcdc_get generators
# ======================================================================================
+def validate_accessor_targets(targets):
+ for object_name, attributes in targets.items():
+ for attribute in attributes:
+ rank = len(attribute.shape)
+ if not 1 <= rank <= 4:
+ raise ValueError(
+ f"Generated accessors support one through four dimensions, "
+ f"but {object_name}.{attribute.name} has rank {rank}."
+ )
+
+
def generate_mcdc_access(targets):
+ validate_accessor_targets(targets)
+
for object_name in targets.keys():
path = f"{Path(mcdc.__file__).parent}"
file_getter = open(f"{path}/mcdc_get/{object_name}.py", "w")
@@ -969,17 +1129,27 @@ def generate_mcdc_access(targets):
"# The following is automatically generated by code_factory.py\n\n"
)
+ if any(attribute.cast_to_int for attribute in targets[object_name]):
+ text_getter += "from numpy import int64\n"
text_getter += "from numba import njit\n\n\n"
text_setter += "from numba import njit\n\n\n"
for attribute in targets[object_name]:
- attribute_name = attribute[0]
- shape = attribute[1]
+ attribute_name = attribute.name
+ shape = attribute.shape
+ cast_to_int = attribute.cast_to_int
if len(shape) == 1:
- text_getter += _accessor_1d_element(object_name, attribute_name)
+ text_getter += _accessor_1d_element(
+ object_name, attribute_name, cast_to_int=cast_to_int
+ )
text_getter += _accessor_1d_all(object_name, attribute_name, shape[0])
- text_getter += _accessor_1d_last(object_name, attribute_name, shape[0])
+ text_getter += _accessor_1d_last(
+ object_name,
+ attribute_name,
+ shape[0],
+ cast_to_int=cast_to_int,
+ )
text_setter += _accessor_1d_element(object_name, attribute_name, True)
text_setter += _accessor_1d_all(
@@ -994,7 +1164,10 @@ def generate_mcdc_access(targets):
object_name, attribute_name, shape[1]
)
text_getter += _accessor_2d_element(
- object_name, attribute_name, shape[1]
+ object_name,
+ attribute_name,
+ shape[1],
+ cast_to_int=cast_to_int,
)
text_setter += _accessor_2d_vector(
@@ -1006,7 +1179,11 @@ def generate_mcdc_access(targets):
elif len(shape) == 3:
text_getter += _accessor_3d_element(
- object_name, attribute_name, shape[1], shape[2]
+ object_name,
+ attribute_name,
+ shape[1],
+ shape[2],
+ cast_to_int=cast_to_int,
)
text_setter += _accessor_3d_element(
@@ -1015,7 +1192,12 @@ def generate_mcdc_access(targets):
elif len(shape) == 4:
text_getter += _accessor_4d_element(
- object_name, attribute_name, shape[1], shape[2], shape[3]
+ object_name,
+ attribute_name,
+ shape[1],
+ shape[2],
+ shape[3],
+ cast_to_int=cast_to_int,
)
text_setter += _accessor_4d_element(
@@ -1040,7 +1222,27 @@ def generate_mcdc_access(targets):
f.write(text)
-def _accessor_1d_element(object_name, attribute_name, setter=False):
+def accessor_return(expression, cast_to_int):
+ if cast_to_int:
+ # Use an explicit NumPy scalar type so Python and Numba return the same
+ # logical integer width from the float64 data arena.
+ expression = f"int64({expression})"
+ return f" return {expression}\n\n\n"
+
+
+def accessor_dimension(variable_name, dimension, object_name):
+ if isinstance(dimension, str):
+ attribute, offset = parse_dimension_expression(dimension)
+ expression = f'{object_name}["{attribute}"]'
+ if offset > 0:
+ expression += f" + {offset}"
+ elif offset < 0:
+ expression += f" - {-offset}"
+ return f" {variable_name} = {expression}\n"
+ return f" {variable_name} = {dimension}\n"
+
+
+def _accessor_1d_element(object_name, attribute_name, setter=False, cast_to_int=False):
text = f"@njit\n"
if setter:
text += f"def {attribute_name}(index, {object_name}, data, value):\n"
@@ -1050,7 +1252,7 @@ def _accessor_1d_element(object_name, attribute_name, setter=False):
if setter:
text += f" data[offset + index] = value\n\n\n"
else:
- text += f" return data[offset + index]\n\n\n"
+ text += accessor_return("data[offset + index]", cast_to_int)
return text
@@ -1061,10 +1263,7 @@ def _accessor_1d_all(object_name, attribute_name, size, setter=False):
else:
text += f"def {attribute_name}_all({object_name}, data):\n"
text += f' start = {object_name}["{attribute_name}_offset"]\n'
- if type(size) == str:
- text += f' size = {object_name}["{size}"]\n'
- else:
- text += f" size = {size}\n"
+ text += accessor_dimension("size", size, object_name)
text += f" end = start + size\n"
if setter:
text += f" data[start:end] = value\n\n\n"
@@ -1073,22 +1272,21 @@ def _accessor_1d_all(object_name, attribute_name, size, setter=False):
return text
-def _accessor_1d_last(object_name, attribute_name, size, setter=False):
+def _accessor_1d_last(
+ object_name, attribute_name, size, setter=False, cast_to_int=False
+):
text = f"@njit\n"
if setter:
text += f"def {attribute_name}_last({object_name}, data, value):\n"
else:
text += f"def {attribute_name}_last({object_name}, data):\n"
text += f' start = {object_name}["{attribute_name}_offset"]\n'
- if type(size) == str:
- text += f' size = {object_name}["{size}"]\n'
- else:
- text += f" size = {size}\n"
+ text += accessor_dimension("size", size, object_name)
text += f" end = start + size\n"
if setter:
text += f" data[end - 1] = value\n\n\n"
else:
- text += f" return data[end - 1]\n\n\n"
+ text += accessor_return("data[end - 1]", cast_to_int)
return text
@@ -1109,21 +1307,22 @@ def _accessor_chunk(object_name, attribute_name, setter=False):
return text
-def _accessor_2d_element(object_name, attribute_name, stride, setter=False):
+def _accessor_2d_element(
+ object_name, attribute_name, stride, setter=False, cast_to_int=False
+):
text = f"@njit\n"
if setter:
text += f"def {attribute_name}(index_1, index_2, {object_name}, data, value):\n"
else:
text += f"def {attribute_name}(index_1, index_2, {object_name}, data):\n"
text += f' offset = {object_name}["{attribute_name}_offset"]\n'
- if isinstance(stride, str):
- text += f' stride = {object_name}["{stride}"]\n'
- else:
- text += f" stride = {stride}\n"
+ text += accessor_dimension("stride", stride, object_name)
if setter:
text += f" data[offset + index_1 * stride + index_2] = value\n\n\n"
else:
- text += f" return data[offset + index_1 * stride + index_2]\n\n\n"
+ text += accessor_return(
+ "data[offset + index_1 * stride + index_2]", cast_to_int
+ )
return text
@@ -1134,10 +1333,7 @@ def _accessor_2d_vector(object_name, attribute_name, stride, setter=False):
else:
text += f"def {attribute_name}_vector(index_1, {object_name}, data):\n"
text += f' offset = {object_name}["{attribute_name}_offset"]\n'
- if isinstance(stride, str):
- text += f' stride = {object_name}["{stride}"]\n'
- else:
- text += f" stride = {stride}\n"
+ text += accessor_dimension("stride", stride, object_name)
text += f" start = offset + index_1 * stride\n"
text += f" end = start + stride\n"
if setter:
@@ -1147,7 +1343,14 @@ def _accessor_2d_vector(object_name, attribute_name, stride, setter=False):
return text
-def _accessor_3d_element(object_name, attribute_name, stride_2, stride_3, setter=False):
+def _accessor_3d_element(
+ object_name,
+ attribute_name,
+ stride_2,
+ stride_3,
+ setter=False,
+ cast_to_int=False,
+):
text = f"@njit\n"
if setter:
text += f"def {attribute_name}(index_1, index_2, index_3, {object_name}, data, value):\n"
@@ -1156,17 +1359,26 @@ def _accessor_3d_element(object_name, attribute_name, stride_2, stride_3, setter
f"def {attribute_name}(index_1, index_2, index_3, {object_name}, data):\n"
)
text += f' offset = {object_name}["{attribute_name}_offset"]\n'
- text += f' stride_2 = {object_name}["{stride_2}"]\n'
- text += f' stride_3 = {object_name}["{stride_3}"]\n'
+ text += accessor_dimension("stride_2", stride_2, object_name)
+ text += accessor_dimension("stride_3", stride_3, object_name)
if setter:
text += f" data[offset + index_1 * stride_2 * stride_3 + index_2 * stride_3 + index_3] = value\n\n\n"
else:
- text += f" return data[offset + index_1 * stride_2 * stride_3 + index_2 * stride_3 + index_3]\n\n\n"
+ text += accessor_return(
+ "data[offset + index_1 * stride_2 * stride_3 + index_2 * stride_3 + index_3]",
+ cast_to_int,
+ )
return text
def _accessor_4d_element(
- object_name, attribute_name, stride_2, stride_3, stride_4, setter=False
+ object_name,
+ attribute_name,
+ stride_2,
+ stride_3,
+ stride_4,
+ setter=False,
+ cast_to_int=False,
):
text = f"@njit\n"
if setter:
@@ -1174,13 +1386,16 @@ def _accessor_4d_element(
else:
text += f"def {attribute_name}(index_1, index_2, index_3, index_4, {object_name}, data):\n"
text += f' offset = {object_name}["{attribute_name}_offset"]\n'
- text += f' stride_2 = {object_name}["{stride_2}"]\n'
- text += f' stride_3 = {object_name}["{stride_3}"]\n'
- text += f' stride_4 = {object_name}["{stride_4}"]\n'
+ text += accessor_dimension("stride_2", stride_2, object_name)
+ text += accessor_dimension("stride_3", stride_3, object_name)
+ text += accessor_dimension("stride_4", stride_4, object_name)
if setter:
text += f" data[offset + index_1 * stride_2 * stride_3 * stride_4 + index_2 * stride_3 * stride_4 + index_3 * stride_4 + index_4] = value\n\n\n"
else:
- text += f" return data[offset + index_1 * stride_2 * stride_3 * stride_4 + index_2 * stride_3 * stride_4 + index_3 * stride_4 + index_4]\n\n\n"
+ text += accessor_return(
+ "data[offset + index_1 * stride_2 * stride_3 * stride_4 + index_2 * stride_3 * stride_4 + index_3 * stride_4 + index_4]",
+ cast_to_int,
+ )
return text
@@ -1208,6 +1423,7 @@ def plural_to_singular(word: str) -> str:
"matrices": "matrix",
"criteria": "criterion",
"data": "data", # invariant
+ "mgxs": "mgxs", # invariant
"spectra": "spectrum",
}
@@ -1249,6 +1465,7 @@ def singular_to_plural(word: str) -> str:
"matrix": "matrices",
"criterion": "criteria",
"data": "data", # invariant
+ "mgxs": "mgxs", # invariant
"spectrum": "spectra",
}
diff --git a/mcdc/code_factory/python_objects_compiler.py b/mcdc/code_factory/python_objects_compiler.py
new file mode 100644
index 000000000..dc734d4bf
--- /dev/null
+++ b/mcdc/code_factory/python_objects_compiler.py
@@ -0,0 +1,139 @@
+from mcdc.object_.base import MCDCObject, MCDCPolymorphic
+from mcdc.object_.cell import Region, Cell
+from mcdc.object_.data import DataBase, DataNone
+from mcdc.object_.distribution import DistributionBase, DistributionNone
+from mcdc.object_.electron_reaction import ElectronReactionBase
+from mcdc.object_.element import Element
+from mcdc.object_.material import Material
+from mcdc.object_.transport_model_data import NeutronMultigroupData
+from mcdc.object_.mesh import MeshBase
+from mcdc.object_.neutron_reaction import NeutronReactionBase
+from mcdc.object_.nuclide import Nuclide
+from mcdc.object_.universe import Universe, Lattice
+from mcdc.object_.simulation import Simulation
+from mcdc.object_.source import Source
+from mcdc.object_.surface import Surface
+from mcdc.object_.tally import Tally
+from mcdc.print_ import print_error
+
+NONE_OBJECT_CLASSES = (DataNone, DistributionNone) # Has customized compilation
+
+
+def compile_simulation(simulation: Simulation):
+ """Discover, register, and finalize one simulation's Python model.
+
+ Recursive object hooks perform object-local compilation. Once discovery is
+ complete, the owning :class:`~mcdc.object_.simulation.Simulation` resolves
+ model-wide state before runtime packing begins.
+ """
+ # Require geometry rooted in at least one cell
+ if len(simulation.root_universe.cells) == 0:
+ print_error("Simulation model has not been set (root universe is empty).")
+
+ # Preserve explicitly configured roots before resetting their registered
+ # object lists. Geometry members may reference these objects and compile
+ # them while the model graph is traversed.
+ sources = simulation.sources
+ tallies = simulation.tallies
+
+ # Reset model
+ simulation._reset_model()
+ simulation.sources = []
+ simulation.tallies = []
+
+ # Reserved objects
+ none_data = DataNone()
+ none_distribution = DistributionNone()
+ none_neutron_multigroup = NeutronMultigroupData()
+
+ # Compile reserved objects
+ none_data._compile_into_simulation(simulation)
+ none_distribution._compile_into_simulation(simulation)
+ none_neutron_multigroup._compile_into_simulation(simulation)
+
+ # Compile model
+ root_universe = simulation.root_universe
+ root_universe._compile_into_simulation(simulation)
+
+ # Compile source
+ for source in sources:
+ source._compile_into_simulation(simulation)
+
+ # Compile tally
+ for tally in tallies:
+ tally._compile_into_simulation(simulation)
+
+ # Compile remaining object members, including those owned by embedded
+ # simulation configuration objects such as transport techniques.
+ simulation._compile_members_into_simulation(simulation)
+
+ # Resolve state that depends on the complete discovered model
+ simulation._finalize_compilation()
+
+
+def register_object(object_: MCDCObject, simulation: Simulation) -> bool:
+ # Skip if already compiled
+ if object_.compile_ID == simulation.compile_ID:
+ return False
+
+ # Get the object list
+ if isinstance(object_, Cell):
+ object_list = simulation.cells
+ elif isinstance(object_, DataBase):
+ object_list = simulation.data
+ elif isinstance(object_, DistributionBase):
+ object_list = simulation.distributions
+ elif isinstance(object_, Lattice):
+ object_list = simulation.lattices
+ elif isinstance(object_, Material):
+ object_list = simulation.materials
+ elif isinstance(object_, NeutronMultigroupData):
+ object_list = simulation.neutron_multigroup_data
+ elif isinstance(object_, MeshBase):
+ object_list = simulation.meshes
+ elif isinstance(object_, Element):
+ object_list = simulation.elements
+ elif isinstance(object_, ElectronReactionBase):
+ object_list = simulation.electron_reactions
+ elif isinstance(object_, Nuclide):
+ object_list = simulation.nuclides
+ elif isinstance(object_, NeutronReactionBase):
+ object_list = simulation.neutron_reactions
+ elif isinstance(object_, Region):
+ object_list = simulation.regions
+ elif isinstance(object_, Source):
+ object_list = simulation.sources
+ elif isinstance(object_, Surface):
+ object_list = simulation.surfaces
+ elif isinstance(object_, Tally):
+ object_list = simulation.tallies
+ elif isinstance(object_, Universe):
+ object_list = simulation.universes
+ else:
+ object_list = []
+ print_error(f"Unidentified object list for object {object_}")
+
+ # Assign IDs
+ object_.ID = len(object_list)
+ if isinstance(object_, MCDCPolymorphic):
+ object_.sub_ID = sum(
+ [
+ x.sub_type == object_.sub_type
+ for x in object_list
+ if isinstance(x, MCDCPolymorphic)
+ ]
+ )
+ object_.compile_ID = simulation.compile_ID
+
+ # Assign name if needed (TODO: Resolve IDE error message)
+ if hasattr(object_, "name") and object_.name.startswith("(Unnamed"):
+ if isinstance(object_, MCDCPolymorphic):
+ ID = object_.sub_ID
+ else:
+ ID = object_.ID
+ object_.name = object_.label + f"_{ID}"
+
+ # Register to simulation (TODO: Resolve IDE error message)
+ object_list.append(object_)
+
+ return True
diff --git a/mcdc/config.py b/mcdc/config.py
index 4979c5fcd..7af96732f 100644
--- a/mcdc/config.py
+++ b/mcdc/config.py
@@ -1,103 +1,103 @@
-import argparse, os
+"""Process-wide command-line and execution configuration for MC/DC.
-parser = argparse.ArgumentParser(description="MC/DC: Monte Carlo Dynamic Code")
+Importing this module parses MC/DC's known command-line arguments, manages
+generated-code caches, and configures Numba for the selected execution mode.
+Simulation-specific command-line overrides are applied later, at the beginning
+of :meth:`mcdc.Simulation.compile`.
+"""
-# ======================================================================================
-# Run mode
-# ======================================================================================
-
-parser.add_argument(
- "--mode",
- type=str,
- help="Run mode",
- choices=["python", "numba", "numba_debug"],
- default="python",
-)
+import argparse
+import shutil
+from pathlib import Path
-parser.add_argument(
- "--target", type=str, help="Target", choices=["cpu", "gpu"], default="cpu"
-)
+from mpi4py import MPI
# ======================================================================================
-# Settings
+# Command-line interface
# ======================================================================================
-parser.add_argument("--N_particle", type=int, help="Number of particles")
-parser.add_argument("--N_batch", type=int, help="Number of batches")
-parser.add_argument("--output", type=str, help="Output file name")
-parser.add_argument("--progress_bar", default=True, action="store_true")
-parser.add_argument("--no-progress_bar", dest="progress_bar", action="store_false")
-parser.add_argument("--runtime_output", default=False, action="store_true")
-
-# ======================================================================================
-# Numba
-# ======================================================================================
-
-parser.add_argument("--clear_cache", action="store_true")
-parser.add_argument("--caching", action="store_true", default=False)
-parser.add_argument("--no_caching", dest="caching", action="store_false")
+def _build_parser() -> argparse.ArgumentParser:
+ """Create the MC/DC command-line argument parser."""
+ parser = argparse.ArgumentParser(description="MC/DC: Monte Carlo Dynamic Code")
-# ======================================================================================
-# GPU mode
-# ======================================================================================
+ # Execution mode and target
+ parser.add_argument(
+ "--mode",
+ type=str,
+ help="Run mode",
+ choices=["python", "numba", "numba_debug"],
+ default="python",
+ )
+ parser.add_argument(
+ "--target", type=str, help="Target", choices=["cpu", "gpu"], default="cpu"
+ )
-parser.add_argument(
- "--gpu_state_storage",
- type=str,
- help="Strategy used in GPU execution (event or async).",
- choices=["separate", "managed", "united"],
- default="separate",
-)
-
-parser.add_argument(
- "--gpu_strategy",
- type=str,
- help="Strategy used in GPU execution (event or async).",
- choices=["async", "event"],
- default="event",
-)
-
-parser.add_argument(
- "--gpu_block_count",
- type=int,
- help="Number of blocks used in GPU execution.",
- default=240,
-)
-
-parser.add_argument(
- "--gpu_arena_size",
- type=int,
- help="Capacity of each intermediate data buffer used, as a particle count.",
- default=0x100000,
-)
-
-parser.add_argument(
- "--gpu_rocm_path",
- type=str,
- help="Path to ROCm installation for use in GPU execution.",
- default=None,
-)
-
-parser.add_argument(
- "--gpu_cuda_path",
- type=str,
- help="Path to CUDA installation for use in GPU execution.",
- default=None,
-)
-
-parser.add_argument(
- "--gpu_share_stride",
- type=int,
- help="Number of gpus that are shared across adjacent ranks.",
- default=1,
-)
+ # Simulation-setting overrides
+ parser.add_argument("--N_particle", type=int, help="Number of particles")
+ parser.add_argument("--N_batch", type=int, help="Number of batches")
+ parser.add_argument("--output", type=str, help="Output file name")
+ parser.add_argument("--progress_bar", default=True, action="store_true")
+ parser.add_argument("--no-progress_bar", dest="progress_bar", action="store_false")
+ parser.add_argument("--runtime_output", default=False, action="store_true")
+
+ # Numba compilation and cache behavior
+ parser.add_argument("--clear_cache", action="store_true")
+ parser.add_argument("--caching", action="store_true", default=False)
+ parser.add_argument("--no_caching", dest="caching", action="store_false")
+
+ # GPU execution
+ parser.add_argument(
+ "--gpu_state_storage",
+ type=str,
+ help="GPU state-storage strategy.",
+ choices=["separate", "managed", "united"],
+ default="separate",
+ )
+ parser.add_argument(
+ "--gpu_strategy",
+ type=str,
+ help="GPU scheduling strategy.",
+ choices=["async", "event"],
+ default="event",
+ )
+ parser.add_argument(
+ "--gpu_block_count",
+ type=int,
+ help="Number of GPU blocks.",
+ default=240,
+ )
+ parser.add_argument(
+ "--gpu_arena_size",
+ type=int,
+ help="Particle capacity of each intermediate GPU data buffer.",
+ default=0x100000,
+ )
+ parser.add_argument(
+ "--gpu_rocm_path",
+ type=str,
+ help="Path to the ROCm installation.",
+ default=None,
+ )
+ parser.add_argument(
+ "--gpu_cuda_path",
+ type=str,
+ help="Path to the CUDA installation.",
+ default=None,
+ )
+ parser.add_argument(
+ "--gpu_share_stride",
+ type=int,
+ help="Number of adjacent MPI ranks sharing each GPU.",
+ default=1,
+ )
+ return parser
-# ======================================================================================
-# Config processor
-# ======================================================================================
+# Preserve these module-level names because execution and GPU modules consume them.
+parser = _build_parser()
+# Ignore unrelated arguments supplied by pytest, notebooks, or outer Python drivers.
args, unargs = parser.parse_known_args()
mode = args.mode
@@ -106,53 +106,115 @@
caching = args.caching
clear_cache = args.clear_cache
-from mpi4py import MPI
-import shutil
-src_path = os.path.dirname(os.path.abspath(__file__))
-cache_path = f"{src_path}/__pycache__"
+# ======================================================================================
+# Simulation-setting overrides
+# ======================================================================================
+
+
+def override_settings(simulation) -> bool:
+ """Apply command-line overrides before compiling a simulation snapshot."""
+ settings = simulation.settings
+ changed = False
+
+ def set_setting(name, value):
+ nonlocal changed
+ if value is None or getattr(settings, name) == value:
+ return
+ setattr(settings, name, value)
+ changed = True
+
+ # These command-line options directly replace public Simulation settings.
+ set_setting("N_particle", args.N_particle)
+ set_setting("N_batch", args.N_batch)
+ set_setting("output_name", args.output)
+ set_setting("use_progress_bar", args.progress_bar)
+
+ # GPU names are translated into the integer constants stored at runtime.
+ if target == "gpu":
+ from mcdc.constant import (
+ GPU_STORAGE_MANAGED,
+ GPU_STORAGE_SEPARATE,
+ GPU_STORAGE_UNITED,
+ GPU_STRATEGY_ASYNC,
+ GPU_STRATEGY_EVENT,
+ )
+
+ strategy = {
+ "async": GPU_STRATEGY_ASYNC,
+ "event": GPU_STRATEGY_EVENT,
+ }[args.gpu_strategy]
+ storage = {
+ "separate": GPU_STORAGE_SEPARATE,
+ "managed": GPU_STORAGE_MANAGED,
+ "united": GPU_STORAGE_UNITED,
+ }[args.gpu_state_storage]
+
+ set_setting("gpu_strategy", strategy)
+ set_setting("gpu_storage", storage)
+
+ return changed
+
+
+# ======================================================================================
+# Process-wide initialization
+# ======================================================================================
+
+
+def _manage_runtime_caches() -> None:
+ """Clear generated-code caches when caching is disabled or reset."""
+ should_clear = not caching or clear_cache
+ if should_clear and MPI.COMM_WORLD.Get_rank() == 0:
+ cache_directories = (
+ Path(__file__).resolve().parent / "__pycache__",
+ Path.cwd() / "__harmonize_cache__",
+ )
+ for cache_directory in cache_directories:
+ if cache_directory.exists():
+ shutil.rmtree(cache_directory)
-if ((caching == False) or (clear_cache == True)) and (MPI.COMM_WORLD.Get_rank() == 0):
- if os.path.exists(cache_path):
- shutil.rmtree(cache_path)
- if os.path.exists("__harmonize_cache__"):
- shutil.rmtree("__harmonize_cache__")
+ # Other ranks must not use a cache while the root rank is removing it.
+ if MPI.COMM_WORLD.Get_size() > 1:
+ MPI.COMM_WORLD.Barrier()
-if MPI.COMM_WORLD.Get_size() > 1:
- MPI.COMM_WORLD.Barrier()
-from mcdc.print_ import (
- print_warning,
-)
-import numba as nb
+def _configure_numba() -> None:
+ """Configure Numba for Python, compiled, or diagnostic execution."""
+ import numba as nb
+
+ if mode == "python":
+ nb.config.DISABLE_JIT = True
+ return
-if mode == "python":
- nb.config.DISABLE_JIT = True
-elif mode == "numba":
nb.config.DISABLE_JIT = False
- nb.config.NUMBA_DEBUG_CACHE = 1
- nb.config.THREADING_LAYER = "workqueue"
-elif mode == "numba_debug":
- msg = "\n >> Entering numba debug mode\n >> will result in slower code and longer compile times\n >> to configure debug options see main.py"
- print_warning(msg)
-
- nb.config.DISABLE_JIT = False # turns on the jitter
- nb.config.DEBUG = False # turns on debugging options
- nb.config.NUMBA_FULL_TRACEBACKS = (
- 1 # enables errors from sub-packages to be printed
- )
- nb.config.NUMBA_BOUNDSCHECK = 1 # checks bounds errors of vectors
- nb.config.NUMBA_COLOR_SCHEME = (
- "dark_bg" # prints error messages for dark background terminals
- )
- nb.config.NUMBA_DEBUG_NRT = 1 # Numba run time (NRT) statistics counter
- nb.config.NUMBA_DEBUG_TYPEINFER = (
- 1 # print out debugging information about type inference.
- )
- nb.config.NUMBA_ENABLE_PROFILING = 1 # enables profiler use
- nb.config.NUMBA_DUMP_CFG = 1 # prints out a control flow diagram
- nb.config.NUMBA_OPT = 0 # forums un optimized code from compilers
- nb.config.NUMBA_DEBUGINFO = 1 #
- nb.config.NUMBA_EXTEND_VARIABLE_LIFETIMES = (
- 1 # allows for inspection of numba variables after end of compilation
+
+ if mode == "numba":
+ nb.config.NUMBA_DEBUG_CACHE = 1
+ nb.config.THREADING_LAYER = "workqueue"
+ return
+
+ from mcdc.print_ import print_warning
+
+ print_warning(
+ "\n >> Entering Numba debug mode"
+ "\n >> This mode is slower and enables additional diagnostics"
)
+
+ # Runtime checks and diagnostic output
+ nb.config.DEBUG = False
+ nb.config.NUMBA_FULL_TRACEBACKS = 1
+ nb.config.NUMBA_BOUNDSCHECK = 1
+ nb.config.NUMBA_COLOR_SCHEME = "dark_bg"
+ nb.config.NUMBA_DEBUG_NRT = 1
+ nb.config.NUMBA_DEBUG_TYPEINFER = 1
+
+ # Generated-code inspection and debugger support
+ nb.config.NUMBA_ENABLE_PROFILING = 1
+ nb.config.NUMBA_DUMP_CFG = 1
+ nb.config.NUMBA_OPT = 0
+ nb.config.NUMBA_DEBUGINFO = 1
+ nb.config.NUMBA_EXTEND_VARIABLE_LIFETIMES = 1
+
+
+_manage_runtime_caches()
+_configure_numba()
diff --git a/mcdc/constant.py b/mcdc/constant.py
index 79ade2223..90381aaab 100644
--- a/mcdc/constant.py
+++ b/mcdc/constant.py
@@ -1,60 +1,123 @@
import math
-# Data index
-TALLY = 0
+# ======================================================================================
+# Mathematical and numerical constants
+# ======================================================================================
-# Tally types
-TALLY_SURFACE_CROSSING = 0
-TALLY_COLLISION = 1
-TALLY_TRACKLENGTH = 2
+PI = math.pi
+PI_HALF = PI / 2.0
+PI_SQRT = math.sqrt(PI)
-# Meshes
-MESH_UNIFORM = 0
-MESH_STRUCTURED = 1
+TINY = 1e-10
+INF = 1e10
-# Tally scores
-## Tracklength
-SCORE_FLUX = 0
-SCORE_DENSITY = 1
-SCORE_COLLISION = 2
-SCORE_CAPTURE = 3
-SCORE_FISSION = 4
-## Surface-crossing
-SCORE_CURRENT_NET = 100
-SCORE_CURRENT_IN = 101
-SCORE_CURRENT_OUT = 102
-## Collision
-SCORE_ENERGY_DEPOSITION = 200
-# Supported tally scores for estimator types
-SUPPORTED_SCORES_SURFACE_CROSSING = {"current-net", "current-in", "current-out"}
-SUPPORTED_SCORES_TRACKLENGTH = {"flux", "density", "collision", "capture", "fission"}
-SUPPORTED_SCORES_COLLISION = {"energy_deposition"}
-SUPPORTED_SCORES = (
- SUPPORTED_SCORES_SURFACE_CROSSING
- | SUPPORTED_SCORES_TRACKLENGTH
- | SUPPORTED_SCORES_COLLISION
-)
+# ======================================================================================
+# Physics and nuclear data
+# ======================================================================================
-# Boundary condition
+# Physical constants
+LIGHT_SPEED = 2.99792458e10 # cm/s
+NEUTRON_MASS = 939.565413e6 # eV/c^2
+ELECTRON_MASS = 510.99895069e3 # eV/c^2
+BOLTZMANN_K = 8.61733326e-5 # eV/K
+
+# Physics thresholds
+ELECTRON_CUTOFF_ENERGY = 100 # eV
+MU_CUTOFF = 0.999999
+THERMAL_THRESHOLD_FACTOR = 400
+
+# Particle types
+PARTICLE_NEUTRON = 0
+PARTICLE_ELECTRON = 1
+PARTICLE_PROTON = 2
+PARTICLE_ANY = 100
+
+# Neutron multigroup energy representation
+NEUTRON_MULTIGROUP_ENERGY_MIDPOINT = 0
+NEUTRON_MULTIGROUP_ENERGY_MIDPOINT_LOG = 1
+NEUTRON_MULTIGROUP_ENERGY_UNIFORM = 2
+NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG = 3
+
+# Neutron reactions
+NEUTRON_REACTION_TOTAL = 0
+NEUTRON_REACTION_ELASTIC_SCATTERING = 1
+NEUTRON_REACTION_CAPTURE = 2
+NEUTRON_REACTION_INELASTIC_SCATTERING = 3
+NEUTRON_REACTION_FISSION = 4
+NEUTRON_REACTION_FISSION_PROMPT = 5
+NEUTRON_REACTION_FISSION_DELAYED = 6
+
+# Electron reactions
+ELECTRON_REACTION_TOTAL = 100
+ELECTRON_REACTION_ELASTIC_SCATTERING = 101
+ELECTRON_REACTION_IONIZATION = 102
+ELECTRON_REACTION_BREMSSTRAHLUNG = 103
+ELECTRON_REACTION_EXCITATION = 104
+
+# Data representations
+DATA_NONE = 0
+DATA_TABLE = 1
+DATA_POLYNOMIAL = 2
+
+# Interpolation laws
+INTERPOLATION_HISTOGRAM = 1
+INTERPOLATION_LINEAR = 2
+INTERPOLATION_SEMILOGX = 3
+INTERPOLATION_SEMILOGY = 4
+INTERPOLATION_LOG = 5
+
+# Probability distributions
+DISTRIBUTION_NONE = 0
+DISTRIBUTION_PMF = 1
+DISTRIBUTION_TABULATED = 2
+DISTRIBUTION_MULTITABLE = 3
+DISTRIBUTION_LEVEL_SCATTERING = 4
+DISTRIBUTION_EVAPORATION = 5
+DISTRIBUTION_MAXWELLIAN = 6
+DISTRIBUTION_KALBACH_MANN = 7
+DISTRIBUTION_TABULATED_ENERGY_ANGLE = 8
+DISTRIBUTION_N_BODY = 9
+
+# Angular distributions
+ANGLE_ISOTROPIC = 0
+ANGLE_DISTRIBUTED = 1
+ANGLE_ENERGY_CORRELATED = 2
+
+# Reference frames
+REFERENCE_FRAME_LAB = 0
+REFERENCE_FRAME_COM = 1
+
+
+# ======================================================================================
+# Geometry
+# ======================================================================================
+
+# Axes
+AXIS_X = 0
+AXIS_Y = 1
+AXIS_Z = 2
+AXIS_T = 3
+
+# Mesh types
+MESH_UNIFORM = 0
+MESH_STRUCTURED = 1
+
+# Boundary conditions
BC_NONE = 0
BC_VACUUM = 1
BC_REFLECTIVE = 2
-# Cell fill
+# Cell fills
FILL_MATERIAL = 0
FILL_UNIVERSE = 1
FILL_LATTICE = 2
FILL_NONE = 3
-# Region
-REGION_HALFSPACE = 0
-REGION_INTERSECTION = 1
-REGION_UNION = 2
-REGION_COMPLEMENT = 3
-REGION_ALL = 4
+# Universe
+UNIVERSE_ROOT = 0
-# Surface type
+# Surface types
SURFACE_PLANE_X = 1
SURFACE_PLANE_Y = 2
SURFACE_PLANE_Z = 3
@@ -73,88 +136,72 @@
SURFACE_TORUS_Z = 16
SURFACE_TORUS = 17
-# Boolean operator
+# Boolean operators
BOOL_AND = -1
BOOL_OR = -2
BOOL_NOT = -3
-# Universe
-UNIVERSE_ROOT = 0
-# Events
-# The << operator represents a bitshift.
-# Each event is assigned 1 << X, which is equal to 2 to the power of X.
+# ======================================================================================
+# Transport
+# ======================================================================================
+
+# Events are bit flags and may be combined with bitwise operations.
EVENT_NONE = 1 << 0
-# Geometry events
EVENT_SURFACE_CROSSING = 1 << 1
EVENT_LATTICE_CROSSING = 1 << 2
EVENT_LOST = 1 << 3
-# Collision/reaction events
EVENT_COLLISION = 1 << 4
-# Miscellanies
EVENT_TIME_CENSUS = 1 << 5
EVENT_TIME_BOUNDARY = 1 << 6
-# Materials
-MATERIAL = 0
-MATERIAL_MG = 1
-MATERIAL_ELEMENTAL = 2
+# Coincidence tolerances
+COINCIDENCE_TOLERANCE = TINY
+COINCIDENCE_TOLERANCE_DIRECTION = 1e-5
+COINCIDENCE_TOLERANCE_ENERGY = 1e-5
+COINCIDENCE_TOLERANCE_TIME = TINY * 1e-2
-# Reactions
-NEUTRON_REACTION_TOTAL = 0
-NEUTRON_REACTION_ELASTIC_SCATTERING = 1
-NEUTRON_REACTION_CAPTURE = 2
-NEUTRON_REACTION_INELASTIC_SCATTERING = 3
-NEUTRON_REACTION_FISSION = 4
-NEUTRON_REACTION_FISSION_PROMPT = 5
-NEUTRON_REACTION_FISSION_DELAYED = 6
-ELECTRON_REACTION_TOTAL = 100
-ELECTRON_REACTION_ELASTIC_SCATTERING = 101
-ELECTRON_REACTION_ELASTIC_SMALL_ANGLE = 102
-ELECTRON_REACTION_ELASTIC_LARGE_ANGLE = 103
-ELECTRON_REACTION_IONIZATION = 104
-ELECTRON_REACTION_BREMSSTRAHLUNG = 105
-ELECTRON_REACTION_EXCITATION = 106
-# Particle types
-PARTICLE_NEUTRON = 0
-PARTICLE_ELECTRON = 1
-PARTICLE_PROTON = 2
+# ======================================================================================
+# Tallies
+# ======================================================================================
-# Data
-DATA_NONE = 0
-DATA_TABLE = 1
-DATA_POLYNOMIAL = 2
+# Tally estimator types
+TALLY_SURFACE_CROSSING = 0
+TALLY_COLLISION = 1
+TALLY_TRACKLENGTH = 2
-# Distribution
-DISTRIBUTION_NONE = 0
-DISTRIBUTION_PMF = 1
-DISTRIBUTION_TABULATED = 2
-DISTRIBUTION_MULTITABLE = 3
-DISTRIBUTION_LEVEL_SCATTERING = 4
-DISTRIBUTION_EVAPORATION = 5
-DISTRIBUTION_MAXWELLIAN = 6
-DISTRIBUTION_KALBACH_MANN = 7
-DISTRIBUTION_TABULATED_ENERGY_ANGLE = 8
-DISTRIBUTION_N_BODY = 9
+# Track-length scores
+SCORE_FLUX = 0
+SCORE_DENSITY = 1
+SCORE_COLLISION = 2
+SCORE_CAPTURE = 3
+SCORE_FISSION = 4
-# Anguler distribution type
-ANGLE_ISOTROPIC = 0
-ANGLE_DISTRIBUTED = 1
-ANGLE_ENERGY_CORRELATED = 2
+# Surface-crossing scores
+SCORE_CURRENT_NET = 100
+SCORE_CURRENT_IN = 101
+SCORE_CURRENT_OUT = 102
-# Referance frame
-REFERENCE_FRAME_LAB = 0
-REFERENCE_FRAME_COM = 1
+# Collision scores
+SCORE_ENERGY_DEPOSITION = 200
-# Interpolation law
-INTERPOLATION_HISTOGRAM = 1
-INTERPOLATION_LINEAR = 2
-INTERPOLATION_SEMILOGX = 3
-INTERPOLATION_SEMILOGY = 4
-INTERPOLATION_LOG = 5
+# Supported scores by estimator type
+SUPPORTED_SCORES_SURFACE_CROSSING = {"current-net", "current-in", "current-out"}
+SUPPORTED_SCORES_TRACKLENGTH = {"flux", "density", "collision", "capture", "fission"}
+SUPPORTED_SCORES_COLLISION = {"energy_deposition"}
+SUPPORTED_SCORES = (
+ SUPPORTED_SCORES_SURFACE_CROSSING
+ | SUPPORTED_SCORES_TRACKLENGTH
+ | SUPPORTED_SCORES_COLLISION
+)
-# Gyration raius type
+
+# ======================================================================================
+# Techniques and diagnostics
+# ======================================================================================
+
+# Gyration-radius types
GYRATION_RADIUS_ALL = 0
GYRATION_RADIUS_INFINITE_X = 1
GYRATION_RADIUS_INFINITE_Y = 2
@@ -163,48 +210,22 @@
GYRATION_RADIUS_ONLY_Y = 5
GYRATION_RADIUS_ONLY_Z = 6
-# Population control
+# Population-control types are currently unused.
PCT_NONE = 0
PCT_COMBING = 1
PCT_COMBING_WEIGHT = 2
PCT_SPLITTING_ROULETTE = 3
PCT_SPLITTING_ROULETTE_WEIGHT = 4
-# Misc.
-TINY = 1e-10
-COINCIDENCE_TOLERANCE = TINY
-COINCIDENCE_TOLERANCE_DIRECTION = 1e-5
-COINCIDENCE_TOLERANCE_ENERGY = 1e-5
-COINCIDENCE_TOLERANCE_TIME = TINY * 1e-2
-INF = 1e10
-PI = math.pi
-PI_SQRT = math.sqrt(PI)
-PI_HALF = PI / 2.0
-BANKMAX = 100 # Default maximum active bank
-MAX_BISECTION_ITERATIONS = 48
-
-# Axes
-AXIS_X = 0
-AXIS_Y = 1
-AXIS_Z = 2
-AXIS_T = 3
-
-# Physics
-LIGHT_SPEED = 2.99792458e10 # cm/s
-NEUTRON_MASS = 939.565413e6 # eV/c^2
-ELECTRON_MASS = 510.99895069e3 # eV/c^2
-BOLTZMANN_K = 8.61733326e-5 # eV/K
-ELECTRON_CUTOFF_ENERGY = 100 # eV
-MU_CUTOFF = 0.999999
-THERMAL_THRESHOLD_FACTOR = 400
-
-# Weight Windows Methods
+# Weight-window methods are currently unused.
WW_USER = 0
WW_PREVIOUS = 1
-# Weight Windows Modifications
+
+# Weight-window modifications are currently unused.
WW_MIN = 0
WW_WOLLABER = 1
+
# ======================================================================================
# GPU settings
# ======================================================================================
@@ -214,7 +235,7 @@
GPU_STRATEGY_ASYNC = 0
GPU_STRATEGY_EVENT = 1
-# GPU async. types
+# GPU asynchronous-operation types
GPU_ASYNC_SIMPLE = 0
# GPU storage types
diff --git a/mcdc/main.py b/mcdc/main.py
index 71db344ae..7f017856a 100644
--- a/mcdc/main.py
+++ b/mcdc/main.py
@@ -1,19 +1,12 @@
+from mcdc.object_.simulation import Simulation
+
# ======================================================================================
-# Run
+# Run Simulation
# ======================================================================================
-def run():
- """
- Execute the MC/DC simulation.
-
- Runs the transport simulation defined by the current problem
- (materials, geometry, sources, tallies, and settings).
- Results are written to an HDF5 output file.
-
- Command-line arguments (``--N_particle``, ``--output``, etc.) override
- the corresponding settings when provided.
- """
+def run_simulation(simulationPy: Simulation):
+ """Compile when needed, prepare runtime state, and execute a simulation."""
import mcdc.print_ as print_module
from mpi4py import MPI
@@ -21,8 +14,6 @@ def run():
time_total_start = MPI.Wtime()
# Get settings and MPI master status
- from mcdc.object_.simulation import simulation as simulationPy
-
settings = simulationPy.settings
master = MPI.COMM_WORLD.Get_rank() == 0
@@ -33,9 +24,6 @@ def run():
# TIMER: preparation
time_prep_start = MPI.Wtime()
- # Override settings with command-line arguments
- override_settings()
-
# Generate the program state:
# - `simulation`: the simulation structure, storing fixed side data and meta data
# that describes arbitrarily-sized data
@@ -44,7 +32,7 @@ def run():
# The use of container is necessary to ensure proper mutability and tracking
# of the structure when running in different kinds of machines supported by
# the Numba-based compilation framework.
- simulation_container, data = preparation()
+ simulation_container, data = prepare(simulationPy)
simulation = simulation_container[0]
# Print headers
@@ -86,7 +74,11 @@ def run():
time_output_start = MPI.Wtime()
# Generate hdf5 output file
- output_module.generate_output(simulation, data)
+ output_module.generate_output(simulation, data, simulationPy)
+
+ # Combine per-batch, per-census tally files into the main output
+ if settings.use_census_based_tally:
+ output_module.recombine_tallies(simulationPy, simulation)
# TIMER: output
time_output_end = MPI.Wtime()
@@ -114,140 +106,27 @@ def run():
# ======================================================================================
-# Preparation
+# Prepare
# ======================================================================================
-def preparation():
- import math
-
- from mpi4py import MPI
-
- from mcdc.object_.simulation import simulation as simulationPy
- from mcdc.object_.material import (
- Material,
- MaterialMG,
- set_elements_from_nuclides,
- set_nuclides_from_elements,
- update_fissionable_from_nuclides,
- )
-
- # ==================================================================================
- # Adjust simulation settings as needed
- # ==================================================================================
-
- # Get settings
- settings = simulationPy.settings
-
- # Set appropriate time boundary
- settings.time_boundary = min(
- [settings.time_boundary] + [tally.time[-1] for tally in simulationPy.tallies]
- )
-
- # ==================================================================================
- # Set material data as needed
- # ==================================================================================
-
- # Set material compositions based on transported particles
- for material in simulationPy.materials:
- if not isinstance(material, Material):
- continue
-
- if settings.neutron_transport and len(material.nuclides) == 0:
- set_nuclides_from_elements(material)
-
- if settings.electron_transport and len(material.elements) == 0:
- set_elements_from_nuclides(material)
-
- # Set nuclear and atomic data for transported particles
- if settings.neutron_transport:
- for nuclide in simulationPy.nuclides:
- nuclide.set_neutron_data()
-
- for material in simulationPy.materials:
- if isinstance(material, Material):
- update_fissionable_from_nuclides(material)
-
- if settings.electron_transport:
- for element in simulationPy.elements:
- element.set_electron_data()
-
- # Set physics mode
- if len(simulationPy.materials) == 0:
- # Default physics in dummy mode
- settings.neutron_multigroup_mode = True
- else:
- settings.neutron_multigroup_mode = isinstance(
- simulationPy.materials[0], MaterialMG
- )
-
- # ==================================================================================
- # Adjust simulation parameters as needed
- # ==================================================================================
-
- # Reset time grid size of all tallies if census-based tally is desired
- if settings.use_census_based_tally:
- N_bin = settings.census_tally_frequency
- for tally in simulationPy.tallies:
- tally._use_census_based_tally(N_bin)
-
- # Normalize source probability
- norm = 0.0
- for source in simulationPy.sources:
- norm += source.probability
- for source in simulationPy.sources:
- source.probability /= norm
-
- # Create root universe if not defined
- if len(simulationPy.universes[0].cells) == 0:
- simulationPy.universes[0].cells = simulationPy.cells
-
- # Initial guess
- simulationPy.k_eff = settings.k_init
-
- # Activate tally scoring for fixed-source
- if not settings.neutron_eigenvalue_mode:
- simulationPy.cycle_active = True
- # All active eigenvalue cycle?
- elif settings.N_inactive == 0:
- simulationPy.cycle_active = True
-
- # ==================================================================================
- # Set particle bank sizes
- # ==================================================================================
-
- # Some sizes
- N_particle = settings.N_particle
- N_work = math.ceil(N_particle / MPI.COMM_WORLD.Get_size())
- N_census = settings.N_census
-
- # Determine bank size
- if settings.neutron_eigenvalue_mode or N_census == 1:
- settings.future_bank_buffer_ratio = 0.0
- if not settings.neutron_eigenvalue_mode and N_census == 1:
- settings.census_bank_buffer_ratio = 0.0
- settings.source_bank_buffer_ratio = 0.0
- size_active = settings.active_bank_buffer
- size_census = int((settings.census_bank_buffer_ratio) * N_work)
- size_source = int((settings.source_bank_buffer_ratio) * N_work)
- size_future = int((settings.future_bank_buffer_ratio) * N_work)
-
- # Set bank size
- simulationPy.bank_active.size[0] = size_active
- simulationPy.bank_census.size[0] = size_census
- simulationPy.bank_source.size[0] = size_source
- simulationPy.bank_future.size[0] = size_future
+def prepare(simulationPy: Simulation):
+ """Create framework-owned runtime state for a compiled simulation.
+ Model-specific finalization occurs during :meth:`mcdc.Simulation.compile`.
+ This function packs that model, allocates execution resources, configures
+ the selected backend, and loads any external source-particle state.
+ """
# ==================================================================================
- # Generate Numba-supported "Objects"
+ # Generate Numba runtime layers
# ==================================================================================
- from mcdc.code_factory.numba_objects_generator import generate_numba_objects
+ from mcdc.code_factory.numba_layers_generator import generate_numba_layers
from mcdc.code_factory.literals_generator import make_literals
make_literals(simulationPy)
- simulation_container, data = generate_numba_objects(simulationPy)
+ simulation_container, data = generate_numba_layers(simulationPy)
simulation = simulation_container[0]
# Reload mcdc getters and setters
@@ -258,21 +137,6 @@ def preparation():
importlib.reload(mcdc_get)
importlib.reload(mcdc_set)
- # ==================================================================================
- # Adapt functions as needed
- # ==================================================================================
-
- # Pick physics model
- import mcdc.transport.physics as physics
-
- if settings.neutron_multigroup_mode:
- physics.neutron.particle_speed = physics.neutron.multigroup.particle_speed
- physics.neutron.macro_xs = physics.neutron.multigroup.macro_xs
- physics.neutron.neutron_production_xs = (
- physics.neutron.multigroup.neutron_production_xs
- )
- physics.neutron.collision = physics.neutron.multigroup.collision
-
# Pick Python-version RNG if needed
import mcdc.config as config
import mcdc.transport.rng as rng
@@ -287,30 +151,26 @@ def preparation():
# ==================================================================================
# Source particles from file
# ==================================================================================
- # TODO: Use parallel h5py, may need to compile for speed
-
- import h5py
-
- # All ranks, take turn
- for i in range(simulation["mpi_size"]):
- if simulation["mpi_rank"] == i:
- if settings.use_source_file:
- with h5py.File(settings.source_file_name, "r") as f:
- # Get source particle size
- N_particle = f["particles_size"][()]
-
- # Redistribute work
- mpi.distribute_work(N_particle, simulation)
- N_local = simulation["mpi_work_size"]
- start = simulation["mpi_work_start"]
- end = start + N_local
-
- # Add particles to source bank
- simulation["bank_source"]["particles"][:N_local] = f["particles"][
- start:end
- ]
- simulation["bank_source"]["size"] = N_local
- MPI.COMM_WORLD.Barrier()
+ # TODO: Re-enable file-backed source initialization after its particle-bank
+ # schema and MPI redistribution path are updated.
+ #
+ # import h5py
+ # import mcdc.transport.mpi as mpi
+ # from mpi4py import MPI
+ #
+ # for i in range(simulation["mpi_size"]):
+ # if simulation["mpi_rank"] == i and settings.use_source_file:
+ # with h5py.File(settings.source_file_name, "r") as f:
+ # N_particle = f["particles_size"][()]
+ # mpi.distribute_work(N_particle, simulation)
+ # N_local = simulation["mpi_work_size"]
+ # start = simulation["mpi_work_start"]
+ # end = start + N_local
+ # simulation["bank_source"]["particle_data"][:N_local] = f[
+ # "particles"
+ # ][start:end]
+ # simulation["bank_source"]["size"] = N_local
+ # MPI.COMM_WORLD.Barrier()
# ==================================================================================
# Finalize
@@ -324,44 +184,6 @@ def preparation():
# ======================================================================================
-def override_settings():
- import mcdc.config as config
- from mcdc.object_.simulation import simulation as simulationPy
-
- settings = simulationPy.settings
-
- if config.args.N_particle is not None:
- settings.N_particle = config.args.N_particle
- if config.args.N_batch is not None:
- settings.N_batch = config.args.N_batch
- if config.args.output is not None:
- settings.output_name = config.args.output
- if config.args.progress_bar is not None:
- settings.use_progress_bar = config.args.progress_bar
-
- # GPU settings
- if config.target == "gpu":
- from mcdc.constant import (
- GPU_STRATEGY_ASYNC,
- GPU_STRATEGY_EVENT,
- GPU_STORAGE_SEPARATE,
- GPU_STORAGE_MANAGED,
- GPU_STORAGE_UNITED,
- )
-
- if config.args.gpu_strategy == "async":
- settings.gpu_strategy = GPU_STRATEGY_ASYNC
- elif config.args.gpu_strategy == "event":
- settings.gpu_strategy = GPU_STRATEGY_EVENT
-
- if config.args.gpu_state_storage == "separate":
- settings.gpu_storage = GPU_STORAGE_SEPARATE
- elif config.args.gpu_state_storage == "managed":
- settings.gpu_storage = GPU_STORAGE_MANAGED
- elif config.args.gpu_state_storage == "united":
- settings.gpu_storage = GPU_STORAGE_UNITED
-
-
def finalize(simulation):
import mcdc.config as config
diff --git a/mcdc/mcdc_get/__init__.py b/mcdc/mcdc_get/__init__.py
index d1d03c34c..991c0c0b7 100644
--- a/mcdc/mcdc_get/__init__.py
+++ b/mcdc/mcdc_get/__init__.py
@@ -60,9 +60,7 @@
import mcdc.mcdc_get.gpu_meta as gpu_meta
-import mcdc.mcdc_get.native_material as native_material
-
-import mcdc.mcdc_get.multigroup_material as multigroup_material
+import mcdc.mcdc_get.neutron_multigroup_data as neutron_multigroup_data
import mcdc.mcdc_get.nuclide as nuclide
@@ -88,24 +86,28 @@
import mcdc.mcdc_get.settings as settings
+import mcdc.mcdc_get.technique as technique
+
+import mcdc.mcdc_get.source as source
+
+import mcdc.mcdc_get.surface as surface
+
+import mcdc.mcdc_get.surface_crossing_tally as surface_crossing_tally
+
+import mcdc.mcdc_get.tally as tally
+
import mcdc.mcdc_get.global_weight_roulette as global_weight_roulette
import mcdc.mcdc_get.implicit_capture as implicit_capture
+import mcdc.mcdc_get.neutron_multigroup as neutron_multigroup
+
import mcdc.mcdc_get.population_control as population_control
import mcdc.mcdc_get.weight_windows as weight_windows
import mcdc.mcdc_get.weighted_emission as weighted_emission
-import mcdc.mcdc_get.source as source
-
-import mcdc.mcdc_get.surface as surface
-
-import mcdc.mcdc_get.surface_crossing_tally as surface_crossing_tally
-
-import mcdc.mcdc_get.tally as tally
-
import mcdc.mcdc_get.bank_active as bank_active
import mcdc.mcdc_get.bank_census as bank_census
diff --git a/mcdc/mcdc_get/cell.py b/mcdc/mcdc_get/cell.py
index 17ee8a8e4..8dbe96150 100644
--- a/mcdc/mcdc_get/cell.py
+++ b/mcdc/mcdc_get/cell.py
@@ -1,12 +1,13 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@njit
def region_RPN_tokens(index, cell, data):
offset = cell["region_RPN_tokens_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -22,7 +23,7 @@ def region_RPN_tokens_last(cell, data):
start = cell["region_RPN_tokens_offset"]
size = cell["region_RPN_tokens_length"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -35,7 +36,7 @@ def region_RPN_tokens_chunk(start, length, cell, data):
@njit
def surface_IDs(index, cell, data):
offset = cell["surface_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -51,7 +52,7 @@ def surface_IDs_last(cell, data):
start = cell["surface_IDs_offset"]
size = cell["N_surface"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -64,7 +65,7 @@ def surface_IDs_chunk(start, length, cell, data):
@njit
def collision_tally_IDs(index, cell, data):
offset = cell["collision_tally_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -80,7 +81,7 @@ def collision_tally_IDs_last(cell, data):
start = cell["collision_tally_IDs_offset"]
size = cell["N_collision_tally"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -93,7 +94,7 @@ def collision_tally_IDs_chunk(start, length, cell, data):
@njit
def tracklength_tally_IDs(index, cell, data):
offset = cell["tracklength_tally_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -109,7 +110,7 @@ def tracklength_tally_IDs_last(cell, data):
start = cell["tracklength_tally_IDs_offset"]
size = cell["N_tracklength_tally"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/electron_ionization_reaction.py b/mcdc/mcdc_get/electron_ionization_reaction.py
index 22775182f..e485ff58d 100644
--- a/mcdc/mcdc_get/electron_ionization_reaction.py
+++ b/mcdc/mcdc_get/electron_ionization_reaction.py
@@ -1,12 +1,13 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@njit
def subshell_x_IDs(index, electron_ionization_reaction, data):
offset = electron_ionization_reaction["subshell_x_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -22,7 +23,7 @@ def subshell_x_IDs_last(electron_ionization_reaction, data):
start = electron_ionization_reaction["subshell_x_IDs_offset"]
size = electron_ionization_reaction["N_subshell_x"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -35,7 +36,7 @@ def subshell_x_IDs_chunk(start, length, electron_ionization_reaction, data):
@njit
def subshell_product_IDs(index, electron_ionization_reaction, data):
offset = electron_ionization_reaction["subshell_product_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -51,7 +52,7 @@ def subshell_product_IDs_last(electron_ionization_reaction, data):
start = electron_ionization_reaction["subshell_product_IDs_offset"]
size = electron_ionization_reaction["N_subshell_product"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/element.py b/mcdc/mcdc_get/element.py
index 6d5873a2c..a2fec6bd9 100644
--- a/mcdc/mcdc_get/element.py
+++ b/mcdc/mcdc_get/element.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -180,7 +181,7 @@ def electron_bremsstrahlung_xs_chunk(start, length, element, data):
@njit
def electron_ionization_reaction_IDs(index, element, data):
offset = element["electron_ionization_reaction_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -196,7 +197,7 @@ def electron_ionization_reaction_IDs_last(element, data):
start = element["electron_ionization_reaction_IDs_offset"]
size = element["N_electron_ionization_reaction"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -209,7 +210,7 @@ def electron_ionization_reaction_IDs_chunk(start, length, element, data):
@njit
def electron_elastic_scattering_reaction_IDs(index, element, data):
offset = element["electron_elastic_scattering_reaction_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -225,7 +226,7 @@ def electron_elastic_scattering_reaction_IDs_last(element, data):
start = element["electron_elastic_scattering_reaction_IDs_offset"]
size = element["N_electron_elastic_scattering_reaction"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -238,7 +239,7 @@ def electron_elastic_scattering_reaction_IDs_chunk(start, length, element, data)
@njit
def electron_excitation_reaction_IDs(index, element, data):
offset = element["electron_excitation_reaction_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -254,7 +255,7 @@ def electron_excitation_reaction_IDs_last(element, data):
start = element["electron_excitation_reaction_IDs_offset"]
size = element["N_electron_excitation_reaction"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -267,7 +268,7 @@ def electron_excitation_reaction_IDs_chunk(start, length, element, data):
@njit
def electron_bremsstrahlung_reaction_IDs(index, element, data):
offset = element["electron_bremsstrahlung_reaction_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -283,7 +284,7 @@ def electron_bremsstrahlung_reaction_IDs_last(element, data):
start = element["electron_bremsstrahlung_reaction_IDs_offset"]
size = element["N_electron_bremsstrahlung_reaction"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/kalbach_mann_distribution.py b/mcdc/mcdc_get/kalbach_mann_distribution.py
index 456942a01..2916d0893 100644
--- a/mcdc/mcdc_get/kalbach_mann_distribution.py
+++ b/mcdc/mcdc_get/kalbach_mann_distribution.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -35,7 +36,7 @@ def energy_chunk(start, length, kalbach_mann_distribution, data):
@njit
def offset(index, kalbach_mann_distribution, data):
offset = kalbach_mann_distribution["offset_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -51,7 +52,7 @@ def offset_last(kalbach_mann_distribution, data):
start = kalbach_mann_distribution["offset_offset"]
size = kalbach_mann_distribution["offset_length"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/lattice.py b/mcdc/mcdc_get/lattice.py
index 437f5be40..4daad9d4b 100644
--- a/mcdc/mcdc_get/lattice.py
+++ b/mcdc/mcdc_get/lattice.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -8,7 +9,7 @@ def universe_IDs(index_1, index_2, index_3, lattice, data):
offset = lattice["universe_IDs_offset"]
stride_2 = lattice["Ny"]
stride_3 = lattice["Nz"]
- return data[offset + index_1 * stride_2 * stride_3 + index_2 * stride_3 + index_3]
+ return int64(data[offset + index_1 * stride_2 * stride_3 + index_2 * stride_3 + index_3])
@njit
diff --git a/mcdc/mcdc_get/material.py b/mcdc/mcdc_get/material.py
index fdbf8e750..1c4539002 100644
--- a/mcdc/mcdc_get/material.py
+++ b/mcdc/mcdc_get/material.py
@@ -1,3 +1,120 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
+
+
+@njit
+def nuclide_IDs(index, material, data):
+ offset = material["nuclide_IDs_offset"]
+ return int64(data[offset + index])
+
+
+@njit
+def nuclide_IDs_all(material, data):
+ start = material["nuclide_IDs_offset"]
+ size = material["N_nuclide"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def nuclide_IDs_last(material, data):
+ start = material["nuclide_IDs_offset"]
+ size = material["N_nuclide"]
+ end = start + size
+ return int64(data[end - 1])
+
+
+@njit
+def nuclide_IDs_chunk(start, length, material, data):
+ start += material["nuclide_IDs_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def element_IDs(index, material, data):
+ offset = material["element_IDs_offset"]
+ return int64(data[offset + index])
+
+
+@njit
+def element_IDs_all(material, data):
+ start = material["element_IDs_offset"]
+ size = material["N_element"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def element_IDs_last(material, data):
+ start = material["element_IDs_offset"]
+ size = material["N_element"]
+ end = start + size
+ return int64(data[end - 1])
+
+
+@njit
+def element_IDs_chunk(start, length, material, data):
+ start += material["element_IDs_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def nuclide_densities(index, material, data):
+ offset = material["nuclide_densities_offset"]
+ return data[offset + index]
+
+
+@njit
+def nuclide_densities_all(material, data):
+ start = material["nuclide_densities_offset"]
+ size = material["nuclide_densities_length"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def nuclide_densities_last(material, data):
+ start = material["nuclide_densities_offset"]
+ size = material["nuclide_densities_length"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def nuclide_densities_chunk(start, length, material, data):
+ start += material["nuclide_densities_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def element_densities(index, material, data):
+ offset = material["element_densities_offset"]
+ return data[offset + index]
+
+
+@njit
+def element_densities_all(material, data):
+ start = material["element_densities_offset"]
+ size = material["element_densities_length"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def element_densities_last(material, data):
+ start = material["element_densities_offset"]
+ size = material["element_densities_length"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def element_densities_chunk(start, length, material, data):
+ start += material["element_densities_offset"]
+ end = start + length
+ return data[start:end]
diff --git a/mcdc/mcdc_get/multi_table_distribution.py b/mcdc/mcdc_get/multi_table_distribution.py
index aad2a755d..de411b77b 100644
--- a/mcdc/mcdc_get/multi_table_distribution.py
+++ b/mcdc/mcdc_get/multi_table_distribution.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -35,7 +36,7 @@ def grid_chunk(start, length, multi_table_distribution, data):
@njit
def table_IDs(index, multi_table_distribution, data):
offset = multi_table_distribution["table_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -51,7 +52,7 @@ def table_IDs_last(multi_table_distribution, data):
start = multi_table_distribution["table_IDs_offset"]
size = multi_table_distribution["N_table"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/multigroup_material.py b/mcdc/mcdc_get/multigroup_material.py
deleted file mode 100644
index 12f63912c..000000000
--- a/mcdc/mcdc_get/multigroup_material.py
+++ /dev/null
@@ -1,385 +0,0 @@
-# The following is automatically generated by code_factory.py
-
-from numba import njit
-
-
-@njit
-def mgxs_speed(index, multigroup_material, data):
- offset = multigroup_material["mgxs_speed_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_speed_all(multigroup_material, data):
- start = multigroup_material["mgxs_speed_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_speed_last(multigroup_material, data):
- start = multigroup_material["mgxs_speed_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_speed_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_speed_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_decay_rate(index, multigroup_material, data):
- offset = multigroup_material["mgxs_decay_rate_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_decay_rate_all(multigroup_material, data):
- start = multigroup_material["mgxs_decay_rate_offset"]
- size = multigroup_material["J"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_decay_rate_last(multigroup_material, data):
- start = multigroup_material["mgxs_decay_rate_offset"]
- size = multigroup_material["J"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_decay_rate_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_decay_rate_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_capture(index, multigroup_material, data):
- offset = multigroup_material["mgxs_capture_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_capture_all(multigroup_material, data):
- start = multigroup_material["mgxs_capture_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_capture_last(multigroup_material, data):
- start = multigroup_material["mgxs_capture_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_capture_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_capture_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_scatter(index, multigroup_material, data):
- offset = multigroup_material["mgxs_scatter_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_scatter_all(multigroup_material, data):
- start = multigroup_material["mgxs_scatter_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_scatter_last(multigroup_material, data):
- start = multigroup_material["mgxs_scatter_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_scatter_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_scatter_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_fission(index, multigroup_material, data):
- offset = multigroup_material["mgxs_fission_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_fission_all(multigroup_material, data):
- start = multigroup_material["mgxs_fission_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_fission_last(multigroup_material, data):
- start = multigroup_material["mgxs_fission_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_fission_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_fission_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_total(index, multigroup_material, data):
- offset = multigroup_material["mgxs_total_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_total_all(multigroup_material, data):
- start = multigroup_material["mgxs_total_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_total_last(multigroup_material, data):
- start = multigroup_material["mgxs_total_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_total_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_total_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_nu_s(index, multigroup_material, data):
- offset = multigroup_material["mgxs_nu_s_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_nu_s_all(multigroup_material, data):
- start = multigroup_material["mgxs_nu_s_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_nu_s_last(multigroup_material, data):
- start = multigroup_material["mgxs_nu_s_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_nu_s_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_nu_s_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_nu_p(index, multigroup_material, data):
- offset = multigroup_material["mgxs_nu_p_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_nu_p_all(multigroup_material, data):
- start = multigroup_material["mgxs_nu_p_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_nu_p_last(multigroup_material, data):
- start = multigroup_material["mgxs_nu_p_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_nu_p_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_nu_p_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_nu_d_vector(index_1, multigroup_material, data):
- offset = multigroup_material["mgxs_nu_d_offset"]
- stride = multigroup_material["J"]
- start = offset + index_1 * stride
- end = start + stride
- return data[start:end]
-
-
-@njit
-def mgxs_nu_d(index_1, index_2, multigroup_material, data):
- offset = multigroup_material["mgxs_nu_d_offset"]
- stride = multigroup_material["J"]
- return data[offset + index_1 * stride + index_2]
-
-
-@njit
-def mgxs_nu_d_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_nu_d_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_nu_d_total(index, multigroup_material, data):
- offset = multigroup_material["mgxs_nu_d_total_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_nu_d_total_all(multigroup_material, data):
- start = multigroup_material["mgxs_nu_d_total_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_nu_d_total_last(multigroup_material, data):
- start = multigroup_material["mgxs_nu_d_total_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_nu_d_total_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_nu_d_total_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_nu_f(index, multigroup_material, data):
- offset = multigroup_material["mgxs_nu_f_offset"]
- return data[offset + index]
-
-
-@njit
-def mgxs_nu_f_all(multigroup_material, data):
- start = multigroup_material["mgxs_nu_f_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def mgxs_nu_f_last(multigroup_material, data):
- start = multigroup_material["mgxs_nu_f_offset"]
- size = multigroup_material["G"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def mgxs_nu_f_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_nu_f_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_chi_s_vector(index_1, multigroup_material, data):
- offset = multigroup_material["mgxs_chi_s_offset"]
- stride = multigroup_material["G"]
- start = offset + index_1 * stride
- end = start + stride
- return data[start:end]
-
-
-@njit
-def mgxs_chi_s(index_1, index_2, multigroup_material, data):
- offset = multigroup_material["mgxs_chi_s_offset"]
- stride = multigroup_material["G"]
- return data[offset + index_1 * stride + index_2]
-
-
-@njit
-def mgxs_chi_s_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_chi_s_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_chi_p_vector(index_1, multigroup_material, data):
- offset = multigroup_material["mgxs_chi_p_offset"]
- stride = multigroup_material["G"]
- start = offset + index_1 * stride
- end = start + stride
- return data[start:end]
-
-
-@njit
-def mgxs_chi_p(index_1, index_2, multigroup_material, data):
- offset = multigroup_material["mgxs_chi_p_offset"]
- stride = multigroup_material["G"]
- return data[offset + index_1 * stride + index_2]
-
-
-@njit
-def mgxs_chi_p_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_chi_p_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def mgxs_chi_d_vector(index_1, multigroup_material, data):
- offset = multigroup_material["mgxs_chi_d_offset"]
- stride = multigroup_material["G"]
- start = offset + index_1 * stride
- end = start + stride
- return data[start:end]
-
-
-@njit
-def mgxs_chi_d(index_1, index_2, multigroup_material, data):
- offset = multigroup_material["mgxs_chi_d_offset"]
- stride = multigroup_material["G"]
- return data[offset + index_1 * stride + index_2]
-
-
-@njit
-def mgxs_chi_d_chunk(start, length, multigroup_material, data):
- start += multigroup_material["mgxs_chi_d_offset"]
- end = start + length
- return data[start:end]
diff --git a/mcdc/mcdc_get/native_material.py b/mcdc/mcdc_get/native_material.py
deleted file mode 100644
index 361294585..000000000
--- a/mcdc/mcdc_get/native_material.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# The following is automatically generated by code_factory.py
-
-from numba import njit
-
-
-@njit
-def nuclide_IDs(index, native_material, data):
- offset = native_material["nuclide_IDs_offset"]
- return data[offset + index]
-
-
-@njit
-def nuclide_IDs_all(native_material, data):
- start = native_material["nuclide_IDs_offset"]
- size = native_material["N_nuclide"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def nuclide_IDs_last(native_material, data):
- start = native_material["nuclide_IDs_offset"]
- size = native_material["N_nuclide"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def nuclide_IDs_chunk(start, length, native_material, data):
- start += native_material["nuclide_IDs_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def element_IDs(index, native_material, data):
- offset = native_material["element_IDs_offset"]
- return data[offset + index]
-
-
-@njit
-def element_IDs_all(native_material, data):
- start = native_material["element_IDs_offset"]
- size = native_material["N_element"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def element_IDs_last(native_material, data):
- start = native_material["element_IDs_offset"]
- size = native_material["N_element"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def element_IDs_chunk(start, length, native_material, data):
- start += native_material["element_IDs_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def nuclide_densities(index, native_material, data):
- offset = native_material["nuclide_densities_offset"]
- return data[offset + index]
-
-
-@njit
-def nuclide_densities_all(native_material, data):
- start = native_material["nuclide_densities_offset"]
- size = native_material["nuclide_densities_length"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def nuclide_densities_last(native_material, data):
- start = native_material["nuclide_densities_offset"]
- size = native_material["nuclide_densities_length"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def nuclide_densities_chunk(start, length, native_material, data):
- start += native_material["nuclide_densities_offset"]
- end = start + length
- return data[start:end]
-
-
-@njit
-def element_densities(index, native_material, data):
- offset = native_material["element_densities_offset"]
- return data[offset + index]
-
-
-@njit
-def element_densities_all(native_material, data):
- start = native_material["element_densities_offset"]
- size = native_material["element_densities_length"]
- end = start + size
- return data[start:end]
-
-
-@njit
-def element_densities_last(native_material, data):
- start = native_material["element_densities_offset"]
- size = native_material["element_densities_length"]
- end = start + size
- return data[end - 1]
-
-
-@njit
-def element_densities_chunk(start, length, native_material, data):
- start += native_material["element_densities_offset"]
- end = start + length
- return data[start:end]
diff --git a/mcdc/mcdc_get/neutron_inelastic_scattering_reaction.py b/mcdc/mcdc_get/neutron_inelastic_scattering_reaction.py
index 5cf2f0dd4..de62aafda 100644
--- a/mcdc/mcdc_get/neutron_inelastic_scattering_reaction.py
+++ b/mcdc/mcdc_get/neutron_inelastic_scattering_reaction.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -58,7 +59,7 @@ def spectrum_probability_chunk(start, length, neutron_inelastic_scattering_react
@njit
def energy_spectrum_IDs(index, neutron_inelastic_scattering_reaction, data):
offset = neutron_inelastic_scattering_reaction["energy_spectrum_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -74,7 +75,7 @@ def energy_spectrum_IDs_last(neutron_inelastic_scattering_reaction, data):
start = neutron_inelastic_scattering_reaction["energy_spectrum_IDs_offset"]
size = neutron_inelastic_scattering_reaction["N_energy_spectrum"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/neutron_multigroup.py b/mcdc/mcdc_get/neutron_multigroup.py
new file mode 100644
index 000000000..fdbf8e750
--- /dev/null
+++ b/mcdc/mcdc_get/neutron_multigroup.py
@@ -0,0 +1,3 @@
+# The following is automatically generated by code_factory.py
+
+from numba import njit
diff --git a/mcdc/mcdc_get/neutron_multigroup_data.py b/mcdc/mcdc_get/neutron_multigroup_data.py
new file mode 100644
index 000000000..dd42f529f
--- /dev/null
+++ b/mcdc/mcdc_get/neutron_multigroup_data.py
@@ -0,0 +1,414 @@
+# The following is automatically generated by code_factory.py
+
+from numba import njit
+
+
+@njit
+def energy_grid(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["energy_grid_offset"]
+ return data[offset + index]
+
+
+@njit
+def energy_grid_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["energy_grid_offset"]
+ size = neutron_multigroup_data["G"] + 1
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def energy_grid_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["energy_grid_offset"]
+ size = neutron_multigroup_data["G"] + 1
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def energy_grid_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["energy_grid_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def speed(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["speed_offset"]
+ return data[offset + index]
+
+
+@njit
+def speed_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["speed_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def speed_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["speed_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def speed_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["speed_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def decay_rate(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["decay_rate_offset"]
+ return data[offset + index]
+
+
+@njit
+def decay_rate_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["decay_rate_offset"]
+ size = neutron_multigroup_data["J"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def decay_rate_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["decay_rate_offset"]
+ size = neutron_multigroup_data["J"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def decay_rate_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["decay_rate_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def capture(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["capture_offset"]
+ return data[offset + index]
+
+
+@njit
+def capture_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["capture_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def capture_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["capture_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def capture_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["capture_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def scatter(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["scatter_offset"]
+ return data[offset + index]
+
+
+@njit
+def scatter_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["scatter_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def scatter_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["scatter_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def scatter_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["scatter_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def fission(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["fission_offset"]
+ return data[offset + index]
+
+
+@njit
+def fission_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["fission_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def fission_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["fission_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def fission_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["fission_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def total(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["total_offset"]
+ return data[offset + index]
+
+
+@njit
+def total_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["total_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def total_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["total_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def total_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["total_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def nu_s(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["nu_s_offset"]
+ return data[offset + index]
+
+
+@njit
+def nu_s_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["nu_s_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def nu_s_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["nu_s_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def nu_s_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["nu_s_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def nu_p(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["nu_p_offset"]
+ return data[offset + index]
+
+
+@njit
+def nu_p_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["nu_p_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def nu_p_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["nu_p_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def nu_p_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["nu_p_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def nu_d_vector(index_1, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["nu_d_offset"]
+ stride = neutron_multigroup_data["J"]
+ start = offset + index_1 * stride
+ end = start + stride
+ return data[start:end]
+
+
+@njit
+def nu_d(index_1, index_2, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["nu_d_offset"]
+ stride = neutron_multigroup_data["J"]
+ return data[offset + index_1 * stride + index_2]
+
+
+@njit
+def nu_d_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["nu_d_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def nu_d_total(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["nu_d_total_offset"]
+ return data[offset + index]
+
+
+@njit
+def nu_d_total_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["nu_d_total_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def nu_d_total_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["nu_d_total_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def nu_d_total_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["nu_d_total_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def nu_f(index, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["nu_f_offset"]
+ return data[offset + index]
+
+
+@njit
+def nu_f_all(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["nu_f_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[start:end]
+
+
+@njit
+def nu_f_last(neutron_multigroup_data, data):
+ start = neutron_multigroup_data["nu_f_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ return data[end - 1]
+
+
+@njit
+def nu_f_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["nu_f_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def chi_s_vector(index_1, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["chi_s_offset"]
+ stride = neutron_multigroup_data["G"]
+ start = offset + index_1 * stride
+ end = start + stride
+ return data[start:end]
+
+
+@njit
+def chi_s(index_1, index_2, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["chi_s_offset"]
+ stride = neutron_multigroup_data["G"]
+ return data[offset + index_1 * stride + index_2]
+
+
+@njit
+def chi_s_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["chi_s_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def chi_p_vector(index_1, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["chi_p_offset"]
+ stride = neutron_multigroup_data["G"]
+ start = offset + index_1 * stride
+ end = start + stride
+ return data[start:end]
+
+
+@njit
+def chi_p(index_1, index_2, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["chi_p_offset"]
+ stride = neutron_multigroup_data["G"]
+ return data[offset + index_1 * stride + index_2]
+
+
+@njit
+def chi_p_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["chi_p_offset"]
+ end = start + length
+ return data[start:end]
+
+
+@njit
+def chi_d_vector(index_1, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["chi_d_offset"]
+ stride = neutron_multigroup_data["G"]
+ start = offset + index_1 * stride
+ end = start + stride
+ return data[start:end]
+
+
+@njit
+def chi_d(index_1, index_2, neutron_multigroup_data, data):
+ offset = neutron_multigroup_data["chi_d_offset"]
+ stride = neutron_multigroup_data["G"]
+ return data[offset + index_1 * stride + index_2]
+
+
+@njit
+def chi_d_chunk(start, length, neutron_multigroup_data, data):
+ start += neutron_multigroup_data["chi_d_offset"]
+ end = start + length
+ return data[start:end]
diff --git a/mcdc/mcdc_get/nuclide.py b/mcdc/mcdc_get/nuclide.py
index af9593f10..63496f6da 100644
--- a/mcdc/mcdc_get/nuclide.py
+++ b/mcdc/mcdc_get/nuclide.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -180,7 +181,7 @@ def neutron_fission_xs_chunk(start, length, nuclide, data):
@njit
def neutron_elastic_scattering_reaction_IDs(index, nuclide, data):
offset = nuclide["neutron_elastic_scattering_reaction_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -196,7 +197,7 @@ def neutron_elastic_scattering_reaction_IDs_last(nuclide, data):
start = nuclide["neutron_elastic_scattering_reaction_IDs_offset"]
size = nuclide["N_neutron_elastic_scattering_reaction"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -209,7 +210,7 @@ def neutron_elastic_scattering_reaction_IDs_chunk(start, length, nuclide, data):
@njit
def neutron_capture_reaction_IDs(index, nuclide, data):
offset = nuclide["neutron_capture_reaction_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -225,7 +226,7 @@ def neutron_capture_reaction_IDs_last(nuclide, data):
start = nuclide["neutron_capture_reaction_IDs_offset"]
size = nuclide["N_neutron_capture_reaction"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -238,7 +239,7 @@ def neutron_capture_reaction_IDs_chunk(start, length, nuclide, data):
@njit
def neutron_inelastic_scattering_reaction_IDs(index, nuclide, data):
offset = nuclide["neutron_inelastic_scattering_reaction_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -254,7 +255,7 @@ def neutron_inelastic_scattering_reaction_IDs_last(nuclide, data):
start = nuclide["neutron_inelastic_scattering_reaction_IDs_offset"]
size = nuclide["N_neutron_inelastic_scattering_reaction"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -267,7 +268,7 @@ def neutron_inelastic_scattering_reaction_IDs_chunk(start, length, nuclide, data
@njit
def neutron_fission_reaction_IDs(index, nuclide, data):
offset = nuclide["neutron_fission_reaction_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -283,7 +284,7 @@ def neutron_fission_reaction_IDs_last(nuclide, data):
start = nuclide["neutron_fission_reaction_IDs_offset"]
size = nuclide["N_neutron_fission_reaction"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -354,7 +355,7 @@ def neutron_fission_delayed_decay_rates_chunk(start, length, nuclide, data):
@njit
def neutron_fission_delayed_spectrum_IDs(index, nuclide, data):
offset = nuclide["neutron_fission_delayed_spectrum_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -370,7 +371,7 @@ def neutron_fission_delayed_spectrum_IDs_last(nuclide, data):
start = nuclide["neutron_fission_delayed_spectrum_IDs_offset"]
size = nuclide["N_neutron_fission_delayed_spectrum"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/surface.py b/mcdc/mcdc_get/surface.py
index 4a0230d76..4e6c1c65e 100644
--- a/mcdc/mcdc_get/surface.py
+++ b/mcdc/mcdc_get/surface.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -108,29 +109,29 @@ def move_translations_chunk(start, length, surface, data):
@njit
-def tally_IDs(index, surface, data):
- offset = surface["tally_IDs_offset"]
- return data[offset + index]
+def surface_crossing_tally_IDs(index, surface, data):
+ offset = surface["surface_crossing_tally_IDs_offset"]
+ return int64(data[offset + index])
@njit
-def tally_IDs_all(surface, data):
- start = surface["tally_IDs_offset"]
- size = surface["N_tally"]
+def surface_crossing_tally_IDs_all(surface, data):
+ start = surface["surface_crossing_tally_IDs_offset"]
+ size = surface["N_surface_crossing_tally"]
end = start + size
return data[start:end]
@njit
-def tally_IDs_last(surface, data):
- start = surface["tally_IDs_offset"]
- size = surface["N_tally"]
+def surface_crossing_tally_IDs_last(surface, data):
+ start = surface["surface_crossing_tally_IDs_offset"]
+ size = surface["N_surface_crossing_tally"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
-def tally_IDs_chunk(start, length, surface, data):
- start += surface["tally_IDs_offset"]
+def surface_crossing_tally_IDs_chunk(start, length, surface, data):
+ start += surface["surface_crossing_tally_IDs_offset"]
end = start + length
return data[start:end]
diff --git a/mcdc/mcdc_get/table_data.py b/mcdc/mcdc_get/table_data.py
index db53747eb..8b3d1b54a 100644
--- a/mcdc/mcdc_get/table_data.py
+++ b/mcdc/mcdc_get/table_data.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -64,7 +65,7 @@ def y_chunk(start, length, table_data, data):
@njit
def interpolations(index, table_data, data):
offset = table_data["interpolations_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -80,7 +81,7 @@ def interpolations_last(table_data, data):
start = table_data["interpolations_offset"]
size = table_data["interpolations_length"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -93,7 +94,7 @@ def interpolations_chunk(start, length, table_data, data):
@njit
def interpolation_boundaries(index, table_data, data):
offset = table_data["interpolation_boundaries_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -109,7 +110,7 @@ def interpolation_boundaries_last(table_data, data):
start = table_data["interpolation_boundaries_offset"]
size = table_data["interpolation_boundaries_length"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/tabulated_energy_angle_distribution.py b/mcdc/mcdc_get/tabulated_energy_angle_distribution.py
index aeb1fe914..9aff254b5 100644
--- a/mcdc/mcdc_get/tabulated_energy_angle_distribution.py
+++ b/mcdc/mcdc_get/tabulated_energy_angle_distribution.py
@@ -1,5 +1,6 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@@ -35,7 +36,7 @@ def energy_chunk(start, length, tabulated_energy_angle_distribution, data):
@njit
def offset(index, tabulated_energy_angle_distribution, data):
offset = tabulated_energy_angle_distribution["offset_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -51,7 +52,7 @@ def offset_last(tabulated_energy_angle_distribution, data):
start = tabulated_energy_angle_distribution["offset_offset"]
size = tabulated_energy_angle_distribution["offset_length"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -151,7 +152,7 @@ def cdf_chunk(start, length, tabulated_energy_angle_distribution, data):
@njit
def cosine_offset_(index, tabulated_energy_angle_distribution, data):
offset = tabulated_energy_angle_distribution["cosine_offset__offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -167,7 +168,7 @@ def cosine_offset__last(tabulated_energy_angle_distribution, data):
start = tabulated_energy_angle_distribution["cosine_offset__offset"]
size = tabulated_energy_angle_distribution["cosine_offset__length"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/tally.py b/mcdc/mcdc_get/tally.py
index dbf02745f..5556883c1 100644
--- a/mcdc/mcdc_get/tally.py
+++ b/mcdc/mcdc_get/tally.py
@@ -1,12 +1,13 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@njit
def scores(index, tally, data):
offset = tally["scores_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -22,7 +23,7 @@ def scores_last(tally, data):
start = tally["scores_offset"]
size = tally["scores_length"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
@@ -238,7 +239,7 @@ def bin_sum_square_chunk(start, length, tally, data):
@njit
def bin_shape(index, tally, data):
offset = tally["bin_shape_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -254,7 +255,7 @@ def bin_shape_last(tally, data):
start = tally["bin_shape_offset"]
size = tally["bin_shape_length"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_get/technique.py b/mcdc/mcdc_get/technique.py
new file mode 100644
index 000000000..fdbf8e750
--- /dev/null
+++ b/mcdc/mcdc_get/technique.py
@@ -0,0 +1,3 @@
+# The following is automatically generated by code_factory.py
+
+from numba import njit
diff --git a/mcdc/mcdc_get/universe.py b/mcdc/mcdc_get/universe.py
index 5bfcf612d..f7822ed22 100644
--- a/mcdc/mcdc_get/universe.py
+++ b/mcdc/mcdc_get/universe.py
@@ -1,12 +1,13 @@
# The following is automatically generated by code_factory.py
+from numpy import int64
from numba import njit
@njit
def cell_IDs(index, universe, data):
offset = universe["cell_IDs_offset"]
- return data[offset + index]
+ return int64(data[offset + index])
@njit
@@ -22,7 +23,7 @@ def cell_IDs_last(universe, data):
start = universe["cell_IDs_offset"]
size = universe["N_cell"]
end = start + size
- return data[end - 1]
+ return int64(data[end - 1])
@njit
diff --git a/mcdc/mcdc_set/__init__.py b/mcdc/mcdc_set/__init__.py
index 8f771c058..26eedd8ba 100644
--- a/mcdc/mcdc_set/__init__.py
+++ b/mcdc/mcdc_set/__init__.py
@@ -60,9 +60,7 @@
import mcdc.mcdc_set.gpu_meta as gpu_meta
-import mcdc.mcdc_set.native_material as native_material
-
-import mcdc.mcdc_set.multigroup_material as multigroup_material
+import mcdc.mcdc_set.neutron_multigroup_data as neutron_multigroup_data
import mcdc.mcdc_set.nuclide as nuclide
@@ -88,24 +86,28 @@
import mcdc.mcdc_set.settings as settings
+import mcdc.mcdc_set.technique as technique
+
+import mcdc.mcdc_set.source as source
+
+import mcdc.mcdc_set.surface as surface
+
+import mcdc.mcdc_set.surface_crossing_tally as surface_crossing_tally
+
+import mcdc.mcdc_set.tally as tally
+
import mcdc.mcdc_set.global_weight_roulette as global_weight_roulette
import mcdc.mcdc_set.implicit_capture as implicit_capture
+import mcdc.mcdc_set.neutron_multigroup as neutron_multigroup
+
import mcdc.mcdc_set.population_control as population_control
import mcdc.mcdc_set.weight_windows as weight_windows
import mcdc.mcdc_set.weighted_emission as weighted_emission
-import mcdc.mcdc_set.source as source
-
-import mcdc.mcdc_set.surface as surface
-
-import mcdc.mcdc_set.surface_crossing_tally as surface_crossing_tally
-
-import mcdc.mcdc_set.tally as tally
-
import mcdc.mcdc_set.bank_active as bank_active
import mcdc.mcdc_set.bank_census as bank_census
diff --git a/mcdc/mcdc_set/material.py b/mcdc/mcdc_set/material.py
index fdbf8e750..f006985cb 100644
--- a/mcdc/mcdc_set/material.py
+++ b/mcdc/mcdc_set/material.py
@@ -1,3 +1,119 @@
# The following is automatically generated by code_factory.py
from numba import njit
+
+
+@njit
+def nuclide_IDs(index, material, data, value):
+ offset = material["nuclide_IDs_offset"]
+ data[offset + index] = value
+
+
+@njit
+def nuclide_IDs_all(material, data, value):
+ start = material["nuclide_IDs_offset"]
+ size = material["N_nuclide"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def nuclide_IDs_last(material, data, value):
+ start = material["nuclide_IDs_offset"]
+ size = material["N_nuclide"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def nuclide_IDs_chunk(start, length, material, data, value):
+ start += material["nuclide_IDs_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def element_IDs(index, material, data, value):
+ offset = material["element_IDs_offset"]
+ data[offset + index] = value
+
+
+@njit
+def element_IDs_all(material, data, value):
+ start = material["element_IDs_offset"]
+ size = material["N_element"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def element_IDs_last(material, data, value):
+ start = material["element_IDs_offset"]
+ size = material["N_element"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def element_IDs_chunk(start, length, material, data, value):
+ start += material["element_IDs_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def nuclide_densities(index, material, data, value):
+ offset = material["nuclide_densities_offset"]
+ data[offset + index] = value
+
+
+@njit
+def nuclide_densities_all(material, data, value):
+ start = material["nuclide_densities_offset"]
+ size = material["nuclide_densities_length"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def nuclide_densities_last(material, data, value):
+ start = material["nuclide_densities_offset"]
+ size = material["nuclide_densities_length"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def nuclide_densities_chunk(start, length, material, data, value):
+ start += material["nuclide_densities_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def element_densities(index, material, data, value):
+ offset = material["element_densities_offset"]
+ data[offset + index] = value
+
+
+@njit
+def element_densities_all(material, data, value):
+ start = material["element_densities_offset"]
+ size = material["element_densities_length"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def element_densities_last(material, data, value):
+ start = material["element_densities_offset"]
+ size = material["element_densities_length"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def element_densities_chunk(start, length, material, data, value):
+ start += material["element_densities_offset"]
+ end = start + length
+ data[start:end] = value
diff --git a/mcdc/mcdc_set/multigroup_material.py b/mcdc/mcdc_set/multigroup_material.py
deleted file mode 100644
index c345a8662..000000000
--- a/mcdc/mcdc_set/multigroup_material.py
+++ /dev/null
@@ -1,385 +0,0 @@
-# The following is automatically generated by code_factory.py
-
-from numba import njit
-
-
-@njit
-def mgxs_speed(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_speed_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_speed_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_speed_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_speed_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_speed_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_speed_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_speed_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_decay_rate(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_decay_rate_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_decay_rate_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_decay_rate_offset"]
- size = multigroup_material["J"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_decay_rate_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_decay_rate_offset"]
- size = multigroup_material["J"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_decay_rate_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_decay_rate_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_capture(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_capture_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_capture_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_capture_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_capture_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_capture_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_capture_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_capture_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_scatter(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_scatter_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_scatter_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_scatter_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_scatter_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_scatter_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_scatter_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_scatter_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_fission(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_fission_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_fission_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_fission_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_fission_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_fission_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_fission_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_fission_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_total(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_total_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_total_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_total_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_total_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_total_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_total_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_total_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_s(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_nu_s_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_nu_s_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_nu_s_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_s_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_nu_s_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_nu_s_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_nu_s_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_p(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_nu_p_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_nu_p_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_nu_p_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_p_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_nu_p_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_nu_p_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_nu_p_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_d_vector(index_1, multigroup_material, data, value):
- offset = multigroup_material["mgxs_nu_d_offset"]
- stride = multigroup_material["J"]
- start = offset + index_1 * stride
- end = start + stride
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_d(index_1, index_2, multigroup_material, data, value):
- offset = multigroup_material["mgxs_nu_d_offset"]
- stride = multigroup_material["J"]
- data[offset + index_1 * stride + index_2] = value
-
-
-@njit
-def mgxs_nu_d_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_nu_d_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_d_total(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_nu_d_total_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_nu_d_total_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_nu_d_total_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_d_total_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_nu_d_total_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_nu_d_total_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_nu_d_total_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_f(index, multigroup_material, data, value):
- offset = multigroup_material["mgxs_nu_f_offset"]
- data[offset + index] = value
-
-
-@njit
-def mgxs_nu_f_all(multigroup_material, data, value):
- start = multigroup_material["mgxs_nu_f_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def mgxs_nu_f_last(multigroup_material, data, value):
- start = multigroup_material["mgxs_nu_f_offset"]
- size = multigroup_material["G"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def mgxs_nu_f_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_nu_f_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_chi_s_vector(index_1, multigroup_material, data, value):
- offset = multigroup_material["mgxs_chi_s_offset"]
- stride = multigroup_material["G"]
- start = offset + index_1 * stride
- end = start + stride
- data[start:end] = value
-
-
-@njit
-def mgxs_chi_s(index_1, index_2, multigroup_material, data, value):
- offset = multigroup_material["mgxs_chi_s_offset"]
- stride = multigroup_material["G"]
- data[offset + index_1 * stride + index_2] = value
-
-
-@njit
-def mgxs_chi_s_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_chi_s_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_chi_p_vector(index_1, multigroup_material, data, value):
- offset = multigroup_material["mgxs_chi_p_offset"]
- stride = multigroup_material["G"]
- start = offset + index_1 * stride
- end = start + stride
- data[start:end] = value
-
-
-@njit
-def mgxs_chi_p(index_1, index_2, multigroup_material, data, value):
- offset = multigroup_material["mgxs_chi_p_offset"]
- stride = multigroup_material["G"]
- data[offset + index_1 * stride + index_2] = value
-
-
-@njit
-def mgxs_chi_p_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_chi_p_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def mgxs_chi_d_vector(index_1, multigroup_material, data, value):
- offset = multigroup_material["mgxs_chi_d_offset"]
- stride = multigroup_material["G"]
- start = offset + index_1 * stride
- end = start + stride
- data[start:end] = value
-
-
-@njit
-def mgxs_chi_d(index_1, index_2, multigroup_material, data, value):
- offset = multigroup_material["mgxs_chi_d_offset"]
- stride = multigroup_material["G"]
- data[offset + index_1 * stride + index_2] = value
-
-
-@njit
-def mgxs_chi_d_chunk(start, length, multigroup_material, data, value):
- start += multigroup_material["mgxs_chi_d_offset"]
- end = start + length
- data[start:end] = value
diff --git a/mcdc/mcdc_set/native_material.py b/mcdc/mcdc_set/native_material.py
deleted file mode 100644
index 303ffb474..000000000
--- a/mcdc/mcdc_set/native_material.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# The following is automatically generated by code_factory.py
-
-from numba import njit
-
-
-@njit
-def nuclide_IDs(index, native_material, data, value):
- offset = native_material["nuclide_IDs_offset"]
- data[offset + index] = value
-
-
-@njit
-def nuclide_IDs_all(native_material, data, value):
- start = native_material["nuclide_IDs_offset"]
- size = native_material["N_nuclide"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def nuclide_IDs_last(native_material, data, value):
- start = native_material["nuclide_IDs_offset"]
- size = native_material["N_nuclide"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def nuclide_IDs_chunk(start, length, native_material, data, value):
- start += native_material["nuclide_IDs_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def element_IDs(index, native_material, data, value):
- offset = native_material["element_IDs_offset"]
- data[offset + index] = value
-
-
-@njit
-def element_IDs_all(native_material, data, value):
- start = native_material["element_IDs_offset"]
- size = native_material["N_element"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def element_IDs_last(native_material, data, value):
- start = native_material["element_IDs_offset"]
- size = native_material["N_element"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def element_IDs_chunk(start, length, native_material, data, value):
- start += native_material["element_IDs_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def nuclide_densities(index, native_material, data, value):
- offset = native_material["nuclide_densities_offset"]
- data[offset + index] = value
-
-
-@njit
-def nuclide_densities_all(native_material, data, value):
- start = native_material["nuclide_densities_offset"]
- size = native_material["nuclide_densities_length"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def nuclide_densities_last(native_material, data, value):
- start = native_material["nuclide_densities_offset"]
- size = native_material["nuclide_densities_length"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def nuclide_densities_chunk(start, length, native_material, data, value):
- start += native_material["nuclide_densities_offset"]
- end = start + length
- data[start:end] = value
-
-
-@njit
-def element_densities(index, native_material, data, value):
- offset = native_material["element_densities_offset"]
- data[offset + index] = value
-
-
-@njit
-def element_densities_all(native_material, data, value):
- start = native_material["element_densities_offset"]
- size = native_material["element_densities_length"]
- end = start + size
- data[start:end] = value
-
-
-@njit
-def element_densities_last(native_material, data, value):
- start = native_material["element_densities_offset"]
- size = native_material["element_densities_length"]
- end = start + size
- data[end - 1] = value
-
-
-@njit
-def element_densities_chunk(start, length, native_material, data, value):
- start += native_material["element_densities_offset"]
- end = start + length
- data[start:end] = value
diff --git a/mcdc/mcdc_set/neutron_multigroup.py b/mcdc/mcdc_set/neutron_multigroup.py
new file mode 100644
index 000000000..fdbf8e750
--- /dev/null
+++ b/mcdc/mcdc_set/neutron_multigroup.py
@@ -0,0 +1,3 @@
+# The following is automatically generated by code_factory.py
+
+from numba import njit
diff --git a/mcdc/mcdc_set/neutron_multigroup_data.py b/mcdc/mcdc_set/neutron_multigroup_data.py
new file mode 100644
index 000000000..dff8f10b1
--- /dev/null
+++ b/mcdc/mcdc_set/neutron_multigroup_data.py
@@ -0,0 +1,414 @@
+# The following is automatically generated by code_factory.py
+
+from numba import njit
+
+
+@njit
+def energy_grid(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["energy_grid_offset"]
+ data[offset + index] = value
+
+
+@njit
+def energy_grid_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["energy_grid_offset"]
+ size = neutron_multigroup_data["G"] + 1
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def energy_grid_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["energy_grid_offset"]
+ size = neutron_multigroup_data["G"] + 1
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def energy_grid_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["energy_grid_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def speed(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["speed_offset"]
+ data[offset + index] = value
+
+
+@njit
+def speed_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["speed_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def speed_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["speed_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def speed_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["speed_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def decay_rate(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["decay_rate_offset"]
+ data[offset + index] = value
+
+
+@njit
+def decay_rate_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["decay_rate_offset"]
+ size = neutron_multigroup_data["J"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def decay_rate_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["decay_rate_offset"]
+ size = neutron_multigroup_data["J"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def decay_rate_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["decay_rate_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def capture(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["capture_offset"]
+ data[offset + index] = value
+
+
+@njit
+def capture_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["capture_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def capture_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["capture_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def capture_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["capture_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def scatter(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["scatter_offset"]
+ data[offset + index] = value
+
+
+@njit
+def scatter_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["scatter_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def scatter_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["scatter_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def scatter_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["scatter_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def fission(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["fission_offset"]
+ data[offset + index] = value
+
+
+@njit
+def fission_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["fission_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def fission_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["fission_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def fission_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["fission_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def total(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["total_offset"]
+ data[offset + index] = value
+
+
+@njit
+def total_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["total_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def total_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["total_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def total_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["total_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def nu_s(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["nu_s_offset"]
+ data[offset + index] = value
+
+
+@njit
+def nu_s_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["nu_s_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def nu_s_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["nu_s_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def nu_s_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["nu_s_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def nu_p(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["nu_p_offset"]
+ data[offset + index] = value
+
+
+@njit
+def nu_p_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["nu_p_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def nu_p_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["nu_p_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def nu_p_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["nu_p_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def nu_d_vector(index_1, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["nu_d_offset"]
+ stride = neutron_multigroup_data["J"]
+ start = offset + index_1 * stride
+ end = start + stride
+ data[start:end] = value
+
+
+@njit
+def nu_d(index_1, index_2, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["nu_d_offset"]
+ stride = neutron_multigroup_data["J"]
+ data[offset + index_1 * stride + index_2] = value
+
+
+@njit
+def nu_d_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["nu_d_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def nu_d_total(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["nu_d_total_offset"]
+ data[offset + index] = value
+
+
+@njit
+def nu_d_total_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["nu_d_total_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def nu_d_total_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["nu_d_total_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def nu_d_total_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["nu_d_total_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def nu_f(index, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["nu_f_offset"]
+ data[offset + index] = value
+
+
+@njit
+def nu_f_all(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["nu_f_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[start:end] = value
+
+
+@njit
+def nu_f_last(neutron_multigroup_data, data, value):
+ start = neutron_multigroup_data["nu_f_offset"]
+ size = neutron_multigroup_data["G"]
+ end = start + size
+ data[end - 1] = value
+
+
+@njit
+def nu_f_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["nu_f_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def chi_s_vector(index_1, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["chi_s_offset"]
+ stride = neutron_multigroup_data["G"]
+ start = offset + index_1 * stride
+ end = start + stride
+ data[start:end] = value
+
+
+@njit
+def chi_s(index_1, index_2, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["chi_s_offset"]
+ stride = neutron_multigroup_data["G"]
+ data[offset + index_1 * stride + index_2] = value
+
+
+@njit
+def chi_s_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["chi_s_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def chi_p_vector(index_1, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["chi_p_offset"]
+ stride = neutron_multigroup_data["G"]
+ start = offset + index_1 * stride
+ end = start + stride
+ data[start:end] = value
+
+
+@njit
+def chi_p(index_1, index_2, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["chi_p_offset"]
+ stride = neutron_multigroup_data["G"]
+ data[offset + index_1 * stride + index_2] = value
+
+
+@njit
+def chi_p_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["chi_p_offset"]
+ end = start + length
+ data[start:end] = value
+
+
+@njit
+def chi_d_vector(index_1, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["chi_d_offset"]
+ stride = neutron_multigroup_data["G"]
+ start = offset + index_1 * stride
+ end = start + stride
+ data[start:end] = value
+
+
+@njit
+def chi_d(index_1, index_2, neutron_multigroup_data, data, value):
+ offset = neutron_multigroup_data["chi_d_offset"]
+ stride = neutron_multigroup_data["G"]
+ data[offset + index_1 * stride + index_2] = value
+
+
+@njit
+def chi_d_chunk(start, length, neutron_multigroup_data, data, value):
+ start += neutron_multigroup_data["chi_d_offset"]
+ end = start + length
+ data[start:end] = value
diff --git a/mcdc/mcdc_set/surface.py b/mcdc/mcdc_set/surface.py
index fa21a5871..9549fffda 100644
--- a/mcdc/mcdc_set/surface.py
+++ b/mcdc/mcdc_set/surface.py
@@ -108,29 +108,29 @@ def move_translations_chunk(start, length, surface, data, value):
@njit
-def tally_IDs(index, surface, data, value):
- offset = surface["tally_IDs_offset"]
+def surface_crossing_tally_IDs(index, surface, data, value):
+ offset = surface["surface_crossing_tally_IDs_offset"]
data[offset + index] = value
@njit
-def tally_IDs_all(surface, data, value):
- start = surface["tally_IDs_offset"]
- size = surface["N_tally"]
+def surface_crossing_tally_IDs_all(surface, data, value):
+ start = surface["surface_crossing_tally_IDs_offset"]
+ size = surface["N_surface_crossing_tally"]
end = start + size
data[start:end] = value
@njit
-def tally_IDs_last(surface, data, value):
- start = surface["tally_IDs_offset"]
- size = surface["N_tally"]
+def surface_crossing_tally_IDs_last(surface, data, value):
+ start = surface["surface_crossing_tally_IDs_offset"]
+ size = surface["N_surface_crossing_tally"]
end = start + size
data[end - 1] = value
@njit
-def tally_IDs_chunk(start, length, surface, data, value):
- start += surface["tally_IDs_offset"]
+def surface_crossing_tally_IDs_chunk(start, length, surface, data, value):
+ start += surface["surface_crossing_tally_IDs_offset"]
end = start + length
data[start:end] = value
diff --git a/mcdc/mcdc_set/technique.py b/mcdc/mcdc_set/technique.py
new file mode 100644
index 000000000..fdbf8e750
--- /dev/null
+++ b/mcdc/mcdc_set/technique.py
@@ -0,0 +1,3 @@
+# The following is automatically generated by code_factory.py
+
+from numba import njit
diff --git a/mcdc/numba_types.py b/mcdc/numba_types.py
index 0948870c6..33d5a3b42 100644
--- a/mcdc/numba_types.py
+++ b/mcdc/numba_types.py
@@ -1,4 +1,4 @@
-# The following is automatically generated by code_factory.py
+# The following is automatically generated by numba_layers_generator.py
from numpy import bool_
from numpy import float64
@@ -8,7 +8,7 @@
###
-from mcdc.code_factory.numba_objects_generator import into_dtype
+from mcdc.code_factory.numba_layers_generator import into_dtype
particle_data = into_dtype([
('x', float64),
@@ -18,7 +18,6 @@
('ux', float64),
('uy', float64),
('uz', float64),
- ('g', int64),
('E', float64),
('w', float64),
('particle_type', int64),
@@ -39,7 +38,6 @@
('ux', float64),
('uy', float64),
('uz', float64),
- ('g', int64),
('E', float64),
('w', float64),
('particle_type', int64),
@@ -48,20 +46,20 @@
cell = into_dtype([
('name', 'U32'),
- ('fill_translated', bool),
- ('fill_rotated', bool),
- ('translation', float64, (3,)),
- ('rotation', float64, (3,)),
('region_RPN_tokens_offset', int64),
('region_RPN_tokens_length', int64),
('N_surface', int64),
('surface_IDs_offset', int64),
+ ('fill_type', int64),
+ ('fill_ID', int64),
+ ('fill_translated', bool),
+ ('fill_rotated', bool),
+ ('translation', float64, (3,)),
+ ('rotation', float64, (3,)),
('N_collision_tally', int64),
('collision_tally_IDs_offset', int64),
('N_tracklength_tally', int64),
('tracklength_tally_IDs_offset', int64),
- ('fill_type', int64),
- ('fill_ID', int64),
('ID', int64),
])
@@ -83,10 +81,19 @@
material = into_dtype([
('name', 'U32'),
+ ('temperature', float64),
('fissionable', bool),
+ ('has_neutron_multigroup', bool),
+ ('neutron_multigroup_ID', int64),
+ ('N_nuclide', int64),
+ ('nuclide_IDs_offset', int64),
+ ('N_element', int64),
+ ('element_IDs_offset', int64),
+ ('nuclide_densities_offset', int64),
+ ('nuclide_densities_length', int64),
+ ('element_densities_offset', int64),
+ ('element_densities_length', int64),
('ID', int64),
- ('child_type', int64),
- ('child_ID', int64),
])
collision_tally = into_dtype([
@@ -99,7 +106,7 @@
('mesh_stride_y', int64),
('mesh_stride_x', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
tracklength_tally = into_dtype([
@@ -112,7 +119,7 @@
('mesh_stride_y', int64),
('mesh_stride_x', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
universe = into_dtype([
@@ -124,20 +131,20 @@
data = into_dtype([
('ID', int64),
- ('child_type', int64),
- ('child_ID', int64),
+ ('sub_type', int64),
+ ('sub_ID', int64),
])
none_data = into_dtype([
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
polynomial_data = into_dtype([
('coefficients_offset', int64),
('coefficients_length', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
table_data = into_dtype([
@@ -154,20 +161,20 @@
('aux_offset', int64),
('aux_length', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
distribution = into_dtype([
('ID', int64),
- ('child_type', int64),
- ('child_ID', int64),
+ ('sub_type', int64),
+ ('sub_ID', int64),
])
evaporation_distribution = into_dtype([
('nuclear_temperature_ID', int64),
('restriction_energy', float64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
kalbach_mann_distribution = into_dtype([
@@ -186,21 +193,21 @@
('angular_slope_offset', int64),
('angular_slope_length', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
level_scattering_distribution = into_dtype([
('C1', float64),
('C2', float64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
maxwellian_distribution = into_dtype([
('nuclear_temperature_ID', int64),
('restriction_energy', float64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
multi_table_distribution = into_dtype([
@@ -209,18 +216,18 @@
('N_table', int64),
('table_IDs_offset', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
nbody_distribution = into_dtype([
('pdf_ID', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
none_distribution = into_dtype([
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
pmf_distribution = into_dtype([
@@ -231,13 +238,13 @@
('cmf_offset', int64),
('cmf_length', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
tabulated_distribution = into_dtype([
('pdf_ID', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
tabulated_energy_angle_distribution = into_dtype([
@@ -260,7 +267,7 @@
('cosine_cdf_offset', int64),
('cosine_cdf_length', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
electron_reaction = into_dtype([
@@ -270,14 +277,14 @@
('xs_offset_', int64),
('reference_frame', int64),
('ID', int64),
- ('child_type', int64),
- ('child_ID', int64),
+ ('sub_type', int64),
+ ('sub_ID', int64),
])
electron_bremsstrahlung_reaction = into_dtype([
('eloss_ID', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
electron_elastic_scattering_reaction = into_dtype([
@@ -285,13 +292,13 @@
('xs_large_ID', int64),
('mu_ID', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
electron_excitation_reaction = into_dtype([
('eloss_ID', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
electron_ionization_reaction = into_dtype([
@@ -301,7 +308,7 @@
('N_subshell_product', int64),
('subshell_product_IDs_offset', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
element = into_dtype([
@@ -333,52 +340,42 @@
('ID', int64),
])
-native_material = into_dtype([
- ('N_nuclide', int64),
- ('nuclide_IDs_offset', int64),
- ('N_element', int64),
- ('element_IDs_offset', int64),
- ('nuclide_densities_offset', int64),
- ('nuclide_densities_length', int64),
- ('element_densities_offset', int64),
- ('element_densities_length', int64),
- ('ID', int64),
- ('parent_ID', int64),
-])
-
-multigroup_material = into_dtype([
+neutron_multigroup_data = into_dtype([
('G', int64),
('J', int64),
- ('mgxs_speed_offset', int64),
- ('mgxs_speed_length', int64),
- ('mgxs_decay_rate_offset', int64),
- ('mgxs_decay_rate_length', int64),
- ('mgxs_capture_offset', int64),
- ('mgxs_capture_length', int64),
- ('mgxs_scatter_offset', int64),
- ('mgxs_scatter_length', int64),
- ('mgxs_fission_offset', int64),
- ('mgxs_fission_length', int64),
- ('mgxs_total_offset', int64),
- ('mgxs_total_length', int64),
- ('mgxs_nu_s_offset', int64),
- ('mgxs_nu_s_length', int64),
- ('mgxs_nu_p_offset', int64),
- ('mgxs_nu_p_length', int64),
- ('mgxs_nu_d_offset', int64),
- ('mgxs_nu_d_length', int64),
- ('mgxs_nu_d_total_offset', int64),
- ('mgxs_nu_d_total_length', int64),
- ('mgxs_nu_f_offset', int64),
- ('mgxs_nu_f_length', int64),
- ('mgxs_chi_s_offset', int64),
- ('mgxs_chi_s_length', int64),
- ('mgxs_chi_p_offset', int64),
- ('mgxs_chi_p_length', int64),
- ('mgxs_chi_d_offset', int64),
- ('mgxs_chi_d_length', int64),
- ('ID', int64),
- ('parent_ID', int64),
+ ('energy_grid_offset', int64),
+ ('energy_grid_length', int64),
+ ('energy_representation', int64),
+ ('speed_offset', int64),
+ ('speed_length', int64),
+ ('decay_rate_offset', int64),
+ ('decay_rate_length', int64),
+ ('capture_offset', int64),
+ ('capture_length', int64),
+ ('scatter_offset', int64),
+ ('scatter_length', int64),
+ ('fission_offset', int64),
+ ('fission_length', int64),
+ ('total_offset', int64),
+ ('total_length', int64),
+ ('nu_s_offset', int64),
+ ('nu_s_length', int64),
+ ('nu_p_offset', int64),
+ ('nu_p_length', int64),
+ ('nu_d_offset', int64),
+ ('nu_d_length', int64),
+ ('nu_d_total_offset', int64),
+ ('nu_d_total_length', int64),
+ ('nu_f_offset', int64),
+ ('nu_f_length', int64),
+ ('chi_s_offset', int64),
+ ('chi_s_length', int64),
+ ('chi_p_offset', int64),
+ ('chi_p_length', int64),
+ ('chi_d_offset', int64),
+ ('chi_d_length', int64),
+ ('fissionable', bool),
+ ('ID', int64),
])
nuclide = into_dtype([
@@ -428,8 +425,8 @@
('Ny', int64),
('Nz', int64),
('ID', int64),
- ('child_type', int64),
- ('child_ID', int64),
+ ('sub_type', int64),
+ ('sub_ID', int64),
])
structured_mesh = into_dtype([
@@ -440,7 +437,7 @@
('z_offset', int64),
('z_length', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
uniform_mesh = into_dtype([
@@ -454,7 +451,7 @@
('dz', float64),
('Nz', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
neutron_reaction = into_dtype([
@@ -465,19 +462,19 @@
('reference_frame', int64),
('q_value', float64),
('ID', int64),
- ('child_type', int64),
- ('child_ID', int64),
+ ('sub_type', int64),
+ ('sub_ID', int64),
])
neutron_capture_reaction = into_dtype([
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
neutron_elastic_scattering_reaction = into_dtype([
('mu_table_ID', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
neutron_fission_reaction = into_dtype([
@@ -485,7 +482,7 @@
('mu_ID', int64),
('spectrum_ID', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
neutron_inelastic_scattering_reaction = into_dtype([
@@ -501,7 +498,7 @@
('N_energy_spectrum', int64),
('energy_spectrum_IDs_offset', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
collision_data = into_dtype([
@@ -541,25 +538,29 @@
('neutron_transport', bool),
('electron_transport', bool),
('proton_transport', bool),
- ('neutron_multigroup_mode', bool),
('neutron_eigenvalue_mode', bool),
('gpu_strategy', int64),
('gpu_async_type', int64),
('gpu_storage', int64),
])
-global_weight_roulette = into_dtype([
- ('active', bool),
- ('weight_threshold', float64),
- ('weight_target', float64),
+neutron_multigroup = into_dtype([
+ ('hybrid', bool),
])
implicit_capture = into_dtype([
('active', bool),
])
-population_control = into_dtype([
+weighted_emission = into_dtype([
+ ('active', bool),
+ ('weight_target', float64),
+])
+
+global_weight_roulette = into_dtype([
('active', bool),
+ ('weight_threshold', float64),
+ ('weight_target', float64),
])
weight_windows = into_dtype([
@@ -579,9 +580,17 @@
('upper_weights_length', int64),
])
-weighted_emission = into_dtype([
+population_control = into_dtype([
('active', bool),
- ('weight_target', float64),
+])
+
+technique = into_dtype([
+ ('neutron_multigroup', neutron_multigroup),
+ ('implicit_capture', implicit_capture),
+ ('weighted_emission', weighted_emission),
+ ('global_weight_roulette', global_weight_roulette),
+ ('weight_windows', weight_windows),
+ ('population_control', population_control),
])
source = into_dtype([
@@ -598,10 +607,10 @@
('polar_cosine', float64, (2,)),
('azimuthal', float64, (2,)),
('mono_energetic', bool),
- ('energy_group', int64),
+ ('discrete_energy', bool),
('energy', float64),
- ('energy_group_pmf_ID', int64),
('energy_pdf_ID', int64),
+ ('energy_pmf_ID', int64),
('discrete_time', bool),
('time', float64),
('time_range', float64, (2,)),
@@ -654,8 +663,8 @@
('move_time_grid_length', int64),
('move_translations_offset', int64),
('move_translations_length', int64),
- ('N_tally', int64),
- ('tally_IDs_offset', int64),
+ ('N_surface_crossing_tally', int64),
+ ('surface_crossing_tally_IDs_offset', int64),
('ID', int64),
])
@@ -665,13 +674,14 @@
('cell_filtered', bool),
('cell_filter_ID', int64),
('ID', int64),
- ('parent_ID', int64),
+ ('base_ID', int64),
])
tally = into_dtype([
('name', 'U32'),
('scores_offset', int64),
('scores_length', int64),
+ ('particle_type', int64),
('filter_direction', bool),
('filter_energy', bool),
('filter_time', bool),
@@ -697,8 +707,8 @@
('stride_energy', int64),
('stride_time', int64),
('ID', int64),
- ('child_type', int64),
- ('child_ID', int64),
+ ('sub_type', int64),
+ ('sub_ID', int64),
])
gpu_meta = into_dtype([
@@ -778,14 +788,16 @@ def set_simulation(N: dict):
('N_tabulated_distribution', int64),
('tabulated_energy_angle_distributions', tabulated_energy_angle_distribution, (N['tabulated_energy_angle_distribution'])),
('N_tabulated_energy_angle_distribution', int64),
- ('materials', material, (N['material'])),
- ('N_material', int64),
- ('native_materials', native_material, (N['native_material'])),
- ('N_native_material', int64),
- ('multigroup_materials', multigroup_material, (N['multigroup_material'])),
- ('N_multigroup_material', int64),
- ('elements', element, (N['element'])),
- ('N_element', int64),
+ ('neutron_reactions', neutron_reaction, (N['neutron_reaction'])),
+ ('N_neutron_reaction', int64),
+ ('neutron_capture_reactions', neutron_capture_reaction, (N['neutron_capture_reaction'])),
+ ('N_neutron_capture_reaction', int64),
+ ('neutron_elastic_scattering_reactions', neutron_elastic_scattering_reaction, (N['neutron_elastic_scattering_reaction'])),
+ ('N_neutron_elastic_scattering_reaction', int64),
+ ('neutron_fission_reactions', neutron_fission_reaction, (N['neutron_fission_reaction'])),
+ ('N_neutron_fission_reaction', int64),
+ ('neutron_inelastic_scattering_reactions', neutron_inelastic_scattering_reaction, (N['neutron_inelastic_scattering_reaction'])),
+ ('N_neutron_inelastic_scattering_reaction', int64),
('electron_reactions', electron_reaction, (N['electron_reaction'])),
('N_electron_reaction', int64),
('electron_bremsstrahlung_reactions', electron_bremsstrahlung_reaction, (N['electron_bremsstrahlung_reaction'])),
@@ -798,26 +810,22 @@ def set_simulation(N: dict):
('N_electron_ionization_reaction', int64),
('nuclides', nuclide, (N['nuclide'])),
('N_nuclide', int64),
- ('neutron_reactions', neutron_reaction, (N['neutron_reaction'])),
- ('N_neutron_reaction', int64),
- ('neutron_capture_reactions', neutron_capture_reaction, (N['neutron_capture_reaction'])),
- ('N_neutron_capture_reaction', int64),
- ('neutron_elastic_scattering_reactions', neutron_elastic_scattering_reaction, (N['neutron_elastic_scattering_reaction'])),
- ('N_neutron_elastic_scattering_reaction', int64),
- ('neutron_fission_reactions', neutron_fission_reaction, (N['neutron_fission_reaction'])),
- ('N_neutron_fission_reaction', int64),
- ('neutron_inelastic_scattering_reactions', neutron_inelastic_scattering_reaction, (N['neutron_inelastic_scattering_reaction'])),
- ('N_neutron_inelastic_scattering_reaction', int64),
+ ('elements', element, (N['element'])),
+ ('N_element', int64),
+ ('materials', material, (N['material'])),
+ ('N_material', int64),
+ ('neutron_multigroup_data', neutron_multigroup_data, (N['neutron_multigroup_data'])),
+ ('N_neutron_multigroup_data', int64),
('sources', source, (N['source'])),
('N_source', int64),
- ('cells', cell, (N['cell'])),
- ('N_cell', int64),
- ('lattices', lattice, (N['lattice'])),
- ('N_lattice', int64),
('surfaces', surface, (N['surface'])),
('N_surface', int64),
+ ('cells', cell, (N['cell'])),
+ ('N_cell', int64),
('universes', universe, (N['universe'])),
('N_universe', int64),
+ ('lattices', lattice, (N['lattice'])),
+ ('N_lattice', int64),
('meshes', mesh, (N['mesh'])),
('N_mesh', int64),
('structured_meshes', structured_mesh, (N['structured_mesh'])),
@@ -833,42 +841,36 @@ def set_simulation(N: dict):
('tallies', tally, (N['tally'])),
('N_tally', int64),
('settings', settings),
- ('implicit_capture', implicit_capture),
- ('weighted_emission', weighted_emission),
- ('global_weight_roulette', global_weight_roulette),
- ('weight_windows', weight_windows),
- ('population_control', population_control),
+ ('technique', technique),
('gpu_meta', gpu_meta),
('bank_future', bank_future),
('bank_source', bank_source),
('bank_census', bank_census),
('bank_active', bank_active),
+ ('name', 'U32'),
('idx_work', int64),
('idx_cycle', int64),
('idx_census', int64),
('idx_batch', int64),
- ('dd_idx', int64),
- ('dd_N_local_source', int64),
- ('dd_local_rank', int64),
('k_eff', float64),
('k_cycle_offset', int64),
('k_cycle_length', int64),
('k_avg', float64),
('k_sdv', float64),
+ ('k_avg_running', float64),
+ ('k_sdv_running', float64),
('n_avg', float64),
('n_sdv', float64),
('n_max', float64),
('C_avg', float64),
('C_sdv', float64),
('C_max', float64),
- ('k_avg_running', float64),
- ('k_sdv_running', float64),
- ('gyration_radius_offset', int64),
- ('gyration_radius_length', int64),
- ('cycle_active', bool),
('eigenvalue_tally_nuSigmaF', float64, (1,)),
('eigenvalue_tally_n', float64, (1,)),
('eigenvalue_tally_C', float64, (1,)),
+ ('gyration_radius_offset', int64),
+ ('gyration_radius_length', int64),
+ ('cycle_active', bool),
('mpi_size', int64),
('mpi_rank', int64),
('mpi_master', bool),
diff --git a/mcdc/object_/base.py b/mcdc/object_/base.py
index 11d87e8ad..4845148bd 100644
--- a/mcdc/object_/base.py
+++ b/mcdc/object_/base.py
@@ -1,383 +1,169 @@
+from mcdc.object_.util import check_type
from mcdc.print_ import print_error
-# ======================================================================================
-# Object base classes
-# ======================================================================================
+class MCDCBase:
+ """Base class for Python-side MC/DC model and runtime objects.
-class ObjectBase:
- def __init__(self, register):
- if register and isinstance(self, ObjectNonSingleton):
- register_object(self)
+ Subclasses declare a :attr:`label` and type-annotated fields. Assignments to
+ annotated fields are checked at runtime so invalid model data is rejected
+ before the simulation is packed for transport. ``compile_ID`` records the
+ simulation compilation in which an object most recently participated,
+ providing shared recompilation and cycle-prevention behavior for both
+ embedded configuration objects and registered model objects.
+ """
- if "non_numba" in dir(self):
- self.non_numba += ["non_numba", "label"]
- else:
- self.non_numba = ["non_numba", "label"]
+ label: str
+ compile_ID: int = 0
+ non_numba = ()
+
+ def __init_subclass__(cls):
+ # Require metadata used by the object and Numba-layer factories
+ if not hasattr(cls, "label"):
+ raise NotImplementedError(
+ f"MC/DC class '{cls.__name__}' must have 'label' class attribute."
+ )
def __setattr__(self, key, value):
+ # Validate annotated fields before updating the object
hints = getattr(self.__class__, "__annotations__", {})
if key in hints and not check_type(value, hints[key], self.__class__, self):
print_error(f"{key} must be {hints[key]!r}, got {value!r}")
super().__setattr__(key, value)
+ def _compile_into_simulation(self, simulation) -> bool:
+ """Compile an embedded object for the current simulation.
+
+ Embedded ``MCDCBase`` objects participate in compilation without being
+ registered in a simulation object collection. Registered
+ :class:`MCDCObject` subclasses extend this lifecycle with an object ID.
+ Subclasses may extend this hook to validate or normalize their state,
+ compile excluded references, and derive fields that require the owning
+ simulation.
+
+ Returns
+ -------
+ bool
+ ``True`` if the object was compiled for the current simulation
+ compilation, or ``False`` if it had already been compiled and was
+ skipped.
+ """
+ # Compile each embedded object once per simulation compilation
+ if self.compile_ID == simulation.compile_ID:
+ return False
+ self.compile_ID = simulation.compile_ID
+
+ # Compile all members represented in the Numba layer
+ self._compile_members_into_simulation(simulation)
+ return True
+
+ def _compile_members_into_simulation(self, simulation) -> None:
+ """Compile object members represented in the Numba layer.
+
+ ``MCDCObject`` members are registered with ``simulation``. Embedded
+ ``MCDCBase`` members and lists are compiled recursively. Members listed
+ in ``non_numba`` are intentionally left to the owning class because
+ they generally require a custom packed representation.
+ """
+ # Compile members represented in the Numba layer
+ excluded = getattr(self, "non_numba", ())
+ for name, value in vars(self).items():
+ if name in excluded:
+ continue
+ self._compile_member_value(value, simulation)
+
+ @staticmethod
+ def _compile_member_value(value, simulation) -> None:
+ # Register direct object members
+ if isinstance(value, MCDCBase):
+ value._compile_into_simulation(simulation)
+
+ # Compile object members stored in lists
+ elif isinstance(value, list):
+ for item in value:
+ MCDCBase._compile_member_value(item, simulation)
+
+ # Scalar, array, and other non-object members require no compilation.
+ else:
+ return
+
+
+class MCDCObject(MCDCBase):
+ """Base class for model objects registered during simulation compilation.
-class ObjectSingleton(ObjectBase):
- def __init__(self):
- super().__init__(register=False)
+ ``ID`` identifies an object in its heterogeneous simulation collection.
+ The inherited ``compile_ID`` prevents duplicate registration when an
+ object is shared by multiple parts of a model.
+ """
+ # MC/DC framework metadata
+ label = "object"
-class ObjectNonSingleton(ObjectBase):
ID: int
- def __init__(self, register=True):
+ def __init__(self) -> None:
+ # Initialize the object as unregistered
self.ID = -1
- super().__init__(register)
- if "non_numba" in dir(self):
- self.non_numba += ["ID"]
- else:
- self.non_numba = ["ID"]
-
-
-class ObjectPolymorphic(ObjectNonSingleton):
- child_ID: int
- type: int
-
- def __init__(self, type_, register=True):
- self.child_ID = -1
- self.type = type_
- super().__init__(register)
-
- self.non_numba += ["child_ID"]
-
-
-# ======================================================================================
-# Helper functions
-# ======================================================================================
-
-
-def register_object(object_):
- from mcdc.object_.simulation import simulation
-
- from mcdc.object_.cell import Region, Cell
- from mcdc.object_.universe import Universe, Lattice
- from mcdc.object_.data import DataBase
- from mcdc.object_.distribution import DistributionBase
- from mcdc.object_.element import Element
- from mcdc.object_.electron_reaction import ElectronReactionBase
- from mcdc.object_.material import MaterialBase
- from mcdc.object_.mesh import MeshBase
- from mcdc.object_.nuclide import Nuclide
- from mcdc.object_.neutron_reaction import NeutronReactionBase
- from mcdc.object_.source import Source
- from mcdc.object_.surface import Surface
- from mcdc.object_.tally import Tally
-
- object_list = []
- if isinstance(object_, Cell):
- object_list = simulation.cells
- elif isinstance(object_, DataBase):
- object_list = simulation.data
- elif isinstance(object_, DistributionBase):
- object_list = simulation.distributions
- elif isinstance(object_, Lattice):
- object_list = simulation.lattices
- elif isinstance(object_, MaterialBase):
- object_list = simulation.materials
- elif isinstance(object_, MeshBase):
- object_list = simulation.meshes
- elif isinstance(object_, Element):
- object_list = simulation.elements
- elif isinstance(object_, ElectronReactionBase):
- object_list = simulation.electron_reactions
- elif isinstance(object_, Nuclide):
- object_list = simulation.nuclides
- elif isinstance(object_, NeutronReactionBase):
- object_list = simulation.neutron_reactions
- elif isinstance(object_, Region):
- object_list = simulation.regions
- elif isinstance(object_, Source):
- object_list = simulation.sources
- elif isinstance(object_, Surface):
- object_list = simulation.surfaces
- elif isinstance(object_, Tally):
- object_list = simulation.tallies
- elif isinstance(object_, Universe):
- object_list = simulation.universes
- else:
- print_error(f"Unidentified object list for object {object_}")
-
- object_.ID = len(object_list)
- if isinstance(object_, ObjectPolymorphic):
- object_.child_ID = sum([x.type == object_.type for x in object_list])
- object_list.append(object_)
-
-
-# ======================================================================================
-# Type checker
-# ======================================================================================
-
-
-import re
-import numpy as np
-from typing import get_origin, get_args, Union, Annotated
-
-
-def _name_from_str(s: str) -> str:
- s = _strip_prefixes(s)
- # strip generic args like "NDArray[float64]" → "NDArray"
- s = s.split("[", 1)[0]
- return s.split(".")[-1].strip()
-
-
-def _mro_name_match(value, want: str) -> bool:
- """Subclass-friendly match without resolving: compare wanted name to any base in MRO."""
- want_name = _name_from_str(want)
- return any(base.__name__ == want_name for base in value.__class__.mro())
-
-
-# ---------- helpers for STRING annotations ----------
-_ANN_RE = re.compile(r"^\s*(?:typing\.)?Annotated\[(.*)\]\s*$")
-
-
-def _split_top_level(s: str, sep: str = ",", brackets: str = "[]()") -> list[str]:
- out, buf, depth = [], [], 0
- opens = set(brackets[::2])
- closes = set(brackets[1::2])
- pairs = dict(zip(brackets[1::2], brackets[::2]))
- for ch in s:
- if ch in opens:
- depth += 1
- elif ch in closes:
- depth -= 1
- if ch == sep and depth == 0:
- out.append("".join(buf).strip())
- buf = []
- else:
- buf.append(ch)
- if buf:
- out.append("".join(buf).strip())
- return out
+ def __repr__(self) -> str:
+ # Build the shared object-registration summary
+ nice_label = self.label.replace("_", " ").title()
+ text = "\n"
+ text += f"{nice_label}\n"
+ if self.compile_ID > 0:
+ text += f" (compile_ID={self.compile_ID}, ID={self.ID})\n"
+ return text
-def _strip_prefixes(s: str) -> str:
- # normalize common module prefixes used in annotations
- return (
- s.replace("typing.", "")
- .replace("numpy.typing.", "")
- .replace("numpy.", "")
- .replace("np.", "")
- )
+ def _compile_into_simulation(self, simulation) -> bool:
+ from mcdc.code_factory.python_objects_compiler import register_object
+ # Register once for the current simulation compilation
+ if not register_object(self, simulation):
+ return False
-def _parse_annotated_str(hint_str: str):
- """
- If hint_str is 'Annotated[ ... ]', return (base_str, meta_list) else None.
- meta_list items remain raw strings (no eval).
- """
- m = _ANN_RE.match(_strip_prefixes(hint_str))
- if not m:
- return None
- inner = m.group(1)
- parts = _split_top_level(inner, sep=",")
- if not parts:
- return None
- base = parts[0].strip()
- meta = [p.strip() for p in parts[1:]]
- return base, meta
-
-
-def _shape_tuple_from_str(s: str):
- """
- Parse '(3,)', '(None, 3)', '(2,3,4)' → tuple[int|None, ...] or None if not a shape.
- """
- s = s.strip()
- if not (s.startswith("(") and s.endswith(")")):
- return None
- body = s[1:-1].strip()
- if not body:
- return ()
- items = _split_top_level(body, sep=",")
- out = []
- for it in items:
- it = it.strip()
- if it == "":
- continue # allow trailing comma
- if it == "None":
- out.append(None)
- else:
- try:
- out.append(int(it))
- except ValueError:
- return None
- return tuple(out)
+ # Compile all members represented in the Numba layer
+ self._compile_members_into_simulation(simulation)
+ return True
-def _is_ndarray_base_str(base_str: str) -> bool:
- base_norm = _strip_prefixes(base_str)
- return base_norm.startswith("NDArray[") or base_norm.startswith("ndarray[")
+class MCDCPolymorphic(MCDCObject):
+ """Base class for model objects with multiple packed representations.
+ In addition to the global :attr:`~MCDCObject.ID`, polymorphic objects carry
+ a subtype code and a subtype-local ``sub_ID``. The transport kernels use
+ these values to dispatch to the correct packed object representation.
+ """
-def _extract_ndarray_dtype_key_from_str(base_str: str) -> str | None:
- base_norm = _strip_prefixes(base_str)
- if "[" not in base_norm or "]" not in base_norm:
- return None
- inside = base_norm[base_norm.find("[") + 1 : base_norm.rfind("]")].strip()
- return _strip_prefixes(inside) # e.g. 'float' or 'float64'
+ # MC/DC framework metadata
+ label = "polymorphic"
+ sub_type: int
+ sub_ID: int
-def _dtype_matches(arr: np.ndarray, dtype_key: str | None) -> bool:
- if dtype_key is None:
- return True
- key = dtype_key.lower()
- if key == "float":
- return np.issubdtype(arr.dtype, np.floating)
- if key == "int":
- return np.issubdtype(arr.dtype, np.integer)
- try:
- return arr.dtype == np.dtype(key) # e.g. 'float64', 'int32'
- except TypeError:
- return True # unknown key → do not fail hard
+ def __init_subclass__(cls):
+ # Apply the common object metadata requirements
+ super().__init_subclass__()
+ # Require the code used for polymorphic Numba dispatch
+ if not hasattr(cls, "sub_type"):
+ raise NotImplementedError(
+ f"MC/DC class '{cls.__name__}' must have 'sub_type' class attribute."
+ )
-def _shape_matches(arr: np.ndarray, shape: tuple[int | None, ...]) -> bool:
- if arr.ndim != len(shape):
- return False
- return all(dim is None or dim == s for s, dim in zip(arr.shape, shape))
+ def __init__(self) -> None:
+ # Initialize common object registration state
+ super().__init__()
+ # Initialize the object as unregistered within its subtype
+ self.sub_ID = -1
-# ---------- main checker ----------
-def check_type(value, hint, cls, obj=None) -> bool:
- """
- Best-effort runtime checker tolerant of *string* annotations (no eval).
- Supports:
- - typing objects: list[T], set[T], dict[K,V], tuple[...,], Union/|, Annotated
- - string 'Annotated[NDArray[float], (shape,)]' (dtype+shape)
- - plain string class names (accept subclasses via MRO)
- - string unions 'A | B'
- """
- # -------- STRING annotations path (no resolution) --------
- if isinstance(hint, str):
- h = hint.strip()
-
- # Handle plain "NDArray[...]" (dtype-only) without Annotated
- if _is_ndarray_base_str(h):
- if not isinstance(value, np.ndarray):
- return False
- dtype_key = _extract_ndarray_dtype_key_from_str(h)
- return _dtype_matches(value, dtype_key)
-
- # String Annotated[...]
- parsed = _parse_annotated_str(h)
- if parsed:
- base_str, meta = parsed
-
- # NDArray with shape metadata
- if _is_ndarray_base_str(base_str) and meta:
- shape = _shape_tuple_from_str(meta[0])
- dtype_key = _extract_ndarray_dtype_key_from_str(base_str)
- if not isinstance(value, np.ndarray):
- return False
- if shape is not None and not _shape_matches(value, shape):
- return False
- return _dtype_matches(value, dtype_key)
-
- # Otherwise treat base as class-like name → accept subclasses via MRO
- return _mro_name_match(value, base_str)
-
- # String union: "A | B"
- if "|" in h:
- parts = _split_top_level(h, sep="|")
- return any(check_type(value, p.strip(), cls) for p in parts)
-
- # Simple string container: "list[str]" (lightweight support)
- if h.startswith("list[") and h.endswith("]"):
- inner = _name_from_str(h[5:-1])
- if not isinstance(value, list):
- return False
- if inner == "str":
- return all(isinstance(x, str) for x in value)
- if inner in ("float", "float32", "float64"):
- return all(isinstance(x, (float, int)) for x in value)
- return True # permissive other inners
-
- # Plain forward-ref name → subclass-friendly check
- return _mro_name_match(value, h)
-
- # -------- Structured typing objects path --------
- origin = get_origin(hint)
-
- # Annotated[T, meta...] (real object)
- if origin is Annotated:
- base, *meta = get_args(hint)
- if isinstance(value, np.ndarray) and meta and isinstance(meta[0], tuple):
- expected_shape = meta[0]
- base_args = get_args(base) # e.g., NDArray[dtype]
- dtype_key = None
- if base_args:
- dtype_arg = base_args[0]
- if dtype_arg is float:
- dtype_key = "float"
- elif hasattr(dtype_arg, "name"): # np.float64
- dtype_key = dtype_arg.name
- expected_shape_list = list(expected_shape)
- for i, item in enumerate(expected_shape):
- if type(item) == str:
- expected_shape_list[i] = getattr(obj, item)
- expected_shape = tuple(expected_shape_list)
- return _shape_matches(value, expected_shape) and _dtype_matches(
- value, dtype_key
- )
- return check_type(value, base, cls)
-
- # NDArray[...] without shape meta
- if origin is np.ndarray:
- return isinstance(value, np.ndarray)
-
- # Builtins / classes
- if origin is None:
- try:
- return isinstance(value, hint)
- except TypeError:
- return True
-
- # list[T]
- if origin is list:
- (t,) = get_args(hint)
- return isinstance(value, list) and all(check_type(x, t, cls) for x in value)
-
- # set[T]
- if origin is set:
- (t,) = get_args(hint)
- return isinstance(value, set) and all(check_type(x, t, cls) for x in value)
-
- # dict[K, V]
- if origin is dict:
- kt, vt = get_args(hint)
- return isinstance(value, dict) and all(
- check_type(k, kt, cls) and check_type(v, vt, cls) for k, v in value.items()
- )
-
- # tuple[T1, T2] or tuple[T, ...]
- if origin is tuple:
- args = get_args(hint)
- if len(args) == 2 and args[1] is Ellipsis:
- return isinstance(value, tuple) and all(
- check_type(x, args[0], cls) for x in value
- )
- return (
- isinstance(value, tuple)
- and len(value) == len(args)
- and all(check_type(x, t, cls) for x, t in zip(value, args))
- )
-
- # Union[...] (incl Optional[T])
- if origin is Union:
- return any(check_type(value, t, cls) for t in get_args(hint))
-
- # Fallback: ABCs (Iterable, Sequence, etc.)
- try:
- return isinstance(value, origin)
- except TypeError:
- return True
+ def __repr__(self) -> str:
+ # Build the shared polymorphic registration summary
+ nice_label = self.label.replace("_", " ").title()
+ text = "\n"
+ text += f"{nice_label}\n"
+ if self.compile_ID > 0:
+ text += f" (compile_ID={self.compile_ID}, ID={self.ID}, sub_ID={self.sub_ID})\n"
+
+ return text
diff --git a/mcdc/object_/cell.py b/mcdc/object_/cell.py
index 54e0947c9..2ae400a07 100644
--- a/mcdc/object_/cell.py
+++ b/mcdc/object_/cell.py
@@ -1,5 +1,5 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Literal, TypeAlias
if TYPE_CHECKING:
from mcdc.object_.surface import Surface
@@ -28,9 +28,8 @@
FILL_UNIVERSE,
PI,
)
-from mcdc.object_.base import ObjectNonSingleton
-from mcdc.object_.material import MaterialBase
-from mcdc.object_.simulation import simulation
+from mcdc.object_.base import MCDCObject
+from mcdc.object_.material import Material
from mcdc.object_.tally import TallyCollision, TallyTracklength
from mcdc.object_.universe import Universe, Lattice
from mcdc.print_ import print_error
@@ -39,62 +38,79 @@
# Region
# ======================================================================================
+RegionType: TypeAlias = Literal[
+ "all",
+ "halfspace",
+ "intersection",
+ "union",
+ "complement",
+]
-# Region-making helper that checks if an identical region is already created
-def make_region(type_, A, B):
- for existing_region in simulation.regions:
- if (
- type_ == existing_region.type
- and A == existing_region.A
- and B == existing_region.B
- ):
- return existing_region
- return Region(type_, A, B)
+class Region:
+ """Boolean combination of oriented surface half-spaces.
-class Region(ObjectNonSingleton):
- type: str
+ Regions are normally built with unary ``+`` and ``-`` on
+ :class:`~mcdc.object_.surface.Surface` objects, followed by ``&``
+ (intersection), ``|`` (union), and ``~`` (complement). During compilation,
+ the expression is converted to reverse Polish notation for evaluation by
+ the geometry kernels.
+
+ The supported region types and components are:
+
+ - ``"all"`` uses ``A=None`` and ``B=None``.
+ - ``"halfspace"`` uses a :class:`~mcdc.object_.surface.Surface` for ``A``
+ and ``-1`` or ``1`` for ``B``.
+ - ``"intersection"`` and ``"union"`` use a :class:`Region` for both
+ ``A`` and ``B``.
+ - ``"complement"`` uses a :class:`Region` for ``A`` and ``B=None``.
+ """
+
+ type: RegionType
A: Surface | Region | NoneType
B: Region | int | NoneType
- def __init__(self, type_, A, B):
- super().__init__()
-
+ def __init__(
+ self,
+ type_: RegionType,
+ A: Surface | Region | NoneType,
+ B: Region | int | NoneType,
+ ) -> None:
self.type = type_
self.A = A
self.B = B
@classmethod
- def make_halfspace(cls, surface, sense):
- region = make_region("halfspace", surface, sense)
+ def make_halfspace(cls, surface: Surface, sense: Literal[-1, 1]) -> Region:
+ """Create the positive or negative half-space of a surface.
+
+ Parameters
+ ----------
+ surface : Surface
+ Bounding surface.
+ sense : int
+ Positive for the positive half-space and negative for the negative
+ half-space.
+
+ Returns
+ -------
+ Region
+ Half-space region used to build a cell expression.
+ """
+ region = Region("halfspace", surface, sense)
return region
- def __and__(self, other):
- return make_region("intersection", self, other)
+ def __and__(self, other: Region) -> Region:
+ return Region("intersection", self, other)
- def __or__(self, other):
- return make_region("union", self, other)
+ def __or__(self, other: Region) -> Region:
+ return Region("union", self, other)
- def __invert__(self):
- return make_region("complement", self, None)
+ def __invert__(self) -> Region:
+ return Region("complement", self, None)
- def __repr__(self):
- text = "Region: "
- if self.type == "halfspace":
- if self.B > 0:
- text += "+s%i" % self.A.ID
- else:
- text += "-s%i" % self.A.ID
- elif self.type == "intersection":
- text += "r%i & r%i" % (self.A.ID, self.B.ID)
- elif self.type == "union":
- text += "r%i | r%i" % (self.A.ID, self.B.ID)
- elif self.type == "complement":
- text += "~r%i" % (self.A.ID)
- elif self.type == "all":
- text += "all"
-
- return text
+ def __repr__(self) -> str:
+ return f"{str.capitalize(self.type)} Region"
# ======================================================================================
@@ -102,73 +118,108 @@ def __repr__(self):
# ======================================================================================
-class Cell(ObjectNonSingleton):
- """
- Define a cell from a region and a fill.
+class Cell(MCDCObject):
+ """Material- or universe-filled regions of the simulation geometry.
Parameters
----------
region : Region, optional
- The spatial region defining the cell boundaries.
- Constructed using ``+surface`` / ``-surface`` half-space operators.
- fill : Material or MaterialMG or Universe or Lattice, optional
- The material or universe that fills the cell.
+ Boolean region expression. If omitted, the cell covers all space.
+ fill : Material, Universe, Lattice, or None, optional
+ Material or nested geometry placed in the cell. ``None`` creates a void
+ cell.
name : str, optional
- User label.
- translation : array_like of float, optional
- Translation vector ``[tx, ty, tz]`` in cm.
- rotation : array_like of float, optional
- Rotation angles ``[rx, ry, rz]`` in degrees.
-
- See Also
+ User-facing name. An automatic name is assigned during compilation when
+ omitted.
+ translation : sequence of 3 float, optional
+ Translation, in cm, applied when entering a universe or lattice fill.
+ rotation : sequence of 3 float, optional
+ Rotation angles about the x, y, and z axes, in degrees, applied when
+ entering a universe or lattice fill.
+
+ Notes
+ -----
+ A cell region is commonly written as ``+left & -right``. Surface signs select
+ half-spaces; intersections, unions, and complements may be combined freely.
+
+ Examples
--------
- mcdc.Surface : Creates surfaces that can be used to define cell regions.
- mcdc.Universe : Groups cells into a universe.
+ Fill a slab between two z planes with a one-group material:
+
+ >>> import numpy as np
+ >>> import mcdc
+ >>> material = mcdc.Material.multigroup(capture=np.array([1.0]))
+ >>> lower = mcdc.Surface.PlaneZ(z=0.0)
+ >>> upper = mcdc.Surface.PlaneZ(z=2.0)
+ >>> cell = mcdc.Cell(region=+lower & -upper, fill=material)
+
+ Create a void cell outside the slab:
+
+ >>> void = mcdc.Cell(name="Upper void", region=+upper)
+
+ Combine regions with a union:
+
+ >>> left_sphere = mcdc.Surface.Sphere(center=[-1.0, 0.0, 0.0], radius=0.5)
+ >>> right_sphere = mcdc.Surface.Sphere(center=[1.0, 0.0, 0.0], radius=0.5)
+ >>> two_spheres = mcdc.Cell(
+ ... region=-left_sphere | -right_sphere,
+ ... fill=material,
+ ... )
+
+ Fill the complement of that union:
+
+ >>> outside_spheres = mcdc.Cell(
+ ... region=~(-left_sphere | -right_sphere),
+ ... fill=material,
+ ... )
+
+ Place a reusable universe with a translation and rotation:
+
+ >>> assembly = mcdc.Universe(name="Assembly", cells=[cell])
+ >>> placed_assembly = mcdc.Cell(
+ ... fill=assembly,
+ ... translation=[5.0, 0.0, 0.0],
+ ... rotation=[0.0, 0.0, 90.0],
+ ... )
"""
- # Annotations for Numba mode
- label: str = "cell"
- non_numba: list[str] = ["region", "fill", "region_RPN"]
- #
+ # MC/DC framework metadata
+ label = "cell"
+ non_numba = ["region", "region_RPN", "fill"]
+
name: str
- region: Region
- fill: MaterialBase | Universe | Lattice | NoneType
+
+ # Region definition
+ region: Region # Non-numba
+ region_RPN_tokens: list[int]
+ region_RPN: Boolean # Non-numba
+ surfaces: list[Surface]
+
+ # Fill definition
+ fill: Material | Universe | Lattice | NoneType # Non-numba
+ fill_type: int
+ fill_ID: int
fill_translated: bool
fill_rotated: bool
translation: Annotated[NDArray[float64], (3,)]
rotation: Annotated[NDArray[float64], (3,)]
- region_RPN_tokens: list[int]
- region_RPN: Boolean
- surfaces: list[Surface]
+
+ # Attached tallies
collision_tallies: list[TallyCollision]
tracklength_tallies: list[TallyTracklength]
- #
- fill_type: int
- fill_ID: int
def __init__(
self,
region: Region | NoneType = None,
- fill: MaterialBase | Universe | Lattice | NoneType = None,
+ fill: Material | Universe | Lattice | NoneType = None,
name: str = "",
translation: Sequence[float] = [0.0, 0.0, 0.0],
rotation: Sequence[float] = [0.0, 0.0, 0.0],
- ):
+ ) -> None:
super().__init__()
- # Set name
- if name != "":
- self.name = name
- else:
- self.name = f"{self.label}_{self.ID}"
-
- # Set region
- if region is None:
- self.region = make_region("all", None, None)
- else:
- self.region = region
-
- # Set fill
+ self.name = name or "(Unnamed cell)"
+ self.region = region or Region("all", None, None)
self.fill = fill
# Local coordinate modifier
@@ -183,28 +234,22 @@ def __init__(
# Convert ritation
self.rotation *= PI / 180.0
- # Set region Reversed Polished Notation (RPN)
- if self.region.type != "all":
- self.region_RPN_tokens = generate_RPN_tokens(self.region)
- self.region_RPN = generate_RPN(self.region_RPN_tokens)
- else:
- self.region_RPN_tokens = []
- self.region_RPN = Boolean(True)
-
- # List surfaces
- self.surfaces = list_surfaces(self.region_RPN_tokens)
-
# Cell tallies
self.collision_tallies = []
self.tracklength_tallies = []
- # ==============================================================================
- # Numba attribute manual set up
- # ==============================================================================
+ def _compile_into_simulation(self, simulation) -> bool:
+ # Already compiled?
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ # Compile fill if needed
+ fill = self.fill
+ if fill:
+ fill._compile_into_simulation(simulation)
# Numba representation of the cell fill
- # (Because polymorphic Ffill object is not supported)
- if isinstance(fill, MaterialBase):
+ if isinstance(fill, Material):
self.fill_type = FILL_MATERIAL
self.fill_ID = fill.ID
elif isinstance(fill, Universe):
@@ -216,36 +261,54 @@ def __init__(
elif fill == None:
self.fill_type = FILL_NONE
self.fill_ID = -1
+
+ # Set region Reversed Polished Notation (RPN)
+ if self.region.type != "all":
+ self.region_RPN_tokens = generate_RPN_tokens(self.region, simulation)
+ self.region_RPN = generate_RPN(self.region_RPN_tokens)
else:
- print_error(f"Unsupported cell fill: {fill}")
+ self.region_RPN_tokens = []
+ self.region_RPN = Boolean(True)
+
+ # List surfaces
+ self.surfaces = list_surfaces(self.region_RPN_tokens, simulation)
+
+ return True
+
+ def __repr__(self) -> str:
+ text = super().__repr__()
- def __repr__(self):
- text = "\n"
- text += f"Cell\n"
- text += f" - ID: {self.ID}\n"
text += f" - Name: {self.name}\n"
- text += f" - {self.region}\n"
- if isinstance(self.fill, MaterialBase):
- text += f" - Fill (material): {self.fill.name}\n"
- elif isinstance(self.fill, Lattice):
- text += f" - Fill (lattice): {self.fill.name}\n"
- elif isinstance(self.fill, Universe):
- text += f" - Fill (universe): {self.fill.name}\n"
+ if self.compile_ID > 0:
+ text += f" - Region RPN: {self.region_RPN}\n"
+ else:
+ text += f" - {self.region}\n"
+ if self.fill:
+ text += f" - Fill [{self.fill.label.title().replace('_', ' ')}]: {self.fill.name}\n"
+ else:
+ text += f" - Fill [None]"
if self.fill_translated:
text += f" - Translation: {self.translation}\n"
if self.fill_rotated:
text += f" - Rotation: {self.rotation * 180 / PI}\n"
- text += f" - Bounding surfaces: {[x.ID for x in self.surfaces]}\n"
+ text += f" - Bounding surfaces: {[x.name for x in self.surfaces]}\n"
if len(self.collision_tallies) > 0:
- text += f" - Collision tallies: {[x.ID for x in self.collision_tallies]}\n"
- if len(self.tracklength_tallies) > 0:
text += (
- f" - Tracklength tallies: {[x.ID for x in self.tracklength_tallies]}\n"
+ f" - Collision tallies: {[x.name for x in self.collision_tallies]}\n"
)
+ if len(self.tracklength_tallies) > 0:
+ text += f" - Tracklength tallies: {[x.name for x in self.tracklength_tallies]}\n"
return text
-def generate_RPN_tokens(region):
+def generate_RPN_tokens(region, simulation):
+ """Compile a region expression into geometry-kernel RPN tokens.
+
+ Surface objects encountered in the expression are registered with
+ ``simulation`` as part of this operation.
+ """
+ from mcdc.object_.surface import Surface
+
# The RPN tokens
rpn_tokens = []
@@ -253,17 +316,44 @@ def generate_RPN_tokens(region):
stack = [region]
while len(stack) > 0:
token = stack.pop()
+
+ # Resolve region token
if isinstance(token, Region):
- if token.type == "halfspace":
- rpn_tokens.append(token.A.ID)
- if token.B < 0:
+ A = token.A
+ B = token.B
+
+ if token.type == "halfspace" and (
+ isinstance(A, Surface) and isinstance(B, int)
+ ):
+ surface = A
+ sense = B
+
+ # Compile and register surface
+ surface._compile_into_simulation(simulation)
+ rpn_tokens.append(surface.ID)
+
+ if sense < 0:
rpn_tokens.append(BOOL_NOT)
- elif token.type == "intersection":
+
+ elif token.type == "intersection" and (
+ isinstance(A, Region) and isinstance(B, Region)
+ ):
stack += ["&", token.A, token.B]
- elif token.type == "union":
+
+ elif token.type == "union" and (
+ isinstance(A, Region) and isinstance(B, Region)
+ ):
stack += ["|", token.A, token.B]
- elif token.type == "complement":
+
+ elif token.type == "complement" and (isinstance(A, Region)):
stack += ["~", token.A]
+
+ else:
+ print_error(
+ f"Invalid RPN tokens for Region of type {token.type}: {A}, {B}"
+ )
+
+ # Register RPN token
else:
if token == "&":
rpn_tokens.append(BOOL_AND)
@@ -272,12 +362,13 @@ def generate_RPN_tokens(region):
elif token == "~":
rpn_tokens.append(BOOL_NOT)
else:
- print_error(f"Unrecognized token in the generating region RPN: {token}")
+ print_error(f"Unrecognized RPN token: {token}")
return rpn_tokens
def generate_RPN(rpn_tokens):
+ """Convert region RPN tokens to a simplified SymPy Boolean expression."""
stack = []
for token in rpn_tokens:
@@ -302,13 +393,13 @@ def generate_RPN(rpn_tokens):
return sympy.logic.boolalg.simplify_logic(stack[0])
-def list_surfaces(rpn_tokens):
+def list_surfaces(rpn_tokens, simulation):
+ """Return the registered surfaces referenced by a token sequence."""
surfaces = []
for token in rpn_tokens:
if token >= 0:
surface = simulation.surfaces[token]
- if surface not in surfaces:
- surfaces.append(surface)
+ surfaces.append(surface)
return sorted(surfaces, key=attrgetter("ID"))
diff --git a/mcdc/object_/data.py b/mcdc/object_/data.py
index 8f513fad7..4babd704c 100644
--- a/mcdc/object_/data.py
+++ b/mcdc/object_/data.py
@@ -1,23 +1,22 @@
-import numpy as np
-
from collections.abc import Sequence
-from numpy import float64, int64
-from numpy.typing import NDArray
+from numbers import Integral
from typing import Annotated
-####
+import numpy as np
+from numpy import float64, int64
+from numpy.typing import NDArray
from mcdc.constant import (
DATA_NONE,
- DATA_TABLE,
DATA_POLYNOMIAL,
+ DATA_TABLE,
INTERPOLATION_HISTOGRAM,
INTERPOLATION_LINEAR,
+ INTERPOLATION_LOG,
INTERPOLATION_SEMILOGX,
INTERPOLATION_SEMILOGY,
- INTERPOLATION_LOG,
)
-from mcdc.object_.base import ObjectPolymorphic
+from mcdc.object_.base import MCDCPolymorphic
from mcdc.print_ import print_1d_array, print_error
# ======================================================================================
@@ -25,44 +24,27 @@
# ======================================================================================
-class DataBase(ObjectPolymorphic):
- # Annotations for Numba mode
- label: str = "data"
+class DataBase(MCDCPolymorphic):
+ """Base class for scalar data evaluated by transport kernels."""
- def __init__(self, type_, register=True):
- super().__init__(type_, register)
-
- def __repr__(self):
- text = "\n"
- text += f"{decode_type(self.type)}\n"
- text += f" - ID: {self.ID}\n"
- return text
-
-
-def decode_type(type_):
- if type_ == DATA_NONE:
- return "Data (None)"
- elif type_ == DATA_TABLE:
- return "Data (Table)"
- elif type_ == DATA_POLYNOMIAL:
- return "Data (Polynomial function)"
+ # MC/DC framework metadata
+ label = "data"
+ sub_type = -1 # Polymorphic base
# ======================================================================================
# None
# ======================================================================================
# Placeholder for data that does not need to store anything:
-# - Fission multiplicity and delayed precursor data for non-fissionable nuclide
+# - Fission multiplicity and delayed precursor data for non-fissionable nuclides
class DataNone(DataBase):
- # Annotations for Numba mode
- label: str = "none_data"
+ """Placeholder used when a reaction has no associated evaluable data."""
- def __init__(self):
- type_ = DATA_NONE
- super().__init__(type_, False)
- self.ID = 0
+ # MC/DC framework metadata
+ label = "none_data"
+ sub_type = DATA_NONE
# ======================================================================================
@@ -71,26 +53,41 @@ def __init__(self):
class DataTable(DataBase):
+ """One-dimensional table with one or more interpolation regions.
+
+ Parameters
+ ----------
+ x, y : ndarray
+ One-dimensional abscissa and ordinate arrays of equal nonzero length.
+ interpolations : int or sequence of int
+ Packed interpolation code for each region. A scalar applies to the full
+ table.
+ interpolation_boundaries : sequence of int, optional
+ Exclusive end index of each interpolation region. Required when
+ ``interpolations`` contains multiple codes; the final value must equal
+ ``len(x)``.
+ aux : ndarray, optional
+ Additional values aligned with ``x``. A one-dimensional array is stored
+ as one auxiliary row; a two-dimensional array must have shape
+ ``(N_aux, len(x))``.
"""
- Tabulated one-dimensional data with ENDF/ACE-style interpolation regions.
- The interpolation laws apply only to `y`. The optional `aux` array stores
- additional data aligned with `x`, such as a CDF associated with a PDF table.
- Auxiliary data are stored for lookup only and are not interpolated.
- """
+ # MC/DC framework metadata
+ label = "table_data"
+ sub_type = DATA_TABLE
- # Annotations for Numba mode
- label: str = "table_data"
- #
+ # Main data
N: int
x: NDArray[float64]
y: NDArray[float64]
- #
+
+ # Interpolation rules
interpolations: NDArray[int64]
interpolation_boundaries: NDArray[int64]
- #
+
+ # Auxiliary data
N_aux: int
- aux: Annotated[NDArray[np.float64], ("N_aux", "N")]
+ aux: Annotated[NDArray[float64], ("N_aux", "N")]
def __init__(
self,
@@ -100,38 +97,18 @@ def __init__(
interpolation_boundaries: Sequence[int] | None = None,
aux: NDArray[float64] | None = None,
) -> None:
- """
- Create a tabulated data object.
-
- Parameters
- ----------
- x : ndarray of float64
- Independent variable values.
- y : ndarray of float64
- Dependent variable values.
- interpolations : int or sequence of int
- Interpolation law or list of interpolation laws.
- interpolation_boundaries : sequence of int, optional
- Region boundaries. Required when `interpolations` is a sequence.
- aux : ndarray of float64, optional
- Auxiliary data aligned with `x`. A one-dimensional array is stored
- internally with shape `(1, N)`. A two-dimensional array must have
- shape `(N_aux, N)`. Auxiliary data are not interpolated.
-
- Notes
- -----
- Boundaries are stored as Python-style exclusive upper indices.
- Therefore the final boundary should be `len(x)`.
- """
-
- # Set type
- type_ = DATA_TABLE
- super().__init__(type_)
+ super().__init__()
# Set primary data
- self.x = x
- self.y = y
- self.N = len(x)
+ self.x = np.asarray(x, dtype=float64)
+ self.y = np.asarray(y, dtype=float64)
+
+ if self.x.ndim != 1:
+ print_error("x must be one-dimensional.")
+ if self.y.ndim != 1:
+ print_error("y must be one-dimensional.")
+
+ self.N = len(self.x)
# Basic size checks
if self.N == 0:
@@ -142,36 +119,51 @@ def __init__(
# Set auxiliary data
if aux is None:
self.N_aux = 0
- self.aux = np.zeros((0, self.N), dtype=float)
- elif aux.ndim == 1:
- self.N_aux = 1
- if len(aux) != self.N:
- print_error("1D aux must have the same length as x.")
- self.aux = np.zeros((1, self.N), dtype=float)
- self.aux[0, :] = aux
- elif aux.ndim == 2:
- self.N_aux = aux.shape[0]
- if aux.shape[1] != self.N:
- print_error("2D aux must have shape (N_aux, len(x)).")
- self.aux = aux
+ self.aux = np.zeros((0, self.N), dtype=float64)
else:
- print_error("aux must be None, 1D, or 2D.")
+ aux_array = np.asarray(aux, dtype=float64)
+
+ if aux_array.ndim == 1:
+ if len(aux_array) != self.N:
+ print_error("One-dimensional aux must have the same length as x.")
- # Set interpolations and boundaries
- if isinstance(interpolations, int):
- self.interpolations = np.array([interpolations], dtype=int)
- self.interpolation_boundaries = np.array([self.N], dtype=int)
+ self.N_aux = 1
+ self.aux = aux_array.reshape(1, self.N)
+
+ elif aux_array.ndim == 2:
+ if aux_array.shape[1] != self.N:
+ print_error("Two-dimensional aux must have shape (N_aux, len(x)).")
+
+ self.N_aux = aux_array.shape[0]
+ self.aux = aux_array
+
+ else:
+ print_error("aux must be None, one-dimensional, or two-dimensional.")
+
+ # Set interpolation rules and boundaries
+ if isinstance(interpolations, Integral):
+ self.interpolations = np.array([interpolations], dtype=int64)
+ self.interpolation_boundaries = np.array([self.N], dtype=int64)
else:
- self.interpolations = np.array(interpolations, dtype=int)
+ self.interpolations = np.asarray(interpolations, dtype=int64)
+
+ if self.interpolations.ndim != 1:
+ print_error("interpolations must be one-dimensional.")
if interpolation_boundaries is None:
- print_error("Missing interpolation boundaries in tabulated data.")
+ print_error(
+ "interpolation_boundaries is required when multiple "
+ "interpolation laws are provided."
+ )
- self.interpolation_boundaries = np.array(
+ self.interpolation_boundaries = np.asarray(
interpolation_boundaries,
- dtype=int,
+ dtype=int64,
)
+ if self.interpolation_boundaries.ndim != 1:
+ print_error("interpolation_boundaries must be one-dimensional.")
+
# Interpolation-region checks
if len(self.interpolations) == 0:
print_error("At least one interpolation law is required.")
@@ -182,7 +174,7 @@ def __init__(
)
if self.interpolation_boundaries[-1] != self.N:
- print_error("Last interpolation boundary must equal len(x).")
+ print_error("The last interpolation boundary must equal len(x).")
previous = 0
for boundary in self.interpolation_boundaries:
@@ -190,68 +182,78 @@ def __init__(
print_error("interpolation_boundaries must be strictly increasing.")
if boundary > self.N:
print_error("interpolation_boundaries cannot exceed len(x).")
+
previous = boundary
+ # Validate interpolation codes
+ for interpolation in self.interpolations:
+ decode_interpolation(interpolation)
+
def __repr__(self) -> str:
- """Return a human-readable summary of the tabulated data."""
text = super().__repr__()
+
text += f" - x {print_1d_array(self.x)}\n"
text += f" - y {print_1d_array(self.y)}\n"
if self.N_aux > 0:
text += f" - aux shape: {self.aux.shape}\n"
- for i in range(len(self.aux)):
- text += f" - aux[{i}]: {print_1d_array(self.aux[i])}\n"
+ for i, values in enumerate(self.aux):
+ text += f" - aux[{i}]: {print_1d_array(values)}\n"
if len(self.interpolations) == 1:
text += (
- f" - Interpolation: "
+ " - Interpolation: "
f"{decode_interpolation(self.interpolations[0])}\n"
)
else:
text += " - Interpolation regions:\n"
start = 0
- for interp, end in zip(
+ for interpolation, end in zip(
self.interpolations,
self.interpolation_boundaries,
):
- text += f" - [{start}, {end}): " f"{decode_interpolation(interp)}\n"
+ text += (
+ f" - [{start}, {end}): "
+ f"{decode_interpolation(interpolation)}\n"
+ )
start = end
return text
-def decode_interpolation(type_) -> str:
- """Convert an interpolation integer code to its string name."""
+def decode_interpolation(type_: int) -> str:
+ """Return the name associated with a packed interpolation code."""
+
if type_ == INTERPOLATION_HISTOGRAM:
return "histogram"
- elif type_ == INTERPOLATION_LINEAR:
+ if type_ == INTERPOLATION_LINEAR:
return "linear"
- elif type_ == INTERPOLATION_SEMILOGX:
+ if type_ == INTERPOLATION_SEMILOGX:
return "semilog-x"
- elif type_ == INTERPOLATION_SEMILOGY:
+ if type_ == INTERPOLATION_SEMILOGY:
return "semilog-y"
- elif type_ == INTERPOLATION_LOG:
+ if type_ == INTERPOLATION_LOG:
return "log"
- else:
- raise ValueError(f"Unknown interpolation type: {type_}")
+ raise ValueError(f"Unknown interpolation type: {type_}")
-def encode_interpolation(type_) -> int:
- """Convert an interpolation string name to its integer code."""
- if type_ == "histogram":
+
+def encode_interpolation(name: str) -> int:
+ """Return the packed code associated with an interpolation name."""
+
+ if name == "histogram":
return INTERPOLATION_HISTOGRAM
- elif type_ == "linear":
+ if name == "linear":
return INTERPOLATION_LINEAR
- elif type_ == "semilog-x":
+ if name == "semilog-x":
return INTERPOLATION_SEMILOGX
- elif type_ == "semilog-y":
+ if name == "semilog-y":
return INTERPOLATION_SEMILOGY
- elif type_ == "log":
+ if name == "log":
return INTERPOLATION_LOG
- else:
- raise ValueError(f"Unknown interpolation name: {type_}")
+
+ raise ValueError(f"Unknown interpolation name: {name}")
# ======================================================================================
@@ -260,18 +262,30 @@ def encode_interpolation(type_) -> int:
class DataPolynomial(DataBase):
- # Annotations for Numba mode
- label: str = "polynomial_data"
- #
+ """Polynomial coefficients evaluated in ascending power order.
+
+ Parameters
+ ----------
+ coefficients : ndarray
+ One-dimensional coefficient array.
+ """
+
+ # MC/DC framework metadata
+ label = "polynomial_data"
+ sub_type = DATA_POLYNOMIAL
+
coefficients: NDArray[float64]
- def __init__(self, coeffs):
- type_ = DATA_POLYNOMIAL
- super().__init__(type_)
+ def __init__(self, coefficients: NDArray[float64]) -> None:
+ super().__init__()
+
+ self.coefficients = np.asarray(coefficients, dtype=float64)
- self.coefficients = coeffs
+ if self.coefficients.ndim != 1:
+ print_error("coefficients must be one-dimensional.")
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
+
text += f" - coefficients {print_1d_array(self.coefficients)}\n"
return text
diff --git a/mcdc/object_/distribution.py b/mcdc/object_/distribution.py
index 8784e1d2d..ca9adbc73 100644
--- a/mcdc/object_/distribution.py
+++ b/mcdc/object_/distribution.py
@@ -2,9 +2,7 @@
from collections.abc import Sequence
from numpy import float64, int64
-from numpy.typing import NDArray
-
-####
+from numpy.typing import ArrayLike, NDArray
from mcdc.constant import (
DISTRIBUTION_NONE,
@@ -20,7 +18,7 @@
INTERPOLATION_HISTOGRAM,
INTERPOLATION_LINEAR,
)
-from mcdc.object_.base import ObjectPolymorphic
+from mcdc.object_.base import MCDCPolymorphic
from mcdc.object_.data import DataTable
from mcdc.object_.util import (
cdf_from_pdf,
@@ -35,85 +33,74 @@
# ======================================================================================
-class DistributionBase(ObjectPolymorphic):
- # Annotations for Numba mode
- label: str = "distribution"
-
- def __init__(self, type_, register=True):
- super().__init__(type_, register)
-
- def __repr__(self):
- text = "\n"
- text += f"{decode_type(self.type)}\n"
- text += f" - ID: {self.ID}\n"
- return text
-
+class DistributionBase(MCDCPolymorphic):
+ """Base class for probability distributions sampled during transport."""
-def decode_type(type_):
- if type_ == DISTRIBUTION_NONE:
- return "Distribution (None)"
- elif type_ == DISTRIBUTION_PMF:
- return "Distribution (PMF)"
- elif type_ == DISTRIBUTION_TABULATED:
- return "Distribution (Tabulated)"
- elif type_ == DISTRIBUTION_MULTITABLE:
- return "Distribution (Multi Table)"
- elif type_ == DISTRIBUTION_LEVEL_SCATTERING:
- return "Distribution (Level scattering)"
- elif type_ == DISTRIBUTION_EVAPORATION:
- return "Distribution (Evaporation)"
- elif type_ == DISTRIBUTION_MAXWELLIAN:
- return "Distribution (Maxwellian spectrum)"
- elif type_ == DISTRIBUTION_KALBACH_MANN:
- return "Distribution (Kalbach-Mann)"
- elif type_ == DISTRIBUTION_TABULATED_ENERGY_ANGLE:
- return "Distribution (Tabulated energy-angle)"
- elif type_ == DISTRIBUTION_N_BODY:
- return "Distribution (N-body)"
+ # MC/DC framework metadata
+ label = "distribution"
+ sub_type = -1 # Polymorphic base
# ======================================================================================
# None
# ======================================================================================
-# Placeholder for distribution that does not need to store data:
-# - Isotropic
-# - Energy-correlated angle (stored in the energy distribution)
+# Placeholder for a distribution that does not need to store data:
+# - Isotropic distributions
+# - Energy-correlated angles stored in the energy distribution
class DistributionNone(DistributionBase):
- # Annotations for Numba mode
- label: str = "none_distribution"
+ """Placeholder for an implicit or externally stored distribution."""
- def __init__(self):
- type_ = DISTRIBUTION_NONE
- super().__init__(type_, False)
- self.ID = 0
+ # MC/DC framework metadata
+ label = "none_distribution"
+ sub_type = DISTRIBUTION_NONE
# ======================================================================================
-# Probability Mass Function (PMF)
+# Probability mass function
# ======================================================================================
class DistributionPMF(DistributionBase):
- # Annotations for Numba mode
- label: str = "pmf_distribution"
- #
+ """Discrete probability mass function.
+
+ Parameters
+ ----------
+ value : array_like
+ Values that may be sampled.
+ pmf : array_like
+ Nonnegative relative masses, normalized internally.
+ """
+
+ # MC/DC framework metadata
+ label = "pmf_distribution"
+ sub_type = DISTRIBUTION_PMF
+
value: NDArray[float64]
pmf: NDArray[float64]
cmf: NDArray[float64]
- def __init__(self, value, pmf):
- type_ = DISTRIBUTION_PMF
- super().__init__(type_)
+ def __init__(self, value: ArrayLike, pmf: ArrayLike) -> None:
+ super().__init__()
+
+ self.value = np.asarray(value, dtype=float64)
+ pmf_array = np.asarray(pmf, dtype=float64)
- self.value = value
- self.pmf = pmf
+ if self.value.ndim != 1:
+ print_error("value must be one-dimensional.")
+ if pmf_array.ndim != 1:
+ print_error("pmf must be one-dimensional.")
+ if len(self.value) == 0:
+ print_error("value and pmf must contain at least one entry.")
+ if len(self.value) != len(pmf_array):
+ print_error("value and pmf must have the same length.")
- self.pmf, self.cmf = cmf_from_pmf(pmf)
+ self.pmf, self.cmf = cmf_from_pmf(pmf_array)
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
+
text += f" - value {print_1d_array(self.value)}\n"
text += f" - pmf {print_1d_array(self.pmf)}\n"
return text
@@ -125,80 +112,77 @@ def __repr__(self):
class DistributionTabulated(DistributionBase):
- """
- One-dimensional tabulated probability distribution.
+ """One-dimensional continuous tabulated distribution.
- The distribution is stored as a DataTable whose independent variable is the
- sample value and whose dependent variable is the normalized PDF. The CDF is
- stored as auxiliary table data and is used for sampling.
-
- If constructed from a PDF, the PDF is assumed to be piecewise linear.
- If constructed from a CDF, the CDF is assumed to be piecewise linear, which
- implies a histogram PDF.
+ Exactly one of ``pdf`` or ``cdf`` must be supplied alongside ``value``.
+ PDF input is treated as piecewise linear; CDF input produces a histogram
+ density. The distribution is normalized internally.
"""
- # Annotations for Numba mode
- label: str = "tabulated_distribution"
- #
+ # MC/DC framework metadata
+ label = "tabulated_distribution"
+ sub_type = DISTRIBUTION_TABULATED
+
pdf: DataTable
def __init__(
self,
- value: NDArray[float64],
- pdf: NDArray[float64] | None = None,
- cdf: NDArray[float64] | None = None,
+ value: ArrayLike,
+ pdf: ArrayLike | None = None,
+ cdf: ArrayLike | None = None,
) -> None:
- """
- Construct a tabulated probability distribution from either a PDF or CDF.
-
- Parameters
- ----------
- value : ndarray of float64
- Sample values.
- pdf : ndarray of float64, optional
- Probability density values at the sample values. If provided, the
- PDF is normalized and the CDF is computed by trapezoidal
- integration.
- cdf : ndarray of float64, optional
- Cumulative distribution values at the sample values. If provided,
- the CDF is normalized and a histogram PDF is derived from it.
-
- Notes
- -----
- Exactly one of `pdf` or `cdf` must be provided. PDF input uses linear
- interpolation. CDF input uses histogram interpolation for the derived
- PDF.
- """
-
- type_ = DISTRIBUTION_TABULATED
- super().__init__(type_)
+ super().__init__()
if (pdf is None) == (cdf is None):
print_error("Exactly one of pdf or cdf must be provided.")
+ value_array = np.asarray(value, dtype=float64)
+
+ if value_array.ndim != 1:
+ print_error("value must be one-dimensional.")
+ if len(value_array) == 0:
+ print_error("value must contain at least one entry.")
+
if pdf is not None:
+ pdf_array = np.asarray(pdf, dtype=float64)
+
+ if pdf_array.ndim != 1:
+ print_error("pdf must be one-dimensional.")
+ if len(pdf_array) != len(value_array):
+ print_error("value and pdf must have the same length.")
+
interpolation = INTERPOLATION_LINEAR
- pdf_normalized, cdf_normalized = cdf_from_pdf(value, pdf)
+ pdf_normalized, cdf_normalized = cdf_from_pdf(
+ value_array,
+ pdf_array,
+ )
else:
+ cdf_array = np.asarray(cdf, dtype=float64)
+
+ if cdf_array.ndim != 1:
+ print_error("cdf must be one-dimensional.")
+ if len(cdf_array) != len(value_array):
+ print_error("value and cdf must have the same length.")
+
interpolation = INTERPOLATION_HISTOGRAM
- pdf_normalized, cdf_normalized = pdf_from_cdf(value, cdf)
+ pdf_normalized, cdf_normalized = pdf_from_cdf(
+ value_array,
+ cdf_array,
+ )
self.pdf = DataTable(
- value,
+ value_array,
pdf_normalized,
interpolation,
aux=cdf_normalized,
)
def __repr__(self) -> str:
- """Return a human-readable summary of the distribution."""
-
text = super().__repr__()
text += f" - value {print_1d_array(self.pdf.x)}\n"
text += f" - probability density {print_1d_array(self.pdf.y)}\n"
- text += f" - cumulative distribution: " f"{print_1d_array(self.pdf.aux[0])}\n"
-
+ text += " - cumulative distribution " f"{print_1d_array(self.pdf.aux[0])}\n"
return text
@@ -208,109 +192,108 @@ def __repr__(self) -> str:
class DistributionMultiTable(DistributionBase):
- """
- Distribution represented by multiple tabulated distributions on a grid.
+ """Family of tabulated distributions indexed by another grid.
- Each grid point owns one DistributionTabulated object. The flattened
- `value` array is split into per-grid tables using `offset`.
+ ``offset`` marks the first entry of each table in the flattened ``value``
+ and probability arrays. Exactly one of ``pdf`` or ``cdf`` is required.
"""
- # Annotations for Numba mode
- label: str = "multi_table_distribution"
- #
+ # MC/DC framework metadata
+ label = "multi_table_distribution"
+ sub_type = DISTRIBUTION_MULTITABLE
+
grid: NDArray[float64]
tables: list[DistributionTabulated]
def __init__(
self,
- grid: NDArray[float64],
- offset: NDArray[int64],
- value: NDArray[float64],
- pdf: NDArray[float64] | None = None,
- cdf: NDArray[float64] | None = None,
+ grid: ArrayLike,
+ offset: ArrayLike,
+ value: ArrayLike,
+ pdf: ArrayLike | None = None,
+ cdf: ArrayLike | None = None,
) -> None:
- """
- Construct a multi-table distribution.
-
- Parameters
- ----------
- grid : ndarray of float64
- Grid values associated with the tabulated distributions.
- offset : ndarray of int64
- Starting index of each table in the flattened `value` array.
- value : ndarray of float64
- Flattened sample values for all tables.
- pdf : ndarray of float64, optional
- Flattened PDF values. Exactly one of `pdf` or `cdf` must be given.
- cdf : ndarray of float64, optional
- Flattened CDF values. Exactly one of `pdf` or `cdf` must be given.
- """
-
- type_ = DISTRIBUTION_MULTITABLE
- super().__init__(type_)
+ super().__init__()
if (pdf is None) == (cdf is None):
print_error("Exactly one of pdf or cdf must be provided.")
- if len(grid) != len(offset):
- print_error("grid and offset must have the same length.")
+ self.grid = np.asarray(grid, dtype=float64)
+ offset_array = np.asarray(offset, dtype=int64)
+ value_array = np.asarray(value, dtype=float64)
- if len(grid) == 0:
- print_error("grid must contain at least one value.")
+ if self.grid.ndim != 1:
+ print_error("grid must be one-dimensional.")
+ if offset_array.ndim != 1:
+ print_error("offset must be one-dimensional.")
+ if value_array.ndim != 1:
+ print_error("value must be one-dimensional.")
- if offset[0] != 0:
+ if len(self.grid) == 0:
+ print_error("grid must contain at least one value.")
+ if len(self.grid) != len(offset_array):
+ print_error("grid and offset must have the same length.")
+ if len(value_array) == 0:
+ print_error("value must contain at least one value.")
+ if offset_array[0] != 0:
print_error("offset[0] must be zero.")
+ if np.any(offset_array[1:] <= offset_array[:-1]):
+ print_error("offset must be strictly increasing.")
+ if offset_array[-1] >= len(value_array):
+ print_error("Every offset must refer to an element in value.")
- if len(value) == 0:
- print_error("value must contain at least one value.")
+ pdf_array = None
+ cdf_array = None
+
+ if pdf is not None:
+ pdf_array = np.asarray(pdf, dtype=float64)
- if pdf is not None and len(pdf) != len(value):
- print_error("pdf and value must have the same length.")
+ if pdf_array.ndim != 1:
+ print_error("pdf must be one-dimensional.")
+ if len(pdf_array) != len(value_array):
+ print_error("pdf and value must have the same length.")
- if cdf is not None and len(cdf) != len(value):
- print_error("cdf and value must have the same length.")
+ if cdf is not None:
+ cdf_array = np.asarray(cdf, dtype=float64)
- self.grid = grid
+ if cdf_array.ndim != 1:
+ print_error("cdf must be one-dimensional.")
+ if len(cdf_array) != len(value_array):
+ print_error("cdf and value must have the same length.")
- stop = np.empty(len(offset), dtype=int)
- stop[:-1] = offset[1:]
- stop[-1] = len(value)
+ stop = np.empty(len(offset_array), dtype=int64)
+ stop[:-1] = offset_array[1:]
+ stop[-1] = len(value_array)
self.tables = []
- for i in range(len(grid)):
- start_i = offset[i]
- stop_i = stop[i]
-
+ for start_i, stop_i in zip(offset_array, stop):
if stop_i <= start_i:
print_error("Each table must contain at least one value.")
- if pdf is not None:
- # Piecewise linear PDF
- new_table = DistributionTabulated(
- value[start_i:stop_i],
- pdf=pdf[start_i:stop_i],
+ if pdf_array is not None:
+ table = DistributionTabulated(
+ value_array[start_i:stop_i],
+ pdf=pdf_array[start_i:stop_i],
)
- else:
- # Piecewise linear CDF
- new_table = DistributionTabulated(
- value[start_i:stop_i],
- cdf=cdf[start_i:stop_i],
+ elif cdf_array is not None:
+ table = DistributionTabulated(
+ value_array[start_i:stop_i],
+ cdf=cdf_array[start_i:stop_i],
)
+ else:
+ table = DistributionTabulated([]) # Unachievable
- self.tables.append(new_table)
+ self.tables.append(table)
def __repr__(self) -> str:
- """Return a human-readable summary of the multi-table distribution."""
-
text = super().__repr__()
+
text += f" - grid: {print_1d_array(self.grid)}\n"
text += f" - tables: {len(self.tables)}\n"
for i, table in enumerate(self.tables):
- text += (
- f" - table[{i}] " f"(grid={self.grid[i]:.6g}, " f"N={table.pdf.N})\n"
- )
+ text += f" - table[{i}] " f"(grid={self.grid[i]:.6g}, N={table.pdf.N})\n"
return text
@@ -321,22 +304,31 @@ def __repr__(self) -> str:
class DistributionLevelScattering(DistributionBase):
- # Annotations for Numba mode
- label: str = "level_scattering_distribution"
- #
+ """Discrete-level inelastic-scattering energy distribution.
+
+ Parameters
+ ----------
+ C1, C2 : float
+ Level-scattering law coefficients.
+ """
+
+ # MC/DC framework metadata
+ label = "level_scattering_distribution"
+ sub_type = DISTRIBUTION_LEVEL_SCATTERING
+
C1: float
C2: float
- def __init__(self, C1, C2):
- type_ = DISTRIBUTION_LEVEL_SCATTERING
- super().__init__(type_)
+ def __init__(self, C1: float, C2: float) -> None:
+ super().__init__()
self.C1 = C1
self.C2 = C2
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
- text += f" - C1 {print_1d_array(self.C1)} [/eV^l]\n"
+
+ text += f" - C1: {self.C1} [/eV^l]\n"
text += f" - C2: {self.C2}\n"
return text
@@ -347,22 +339,24 @@ def __repr__(self):
class DistributionEvaporation(DistributionBase):
- # Annotations for Numba mode
- label: str = "evaporation_distribution"
- #
+ """Evaporation spectrum with incident-energy-dependent temperature."""
+
+ # MC/DC framework metadata
+ label = "evaporation_distribution"
+ sub_type = DISTRIBUTION_EVAPORATION
+
nuclear_temperature: DataTable
restriction_energy: float
def __init__(
self,
- nuclear_temperature_energy_grid,
- nuclear_temperature_value,
- restriction_energy,
- temperature_interpolations,
- interpolation_boundaries,
- ):
- type_ = DISTRIBUTION_EVAPORATION
- super().__init__(type_)
+ nuclear_temperature_energy_grid: NDArray[float64],
+ nuclear_temperature_value: NDArray[float64],
+ restriction_energy: float,
+ temperature_interpolations: int | Sequence[int],
+ interpolation_boundaries: Sequence[int] | None,
+ ) -> None:
+ super().__init__()
self.restriction_energy = restriction_energy
self.nuclear_temperature = DataTable(
@@ -372,36 +366,45 @@ def __init__(
interpolation_boundaries,
)
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
+
text += f" - Restriction energy: {self.restriction_energy} [eV]\n"
- text += f" - Nuclear temperature {print_1d_array(self.nuclear_temperature.y)} [eV]\n"
- text += f" - Nuclear temperature energy grid {print_1d_array(self.nuclear_temperature.x)} [eV]\n"
+ text += (
+ " - Nuclear temperature "
+ f"{print_1d_array(self.nuclear_temperature.y)} [eV]\n"
+ )
+ text += (
+ " - Nuclear temperature energy grid "
+ f"{print_1d_array(self.nuclear_temperature.x)} [eV]\n"
+ )
return text
# ======================================================================================
-# Maxwellian distribution
+# Maxwellian
# ======================================================================================
class DistributionMaxwellian(DistributionBase):
- # Annotations for Numba mode
- label: str = "maxwellian_distribution"
- #
+ """Maxwellian spectrum with incident-energy-dependent temperature."""
+
+ # MC/DC framework metadata
+ label = "maxwellian_distribution"
+ sub_type = DISTRIBUTION_MAXWELLIAN
+
nuclear_temperature: DataTable
restriction_energy: float
def __init__(
self,
- nuclear_temperature_energy_grid,
- nuclear_temperature_value,
- restriction_energy,
- temperature_interpolations,
- interpolation_boundaries,
- ):
- type_ = DISTRIBUTION_MAXWELLIAN
- super().__init__(type_)
+ nuclear_temperature_energy_grid: NDArray[float64],
+ nuclear_temperature_value: NDArray[float64],
+ restriction_energy: float,
+ temperature_interpolations: int | Sequence[int],
+ interpolation_boundaries: Sequence[int] | None,
+ ) -> None:
+ super().__init__()
self.restriction_energy = restriction_energy
self.nuclear_temperature = DataTable(
@@ -411,11 +414,18 @@ def __init__(
interpolation_boundaries,
)
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
+
text += f" - Restriction energy: {self.restriction_energy} [eV]\n"
- text += f" - Nuclear temperature {print_1d_array(self.nuclear_temperature.y)} [eV]\n"
- text += f" - Nuclear temperature energy grid {print_1d_array(self.nuclear_temperature.x)} [eV]\n"
+ text += (
+ " - Nuclear temperature "
+ f"{print_1d_array(self.nuclear_temperature.y)} [eV]\n"
+ )
+ text += (
+ " - Nuclear temperature energy grid "
+ f"{print_1d_array(self.nuclear_temperature.x)} [eV]\n"
+ )
return text
@@ -425,9 +435,16 @@ def __repr__(self):
class DistributionKalbachMann(DistributionBase):
- # Annotations for Numba mode
- label: str = "kalbach_mann_distribution"
- #
+ """Correlated Kalbach-Mann outgoing energy-angle distribution.
+
+ Incident-energy tables are stored in flattened arrays delimited by
+ ``offset``. Probability densities are normalized per table.
+ """
+
+ # MC/DC framework metadata
+ label = "kalbach_mann_distribution"
+ sub_type = DISTRIBUTION_KALBACH_MANN
+
energy: NDArray[float64]
offset: NDArray[int64]
energy_out: NDArray[float64]
@@ -437,29 +454,40 @@ class DistributionKalbachMann(DistributionBase):
angular_slope: NDArray[float64]
def __init__(
- self, energy, offset, energy_out, pdf, precompound_factor, angular_slope
- ):
- type_ = DISTRIBUTION_KALBACH_MANN
- super().__init__(type_)
-
- self.energy = energy
- self.offset = offset
-
- self.energy_out = energy_out
- self.pdf = pdf
-
- self.precompound_factor = precompound_factor
- self.angular_slope = angular_slope
+ self,
+ energy: ArrayLike,
+ offset: ArrayLike,
+ energy_out: ArrayLike,
+ pdf: ArrayLike,
+ precompound_factor: ArrayLike,
+ angular_slope: ArrayLike,
+ ) -> None:
+ super().__init__()
+
+ self.energy = np.asarray(energy, dtype=float64)
+ self.offset = np.asarray(offset, dtype=int64)
+ self.energy_out = np.asarray(energy_out, dtype=float64)
+ pdf_array = np.asarray(pdf, dtype=float64)
+ self.precompound_factor = np.asarray(
+ precompound_factor,
+ dtype=float64,
+ )
+ self.angular_slope = np.asarray(angular_slope, dtype=float64)
- self.pdf, self.cdf = multi_cdf_from_pdf(offset, energy_out, pdf)
+ self.pdf, self.cdf = multi_cdf_from_pdf(
+ self.offset,
+ self.energy_out,
+ pdf_array,
+ )
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
+
text += f" - grid {print_1d_array(self.energy)} [eV]\n"
text += f" - offset {print_1d_array(self.offset)}\n"
text += f" - energy {print_1d_array(self.energy_out)} [eV]\n"
text += f" - energy-pdf {print_1d_array(self.pdf)} [/eV]\n"
- text += f" - precompound factor {print_1d_array(self.precompound_factor)}\n"
+ text += " - precompound factor " f"{print_1d_array(self.precompound_factor)}\n"
text += f" - angular slope {print_1d_array(self.angular_slope)}\n"
return text
@@ -470,118 +498,154 @@ def __repr__(self):
class DistributionTabulatedEnergyAngle(DistributionBase):
- # Annotations for Numba mode
- label: str = "tabulated_energy_angle_distribution"
- #
+ """Correlated tabulated outgoing energy and scattering-angle distribution.
+
+ ``offset`` delimits outgoing-energy tables and ``cosine_offset`` delimits
+ conditional cosine tables in the flattened arrays.
+ """
+
+ # MC/DC framework metadata
+ label = "tabulated_energy_angle_distribution"
+ sub_type = DISTRIBUTION_TABULATED_ENERGY_ANGLE
+
energy: NDArray[float64]
offset: NDArray[int64]
energy_out: NDArray[float64]
pdf: NDArray[float64]
cdf: NDArray[float64]
- cosine_offset_: NDArray[int64] # "cosine_offset" is reserved to describe "cosine"
+ cosine_offset_: NDArray[int64]
cosine: NDArray[float64]
cosine_pdf: NDArray[float64]
cosine_cdf: NDArray[float64]
def __init__(
- self, energy, offset, energy_out, pdf, cosine_offset, cosine, cosine_pdf
- ):
- type_ = DISTRIBUTION_TABULATED_ENERGY_ANGLE
- super().__init__(type_)
+ self,
+ energy: ArrayLike,
+ offset: ArrayLike,
+ energy_out: ArrayLike,
+ pdf: ArrayLike,
+ cosine_offset: ArrayLike,
+ cosine: ArrayLike,
+ cosine_pdf: ArrayLike,
+ ) -> None:
+ super().__init__()
+
+ self.energy = np.asarray(energy, dtype=float64)
+ self.offset = np.asarray(offset, dtype=int64)
+ self.energy_out = np.asarray(energy_out, dtype=float64)
+ pdf_array = np.asarray(pdf, dtype=float64)
+ self.cosine_offset_ = np.asarray(cosine_offset, dtype=int64)
+ self.cosine = np.asarray(cosine, dtype=float64)
+ cosine_pdf_array = np.asarray(cosine_pdf, dtype=float64)
+
+ self.pdf, self.cdf = multi_cdf_from_pdf(
+ self.offset,
+ self.energy_out,
+ pdf_array,
+ )
- self.energy = energy
- self.offset = offset
+ self.cosine_pdf = cosine_pdf_array.copy()
+ self.cosine_cdf = np.zeros_like(self.cosine_pdf)
- self.energy_out = energy_out
- self.pdf = pdf
- self.cosine_offset_ = cosine_offset
+ for i in range(len(self.offset)):
+ energy_start = self.offset[i]
- self.cosine = cosine
- self.cosine_pdf = cosine_pdf
+ if i + 1 < len(self.offset):
+ energy_stop = self.offset[i + 1]
+ else:
+ energy_stop = len(self.energy_out)
- self.pdf, self.cdf = multi_cdf_from_pdf(offset, energy_out, pdf)
+ inner_offset = self.cosine_offset_[energy_start:energy_stop]
- self.cosine_cdf = np.zeros_like(self.cosine_pdf)
- for i in range(len(offset)):
- start = offset[i]
- if i + 1 < len(offset):
- end = offset[i + 1]
- else:
- end = len(cosine)
- inner_offset = cosine_offset[start:end]
+ if len(inner_offset) == 0:
+ print_error(
+ "Each incident-energy table must reference at least one "
+ "cosine distribution."
+ )
+
+ cosine_start = inner_offset[0]
- start = inner_offset[0]
- if i + 1 < len(offset):
- end = cosine_offset[end]
+ if i + 1 < len(self.offset):
+ cosine_stop = self.cosine_offset_[energy_stop]
else:
- end = len(cosine)
+ cosine_stop = len(self.cosine)
+
+ inner_offset_local = inner_offset - cosine_start
- inner_offset_local = inner_offset - inner_offset[0]
- self.cosine_pdf[start:end], self.cosine_cdf[start:end] = multi_cdf_from_pdf(
- inner_offset_local, cosine[start:end], cosine_pdf[start:end]
+ (
+ self.cosine_pdf[cosine_start:cosine_stop],
+ self.cosine_cdf[cosine_start:cosine_stop],
+ ) = multi_cdf_from_pdf(
+ inner_offset_local,
+ self.cosine[cosine_start:cosine_stop],
+ cosine_pdf_array[cosine_start:cosine_stop],
)
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
+
text += f" - grid {print_1d_array(self.energy)} [eV]\n"
text += f" - offset {print_1d_array(self.offset)}\n"
text += f" - energy {print_1d_array(self.energy_out)} [eV]\n"
text += f" - energy-pdf {print_1d_array(self.pdf)} [/eV]\n"
- text += f" - cosine-offset {print_1d_array(self.cosine_offset_)}\n"
+ text += " - cosine-offset " f"{print_1d_array(self.cosine_offset_)}\n"
text += f" - cosine {print_1d_array(self.cosine)}\n"
text += f" - cosine-pdf {print_1d_array(self.cosine_pdf)}\n"
return text
# ======================================================================================
-# N-Body
+# N-body
# ======================================================================================
class DistributionNBody(DistributionBase):
- """
- N-body energy distribution represented by a tabulated PDF and CDF.
+ """N-body phase-space outgoing-energy distribution.
- The input PDF is normalized internally, and the corresponding CDF is
- constructed for sampling.
+ Parameters
+ ----------
+ values, probabilities : array_like
+ Outgoing values and their piecewise-linear relative density.
"""
- # Annotations for Numba mode
- label: str = "nbody_distribution"
- #
+ # MC/DC framework metadata
+ label = "nbody_distribution"
+ sub_type = DISTRIBUTION_N_BODY
+
pdf: DataTable
def __init__(
self,
- values: NDArray[float64],
- probabilities: NDArray[float64],
+ values: ArrayLike,
+ probabilities: ArrayLike,
) -> None:
- """
- Construct an N-body distribution from tabulated PDF values.
+ super().__init__()
- Parameters
- ----------
- value : ndarray
- Tabulated sample values.
- probabilities : ndarray
- Probability density values at the tabulated sample values. The
- values do not need to be normalized.
- """
+ value_array = np.asarray(values, dtype=float64)
+ probability_array = np.asarray(probabilities, dtype=float64)
- type_ = DISTRIBUTION_N_BODY
- super().__init__(type_)
+ if value_array.ndim != 1:
+ print_error("values must be one-dimensional.")
+ if probability_array.ndim != 1:
+ print_error("probabilities must be one-dimensional.")
+ if len(value_array) != len(probability_array):
+ print_error("values and probabilities must have the same length.")
- pdf_normalized, cdf_normalized = cdf_from_pdf(values, probabilities)
+ pdf_normalized, cdf_normalized = cdf_from_pdf(
+ value_array,
+ probability_array,
+ )
self.pdf = DataTable(
- values,
+ value_array,
pdf_normalized,
INTERPOLATION_LINEAR,
aux=cdf_normalized,
)
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
- text += f" - value {print_1d_array(self.value)}\n"
- text += f" - pdf {print_1d_array(self.pdf)}\n"
+
+ text += f" - value {print_1d_array(self.pdf.x)}\n"
+ text += f" - pdf {print_1d_array(self.pdf.y)}\n"
return text
diff --git a/mcdc/object_/electron_reaction.py b/mcdc/object_/electron_reaction.py
index 441ebb189..804605c65 100644
--- a/mcdc/object_/electron_reaction.py
+++ b/mcdc/object_/electron_reaction.py
@@ -15,7 +15,7 @@
REFERENCE_FRAME_COM,
REFERENCE_FRAME_LAB,
)
-from mcdc.object_.base import ObjectPolymorphic
+from mcdc.object_.base import MCDCPolymorphic
from mcdc.object_.data import DataBase, DataTable
from mcdc.object_.distribution import DistributionBase, DistributionMultiTable
from mcdc.print_ import print_1d_array
@@ -25,25 +25,32 @@
# ======================================================================================
-class ElectronReactionBase(ObjectPolymorphic):
- # Annotations for Numba mode
- label: str = "electron_reaction"
- #
+class ElectronReactionBase(MCDCPolymorphic):
+ """Base electron reaction data loaded from the HDF5 physics library.
+
+ Stores the ENDF reaction identifier, cross-section segment and its offset
+ into the element energy grid, and the reaction reference frame.
+ """
+
+ # MC/DC framework metadata
+ label = "electron_reaction"
+ sub_type = -1 # Polymorphic base
+
MT: int
xs: NDArray[float64]
xs_offset_: int # "xs_offset" is reserved for "xs"
reference_frame: int
- def __init__(self, type_, MT, xs, xs_offset, reference_frame):
- super().__init__(type_)
+ def __init__(self, MT, xs, xs_offset, reference_frame):
+ super().__init__()
self.MT = MT
self.xs = xs
self.xs_offset_ = xs_offset
self.reference_frame = reference_frame
def __repr__(self):
- text = "\n"
- text += f"{decode_type(self.type)}\n"
+ text = super().__repr__()
+
text += f" - ID: {self.ID}\n"
text += f" - MT: {self.MT}\n"
text += f" - XS {print_1d_array(self.xs)} barn\n"
@@ -51,18 +58,9 @@ def __repr__(self):
return text
-def decode_type(type_):
- if type_ == ELECTRON_REACTION_IONIZATION:
- return "Electron ionization"
- elif type_ == ELECTRON_REACTION_ELASTIC_SCATTERING:
- return "Electron elastic scattering"
- elif type_ == ELECTRON_REACTION_BREMSSTRAHLUNG:
- return "Electron bremsstrahlung"
- elif type_ == ELECTRON_REACTION_EXCITATION:
- return "Electron excitation"
-
-
def decode_reference_frame(type_):
+ """Return the display name for a packed reference-frame code."""
+
if type_ == REFERENCE_FRAME_LAB:
return "Laboratory"
elif type_ == REFERENCE_FRAME_COM:
@@ -75,9 +73,12 @@ def decode_reference_frame(type_):
class ElectronReactionIonization(ElectronReactionBase):
- # Annotations for Numba mode
- label: str = "electron_ionization_reaction"
- #
+ """Electron ionization reaction with subshell cross sections and products."""
+
+ # MC/DC framework metadata
+ label = "electron_ionization_reaction"
+ sub_type = ELECTRON_REACTION_IONIZATION
+
N_subshell: int
subshell_xs: list[DataBase]
subshell_product: list[DistributionBase]
@@ -91,8 +92,7 @@ def __init__(
subshell_xs,
subshell_product,
):
- type_ = ELECTRON_REACTION_IONIZATION
- super().__init__(type_, MT, xs, xs_offset, reference_frame)
+ super().__init__(MT, xs, xs_offset, reference_frame)
self.N_subshell = len(subshell_xs)
self.subshell_xs = subshell_xs
@@ -100,6 +100,7 @@ def __init__(
@classmethod
def from_h5_group(cls, h5_group):
+ """Build an ionization reaction from a library HDF5 group."""
MT, xs, xs_offset, reference_frame = set_basic_properties(h5_group)
subshells = h5_group["subshells"]
@@ -167,9 +168,12 @@ def __repr__(self):
class ElectronReactionElasticScattering(ElectronReactionBase):
- # Annotations for Numba mode
- label: str = "electron_elastic_scattering_reaction"
- #
+ """Elastic electron scattering with large-angle cross section and cosine law."""
+
+ # MC/DC framework metadata
+ label = "electron_elastic_scattering_reaction"
+ sub_type = ELECTRON_REACTION_ELASTIC_SCATTERING
+
mu_cut: float
xs_large: DataBase
mu: DistributionMultiTable
@@ -183,14 +187,14 @@ def __init__(
xs_large,
mu,
):
- type_ = ELECTRON_REACTION_ELASTIC_SCATTERING
- super().__init__(type_, MT, xs, xs_offset, reference_frame)
+ super().__init__(MT, xs, xs_offset, reference_frame)
self.mu_cut = MU_CUTOFF
self.xs_large = xs_large
self.mu = mu
@classmethod
def from_h5_group(cls, h5_group):
+ """Build an elastic-scattering reaction from a library HDF5 group."""
MT, xs, xs_offset, reference_frame = set_basic_properties(h5_group)
large_angle = h5_group["large_angle"]
@@ -230,18 +234,21 @@ def __repr__(self):
class ElectronReactionBremsstrahlung(ElectronReactionBase):
- # Annotations for Numba mode
- label: str = "electron_bremsstrahlung_reaction"
- #
+ """Electron bremsstrahlung reaction with tabulated energy loss."""
+
+ # MC/DC framework metadata
+ label = "electron_bremsstrahlung_reaction"
+ sub_type = ELECTRON_REACTION_BREMSSTRAHLUNG
+
eloss: DataBase
def __init__(self, MT, xs, xs_offset, reference_frame, eloss):
- type_ = ELECTRON_REACTION_BREMSSTRAHLUNG
- super().__init__(type_, MT, xs, xs_offset, reference_frame)
+ super().__init__(MT, xs, xs_offset, reference_frame)
self.eloss = eloss
@classmethod
def from_h5_group(cls, h5_group):
+ """Build a bremsstrahlung reaction from a library HDF5 group."""
MT, xs, xs_offset, reference_frame = set_basic_properties(h5_group)
base = h5_group["energy_loss"]
@@ -261,18 +268,21 @@ def __repr__(self):
class ElectronReactionExcitation(ElectronReactionBase):
- # Annotations for Numba mode
- label: str = "electron_excitation_reaction"
- #
+ """Electron excitation reaction with tabulated energy loss."""
+
+ # MC/DC framework metadata
+ label = "electron_excitation_reaction"
+ sub_type = ELECTRON_REACTION_EXCITATION
+
eloss: DataBase
def __init__(self, MT, xs, xs_offset, reference_frame, eloss):
- type_ = ELECTRON_REACTION_EXCITATION
- super().__init__(type_, MT, xs, xs_offset, reference_frame)
+ super().__init__(MT, xs, xs_offset, reference_frame)
self.eloss = eloss
@classmethod
def from_h5_group(cls, h5_group):
+ """Build an excitation reaction from a library HDF5 group."""
MT, xs, xs_offset, reference_frame = set_basic_properties(h5_group)
base = h5_group["energy_loss"]
@@ -292,6 +302,8 @@ def __repr__(self):
def set_basic_properties(h5_group):
+ """Read properties shared by all electron reactions from an HDF5 group."""
+
MT = h5_group.attrs["MT"][()]
xs = h5_group["xs"][()]
xs_offset = h5_group["xs"].attrs["offset"]
diff --git a/mcdc/object_/element.py b/mcdc/object_/element.py
index acc1270a0..c0234f20f 100644
--- a/mcdc/object_/element.py
+++ b/mcdc/object_/element.py
@@ -7,19 +7,35 @@
####
-from mcdc.object_.base import ObjectNonSingleton
+from mcdc.object_.base import MCDCObject
from mcdc.object_.electron_reaction import (
ElectronReactionBremsstrahlung,
ElectronReactionElasticScattering,
ElectronReactionExcitation,
ElectronReactionIonization,
)
+from mcdc.print_ import print_error
-class Element(ObjectNonSingleton):
- # Annotations for Numba mode
- label: str = "element"
- #
+class Element(MCDCObject):
+ """Element definition for the MC/DC HDF5 library.
+
+ Parameters
+ ----------
+ element_name : str
+ Chemical symbol used to locate ``.h5`` under ``MCDC_LIB``.
+
+ Notes
+ -----
+ Construction records the element identity without accessing the data
+ library. Basic properties are loaded when the element is compiled into a
+ simulation. Electron reaction cross sections and secondary distributions
+ are loaded later by :meth:`set_electron_data`.
+ """
+
+ # MC/DC framework metadata
+ label = "element"
+
name: str
atomic_weight_ratio: float
atomic_number: int
@@ -43,15 +59,28 @@ def __init__(self, element_name: str):
self.name = element_name
- # Basic properties
+ def _compile_into_simulation(self, simulation) -> bool:
+ """Load basic properties and register with the owning simulation."""
+ if self.compile_ID == simulation.compile_ID:
+ return False
+
dir_name = os.getenv("MCDC_LIB")
- file_name = f"{element_name}.h5"
- file = h5py.File(f"{dir_name}/{file_name}", "r")
- self.atomic_weight_ratio = float(file["atomic_weight_ratio"][()])
- self.atomic_number = int(file["atomic_number"][()])
- file.close()
+ if dir_name is None:
+ print_error("Environment variable MCDC_LIB is not set")
+
+ file_name = f"{self.name}.h5"
+ file_path = os.path.join(dir_name, file_name)
+ if not os.path.isfile(file_path):
+ print_error(f"Element {self.name} is not available in the library")
- def set_electron_data(self):
+ with h5py.File(file_path, "r") as file:
+ self.atomic_weight_ratio = float(file["atomic_weight_ratio"][()])
+ self.atomic_number = int(file["atomic_number"][()])
+
+ return super()._compile_into_simulation(simulation)
+
+ def set_electron_data(self, simulation):
+ """Load and register electron reaction data from ``MCDC_LIB``."""
element_name = self.name
# Load data library
@@ -150,6 +179,11 @@ def set_electron_data(self):
file.close()
+ # Register data loaded during object-model finalization.
+ for reaction_container in rx_containers:
+ for reaction in reaction_container:
+ reaction._compile_into_simulation(simulation)
+
def __repr__(self):
text = "\n"
text += f"Element\n"
diff --git a/mcdc/object_/gpu_tools.py b/mcdc/object_/gpu_tools.py
index b4d4965a3..854891697 100644
--- a/mcdc/object_/gpu_tools.py
+++ b/mcdc/object_/gpu_tools.py
@@ -3,14 +3,16 @@
####
-from mcdc.object_.base import ObjectSingleton
+from mcdc.object_.base import MCDCBase
@dataclass
-class GPUMeta(ObjectSingleton):
- # Annotations for Numba mode
- label: str = "gpu_meta"
- #
+class GPUMeta(MCDCBase):
+ """Opaque device pointers owned by the GPU execution bridge."""
+
+ # MC/DC framework metadata
+ label = "gpu_meta"
+
state_pointer: uintp = uintp(0)
program_pointer: uintp = uintp(0)
simulation_pointer: uintp = uintp(0)
diff --git a/mcdc/object_/material.py b/mcdc/object_/material.py
index c2b2fbfbe..5c7d85a06 100644
--- a/mcdc/object_/material.py
+++ b/mcdc/object_/material.py
@@ -1,102 +1,101 @@
-import numpy as np
-import os
-
-from numpy import float64
-from numpy.typing import NDArray
from types import NoneType
-from typing import Annotated
+from typing import Self
-####
+import numpy as np
+from numpy import float64
+from numpy.typing import ArrayLike, NDArray
-from mcdc.constant import MATERIAL, MATERIAL_MG
-from mcdc.object_.base import ObjectPolymorphic
+from mcdc.object_.base import MCDCObject
from mcdc.object_.element import Element
+from mcdc.object_.transport_model_data import NeutronMultigroupData
from mcdc.object_.nuclide import Nuclide
-from mcdc.object_.simulation import simulation
from mcdc.object_.util import ISOTOPIC_ABUNDANCE
-from mcdc.print_ import print_1d_array, print_error
+from mcdc.print_ import print_error
# ======================================================================================
-# Material base class
+# Material
# ======================================================================================
-class MaterialBase(ObjectPolymorphic):
- # Annotations for Numba mode
- label: str = "material"
- #
- name: str
- fissionable: bool
-
- def __init__(self, type_, name):
- super().__init__(type_)
-
- # Set name
- if name != "":
- self.name = name
- else:
- self.name = f"{self.label}_{self.child_ID}"
-
- self.fissionable = False
-
- def __repr__(self):
- text = "\n"
- text += f"{decode_type(self.type)}\n"
- text += f" - ID: {self.ID}\n"
- text += f" - Name: {self.name}\n"
- text += f" - Fissionable: {self.fissionable}\n"
- return text
-
-
-def decode_type(type_):
- if type_ == MATERIAL:
- return "Material"
- elif type_ == MATERIAL_MG:
- return "Multigroup material"
-
-
-# ======================================================================================
-# Native material
-# ======================================================================================
-
-
-class Material(MaterialBase):
- """
- Define a continuous-energy material from a nuclide composition.
+class Material(MCDCObject):
+ """Particle-interaction properties assigned to simulation cells.
Parameters
----------
name : str, optional
- User label.
- nuclide_composition : dict
- Dictionary mapping nuclide names (str) to atom densities (float).
- element_composition : dict
- Dictionary mapping element names (str) to atom densities (float).
+ User-facing material name.
+ nuclide_composition : dict of str to float, optional
+ Nuclide names and atomic densities in atoms/(barn cm).
+ element_composition : dict of str to float, optional
+ Element symbols and atomic densities in atoms/(barn cm).
temperature : float, optional
- Temperature in Kelvin (default 293.6 K).
-
- Returns
- -------
- Material
- The material object.
+ Material temperature in kelvin. Each nuclide uses the closest
+ temperature available in the data library.
+ neutron_multigroup : NeutronMultigroupData, optional
+ Groupwise macroscopic cross sections and related data for neutron
+ multigroup transport, where neutron energy is represented by discrete
+ groups. When supplied with a
+ :ref:`native composition `, its explicit
+ ``energy_grid`` defines the energy range where the multigroup treatment
+ applies. In hybrid transport, native neutron data is used outside that
+ range when present; without native composition, the material has zero
+ interaction cross section.
Notes
-----
- Requires the ``MCDC_LIB`` environment variable to point to the nuclear
- data library directory.
-
- See Also
+ A nuclide or element composition connects the material to MC/DC's native
+ transport physics through its data libraries. Particle-specific data can
+ augment native interaction data or support specialized and reduced
+ transport treatments.
+ ``NeutronMultigroupData`` describes discrete neutron energy groups and
+ their macroscopic interaction, production, and timing data. It can be used
+ alone or alongside a native composition.
+
+ Examples
--------
- mcdc.MaterialMG : Creates a multigroup material.
+ Define uranium dioxide from nuclide atomic densities:
+
+ >>> import mcdc
+ >>> fuel = mcdc.Material(
+ ... name="UO2",
+ ... nuclide_composition={"U235": 5.0e-4, "U238": 2.2e-2, "O16": 4.5e-2},
+ ... temperature=293.6,
+ ... )
+
+ Define a one-group multigroup material:
+
+ >>> import numpy as np
+ >>> absorber = mcdc.Material.multigroup(
+ ... name="Absorber", capture=np.array([1.0])
+ ... )
+
+ Attach native data and multigroup neutron data to the same material:
+
+ >>> hybrid_fuel = mcdc.Material(
+ ... name="Hybrid fuel",
+ ... nuclide_composition={"U235": 5.0e-4, "U238": 2.2e-2},
+ ... neutron_multigroup=mcdc.NeutronMultigroupData(
+ ... capture=np.array([0.10]),
+ ... fission=np.array([0.20]),
+ ... nu_p=np.array([2.50]),
+ ... energy_grid=np.array([1.0e-5, 20.0e6]),
+ ... ),
+ ... )
"""
- # Annotations for Numba mode
- label: str = "native_material"
- non_numba: list[str] = ["nuclide_composition", "element_composition"]
- #
- nuclide_composition: dict[Nuclide, float]
- element_composition: dict[Element, float]
- #
+ # MC/DC framework metadata
+ label = "material"
+ non_numba = ["nuclide_composition", "element_composition"]
+
+ name: str
+ temperature: float
+ fissionable: bool
+ has_neutron_multigroup: bool
+
+ nuclide_composition: dict[Nuclide, float] # Non-Numba
+ element_composition: dict[Element, float] # Non-Numba
+ neutron_multigroup: NeutronMultigroupData
+
nuclides: list[Nuclide]
elements: list[Element]
nuclide_densities: NDArray[float64]
@@ -105,120 +104,196 @@ class Material(MaterialBase):
def __init__(
self,
name: str = "",
- nuclide_composition: dict[str, float] = {},
- element_composition: dict[str, float] = {},
+ nuclide_composition: dict[str, float] | NoneType = None,
+ element_composition: dict[str, float] | NoneType = None,
temperature: float = 293.6,
- ):
- type_ = MATERIAL
- super().__init__(type_, name)
+ neutron_multigroup: NeutronMultigroupData | NoneType = None,
+ ) -> None:
+ super().__init__()
- # Temperature
- self.temperature = temperature
+ # Normalize optional compositions without mutable argument defaults
+ nuclide_composition = nuclide_composition or {}
+ element_composition = element_composition or {}
- # Dictionary connecting nuclides to respective densities
- self.nuclide_composition = {}
-
- # Dictionary connecting elements to respective densities
- self.element_composition = {}
-
- # Numba representation of nuclide_composition
- self.nuclides = []
- self.nuclide_densities = np.zeros(len(nuclide_composition))
-
- # Numba representation of element_composition
- self.elements = []
- self.element_densities = np.zeros(len(element_composition))
-
- # Check if library directory is set
- lib_dir = os.getenv("MCDC_LIB")
- if lib_dir is None:
- print_error("Environment variable MCDC_LIB is not set")
-
- # Check that only one composition is supplied
- if len(nuclide_composition) > 0 and len(element_composition) > 0:
+ # Require one valid material-data representation
+ if nuclide_composition and element_composition:
print_error(
- "Cannot specify both nuclide_composition and element_composition"
+ "Cannot specify both nuclide_composition and element_composition."
)
-
- if len(nuclide_composition) == 0 and len(element_composition) == 0:
+ if (
+ not nuclide_composition
+ and not element_composition
+ and neutron_multigroup is None
+ ):
print_error(
- "Must specify either nuclide_composition or element_composition"
+ "Material requires nuclide_composition, element_composition, "
+ "or neutron_multigroup."
)
-
- # Loop over the items in the elemental composition
- for i, (key, value) in enumerate(element_composition.items()):
- element_name = key
- element_density = value
-
- # Check if element is already created
- found = False
- for element in simulation.elements:
- if element.name == element_name:
- found = True
- break
-
- # Create the element object if needed
- if not found:
- element = Element(element_name)
-
- # Register the element composition
- self.elements.append(element)
- self.element_densities[i] = element_density
- self.element_composition[element] = element_density
-
- # Loop over the items in the nuclide composition
- for i, (key, value) in enumerate(nuclide_composition.items()):
- nuclide_name = key
- nuclide_density = value
-
- # Get supported temperature
- nearest_temperature = min(TEMPERATURES, key=lambda x: abs(x - temperature))
-
- # Check if nuclide-temperature is available in the library
- file_name = f"{nuclide_name}-{nearest_temperature}K.h5"
- if not file_name in os.listdir(lib_dir):
- print_error(
- f"Nuclide {nuclide_name} at temperature {nearest_temperature} K is not available in the library"
- )
-
- # Check if nuclide is already created
- found = False
- for nuclide in simulation.nuclides:
- if (
- nuclide.name == nuclide_name
- and nearest_temperature == nuclide.temperature
- ):
- found = True
- break
-
- # Create the nuclide to objects if needed
- if not found:
- nuclide = Nuclide(nuclide_name, nearest_temperature)
-
- # Register the nuclide composition
- self.nuclides.append(nuclide)
- self.nuclide_densities[i] = nuclide_density
- self.nuclide_composition[nuclide] = nuclide_density
-
- # Promote nuclide flags to material
- if nuclide.fissionable:
- self.fissionable = True
-
- def __repr__(self):
+ if neutron_multigroup is not None and not isinstance(
+ neutron_multigroup, NeutronMultigroupData
+ ):
+ print_error("neutron_multigroup must be a NeutronMultigroupData object.")
+ if neutron_multigroup is not None and neutron_multigroup.G == 0:
+ print_error(
+ "Material neutron_multigroup must define at least one energy group."
+ )
+ if (
+ (nuclide_composition or element_composition)
+ and neutron_multigroup is not None
+ and neutron_multigroup.G > 0
+ and not np.any(neutron_multigroup.energy_grid)
+ ):
+ print_error(
+ "Material requires an explicit neutron multigroup energy_grid "
+ "when a nuclide or element composition is supplied."
+ )
+ # Initialize shared material state
+ self.name = name or "(Unnamed material)"
+ self.temperature = float(temperature)
+
+ # Use a zero-group placeholder until compilation can select ID 0
+ self.neutron_multigroup = (
+ neutron_multigroup
+ if neutron_multigroup is not None
+ else NeutronMultigroupData()
+ )
+ self.has_neutron_multigroup = self.neutron_multigroup.G > 0
+ self.fissionable = self.neutron_multigroup.fissionable
+
+ # Create lightweight native-composition objects without loading data
+ nearest_temperature = _get_supported_temperature(self.temperature)
+ self.nuclide_composition = {
+ Nuclide(name, nearest_temperature): float(density)
+ for name, density in nuclide_composition.items()
+ }
+ self.element_composition = {
+ Element(name): float(density)
+ for name, density in element_composition.items()
+ }
+
+ # Initialize the packed native-composition representation
+ self.nuclides = list(self.nuclide_composition)
+ self.elements = list(self.element_composition)
+ self.nuclide_densities = np.asarray(
+ list(self.nuclide_composition.values()), dtype=float64
+ )
+ self.element_densities = np.asarray(
+ list(self.element_composition.values()), dtype=float64
+ )
+
+ @classmethod
+ def multigroup(
+ cls,
+ *,
+ name: str = "",
+ capture: ArrayLike | NoneType = None,
+ scatter: ArrayLike | NoneType = None,
+ fission: ArrayLike | NoneType = None,
+ nu_s: ArrayLike | NoneType = None,
+ nu_p: ArrayLike | NoneType = None,
+ nu_d: ArrayLike | NoneType = None,
+ chi_p: ArrayLike | NoneType = None,
+ chi_d: ArrayLike | NoneType = None,
+ speed: ArrayLike | NoneType = None,
+ decay_rate: ArrayLike | NoneType = None,
+ energy_grid: ArrayLike | NoneType = None,
+ energy_representation: str | int = "midpoint",
+ ) -> Self:
+ """Create a material for neutron multigroup transport.
+
+ The supplied arguments define groupwise macroscopic interaction and
+ production data. This helper stores them in
+ :class:`NeutronMultigroupData` and attaches the data to the material.
+ Macroscopic cross sections use ``cm^-1``, group speeds use cm/s,
+ precursor decay rates use ``s^-1``, and explicit physical energy
+ boundaries use eV. Omitting ``energy_grid`` creates a zero-valued
+ placeholder that is valid only when every material with neutron
+ multigroup data also omits its energy grid.
+ """
+ # Build the neutron multigroup data while preserving Material as the sole type
+ neutron_multigroup = NeutronMultigroupData(
+ capture=capture,
+ scatter=scatter,
+ fission=fission,
+ nu_s=nu_s,
+ nu_p=nu_p,
+ nu_d=nu_d,
+ chi_p=chi_p,
+ chi_d=chi_d,
+ speed=speed,
+ decay_rate=decay_rate,
+ energy_grid=energy_grid,
+ energy_representation=energy_representation,
+ )
+ return cls(name=name, neutron_multigroup=neutron_multigroup)
+
+ def _compile_into_simulation(self, simulation) -> bool:
+ """Canonicalize owned data and register the unified material."""
+ # Skip canonicalization when this material is already compiled
+ if self.compile_ID == simulation.compile_ID:
+ return False
+
+ # Resolve native composition objects before generic member traversal
+ nuclide_composition = {}
+ element_composition = {}
+
+ for element, density in self.element_composition.items():
+ element = _get_or_create_element(element.name, simulation, element)
+ element_composition[element] = density
+
+ for nuclide, density in self.nuclide_composition.items():
+ nuclide = _get_or_create_nuclide(
+ nuclide.name, nuclide.temperature, simulation, nuclide
+ )
+ nuclide_composition[nuclide] = density
+
+ # Synchronize packed fields with canonical native objects
+ self.nuclide_composition = nuclide_composition
+ self.element_composition = element_composition
+ self.nuclides = list(nuclide_composition)
+ self.elements = list(element_composition)
+ self.nuclide_densities = np.asarray(
+ list(nuclide_composition.values()), dtype=float64
+ )
+ self.element_densities = np.asarray(
+ list(element_composition.values()), dtype=float64
+ )
+
+ # Point absent multigroup data to the reserved neutron model
+ if self.neutron_multigroup.G == 0:
+ self.neutron_multigroup = simulation.neutron_multigroup_data[0]
+ self.has_neutron_multigroup = self.neutron_multigroup.G > 0
+
+ # Register the material, then compile only canonical owned members
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ # Resolve fissionability from every available neutron representation
+ self.fissionable = self.neutron_multigroup.fissionable or any(
+ nuclide.fissionable for nuclide in self.nuclides
+ )
+ return True
+
+ def __repr__(self) -> str:
text = super().__repr__()
+ text += f" - Name: {self.name}\n"
+ text += f" - Fissionable: {self.fissionable}\n"
text += f" - Temperature: {self.temperature} K\n"
- if len(self.nuclide_composition) > 0:
- text += f" - Nuclide composition [atoms/barn-cm]\n"
- for nuclide in self.nuclide_composition.keys():
- text += (
- f" - {nuclide.name:<5} | {self.nuclide_composition[nuclide]}\n"
- )
- if len(self.element_composition) > 0:
- text += f" - Element composition [atoms/barn-cm]\n"
- for element in self.element_composition.keys():
- text += (
- f" - {element.name:<5} | {self.element_composition[element]}\n"
- )
+
+ if self.nuclide_composition:
+ text += " - Nuclide composition [atoms/barn-cm]\n"
+ for nuclide, density in self.nuclide_composition.items():
+ text += f" - {nuclide.name:<5} | {density}\n"
+
+ if self.element_composition:
+ text += " - Element composition [atoms/barn-cm]\n"
+ for element, density in self.element_composition.items():
+ text += f" - {element.name:<5} | {density}\n"
+
+ if self.neutron_multigroup.G > 0:
+ text += " - Neutron multigroup model\n"
+ text += f" - G: {self.neutron_multigroup.G}\n"
+ text += f" - J: {self.neutron_multigroup.J}\n"
return text
@@ -227,272 +302,43 @@ def __repr__(self):
# ======================================================================================
-# Multigroup material
+# Native-composition helpers
# ======================================================================================
-class MaterialMG(MaterialBase):
- """
- Define a multigroup material.
-
- Cross-section arrays are provided as NumPy arrays of length ``G`` (number
- of energy groups). Scatter and fission matrices are ``(G, G)``.
-
- Parameters
- ----------
- name : str, optional
- User label.
- capture : ndarray, optional
- Capture cross section for each group.
- scatter : ndarray, optional
- Scattering matrix ``(G, G)``.
- fission : ndarray, optional
- Fission cross section for each group.
- nu_s : ndarray, optional
- Average scattering multiplicity.
- nu_p : ndarray, optional
- Average prompt fission neutron yield.
- nu_d : ndarray, optional
- Average delayed fission neutron yield.
- chi_p : ndarray, optional
- Prompt fission spectrum.
- chi_d : ndarray, optional
- Delayed fission spectrum.
- speed : ndarray, optional
- Neutron speeds for each group (cm/s).
- decay_rate : ndarray, optional
- Delayed neutron precursor decay rates (1/s).
-
- Returns
- -------
- MaterialMG
- The multigroup material object.
-
- See Also
- --------
- mcdc.Material : Creates a continuous-energy material.
- """
-
- # Annotations for Numba mode
- label: str = "multigroup_material"
- #
- G: int
- J: int
- mgxs_speed: Annotated[NDArray[float64], ("G",)]
- mgxs_decay_rate: Annotated[NDArray[float64], ("J",)]
- mgxs_capture: Annotated[NDArray[float64], ("G",)]
- mgxs_scatter: Annotated[NDArray[float64], ("G",)]
- mgxs_fission: Annotated[NDArray[float64], ("G",)]
- mgxs_total: Annotated[NDArray[float64], ("G",)]
- mgxs_nu_s: Annotated[NDArray[float64], ("G",)]
- mgxs_nu_p: Annotated[NDArray[float64], ("G",)]
- mgxs_nu_d: Annotated[NDArray[float64], ("G", "J")]
- mgxs_nu_d_total: Annotated[NDArray[float64], ("G",)]
- mgxs_nu_f: Annotated[NDArray[float64], ("G",)]
- mgxs_chi_s: Annotated[NDArray[float64], ("G", "G")]
- mgxs_chi_p: Annotated[NDArray[float64], ("G", "G")]
- mgxs_chi_d: Annotated[NDArray[float64], ("J", "G")]
-
- def __init__(
- self,
- name: str = "",
- capture: NDArray[float64] | NoneType = None,
- scatter: NDArray[float64] | NoneType = None,
- fission: NDArray[float64] | NoneType = None,
- nu_s: NDArray[float64] | NoneType = None,
- nu_p: NDArray[float64] | NoneType = None,
- nu_d: NDArray[float64] | NoneType = None,
- chi_p: NDArray[float64] | NoneType = None,
- chi_d: NDArray[float64] | NoneType = None,
- speed: NDArray[float64] | NoneType = None,
- decay_rate: NDArray[float64] | NoneType = None,
- ):
- type_ = MATERIAL_MG
- super().__init__(type_, name)
-
- # Energy group size
- if capture is not None:
- G = len(capture)
- elif scatter is not None:
- G = len(scatter)
- elif fission is not None:
- G = len(fission)
- else:
- print_error("Need to supply capture, scatter, or fission for MaterialMG")
- self.G = G
-
- # Delayed group size
- J = 0
- if nu_d is not None:
- J = len(nu_d)
- self.J = J
-
- # Allocate the attributes
- self.mgxs_speed = np.ones(G)
- self.mgxs_decay_rate = np.ones(J) * np.inf
- self.mgxs_capture = np.zeros(G)
- self.mgxs_scatter = np.zeros(G)
- self.mgxs_fission = np.zeros(G)
- self.mgxs_total = np.zeros(G)
- self.mgxs_nu_s = np.ones(G)
- self.mgxs_nu_p = np.zeros(G)
- self.mgxs_nu_d = np.zeros([G, J])
- self.mgxs_nu_d_total = np.zeros([G])
- self.mgxs_nu_f = np.zeros(G)
- self.mgxs_chi_s = np.zeros([G, G])
- self.mgxs_chi_p = np.zeros([G, G])
- self.mgxs_chi_d = np.zeros([J, G])
-
- # Speed (vector of size G)
- if speed is not None:
- self.mgxs_speed = speed
-
- # Decay constant (vector of size J)
- if decay_rate is not None:
- self.mgxs_decay_rate = decay_rate
-
- # Cross-sections (vector of size G)
- if capture is not None:
- self.mgxs_capture = capture
- if scatter is not None:
- self.mgxs_scatter = np.sum(scatter, 0)
- if fission is not None:
- self.mgxs_fission = fission
- self.fissionable = True
- self.mgxs_total = self.mgxs_capture + self.mgxs_scatter + self.mgxs_fission
-
- # Scattering multiplication (vector of size G)
- if nu_s is not None:
- self.mgxs_nu_s = nu_s
-
- # Check if nu_p or nu_d is not provided, give fission
- if fission is not None:
- if nu_p is None and nu_d is None:
- print_error("Need to supply nu_p or nu_d for fissionable MaterialMG")
-
- # Prompt fission production (vector of size G)
- if nu_p is not None:
- self.mgxs_nu_p = nu_p
-
- # Delayed fission production (matrix of size GxJ)
- if nu_d is not None:
- # Transpose: [dg, gin] -> [gin, dg]
- self.mgxs_nu_d = np.swapaxes(nu_d, 0, 1)[:, :]
- self.mgxs_nu_d_total = np.sum(self.mgxs_nu_d, axis=1)
-
- # Total fission production (vector of size G)
- self.mgxs_nu_f = np.zeros_like(self.mgxs_nu_p)
- self.mgxs_nu_f += self.mgxs_nu_p
- for j in range(J):
- self.mgxs_nu_f += self.mgxs_nu_d[:, j]
-
- # Scattering spectrum (matrix of size GxG)
- if scatter is not None:
- # Transpose: [gout, gin] -> [gin, gout]
- self.mgxs_chi_s = np.swapaxes(scatter, 0, 1)[:, :]
- for g in range(G):
- if self.mgxs_scatter[g] > 0.0:
- self.mgxs_chi_s[g, :] /= self.mgxs_scatter[g]
-
- # Prompt fission spectrum (matrix of size GxG)
- if nu_p is not None:
- if G == 1:
- self.mgxs_chi_p[:, :] = np.array([[1.0]])
- elif chi_p is None:
- print_error("Need to supply chi_p if nu_p is provided and G > 1")
- else:
- # Convert 1D spectrum to 2D
- if chi_p.ndim == 1:
- tmp = np.zeros((G, G))
- for g in range(G):
- tmp[:, g] = chi_p
- chi_p = tmp
- # Transpose: [gout, gin] -> [gin, gout]
- self.mgxs_chi_p[:, :] = np.swapaxes(chi_p, 0, 1)[:, :]
- # Normalize
- for g in range(G):
- if np.sum(self.mgxs_chi_p[g, :]) > 0.0:
- self.mgxs_chi_p[g, :] /= np.sum(self.mgxs_chi_p[g, :])
-
- # Delayed fission spectrum (matrix of size JxG)
- if nu_d is not None:
- if G == 1:
- self.mgxs_chi_d = np.ones([J, G])
- else:
- if chi_d is None:
- print_error("Need to supply chi_d if nu_d is provided and G > 1")
- # Transpose: [gout, dg] -> [dg, gout]
- self.mgxs_chi_d = np.swapaxes(chi_d, 0, 1)[:, :]
- # Normalize
- for dg in range(J):
- if np.sum(self.mgxs_chi_d[dg, :]) > 0.0:
- self.mgxs_chi_d[dg, :] /= np.sum(self.mgxs_chi_d[dg, :])
-
- def __repr__(self):
- text = super().__repr__()
- text += f" - Multigroup data\n"
- text += f" - G: {self.G}\n"
- text += f" - J: {self.J}\n"
- text += f" - Sigma_c {print_1d_array(self.mgxs_capture)}\n"
- text += f" - Sigma_s {print_1d_array(self.mgxs_scatter)}\n"
- text += f" - Sigma_f {print_1d_array(self.mgxs_fission)}\n"
- text += f" - nu_s {print_1d_array(self.mgxs_nu_s)}\n"
- text += f" - nu_p {print_1d_array(self.mgxs_nu_p)}\n"
- text += f" - nu_d {print_1d_array(self.mgxs_nu_d.flatten())}\n"
- text += f" - chi_s {print_1d_array(self.mgxs_chi_s.flatten())}\n"
- text += f" - chi_fp {print_1d_array(self.mgxs_chi_p.flatten())}\n"
- text += f" - chi_fd {print_1d_array(self.mgxs_chi_d.flatten())}\n"
- text += f" - speed {print_1d_array(self.mgxs_speed)}\n"
- text += f" - lambda {print_1d_array(self.mgxs_decay_rate)}\n"
- return text
-
-
-def set_nuclides_from_elements(material):
+def set_nuclides_from_elements(material, simulation):
+ """Expand an elemental composition and register its natural isotopes."""
material.nuclides = []
material.nuclide_composition = {}
nuclide_densities = []
- # Get supported temperature
- nearest_temperature = min(TEMPERATURES, key=lambda x: abs(x - material.temperature))
+ # Select the nearest supported native-data temperature
+ nearest_temperature = _get_supported_temperature(material.temperature)
+ # Expand each natural element into its normalized isotopic composition
for element, element_density in material.element_composition.items():
- # To make sure that the abundance is normalized
- norm = 0.0
- for abundance in ISOTOPIC_ABUNDANCE[element.name].values():
- norm += abundance
+ norm = sum(ISOTOPIC_ABUNDANCE[element.name].values())
- # Loop over the nuclide composition
for nuclide_name, abundance in ISOTOPIC_ABUNDANCE[element.name].items():
- # Check if nuclide is already created
- found = False
- for nuclide in simulation.nuclides:
- if (
- nuclide.name == nuclide_name
- and nearest_temperature == nuclide.temperature
- ):
- found = True
- break
-
- # Create the nuclide object if needed
- if not found:
- nuclide = Nuclide(nuclide_name, nearest_temperature)
-
- # Calculate nuclide density
- nuclide_density = element_density * abundance / norm
+ nuclide = _get_or_create_nuclide(
+ nuclide_name, nearest_temperature, simulation
+ )
+ nuclide._compile_into_simulation(simulation)
- # Register the nuclide composition
+ nuclide_density = element_density * abundance / norm
material.nuclides.append(nuclide)
nuclide_densities.append(nuclide_density)
material.nuclide_composition[nuclide] = nuclide_density
- material.nuclide_densities = np.array(nuclide_densities)
+ material.nuclide_densities = np.asarray(nuclide_densities, dtype=float64)
-def set_elements_from_nuclides(material):
+def set_elements_from_nuclides(material, simulation):
+ """Collapse a nuclide composition and register its elements."""
material.elements = []
material.element_composition = {}
- # Get the list of the element names
+ # Gather the unique element names represented by the nuclides
element_names = []
for nuclide in material.nuclides:
element_name = nuclide.name[:2]
@@ -500,31 +346,18 @@ def set_elements_from_nuclides(material):
element_name = element_name[0]
if element_name not in element_names:
element_names.append(element_name)
- element_densities = np.zeros(len(element_names))
+ element_densities = np.zeros(len(element_names), dtype=float64)
- # Iterate over all named elements
+ # Accumulate each element's density and register its canonical object
for i, element_name in enumerate(element_names):
- # Check if element is already created
- found = False
- for element in simulation.elements:
- if element.name == element_name:
- found = True
- break
-
- # Create the element object if needed
- if not found:
- element = Element(element_name)
-
+ element = _get_or_create_element(element_name, simulation)
+ element._compile_into_simulation(simulation)
material.elements.append(element)
- # Iterate over all nuclides to get the total density
density = 0.0
for nuclide, nuclide_density in material.nuclide_composition.items():
- # Skip if non-isotope
if nuclide.name[: len(element_name)] != element_name:
continue
-
- # Accumulate density
density += nuclide_density
element_densities[i] = density
@@ -533,9 +366,26 @@ def set_elements_from_nuclides(material):
material.element_densities = element_densities
+def _get_supported_temperature(temperature):
+ return min(TEMPERATURES, key=lambda value: abs(value - temperature))
+
+
+def _get_or_create_element(element_name, simulation, candidate=None):
+ for element in simulation.elements:
+ if element.name == element_name:
+ return element
+ return candidate or Element(element_name)
+
+
+def _get_or_create_nuclide(nuclide_name, temperature, simulation, candidate=None):
+ for nuclide in simulation.nuclides:
+ if nuclide.name == nuclide_name and nuclide.temperature == temperature:
+ return nuclide
+ return candidate or Nuclide(nuclide_name, temperature)
+
+
def update_fissionable_from_nuclides(material):
- material.fissionable = False
- for nuclide in material.nuclides:
- if nuclide.fissionable:
- material.fissionable = True
- break
+ """Update fissionability from native and multigroup neutron data."""
+ material.fissionable = material.neutron_multigroup.fissionable or any(
+ nuclide.fissionable for nuclide in material.nuclides
+ )
diff --git a/mcdc/object_/mesh.py b/mcdc/object_/mesh.py
index 6307b6ef0..b2ed3779d 100644
--- a/mcdc/object_/mesh.py
+++ b/mcdc/object_/mesh.py
@@ -7,7 +7,7 @@
####
from mcdc.constant import INF, MESH_STRUCTURED, MESH_UNIFORM
-from mcdc.object_.base import ObjectPolymorphic
+from mcdc.object_.base import MCDCPolymorphic
from mcdc.print_ import print_1d_array
# ======================================================================================
@@ -15,79 +15,76 @@
# ======================================================================================
-class MeshBase(ObjectPolymorphic):
- # Annotations for Numba mode
- label: str = "mesh"
- #
+class MeshBase(MCDCPolymorphic):
+ """Base class for spatial meshes used by tallies and techniques."""
+
+ # MC/DC framework metadata
+ label = "mesh"
+ sub_type = -1 # Polymorphic base
+
name: str
N_bin: int
Nx: int
Ny: int
Nz: int
- def __init__(self, type_, name):
- super().__init__(type_)
-
- # Set name
- if name != "":
- self.name = name
- else:
- self.name = f"{self.label}_{self.child_ID}"
+ def __init__(self, name: str) -> None:
+ super().__init__()
+ self.name = name or "(Unnamed mesh)"
self.N_bin = 0
- def __repr__(self):
- text = "\n"
- text += f"{decode_type(self.type)}\n"
- text += f" - ID: {self.ID}\n"
+ def __repr__(self) -> str:
+ text = super().__repr__()
+
text += f" - Name: {self.name}\n"
text += f" - # of bins: {self.N_bin}\n"
return text
-def decode_type(type_):
- if type_ == MESH_UNIFORM:
- return "Uniform mesh"
- elif type_ == MESH_STRUCTURED:
- return "Structured mesh"
-
-
# ======================================================================================
# Uniform mesh
# ======================================================================================
class MeshUniform(MeshBase):
- """
- Define a uniform rectilinear mesh.
-
- Each axis is specified as ``(origin, width, N_bins)``.
+ """Uniform spatial bins for simulation tallies and transport techniques.
Parameters
----------
name : str, optional
- User label.
- x : tuple of (float, float, int), optional
- ``(x0, dx, Nx)`` — origin, bin width, and number of bins along x.
- y : tuple of (float, float, int), optional
- ``(y0, dy, Ny)`` — origin, bin width, and number of bins along y.
- z : tuple of (float, float, int), optional
- ``(z0, dz, Nz)`` — origin, bin width, and number of bins along z.
-
- Returns
- -------
- MeshUniform
- The uniform mesh object.
-
- See Also
+ User-facing mesh name.
+ x, y, z : tuple of (float, float, int), optional
+ ``(origin, spacing, number_of_bins)`` for each axis, in cm. Omitted
+ axes default to one effectively unbounded bin.
+
+ Examples
--------
- mcdc.MeshStructured : Creates a mesh with arbitrary bin edges.
- mcdc.TallyMesh : Creates a tally on a mesh.
+ Create 100 uniform bins along z from 0 to 10 cm:
+
+ >>> import mcdc
+ >>> mesh = mcdc.MeshUniform(z=(0.0, 0.1, 100))
+
+ Create a two-dimensional x-y mesh:
+
+ >>> mesh_xy = mcdc.MeshUniform(
+ ... x=(-5.0, 0.5, 20),
+ ... y=(-5.0, 0.5, 20),
+ ... )
+
+ Create a three-dimensional mesh with different axis spacings:
+
+ >>> mesh_xyz = mcdc.MeshUniform(
+ ... x=(0.0, 1.0, 10),
+ ... y=(0.0, 2.0, 5),
+ ... z=(-3.0, 0.25, 24),
+ ... )
"""
- # Annotations for Numba mode
- label: str = "uniform_mesh"
- #
+ # MC/DC framework metadata
+ label = "uniform_mesh"
+ sub_type = MESH_UNIFORM
+
x0: float
dx: float
Nx: int
@@ -104,9 +101,10 @@ def __init__(
x: tuple[float, float, int] = (-INF, 2 * INF, 1),
y: tuple[float, float, int] = (-INF, 2 * INF, 1),
z: tuple[float, float, int] = (-INF, 2 * INF, 1),
- ):
- type_ = MESH_UNIFORM
- super().__init__(type_, name)
+ ) -> None:
+ super().__init__(
+ name,
+ )
# Set the grid
self.x0 = x[0]
@@ -121,7 +119,7 @@ def __init__(
self.N_bin = self.Nx * self.Ny * self.Nz
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
text += f" - Grid specification\n"
text += f" - (x0, dx, Nx): ({self.x0}, {self.dx}, {self.Nx}) [cm]\n"
@@ -136,34 +134,44 @@ def __repr__(self):
class MeshStructured(MeshBase):
- """
- Define a structured rectilinear mesh with arbitrary bin edges.
+ """Structured spatial bins for simulation tallies and transport techniques.
Parameters
----------
name : str, optional
- User label.
- x : array_like of float, optional
- Bin edges along x (cm).
- y : array_like of float, optional
- Bin edges along y (cm).
- z : array_like of float, optional
- Bin edges along z (cm).
-
- Returns
- -------
- MeshStructured
- The structured mesh object.
-
- See Also
+ User-facing mesh name.
+ x, y, z : sequence of float, optional
+ Strictly ordered grid boundaries in cm. Each omitted axis defaults to
+ one effectively unbounded bin.
+
+ Examples
--------
- mcdc.MeshUniform : Creates a uniform mesh.
- mcdc.TallyMesh : Creates a tally on a mesh.
+ Create nonuniform bins along z:
+
+ >>> import mcdc
+ >>> mesh = mcdc.MeshStructured(z=[0.0, 0.5, 2.0, 10.0])
+
+ Create a two-dimensional mesh from explicit boundaries:
+
+ >>> mesh_xy = mcdc.MeshStructured(
+ ... x=[-2.0, -1.0, 0.0, 2.0],
+ ... y=[-3.0, 0.0, 1.0, 3.0],
+ ... )
+
+ Mix uniformly generated and explicitly listed boundaries:
+
+ >>> import numpy as np
+ >>> mesh_xyz = mcdc.MeshStructured(
+ ... x=np.linspace(-5.0, 5.0, 21),
+ ... y=[-1.0, 0.0, 1.0],
+ ... z=np.linspace(0.0, 10.0, 101),
+ ... )
"""
- # Annotations for Numba mode
- label: str = "structured_mesh"
- #
+ # MC/DC framework metadata
+ label = "structured_mesh"
+ sub_type = MESH_STRUCTURED
+
x: NDArray[float64]
y: NDArray[float64]
z: NDArray[float64]
@@ -171,12 +179,11 @@ class MeshStructured(MeshBase):
def __init__(
self,
name: str = "",
- x: Sequence[float] = [-INF, INF],
- y: Sequence[float] = [-INF, INF],
- z: Sequence[float] = [-INF, INF],
- ):
- type_ = MESH_STRUCTURED
- super().__init__(type_, name)
+ x: Sequence[float] | NDArray[float64] = np.array([-INF, INF]),
+ y: Sequence[float] | NDArray[float64] = np.array([-INF, INF]),
+ z: Sequence[float] | NDArray[float64] = np.array([-INF, INF]),
+ ) -> None:
+ super().__init__(name)
# Set the grid
self.x = np.array(x)
@@ -189,7 +196,7 @@ def __init__(
self.N_bin = self.Nx * self.Ny * self.Nz
- def __repr__(self):
+ def __repr__(self) -> str:
text = super().__repr__()
text += f" - Grid specification\n"
text += f" - x {print_1d_array(self.x)} cm\n"
diff --git a/mcdc/object_/neutron_reaction.py b/mcdc/object_/neutron_reaction.py
index 4faac2db3..6e1dc87a3 100644
--- a/mcdc/object_/neutron_reaction.py
+++ b/mcdc/object_/neutron_reaction.py
@@ -10,8 +10,6 @@
ANGLE_ISOTROPIC,
ANGLE_ENERGY_CORRELATED,
ANGLE_DISTRIBUTED,
- INTERPOLATION_LINEAR,
- INTERPOLATION_LOG,
NEUTRON_REACTION_CAPTURE,
NEUTRON_REACTION_ELASTIC_SCATTERING,
NEUTRON_REACTION_FISSION,
@@ -19,7 +17,7 @@
REFERENCE_FRAME_COM,
REFERENCE_FRAME_LAB,
)
-from mcdc.object_.base import ObjectPolymorphic
+from mcdc.object_.base import MCDCPolymorphic
from mcdc.object_.data import encode_interpolation
from mcdc.object_.distribution import (
DistributionBase,
@@ -31,7 +29,6 @@
DistributionTabulatedEnergyAngle,
DistributionNBody,
)
-from mcdc.object_.simulation import simulation
from mcdc.print_ import print_1d_array, print_error
# ======================================================================================
@@ -39,18 +36,25 @@
# ======================================================================================
-class NeutronReactionBase(ObjectPolymorphic):
- # Annotations for Numba mode
- label: str = "neutron_reaction"
- #
+class NeutronReactionBase(MCDCPolymorphic):
+ """Base neutron reaction data loaded from the HDF5 physics library.
+
+ Stores the ENDF reaction identifier, cross-section segment and offset,
+ reaction reference frame, and Q value.
+ """
+
+ # MC/DC framework metadata
+ label = "neutron_reaction"
+ sub_type = -1 # Polymorphic base
+
MT: int
xs: NDArray[float64]
xs_offset_: int # "xs_offset" ir reserved for "xs"
reference_frame: int
q_value: float64
- def __init__(self, type_, MT, xs, xs_offset, reference_frame, q_value):
- super().__init__(type_)
+ def __init__(self, MT, xs, xs_offset, reference_frame, q_value):
+ super().__init__()
self.MT = MT
self.xs = xs
self.xs_offset_ = xs_offset
@@ -58,8 +62,8 @@ def __init__(self, type_, MT, xs, xs_offset, reference_frame, q_value):
self.q_value = q_value
def __repr__(self):
- text = "\n"
- text += f"{decode_type(self.type)}\n"
+ text = super().__repr__()
+
text += f" - ID: {self.ID}\n"
text += f" - MT: {self.MT}\n"
text += f" - XS {print_1d_array(self.xs)} barn\n"
@@ -68,18 +72,9 @@ def __repr__(self):
return text
-def decode_type(type_):
- if type_ == NEUTRON_REACTION_ELASTIC_SCATTERING:
- return "Neutron elastic scattering"
- elif type_ == NEUTRON_REACTION_CAPTURE:
- return "Neutron capture"
- elif type_ == NEUTRON_REACTION_INELASTIC_SCATTERING:
- return "Neutron inelastic scattering"
- elif type_ == NEUTRON_REACTION_FISSION:
- return "Neutron fission"
-
-
def decode_reference_frame(type_):
+ """Return the display name for a packed reference-frame code."""
+
if type_ == REFERENCE_FRAME_LAB:
return "Laboratory"
elif type_ == REFERENCE_FRAME_COM:
@@ -92,20 +87,25 @@ def decode_reference_frame(type_):
class NeutronReactionElasticScattering(NeutronReactionBase):
- # Annotations for Numba mode
- label: str = "neutron_elastic_scattering_reaction"
- #
+ """Elastic neutron scattering with incident-energy-dependent cosine data."""
+
+ # MC/DC framework metadata
+ label = "neutron_elastic_scattering_reaction"
+ sub_type = NEUTRON_REACTION_ELASTIC_SCATTERING
+
mu_table: DistributionMultiTable
def __init__(self, MT, xs, xs_offset, reference_frame, mu):
- type_ = NEUTRON_REACTION_ELASTIC_SCATTERING
- super().__init__(type_, MT, xs, xs_offset, reference_frame, 0.0)
+ super().__init__(MT, xs, xs_offset, reference_frame, 0.0)
self.mu_table = mu
@classmethod
- def from_h5_group(cls, h5_group):
+ def from_h5_group(cls, h5_group, simulation):
+ """Build an elastic-scattering reaction from a library HDF5 group."""
MT, xs, xs_offset, reference_frame, _ = set_basic_properties(h5_group)
- _, mu = set_angular_distribution(h5_group["angular_cosine_distribution"])
+ _, mu = set_angular_distribution(
+ h5_group["angular_cosine_distribution"], simulation
+ )
return cls(MT, xs, xs_offset, reference_frame, mu)
def __repr__(self):
@@ -120,15 +120,18 @@ def __repr__(self):
class NeutronReactionCapture(NeutronReactionBase):
- # Annotations for Numba mode
- label: str = "neutron_capture_reaction"
+ """Neutron capture reaction."""
+
+ # MC/DC framework metadata
+ label = "neutron_capture_reaction"
+ sub_type = NEUTRON_REACTION_CAPTURE
def __init__(self, MT, xs, xs_offset, reference_frame, q_value):
- type_ = NEUTRON_REACTION_CAPTURE
- super().__init__(type_, MT, xs, xs_offset, reference_frame, q_value)
+ super().__init__(MT, xs, xs_offset, reference_frame, q_value)
@classmethod
- def from_h5_group(cls, h5_group):
+ def from_h5_group(cls, h5_group, simulation):
+ """Build a capture reaction from a library HDF5 group."""
MT, xs, xs_offset, reference_frame, q_value = set_basic_properties(h5_group)
return cls(MT, xs, xs_offset, reference_frame, q_value)
@@ -139,9 +142,12 @@ def from_h5_group(cls, h5_group):
class NeutronReactionInelasticScattering(NeutronReactionBase):
- # Annotations for Numba mode
- label: str = "neutron_inelastic_scattering_reaction"
- #
+ """Inelastic scattering with angular data and one or more energy spectra."""
+
+ # MC/DC framework metadata
+ label = "neutron_inelastic_scattering_reaction"
+ sub_type = NEUTRON_REACTION_INELASTIC_SCATTERING
+
multiplicity: int
angle_type: int
mu: DistributionBase
@@ -167,8 +173,7 @@ def __init__(
spectrum_probability,
energy_spectra,
):
- type_ = NEUTRON_REACTION_INELASTIC_SCATTERING
- super().__init__(type_, MT, xs, xs_offset, reference_frame, q_value)
+ super().__init__(MT, xs, xs_offset, reference_frame, q_value)
self.multiplicity = multiplicity
self.angle_type = angle_type
@@ -180,12 +185,13 @@ def __init__(
self.energy_spectra = energy_spectra
@classmethod
- def from_h5_group(cls, h5_group):
+ def from_h5_group(cls, h5_group, simulation):
+ """Build an inelastic-scattering reaction from a library HDF5 group."""
MT, xs, xs_offset, reference_frame, q_value = set_basic_properties(h5_group)
multiplicity = int(h5_group["multiplicity"][()])
angle_type, mu = set_angular_distribution(
- h5_group["angular_cosine_distribution"]
+ h5_group["angular_cosine_distribution"], simulation
)
# Energy spectra
@@ -233,9 +239,12 @@ def __repr__(self):
class NeutronReactionFission(NeutronReactionBase):
- # Annotations for Numba mode
- label: str = "neutron_fission_reaction"
- #
+ """Fission reaction with prompt angular and energy distributions."""
+
+ # MC/DC framework metadata
+ label = "neutron_fission_reaction"
+ sub_type = NEUTRON_REACTION_FISSION
+
angle_type: int
mu: DistributionBase
spectrum: DistributionBase
@@ -251,19 +260,19 @@ def __init__(
mu,
spectrum,
):
- type_ = NEUTRON_REACTION_FISSION
- super().__init__(type_, MT, xs, xs_offset, reference_frame, q_value)
+ super().__init__(MT, xs, xs_offset, reference_frame, q_value)
self.angle_type = angle_type
self.mu = mu
self.spectrum = spectrum
@classmethod
- def from_h5_group(cls, h5_group):
+ def from_h5_group(cls, h5_group, simulation):
+ """Build a fission reaction from a library HDF5 group."""
MT, xs, xs_offset, reference_frame, q_value = set_basic_properties(h5_group)
# Prompt angular distribution
angle_type, mu = set_angular_distribution(
- h5_group["angular_cosine_distribution"]
+ h5_group["angular_cosine_distribution"], simulation
)
# Prompt spectrum
@@ -296,6 +305,8 @@ def __repr__(self):
def set_basic_properties(h5_group):
+ """Read properties shared by all neutron reactions from an HDF5 group."""
+
MT = h5_group.attrs["MT"][()]
xs = h5_group["xs"][()]
xs_offset = h5_group["xs"].attrs["offset"]
@@ -308,7 +319,9 @@ def set_basic_properties(h5_group):
return MT, xs, xs_offset, reference_frame, q_value
-def set_angular_distribution(h5_group):
+def set_angular_distribution(h5_group, simulation):
+ """Create the packed angle type and distribution from an HDF5 group."""
+
mu_type = h5_group.attrs["type"]
if mu_type == "isotropic":
angle_type = ANGLE_ISOTROPIC
@@ -328,6 +341,8 @@ def set_angular_distribution(h5_group):
def set_energy_distribution(h5_group):
+ """Create an outgoing-energy distribution from an HDF5 group."""
+
spectrum_type = h5_group.attrs["type"]
if spectrum_type == "tabulated":
diff --git a/mcdc/object_/nuclide.py b/mcdc/object_/nuclide.py
index 80ca3e669..34f6f13f9 100644
--- a/mcdc/object_/nuclide.py
+++ b/mcdc/object_/nuclide.py
@@ -8,7 +8,7 @@
####
from mcdc.constant import INTERPOLATION_LINEAR
-from mcdc.object_.base import ObjectNonSingleton
+from mcdc.object_.base import MCDCObject
from mcdc.object_.data import DataBase, DataPolynomial, DataTable
from mcdc.object_.distribution import DistributionBase
from mcdc.object_.neutron_reaction import (
@@ -18,7 +18,6 @@
NeutronReactionInelasticScattering,
set_energy_distribution,
)
-from mcdc.object_.simulation import simulation
from mcdc.print_ import print_1d_array, print_error
# ======================================================================================
@@ -26,10 +25,27 @@
# ======================================================================================
-class Nuclide(ObjectNonSingleton):
- # Annotations for Numba mode
- label: str = "nuclide"
- #
+class Nuclide(MCDCObject):
+ """Temperature-specific nuclide definition for the MC/DC HDF5 library.
+
+ Parameters
+ ----------
+ nuclide_name : str
+ Nuclide identifier, such as ``"U235"``.
+ temperature : float
+ Library temperature in kelvin.
+
+ Notes
+ -----
+ Construction records the nuclide identity without accessing the data
+ library. Basic properties are loaded when the nuclide is compiled into a
+ simulation. Neutron cross sections, reactions, multiplicities, and
+ delayed-neutron data are loaded later by :meth:`set_neutron_data`.
+ """
+
+ # MC/DC framework metadata
+ label = "nuclide"
+
name: str
temperature: float
atomic_number: int
@@ -63,18 +79,40 @@ def __init__(self, nuclide_name, temperature):
self.name = nuclide_name
self.temperature = temperature
- # Basic properties
+ def _compile_into_simulation(self, simulation) -> bool:
+ """Load basic properties and register with the owning simulation."""
+ if self.compile_ID == simulation.compile_ID:
+ return False
+
dir_name = os.getenv("MCDC_LIB")
- file_name = f"{nuclide_name}-{temperature}K.h5"
- file = h5py.File(f"{dir_name}/{file_name}", "r")
- self.atomic_number = int(file["atomic_number"][()])
- self.mass_number = int(file["mass_number"][()])
- self.atomic_weight_ratio = file["atomic_weight_ratio"][()]
- self.fissionable = bool(file["fissionable"][()])
- self.excitation_level = int(file["excitation_level"][()])
- file.close()
+ if dir_name is None:
+ print_error("Environment variable MCDC_LIB is not set")
+
+ file_name = f"{self.name}-{self.temperature}K.h5"
+ file_path = os.path.join(dir_name, file_name)
+ if not os.path.isfile(file_path):
+ print_error(
+ f"Nuclide {self.name} at temperature {self.temperature} K "
+ "is not available in the library"
+ )
+
+ with h5py.File(file_path, "r") as file:
+ self.atomic_number = int(file["atomic_number"][()])
+ self.mass_number = int(file["mass_number"][()])
+ self.atomic_weight_ratio = file["atomic_weight_ratio"][()]
+ self.fissionable = bool(file["fissionable"][()])
+ self.excitation_level = int(file["excitation_level"][()])
+
+ return super()._compile_into_simulation(simulation)
- def set_neutron_data(self):
+ def set_neutron_data(self, simulation):
+ """Load and attach neutron physics data from ``MCDC_LIB``.
+
+ Parameters
+ ----------
+ simulation : Simulation
+ Simulation that owns placeholder data and compiled distributions.
+ """
nuclide_name = self.name
temperature = self.temperature
@@ -161,7 +199,7 @@ def set_neutron_data(self):
):
for MT in MTs[rx_name]:
h5_group = file[f"neutron_reactions/{rx_name}/{MT}"]
- reaction = rx_class.from_h5_group(h5_group)
+ reaction = rx_class.from_h5_group(h5_group, simulation)
rx_container.append(reaction)
# ==============================================================================
@@ -213,6 +251,15 @@ def set_neutron_data(self):
file.close()
+ # Register data loaded during object-model finalization.
+ for reaction_container in rx_containers:
+ for reaction in reaction_container:
+ reaction._compile_into_simulation(simulation)
+ self.neutron_fission_prompt_multiplicity._compile_into_simulation(simulation)
+ self.neutron_fission_delayed_multiplicity._compile_into_simulation(simulation)
+ for spectrum in self.neutron_fission_delayed_spectra:
+ spectrum._compile_into_simulation(simulation)
+
def __repr__(self):
text = "\n"
text += f"Nuclide\n"
@@ -248,6 +295,8 @@ def __repr__(self):
def set_fission_multiplicity(h5_group):
+ """Build tabulated or polynomial fission multiplicity from HDF5 data."""
+
multiplicity_type = h5_group.attrs["type"]
if multiplicity_type == "tabulated":
diff --git a/mcdc/object_/particle.py b/mcdc/object_/particle.py
index 3d9fa43f5..c28b925f0 100644
--- a/mcdc/object_/particle.py
+++ b/mcdc/object_/particle.py
@@ -1,6 +1,6 @@
import numpy as np
-from dataclasses import dataclass, field
+from dataclasses import dataclass
from typing import Annotated
from numpy import int64, uint64
from numpy.typing import NDArray
@@ -8,12 +8,16 @@
####
from mcdc.constant import PARTICLE_NEUTRON
-from mcdc.object_.base import ObjectBase, ObjectSingleton
+from mcdc.object_.base import MCDCBase
@dataclass
-class ParticleData(ObjectBase):
- label: str = "particle_data"
+class ParticleData(MCDCBase):
+ """Serializable phase-space state stored in particle banks."""
+
+ # MC/DC framework metadata
+ label = "particle_data"
+
x: float = 0.0
y: float = 0.0
z: float = 0.0
@@ -21,7 +25,6 @@ class ParticleData(ObjectBase):
ux: float = 0.0
uy: float = 0.0
uz: float = 0.0
- g: int = -1
E: float = 0.0
w: float = 0.0
particle_type: int = PARTICLE_NEUTRON
@@ -29,14 +32,22 @@ class ParticleData(ObjectBase):
@dataclass
-class CollisionData(ObjectBase):
- label: str = "collision_data"
+class CollisionData(MCDCBase):
+ """Per-collision values passed from physics to tally scoring."""
+
+ # MC/DC framework metadata
+ label = "collision_data"
+
energy_deposition: float = 0.0
@dataclass
class Particle(ParticleData):
- label: str = "particle"
+ """Active transport particle with geometry and event-tracking state."""
+
+ # MC/DC framework metadata
+ label = "particle"
+
cell_ID: int = -1
material_ID: int = -1
surface_ID: int = -1
@@ -45,14 +56,24 @@ class Particle(ParticleData):
event: int = -1
-class ParticleBank(ObjectSingleton):
- label: str = "particle_bank"
- non_numba: list[str] = ["particles"]
- particles: list[ParticleData] = []
+class ParticleBank(MCDCBase):
+ """Particle storage metadata used by the compiled runtime.
+
+ Parameters
+ ----------
+ tag : str
+ Bank role, such as ``"active"``, ``"source"``, ``"census"``, or
+ ``"future"``.
+ """
+
+ # MC/DC framework metadata
+ label = "particle_bank"
+ non_numba = ["particles"]
+
+ particles: list[ParticleData] = [] # Non-numba
size: Annotated[NDArray[int64], (1,)]
tag: str = ""
def __init__(self, tag):
- super().__init__()
self.tag = tag
self.size = np.zeros(1, dtype=int64)
diff --git a/mcdc/object_/settings.py b/mcdc/object_/settings.py
index 60a28d896..b8b4207ae 100644
--- a/mcdc/object_/settings.py
+++ b/mcdc/object_/settings.py
@@ -1,15 +1,13 @@
-from typing import List
import h5py
-from h5py._hl.dataset import sel
import numpy as np
from dataclasses import dataclass, field
-from numpy.typing import NDArray
+from numpy.typing import ArrayLike, NDArray
####
from mcdc.constant import *
-from mcdc.object_.base import ObjectSingleton
+from mcdc.object_.base import MCDCBase
from mcdc.object_.util import is_sorted
from mcdc.print_ import print_error
@@ -19,13 +17,21 @@
@dataclass
-class Settings(ObjectSingleton):
- # Annotations for Numba mode
- label: str = "settings"
+class Settings(MCDCBase):
+ """Execution and transport settings owned by a simulation."""
+
+ # MC/DC framework metadata
+ label = "settings"
# Basic
+ #: Number of particle histories simulated per batch or eigenvalue cycle.
+ #: The default is ``0``.
N_particle: int = 0
+ #: Number of statistically independent fixed-source batches. The default
+ #: is ``1``.
N_batch: int = 1
+ #: Seed used to initialize the pseudorandom-number generator. The default
+ #: is ``1``.
rng_seed: int = 1
# k-eigenvalue
@@ -41,8 +47,12 @@ class Settings(ObjectSingleton):
source_file_name: str = ""
# Misc.
+ #: Time in seconds at which particle transport terminates. The default is
+ #: infinity.
time_boundary: float = np.inf
+ #: Base name used for the HDF5 output file. The default is ``"output"``.
output_name: str = "output"
+ #: Whether to display transport progress. The default is ``True``.
use_progress_bar: bool = True
# Time census
@@ -53,9 +63,17 @@ class Settings(ObjectSingleton):
# Particle bank-related
save_particle: bool = False
+ #: Additional particle capacity allocated for the active bank. The default
+ #: is ``100``.
active_bank_buffer: int = 100
+ #: Capacity multiplier used when allocating the census bank. The default
+ #: is ``2.0``.
census_bank_buffer_ratio: float = 2.0
+ #: Capacity multiplier used when allocating the source bank. The default
+ #: is ``2.0``.
source_bank_buffer_ratio: float = 2.0
+ #: Capacity multiplier used when allocating the future bank. The default
+ #: is ``1.5``.
future_bank_buffer_ratio: float = 1.5
# Multi-particle options
@@ -64,7 +82,6 @@ class Settings(ObjectSingleton):
proton_transport: bool = False
# Neutron transport modes
- neutron_multigroup_mode: bool = False
neutron_eigenvalue_mode: bool = False
# GPU mode
@@ -72,10 +89,37 @@ class Settings(ObjectSingleton):
gpu_async_type: int = GPU_ASYNC_SIMPLE
gpu_storage: int = GPU_STORAGE_SEPARATE
- def __post_init__(self):
- super().__init__()
+ def set_time_census(
+ self, time: ArrayLike, tally_frequency: int | None = None
+ ) -> None:
+ """Configure census times for time-dependent transport.
+
+ Parameters
+ ----------
+ time : array_like of float
+ Positive, nondecreasing census times in seconds. An infinite final
+ census is appended automatically.
+ tally_frequency : int, optional
+ Number of tally intervals per census period. A positive value
+ enables census-based tally output.
+
+ Examples
+ --------
+ Configure explicit census times:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> simulation.settings.set_time_census(
+ ... time=[1.0e-6, 2.0e-6, 5.0e-6],
+ ... )
+
+ Enable census-based tallies with ten intervals per census period:
- def set_time_census(self, time, tally_frequency=None):
+ >>> simulation.settings.set_time_census(
+ ... time=[1.0e-6, 2.0e-6, 5.0e-6],
+ ... tally_frequency=10,
+ ... )
+ """
# Make sure that the time grid points are sorted
if not is_sorted(time):
print_error("Time census: Time grid points have to be sorted.")
@@ -93,18 +137,56 @@ def set_time_census(self, time, tally_frequency=None):
# Set the census-based tallying
if tally_frequency is not None and tally_frequency > 0:
- # Flag to reset all tallies' time grids (done in main.py)
+ # Flag to reset all tally time grids during simulation compilation
self.use_census_based_tally = True
self.census_tally_frequency = tally_frequency
def set_eigenmode(
self,
- N_inactive=0,
- N_active=0,
- k_init=1.0,
- gyration_radius=None,
- save_particle=False,
- ):
+ N_inactive: int = 0,
+ N_active: int = 0,
+ k_init: float = 1.0,
+ gyration_radius: str | None = None,
+ save_particle: bool = False,
+ ) -> None:
+ """Enable neutron k-eigenvalue mode.
+
+ Parameters
+ ----------
+ N_inactive : int, optional
+ Number of inactive cycles.
+ N_active : int, optional
+ Number of active cycles used for statistics.
+ k_init : float, optional
+ Initial multiplication-factor estimate.
+ gyration_radius : str, optional
+ Gyration-radius mode: ``"all"``, ``"infinite-x"``,
+ ``"infinite-y"``, ``"infinite-z"``, ``"only-x"``, ``"only-y"``,
+ or ``"only-z"``.
+ save_particle : bool, optional
+ Whether to save source-bank particles.
+
+ Examples
+ --------
+ Configure a standard eigenvalue calculation:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> simulation.settings.set_eigenmode(
+ ... N_inactive=20,
+ ... N_active=100,
+ ... k_init=1.0,
+ ... )
+
+ Score the source gyration radius and save source particles:
+
+ >>> simulation.settings.set_eigenmode(
+ ... N_inactive=20,
+ ... N_active=100,
+ ... gyration_radius="all",
+ ... save_particle=True,
+ ... )
+ """
# Update setting self
self.N_inactive = N_inactive
self.N_active = N_active
@@ -133,13 +215,24 @@ def set_eigenmode(
else:
print_error("Unknown gyration radius type")
- # Allocate cycle-wise quantities
- from mcdc.object_.simulation import simulation
+ def set_source_file(self, source_file_name: str) -> None:
+ """Use particles from an HDF5 source file.
+
+ The particle count is read from the file's ``particles_size`` dataset.
- simulation.k_cycle = np.zeros(self.N_cycle)
- simulation.gyration_radius = np.zeros(self.N_cycle)
+ Parameters
+ ----------
+ source_file_name : str or path-like
+ Source-particle HDF5 file.
- def set_source_file(self, source_file_name):
+ Examples
+ --------
+ Initialize a simulation from a previously written particle source:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> simulation.settings.set_source_file("source.h5")
+ """
self.use_source_file = True
self.source_file_name = source_file_name
@@ -147,7 +240,28 @@ def set_source_file(self, source_file_name):
with h5py.File(source_file_name, "r") as f:
self.N_particle = int(f["particles_size"][()])
- def set_transported_particles(self, transported_particles: List[str]):
+ def set_transported_particles(self, transported_particles: list[str]) -> None:
+ """Select the particle species enabled during transport.
+
+ Parameters
+ ----------
+ transported_particles : list of {"neutron", "electron", "proton"}
+ Particle species to enable. Species not listed are disabled.
+
+ Examples
+ --------
+ Transport neutrons only:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> simulation.settings.set_transported_particles(["neutron"])
+
+ Enable coupled neutron and electron transport:
+
+ >>> simulation.settings.set_transported_particles(
+ ... ["neutron", "electron"],
+ ... )
+ """
# Reset the flags
self.neutron_transport = False
self.electron_transport = False
diff --git a/mcdc/object_/simulation.py b/mcdc/object_/simulation.py
index 48e7e3433..bd9cbd06d 100644
--- a/mcdc/object_/simulation.py
+++ b/mcdc/object_/simulation.py
@@ -1,19 +1,14 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Annotated
-
-from mcdc.object_.technique import (
- ImplicitCapture,
- PopulationControl,
- GlobalWeightRoulette,
- WeightWindows,
- WeightedEmission,
-)
+from typing import TYPE_CHECKING, Annotated, Literal
if TYPE_CHECKING:
+ from matplotlib.typing import ColorType
+
from mcdc.object_.cell import Cell, Region
from mcdc.object_.element import Element
from mcdc.object_.electron_reaction import ElectronReactionBase
- from mcdc.object_.material import MaterialBase
+ from mcdc.object_.material import Material
+ from mcdc.object_.transport_model_data import NeutronMultigroupData
from mcdc.object_.nuclide import Nuclide
from mcdc.object_.neutron_reaction import NeutronReactionBase
from mcdc.object_.source import Source
@@ -22,21 +17,28 @@
####
-import numpy as np
+import math
+from collections.abc import Mapping, Sequence
+from os import PathLike, fspath
+import numpy as np
from mpi4py import MPI
from numpy import float64, int64
from numpy.typing import NDArray
####
-from mcdc.object_.base import ObjectSingleton
-from mcdc.object_.data import DataBase, DataNone
-from mcdc.object_.distribution import DistributionBase, DistributionNone
+from mcdc.constant import PARTICLE_NEUTRON
+from mcdc.object_.base import MCDCBase
+from mcdc.object_.data import DataBase
+from mcdc.object_.distribution import DistributionBase
from mcdc.object_.gpu_tools import GPUMeta
-from mcdc.object_.mesh import MeshBase, MeshUniform
+from mcdc.object_.mesh import MeshBase
from mcdc.object_.particle import ParticleBank
from mcdc.object_.settings import Settings
+from mcdc.object_.technique import Technique
+from mcdc.print_ import print_error
+
from mcdc.object_.universe import Universe, Lattice
# ======================================================================================
@@ -44,33 +46,86 @@
# ======================================================================================
-class Simulation(ObjectSingleton):
- # Annotations for Numba mode
- label: str = "simulation"
- non_numba: list[str] = [
+class Simulation(MCDCBase):
+ """The complete model and configuration for one MC/DC calculation.
+
+ Parameters
+ ----------
+ name : str, optional
+ User-facing simulation name.
+
+ Notes
+ -----
+ Geometry, sources, and tallies are supplied with :meth:`set_model`,
+ :meth:`set_sources`, and :meth:`set_tallies`. :meth:`compile` walks the
+ resulting object graph, assigns IDs, and finalizes object-local and
+ model-wide state before conversion to the packed arrays consumed by
+ :mod:`mcdc.transport`.
+
+ MC/DC uses one active simulation context per Python process. A model-object
+ instance belongs to one simulation, although it may be referenced multiple
+ times within that model. Construct independent object graphs and use
+ separate processes for concurrent simulations.
+
+ Each simulation owns its execution settings. Access them through
+ ``simulation.settings`` by assigning values such as
+ :attr:`settings.N_particle ` or by
+ calling configuration methods such as
+ :meth:`settings.set_eigenmode `.
+
+ Transport techniques are grouped under ``simulation.technique`` and are
+ configured through callable members such as
+ ``simulation.technique.implicit_capture()`` and
+ ``simulation.technique.weight_windows(...)``.
+
+ Examples
+ --------
+ Configure commonly adjusted settings:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation(name="Slab")
+ >>> simulation.settings.N_particle = 10_000
+ >>> simulation.settings.N_batch = 20
+ >>> simulation.settings.rng_seed = 12345
+ >>> simulation.settings.output_name = "slab"
+ """
+
+ # MC/DC framework metadata
+ label = "simulation"
+ non_numba = [
+ "_next_compile_ID",
+ "compiled",
"regions",
+ "root_universe",
"bank_active",
"bank_census",
"bank_source",
"bank_future",
]
+ _next_compile_ID: int = 1 # Non-Numba
+
+ # Basic parameters
+ name: str
+ compiled: bool # Non-Numba
# Physics
data: list[DataBase]
distributions: list[DistributionBase]
- materials: list[MaterialBase]
- elements: list[Element]
+ neutron_reactions: list[NeutronReactionBase]
electron_reactions: list[ElectronReactionBase]
nuclides: list[Nuclide]
- neutron_reactions: list[NeutronReactionBase]
+ elements: list[Element]
+ materials: list[Material]
+ neutron_multigroup_data: list[NeutronMultigroupData]
sources: list[Source]
# Geometry
- cells: list[Cell]
- lattices: list[Lattice]
- regions: list[Region]
surfaces: list[Surface]
+ regions: list[Region] # Non-Numba
+ cells: list[Cell]
universes: list[Universe]
+ root_universe: Universe # Non-Numba
+ lattices: list[Lattice]
meshes: list[MeshBase]
# Tallies
@@ -80,43 +135,45 @@ class Simulation(ObjectSingleton):
settings: Settings
# Techniques
- implicit_capture: ImplicitCapture
- weighted_emission: WeightedEmission
- global_weight_roulette: GlobalWeightRoulette
- weight_windows: WeightWindows
- population_control: PopulationControl
+ technique: Technique
# Particle banks
- bank_active: ParticleBank
- bank_census: ParticleBank
- bank_source: ParticleBank
- bank_future: ParticleBank
+ bank_active: ParticleBank # Non-Numba
+ bank_census: ParticleBank # Non-Numba
+ bank_source: ParticleBank # Non-Numba
+ bank_future: ParticleBank # Non-Numba
- # Simulation parameters
+ # Simulation indices
idx_work: int
idx_cycle: int
idx_census: int
idx_batch: int
- dd_idx: int
- dd_N_local_source: int
- dd_local_rank: int
+
+ # k-eigenvalue globals
k_eff: float
k_cycle: NDArray[float64]
k_avg: float
k_sdv: float
+ k_avg_running: float
+ k_sdv_running: float
+ #
n_avg: float
n_sdv: float
n_max: float
+ #
C_avg: float
C_sdv: float
C_max: float
- k_avg_running: float
- k_sdv_running: float
- gyration_radius: NDArray[float64]
- cycle_active: bool
+ #
eigenvalue_tally_nuSigmaF: Annotated[NDArray[float64], (1,)]
eigenvalue_tally_n: Annotated[NDArray[float64], (1,)]
eigenvalue_tally_C: Annotated[NDArray[float64], (1,)]
+ #
+ gyration_radius: NDArray[float64]
+ #
+ cycle_active: bool
+
+ # MPI parameters
mpi_size: int
mpi_rank: int
mpi_master: bool
@@ -124,6 +181,8 @@ class Simulation(ObjectSingleton):
mpi_work_size: int
mpi_work_size_total: int
mpi_work_iter: Annotated[NDArray[int64], (1,)]
+
+ # Runtimes
runtime_total: float
runtime_preparation: float
runtime_simulation: float
@@ -134,43 +193,26 @@ class Simulation(ObjectSingleton):
gpu_meta: GPUMeta
source_seed: int
- def __init__(self):
- super().__init__()
+ def __init__(self, name: str = "") -> None:
+ self.compiled = False
- # ==============================================================================
- # Simulation objects
- # ==============================================================================
-
- # Physics
- self.data = [DataNone()]
- self.distributions = [DistributionNone()]
- self.materials = []
- self.elements = []
- self.electron_reactions = []
- self.nuclides = []
- self.neutron_reactions = []
- self.sources = []
+ self.name = name or "(Unnamed simulation)"
+ self.root_universe = Universe("Root Universe")
- # Geometry
- self.cells = []
- self.lattices = []
- self.regions = []
- self.surfaces = []
- self.universes = [Universe("Root Universe", root=True)]
- self.meshes = []
+ # Initialize with empty model objects
+ self._reset_model()
- # Tallies
+ # Also empty sources and tallies
+ self.sources = []
self.tallies = []
- # Settings
+ # ==============================================================================
+ # Simulation settings and techniques
+ # ==============================================================================
+
self.settings = Settings()
- # Techniques
- self.implicit_capture = ImplicitCapture()
- self.weighted_emission = WeightedEmission()
- self.global_weight_roulette = GlobalWeightRoulette()
- self.weight_windows = WeightWindows()
- self.population_control = PopulationControl()
+ self.technique = Technique()
# ==============================================================================
# Particle banks
@@ -182,7 +224,7 @@ def __init__(self):
self.bank_future = ParticleBank(tag="future")
# ==============================================================================
- # Simulation parameters
+ # Simulation variables and parameters
# ==============================================================================
# Simulation indices
@@ -191,29 +233,29 @@ def __init__(self):
self.idx_census = 0
self.idx_batch = 0
- # Domain decomposition
- self.dd_idx = 0
- self.dd_N_local_source = 0
- self.dd_local_rank = 0
-
# Eigenvalue simulation
self.k_eff = 0.0
self.k_cycle = np.ones(1)
self.k_avg = 0.0
self.k_sdv = 0.0
+ self.k_avg_running = 0.0
+ self.k_sdv_running = 0.0
+ #
self.n_avg = 0.0 # Neutron density
self.n_sdv = 0.0
self.n_max = 0.0
+ #
self.C_avg = 0.0 # Precursor density
self.C_sdv = 0.0
self.C_max = 0.0
- self.k_avg_running = 0.0
- self.k_sdv_running = 0.0
- self.gyration_radius = np.zeros(1)
- self.cycle_active = False
+ #
self.eigenvalue_tally_nuSigmaF = np.zeros(1)
self.eigenvalue_tally_n = np.zeros(1)
self.eigenvalue_tally_C = np.zeros(1)
+ #
+ self.gyration_radius = np.zeros(1)
+ #
+ self.cycle_active = False
# MPI parameters
self.mpi_size = MPI.COMM_WORLD.Get_size()
@@ -224,7 +266,7 @@ def __init__(self):
self.mpi_work_size_total = 0
self.mpi_work_iter = np.zeros(1, dtype=int64)
- # Runtime records
+ # Runtimes
self.runtime_total = 0.0
self.runtime_preparation = 0.0
self.runtime_simulation = 0.0
@@ -235,8 +277,345 @@ def __init__(self):
self.gpu_meta = GPUMeta()
self.source_seed = 0
- def set_root_universe(self, cells=[]):
- self.universes[0].cells = cells
+ def _reset_model(self) -> None:
+ # Physics
+ self.data = []
+ self.distributions = []
+ self.neutron_reactions = []
+ self.electron_reactions = []
+ self.nuclides = []
+ self.elements = []
+ self.materials = []
+ self.neutron_multigroup_data = []
+ # Geometry
+ self.surfaces = []
+ self.regions = []
+ self.cells = []
+ self.universes = []
+ self.lattices = []
+ self.meshes = []
-simulation = Simulation()
+ def _finalize_compilation(self) -> None:
+ """Finalize model-wide state after object discovery.
+
+ Object-specific compilation hooks register dependencies and derive
+ fields owned by one object. This method handles relationships and
+ invariants that require the complete simulation, before the model is
+ packed into the runtime representation. Any runtime-visible object
+ created here must be compiled explicitly because recursive discovery
+ has already completed.
+ """
+ from mcdc.object_.material import (
+ set_elements_from_nuclides,
+ set_nuclides_from_elements,
+ update_fissionable_from_nuclides,
+ )
+
+ settings = self.settings
+
+ # Select standard multigroup or hybrid neutron transport.
+ materials_have_native_composition = any(
+ material.nuclide_composition or material.element_composition
+ for material in self.materials
+ )
+ materials_have_multigroup = bool(self.materials) and all(
+ material.has_neutron_multigroup for material in self.materials
+ )
+ multigroup_grids_are_identical = False
+ if materials_have_multigroup:
+ shared_grid = self.materials[0].neutron_multigroup.energy_grid
+ multigroup_grids_are_identical = all(
+ np.array_equal(material.neutron_multigroup.energy_grid, shared_grid)
+ for material in self.materials[1:]
+ )
+
+ self.technique.neutron_multigroup.hybrid = not (
+ not materials_have_native_composition
+ and materials_have_multigroup
+ and multigroup_grids_are_identical
+ )
+
+ # Require physical energy boundaries wherever energy selects local groups.
+ if self.technique.neutron_multigroup.hybrid:
+ for material in self.materials:
+ model = material.neutron_multigroup
+ if model.G > 0 and not np.any(model.energy_grid):
+ print_error(
+ "Hybrid neutron multigroup transport requires an explicit "
+ "energy_grid for every multigroup material."
+ )
+
+ # Validate neutron source coordinates for standard multigroup transport
+ else:
+ G = self.materials[0].neutron_multigroup.G
+ for source in self.sources:
+ if source.particle_type != PARTICLE_NEUTRON:
+ continue
+
+ if source.mono_energetic:
+ source_energies = np.array([source.energy])
+ elif source.discrete_energy:
+ source_energies = source.energy_pmf.value
+ else:
+ print_error(
+ "Standard neutron multigroup transport requires neutron "
+ "sources to use a scalar energy or discrete_energy "
+ "group-coordinate distribution."
+ )
+
+ if not np.all(np.isfinite(source_energies)) or not np.all(
+ source_energies == np.floor(source_energies)
+ ):
+ print_error(
+ "Standard neutron multigroup source energies must be finite, "
+ "integer-valued group coordinates."
+ )
+ if np.any(source_energies < 0) or np.any(source_energies >= G):
+ print_error(
+ "Standard neutron multigroup source energies must satisfy "
+ f"0 <= energy < G (G={G})."
+ )
+
+ # Limit transport to the latest requested tally boundary
+ settings.time_boundary = min(
+ [settings.time_boundary] + [tally.time[-1] for tally in self.tallies]
+ )
+
+ # Complete native-material compositions for the transported particles
+ for material in self.materials:
+ if (
+ settings.neutron_transport
+ and material.element_composition
+ and len(material.nuclides) == 0
+ ):
+ set_nuclides_from_elements(material, self)
+ if (
+ settings.electron_transport
+ and material.nuclide_composition
+ and len(material.elements) == 0
+ ):
+ set_elements_from_nuclides(material, self)
+
+ # Load the physics data required by the completed material model
+ if settings.neutron_transport:
+ for nuclide in self.nuclides:
+ nuclide.set_neutron_data(self)
+ for material in self.materials:
+ update_fissionable_from_nuclides(material)
+
+ if settings.electron_transport:
+ for element in self.elements:
+ element.set_electron_data(self)
+
+ # Resolve tally filters and shapes that require the complete model
+ for tally in self.tallies:
+ tally._resolve_energy_filter(self)
+
+ if settings.use_census_based_tally:
+ for tally in self.tallies:
+ tally._use_census_based_tally(settings.census_tally_frequency, self)
+
+ # Normalize source-selection probabilities across the complete source set
+ source_probability = sum(source.probability for source in self.sources)
+ for source in self.sources:
+ source.probability /= source_probability
+
+ # Derive particle-bank capacities from settings and the MPI decomposition
+ N_work = math.ceil(settings.N_particle / self.mpi_size)
+ N_census = settings.N_census
+
+ if settings.neutron_eigenvalue_mode or N_census == 1:
+ settings.future_bank_buffer_ratio = 0.0
+ if not settings.neutron_eigenvalue_mode and N_census == 1:
+ settings.census_bank_buffer_ratio = 0.0
+ settings.source_bank_buffer_ratio = 0.0
+
+ self.bank_active.size[0] = settings.active_bank_buffer
+ self.bank_census.size[0] = int(settings.census_bank_buffer_ratio * N_work)
+ self.bank_source.size[0] = int(settings.source_bank_buffer_ratio * N_work)
+ self.bank_future.size[0] = int(settings.future_bank_buffer_ratio * N_work)
+
+ # Initialize run state derived from the compiled settings
+ self.k_eff = settings.k_init
+ self.cycle_active = (
+ not settings.neutron_eigenvalue_mode or settings.N_inactive == 0
+ )
+ if settings.neutron_eigenvalue_mode:
+ self.k_cycle = np.zeros(settings.N_cycle)
+ self.gyration_radius = np.zeros(settings.N_cycle)
+
+ # ==================================================================================
+ # Simulation object setters
+ # ==================================================================================
+
+ def set_model(self, cells: Sequence[Cell]) -> None:
+ """Set the root cells that define a complete model (geometry and materials) of
+ the simulation.
+
+ Pass only cells that belong directly to the root universe. Do not include
+ cells nested inside subuniverses; they are discovered automatically during
+ model traversal as long as they are reachable through the root cells.
+
+ Parameters
+ ----------
+ cells : sequence of Cell
+ Cells to place in the root universe.
+
+ Examples
+ --------
+ Attach previously constructed cells:
+
+ >>> simulation.set_model([fuel_cell, moderator_cell])
+ """
+ self.root_universe.cells = list(cells)
+ self.compiled = False
+
+ def set_sources(self, sources: Sequence[Source]) -> None:
+ """Set particle sources for the simulation.
+
+ Parameters
+ ----------
+ sources : sequence of Source
+ Particle sources to sample during transport.
+
+ Examples
+ --------
+ Attach previously constructed sources:
+
+ >>> simulation.set_sources([volume_source, boundary_source])
+ """
+ self.sources = list(sources)
+ self.compiled = False
+
+ def set_tallies(self, tallies: Sequence[Tally]) -> None:
+ """Set requested tallies for the simulation.
+
+ Parameters
+ ----------
+ tallies : sequence of Tally
+ Tallies to score during transport.
+
+ Examples
+ --------
+ Attach previously constructed tallies:
+
+ >>> simulation.set_tallies([flux_tally, current_tally])
+ """
+ self.tallies = list(tallies)
+ self.compiled = False
+
+ # ==================================================================================
+ # Operations
+ # ==================================================================================
+
+ def compile(self) -> None:
+ """Compile and finalize the Python model into a simulation snapshot.
+
+ A globally unique ``compile_ID`` identifies the snapshot. Command-line overrides
+ are applied before any derived state is resolved. Every embedded or registered
+ :class:`~mcdc.object_.base.MCDCBase` reached during compilation records that ID.
+ Object-local hooks discover and prepare their dependencies, then model-wide
+ finalization resolves state that requires the complete simulation.
+ """
+ from mcdc.config import override_settings
+ from mcdc.code_factory.python_objects_compiler import compile_simulation
+
+ self.compile_ID = type(self)._next_compile_ID
+ type(self)._next_compile_ID += 1
+
+ override_settings(self)
+
+ compile_simulation(self)
+ self.compiled = True
+
+ def visualize_model(
+ self,
+ vis_plane: Literal["xy", "xz", "yz", "yx", "zx", "zy"],
+ x: float | Sequence[float],
+ y: float | Sequence[float],
+ z: float | Sequence[float],
+ pixels: Sequence[int],
+ colors: Mapping[Material, ColorType] | None,
+ time: Sequence[float] | NDArray[float64],
+ save_as: str | PathLike[str] | None,
+ ) -> None:
+ """Render a two-dimensional material map of the compiled model.
+
+ The model is compiled first when necessary.
+
+ Parameters
+ ----------
+ vis_plane : {"xy", "xz", "yz", "yx", "zx", "zy"}
+ Coordinate plane to render. Its order sets the horizontal and
+ vertical axes.
+ x, y, z : float or sequence of 2 float
+ Slice position for the axis normal to ``vis_plane``, or plotting
+ range for an axis contained in the plane, in cm.
+ pixels : sequence of 2 int
+ Number of pixels along the two plotted axes.
+ colors : dict of Material to color or None
+ Optional material-color mapping. Matplotlib color specifications
+ are accepted.
+ time : sequence of float
+ Geometry snapshot times in seconds.
+ save_as : str or path-like or None
+ Output file name. If omitted, display the rendered image.
+
+ Examples
+ --------
+ Render an x-z slice of the model:
+
+ >>> simulation.visualize_model(
+ ... vis_plane="xz",
+ ... x=[0.0, 1.0],
+ ... y=0.0,
+ ... z=[-0.5, 0.5],
+ ... pixels=(100, 100),
+ ... colors=None,
+ ... time=[0.0],
+ ... save_as="slab",
+ ... )
+ """
+ if not self.compiled:
+ self.compile()
+
+ from mcdc.visualize import visualize_model
+
+ if not np.isscalar(x):
+ x = tuple(x)
+ if not np.isscalar(y):
+ y = tuple(y)
+ if not np.isscalar(z):
+ z = tuple(z)
+ pixels = tuple(pixels)
+ time = tuple(time)
+ if save_as is not None:
+ save_as = fspath(save_as)
+
+ visualize_model(self, vis_plane, x, y, z, pixels, colors, time, save_as)
+
+ def run(self) -> None:
+ """Compile when needed, execute transport, and write output.
+
+ Examples
+ --------
+ Run a fully configured simulation:
+
+ >>> simulation.run()
+ """
+ if not self.compiled:
+ self.compile()
+
+ from mcdc.main import run_simulation
+
+ run_simulation(self)
+ self.compiled = False
+
+ def __repr__(self) -> str:
+ return (
+ f"{self.__class__.__name__}("
+ f"name={self.name!r}, "
+ f"compiled={self.compiled}, "
+ f"compile_ID={self.compile_ID})"
+ )
diff --git a/mcdc/object_/source.py b/mcdc/object_/source.py
index 3f29677c4..b4bd3da4d 100644
--- a/mcdc/object_/source.py
+++ b/mcdc/object_/source.py
@@ -1,49 +1,35 @@
import numpy as np
-from numpy import float64, int64
-from numpy.typing import NDArray
+from numbers import Real
+from numpy import float64
+from numpy.typing import ArrayLike, NDArray
from types import NoneType
from typing import Annotated, Sequence
####
-import mcdc.object_.distribution as distribution
-
from mcdc.constant import (
- INTERPOLATION_LINEAR,
PARTICLE_NEUTRON,
PARTICLE_ELECTRON,
PARTICLE_PROTON,
INF,
PI,
)
-from mcdc.object_.base import ObjectNonSingleton
+from mcdc.object_.base import MCDCObject
from mcdc.object_.distribution import DistributionTabulated, DistributionPMF
-from mcdc.object_.simulation import simulation
from mcdc.object_.util import move_object
from mcdc.print_ import print_error
-
-def decode_particle_type(type_):
- if type_ == PARTICLE_NEUTRON:
- return "Neutron"
- elif type_ == PARTICLE_ELECTRON:
- return "Electron"
- elif type_ == PARTICLE_PROTON:
- return "Proton"
-
-
# ======================================================================================
# Source
# ======================================================================================
-class Source(ObjectNonSingleton):
- """
- Define a particle source.
+class Source(MCDCObject):
+ """Distributions of particles introduced into the simulation.
- A source specifies the initial position, direction, energy, time, particle
- type, and relative sampling probability for emitted particles.
+ A source specifies the position, direction, energy, time, particle type, and
+ relative sampling probability for emitted particles.
Parameters
----------
@@ -51,43 +37,56 @@ class Source(ObjectNonSingleton):
User label. If omitted, a default name is generated from the source ID.
position : array_like of float, optional
Point-source position ``[x, y, z]`` in cm. If provided, the source is
- treated as a point source.
+ treated as a point source. Cannot be supplied with ``x``, ``y``, or
+ ``z``.
x, y, z : array_like of float, optional
Spatial bounds of a box source in cm, given as ``[min, max]`` for each
- coordinate. These are used when ``position`` is not provided.
+ coordinate. Cannot be supplied with ``position``.
direction : array_like of float, optional
Source direction vector ``[ux, uy, uz]``. The vector is normalized
internally. If provided without angular bounds, the source is
- mono-directional.
+ mono-directional. Cannot be supplied with ``isotropic=True`` or
+ ``white_direction``.
When ``polar_cosine`` and/or ``azimuthal`` are specified, this vector
defines the reference (polar) axis about which directions are sampled.
white_direction : array_like of float, optional
Outward normal direction for a white boundary source. The vector is
- normalized internally.
+ normalized internally. Cannot be supplied with ``isotropic=True`` or
+ ``direction``.
isotropic : bool, optional
- If True, emit particles isotropically.
+ If True, emit particles isotropically. Cannot be supplied with
+ ``direction`` or ``white_direction``.
polar_cosine : array_like of float, optional
Bounds for the sampled polar cosine,
``[mu_min, mu_max]``, measured with respect to ``direction``.
- Defaults to ``[-1.0, 1.0]``.
+ Requires ``direction``. Defaults to ``[-1.0, 1.0]``.
azimuthal : array_like of float, optional
Bounds for the sampled azimuthal angle,
``[azi_min, azi_max]`` in radians, measured about ``direction``.
- Defaults to ``[0.0, 2π]``.
- energy : float or ndarray, optional
- Source energy in eV. A float defines a mono-energetic source. An array
- defines a tabulated energy distribution. Defaults to a mono-energetic
- source at **1 MeV**.
-
- energy_group : int or ndarray, optional
- Source energy group. An integer defines a mono-group source. An array
- defines a discrete group probability mass function. In multigroup
- simulations, the default is **group 0**.
- time : float or array_like of float, optional
- Emission time in seconds. A float defines a discrete emission time.
- A two-entry array-like value defines a time interval
- ``[t_min, t_max]``. Defaults to ``0.0``.
+ Requires ``direction``. Defaults to ``[0.0, 2π]``.
+ energy : float, array_like of float, or int, optional
+ Source energy in eV. A real scalar, including a NumPy scalar, defines a
+ mono-energetic source. An array-like value with shape ``(2, N)`` defines
+ a tabulated distribution: the first row contains energy values and the
+ second row contains their probability density in ``eV^-1``. Defaults
+ to a mono-energetic source at **1 MeV**. In standard neutron multigroup
+ transport, an integer conventionally specifies a group-coordinate
+ energy and is stored internally as a float. The coordinate must identify
+ an available group. Continuous energy distributions are not supported
+ in standard multigroup transport.
+ discrete_energy : array_like of float, optional
+ Discrete source-energy distribution with shape ``(2, N)``. The first
+ row contains sampled energy values and the second row contains their
+ probabilities. Values are physical energies in eV for continuous-energy
+ transport and group-coordinate energies for standard multigroup
+ transport. Standard-multigroup coordinates must be integer-valued and
+ identify available groups. Cannot be supplied with ``energy``.
+ time : real or array_like of float, optional
+ Emission time in seconds. A real scalar, including a NumPy scalar,
+ defines a discrete emission time. An array-like value with shape
+ ``(2,)`` defines a uniform interval ``[t_min, t_max]``. Defaults to
+ ``0.0``.
particle_type : {"neutron", "electron", "proton"}, optional
Type of emitted particle. Defaults to ``"neutron"``.
probability : float, optional
@@ -95,27 +94,26 @@ class Source(ObjectNonSingleton):
Notes
-----
- If ``position`` is provided, ``x``, ``y``, and ``z`` are ignored.
+ ``position`` and the box bounds ``x``, ``y``, and ``z`` are alternative
+ spatial specifications and cannot be combined.
When ``position`` is not provided, the source is treated as a box source.
Any unspecified coordinate range defaults to ``[0.0, 0.0]`` cm. For
example, if only ``z=[-1.0, 1.0]`` is specified, then the source occupies
``x=[0.0, 0.0]``, ``y=[0.0, 0.0]``, and ``z=[-1.0, 1.0]``.
- Direction options are interpreted in the following order:
-
- - if ``isotropic=True``, the source is isotropic;
- - else if ``direction`` is provided, the source uses that direction;
- - else if ``white_direction`` is provided, the source is a white boundary
- source;
- - otherwise, the default direction behavior is used.
-
- ``energy_group`` takes precedence over ``energy`` when both are provided.
+ ``isotropic=True``, ``direction``, and ``white_direction`` are alternative
+ angular specifications and cannot be combined. ``polar_cosine`` and
+ ``azimuthal`` describe an angular spread about ``direction`` and therefore
+ require it. If no angular specification is provided, the source is
+ isotropic.
Examples
--------
Point source at the origin emitting mono-energetic neutrons isotropically:
+ >>> import numpy as np
+ >>> import mcdc
>>> src = mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True)
Uniform box source distributed along z:
@@ -152,10 +150,34 @@ class Source(ObjectNonSingleton):
... azimuthal=[0.0, np.pi / 2],
... )
- Discrete energy-group source:
+ Sample discrete emission lines in a continuous-energy calculation:
- >>> src = mcdc.Source(
- ... energy_group=3,
+ >>> decay_electrons = mcdc.Source(
+ ... particle_type="electron",
+ ... discrete_energy=(
+ ... [1.0e5, 2.0e5],
+ ... [0.8, 0.2],
+ ... ),
+ ... )
+
+ Sample a continuous-energy source from a tabulated probability density:
+
+ >>> electron_source = mcdc.Source(
+ ... particle_type="electron",
+ ... energy=np.array([
+ ... [9_999.0, 10_001.0],
+ ... [0.5, 0.5],
+ ... ]),
+ ... direction=[0.0, 0.0, 1.0],
+ ... )
+
+ Sample between groups 0 and 1 in standard multigroup transport:
+
+ >>> multigroup_source = mcdc.Source(
+ ... discrete_energy=(
+ ... [0.0, 1.0],
+ ... [0.25, 0.75],
+ ... ),
... )
Time-dependent source:
@@ -165,16 +187,18 @@ class Source(ObjectNonSingleton):
... )
"""
- # Annotations for Numba mode
- label: str = "source"
- #
+ # MC/DC framework metadata
+ label = "source"
+
name: str
+
# Position
point_source: bool
point: Annotated[NDArray[float64], (3,)]
x: Annotated[NDArray[float64], (2,)]
y: Annotated[NDArray[float64], (2,)]
z: Annotated[NDArray[float64], (2,)]
+
# Direction
isotropic_direction: bool
mono_direction: bool
@@ -182,19 +206,24 @@ class Source(ObjectNonSingleton):
direction: Annotated[NDArray[float64], (3,)]
polar_cosine: Annotated[NDArray[float64], (2,)]
azimuthal: Annotated[NDArray[float64], (2,)]
+
# Energy
mono_energetic: bool
- energy_group: int
+ discrete_energy: bool
energy: float
- energy_group_pmf: DistributionPMF
energy_pdf: DistributionTabulated
+ energy_pmf: DistributionPMF
+
# Time
discrete_time: bool
time: float
time_range: Annotated[NDArray[float64], (2,)]
- #
+
+ # Misc.
particle_type: int
probability: float
+
+ # Movement
moving: bool
N_move: int
N_move_grid: int
@@ -217,28 +246,26 @@ def __init__(
polar_cosine: Sequence[float] | NoneType = None,
azimuthal: Sequence[float] | NoneType = None,
#
- energy: float | NDArray[float64] | NoneType = None,
- energy_group: int | NDArray[int64] | NoneType = None,
+ energy: float | ArrayLike | int | NoneType = None,
+ discrete_energy: ArrayLike | NoneType = None,
#
- time: float | Sequence[float] = 0.0,
+ time: ArrayLike = 0.0,
#
particle_type: str = "neutron",
#
probability: float = 1.0,
- ):
-
+ ) -> None:
super().__init__()
- # Set name
- if name != "":
- self.name = name
- else:
- self.name = f"{self.label}_{self.ID}"
+ self.name = name or "(Unnamed source)"
# ==============================================================================
# Default attributes
- # Point source at origin, isotropic, mono-energetic at 1 MeV or at group 0,
- # time = 0, neutron
+ # Point source at origin,
+ # isotropic,
+ # mono-energetic at 1 MeV,
+ # time = 0,
+ # neutron
# ==============================================================================
# Position
@@ -258,13 +285,13 @@ def __init__(
# Energy
self.mono_energetic = True
- self.energy_group = 0
+ self.discrete_energy = False
self.energy = 1.0e6
- self.energy_group_pmf = DistributionPMF(np.array([0.0]), np.array([1.0]))
self.energy_pdf = DistributionTabulated(
np.array([1.0e6 - 1.0, 1.0e6 + 1.0]),
np.array([1.0, 1.0]),
)
+ self.energy_pmf = DistributionPMF(np.array([1.0e6]), np.array([1.0]))
# Time
self.discrete_time = True
@@ -281,6 +308,10 @@ def __init__(
# Assignment
# ==============================================================================
+ # Require one unambiguous source-position representation
+ if position is not None and any(value is not None for value in (x, y, z)):
+ print_error("Cannot specify position together with x, y, or z.")
+
# Position
if position is not None:
self.point = np.array(position)
@@ -293,8 +324,25 @@ def __init__(
if z is not None:
self.z = np.array(z)
+ # Require one unambiguous source-direction representation
+ isotropic_enabled = isotropic is not None and bool(isotropic)
+ direction_modes = sum(
+ (
+ isotropic_enabled,
+ direction is not None,
+ white_direction is not None,
+ )
+ )
+ if direction_modes > 1:
+ print_error(
+ "Cannot specify more than one of isotropic=True, direction, and "
+ "white_direction."
+ )
+ if direction is None and (polar_cosine is not None or azimuthal is not None):
+ print_error("polar_cosine and azimuthal require direction.")
+
# Direction
- if isotropic is not None and isotropic:
+ if isotropic_enabled:
pass
elif direction is not None:
self.isotropic_direction = False
@@ -314,30 +362,34 @@ def __init__(
# Normalize direction
self.direction /= np.linalg.norm(self.direction)
+ # Require one unambiguous source-energy representation
+ if discrete_energy is not None and energy is not None:
+ print_error("Cannot specify both energy and discrete_energy.")
+
+ # Discrete energy
+ if discrete_energy is not None:
+ values, probabilities = _distribution_pair(
+ discrete_energy, "Discrete energy"
+ )
+ self.mono_energetic = False
+ self.discrete_energy = True
+ self.energy_pmf = DistributionPMF(values, probabilities)
+
# Energy
- if energy_group is not None:
- if type(energy_group) == int:
- self.energy_group = energy_group
+ if energy is not None:
+ if isinstance(energy, Real) and not isinstance(energy, (bool, np.bool_)):
+ self.energy = float(energy)
else:
+ values, pdf = _distribution_pair(energy, "Energy")
self.mono_energetic = False
- self.energy_group_pmf = DistributionPMF(
- energy_group[0], energy_group[1]
- )
- elif energy is not None:
- if type(energy) == float:
- self.energy = energy
- else:
- self.mono_energetic = False
- self.energy_pdf = DistributionTabulated(
- np.array(energy[0]), np.array(energy[1])
- )
+ self.energy_pdf = DistributionTabulated(values, pdf)
# Time
- if type(time) == float:
- self.time = time
+ if isinstance(time, Real) and not isinstance(time, (bool, np.bool_)):
+ self.time = float(time)
else:
self.discrete_time = False
- self.time_range = np.array(time)
+ self.time_range = _time_range(time)
# Particle type
if particle_type == "neutron":
@@ -358,10 +410,9 @@ def __init__(
self.move_time_grid = np.array([0.0, INF])
self.move_translations = np.zeros((2, 3))
- def __repr__(self):
- text = "\n"
- text += f"Source\n"
- text += f" - ID: {self.ID}\n"
+ def __repr__(self) -> str:
+ text = super().__repr__()
+
text += f" - Name: {self.name}\n"
text += f" - Particle: {decode_particle_type(self.particle_type)}\n"
text += f" - Probability: {self.probability * 100}%\n"
@@ -378,16 +429,13 @@ def __repr__(self):
text += f" - Direction [ux, uy, yz]: {self.direction}\n"
elif self.white_direction:
text += f" - Isotropic halfspace: {self.direction}\n"
- if simulation.materials[0].label == "multigroup_material":
- if self.mono_energetic:
- text += f" - Energy group: {self.energy_group} \n"
- else:
- text += f" - Energy group: {distribution.decode_type(self.energy_group_pmf.type)} [ID: {self.energy_group_pmf.ID}]\n"
+ if self.mono_energetic:
+ energy_text = f"{self.energy} eV"
+ elif self.discrete_energy:
+ energy_text = "PMF"
else:
- if self.mono_energetic:
- text += f" - Energy: {self.energy} eV\n"
- else:
- text += f" - Energy: {distribution.decode_type(self.energy_pdf)} [ID: {self.energy_pdf.ID}]\n"
+ energy_text = "PDF"
+ text += f" - Energy: {energy_text}\n"
if self.discrete_time:
text += f" - Time: {self.time} s\n"
else:
@@ -399,7 +447,7 @@ def __repr__(self):
# Source moving
# ==================================================================================
- def move(self, velocities, durations):
+ def move(self, velocities: ArrayLike, durations: ArrayLike) -> None:
"""
Define piecewise-constant motion for the source.
@@ -457,3 +505,47 @@ def move(self, velocities, durations):
... )
"""
move_object(self, velocities, durations)
+
+
+def decode_particle_type(type_):
+ """Return the display name for a packed particle-type code."""
+
+ if type_ == PARTICLE_NEUTRON:
+ return "Neutron"
+ elif type_ == PARTICLE_ELECTRON:
+ return "Electron"
+ elif type_ == PARTICLE_PROTON:
+ return "Proton"
+
+
+# ======================================================================================
+# Helper functions
+# ======================================================================================
+
+
+def _distribution_pair(
+ value: ArrayLike, name: str
+) -> tuple[NDArray[float64], NDArray[float64]]:
+ """Normalize a two-row distribution input and validate its shape."""
+ try:
+ array = np.asarray(value, dtype=float64)
+ except (TypeError, ValueError):
+ print_error(f"{name} distribution must be a rectangular array")
+
+ if array.ndim != 2 or array.shape[0] != 2:
+ print_error(f"{name} distribution must have shape (2, N)")
+
+ return array[0], array[1]
+
+
+def _time_range(value: ArrayLike) -> NDArray[float64]:
+ """Normalize and validate a source time interval."""
+ try:
+ array = np.asarray(value, dtype=float64)
+ except (TypeError, ValueError):
+ print_error("Source time interval must be an array with shape (2,)")
+
+ if array.shape != (2,):
+ print_error("Source time interval must have shape (2,)")
+
+ return array
diff --git a/mcdc/object_/surface.py b/mcdc/object_/surface.py
index ececf78be..41c00ca0c 100644
--- a/mcdc/object_/surface.py
+++ b/mcdc/object_/surface.py
@@ -2,7 +2,7 @@
import numpy as np
from numpy import float64
-from numpy.typing import NDArray
+from numpy.typing import ArrayLike, NDArray
####
@@ -29,7 +29,7 @@
SURFACE_TORUS_Z,
SURFACE_TORUS,
)
-from mcdc.object_.base import ObjectNonSingleton
+from mcdc.object_.base import MCDCObject
from mcdc.object_.cell import Region
from mcdc.object_.tally import TallySurfaceCrossing
from mcdc.object_.util import move_object
@@ -40,70 +40,64 @@
# ======================================================================================
-class Surface(ObjectNonSingleton):
- """
- Geometric surface primitive with optional boundary condition and motion.
-
- Surfaces are registered non-singletons and receive a stable ``ID``. Factory
- constructors (:meth:`PlaneX`, :meth:`CylinderZ`, etc.) set the quadric
- coefficients (A..J) and linearity flag. Motion segments can be defined with
- :meth:`move`.
-
- Parameters
- ----------
- type\\_ : int
- One of ``SURFACE_*`` constants (e.g., ``SURFACE_PLANE_X``).
- name : str
- Optional label for reporting.
- boundary_condition : str
- Boundary behavior at the surface (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Attributes
- ----------
- ID : int
- Index in the global registry (assigned on construction).
- type\\_ : int
- Surface type code (``SURFACE_*``).
- name : str
- User label.
- boundary_condition : int
- One of ``BC_NONE``, ``BC_VACUUM``, ``BC_REFLECTIVE``.
- A,B,C,D,E,F,G,H,I,J : float
- Quadric coefficients defining the implicit surface.
- linear : bool
- True for linear (plane) surfaces.
- quadric : bool
- True for quadric (e.g.,cylinder) surfaces.
- quartic : bool
- True for quartic (e.g., torus) surfaces.
- nx, ny, nz : float
- Outward normal components for linear planes.
- moving : bool
- True if :meth:`move` has been called.
- N_move : int
- Number of motion segments plus the final static segment.
- move_velocities : (N_move, 3) ndarray
- Per-segment velocity vectors.
- move_durations : (N_move,) ndarray
- Per-segment durations (s).
- move_time_grid : (N_move+1,) ndarray
- Cumulative time breakpoints.
- move_translations : (N_move+1, 3) ndarray
- Cumulative translations at each breakpoint.
-
- See Also
+class Surface(MCDCObject):
+ """Geometric boundaries of simulation cells.
+
+ Surfaces are created with class methods such as :meth:`PlaneX`,
+ :meth:`CylinderZ`, and :meth:`Sphere`. Unary ``+`` and ``-`` return the
+ corresponding positive and negative half-space
+ :class:`~mcdc.object_.cell.Region`.
+
+ Boundary conditions may be ``"none"``, ``"vacuum"``, or ``"reflective"``.
+ A surface can also undergo piecewise-constant translational motion configured
+ with :meth:`move`.
+
+ Examples
--------
- Region
- Use unary ``+`` / ``-`` to form half-spaces: ``+surface`` or ``-surface``.
- decode_type
- Human-readable surface type.
- decode_BC_type
- Human-readable boundary condition name.
+ Create a vacuum x plane and select its positive half-space:
+
+ >>> import numpy as np
+ >>> import mcdc
+ >>> plane = mcdc.Surface.PlaneX(x=0.0, boundary_condition="vacuum")
+ >>> region = +plane
+
+ Create a sphere and select its interior:
+
+ >>> sphere = mcdc.Surface.Sphere(center=[0.0, 0.0, 0.0], radius=2.0)
+ >>> interior = -sphere
+
+ Create a cylinder parallel to the z axis:
+
+ >>> cylinder = mcdc.Surface.CylinderZ(
+ ... center=[1.0, -1.0],
+ ... radius=0.5,
+ ... )
+
+ Create an oblique plane from its equation coefficients:
+
+ >>> oblique = mcdc.Surface.Plane(A=1.0, B=1.0, C=0.0, D=-2.0)
+
+ Create a torus with an arbitrary symmetry axis:
+
+ >>> torus = mcdc.Surface.Torus(
+ ... center=[0.0, 0.0, 0.0],
+ ... axis=[1.0, 1.0, 0.0],
+ ... R=2.0,
+ ... r=0.5,
+ ... )
+
+ Define piecewise-constant motion for a plane:
+
+ >>> moving_plane = mcdc.Surface.PlaneX(x=0.0)
+ >>> moving_plane.move(
+ ... velocities=np.array([[1.0, 0.0, 0.0]]),
+ ... durations=np.array([0.5]),
+ ... )
"""
- # Annotations for Numba mode
- label: str = "surface"
- #
+ # MC/DC framework metadata
+ label = "surface"
+
type: int
name: str
boundary_condition: int
@@ -132,17 +126,13 @@ class Surface(ObjectNonSingleton):
move_durations: Annotated[NDArray[float64], ("N_move",)]
move_time_grid: Annotated[NDArray[float64], ("N_move_grid",)]
move_translations: Annotated[NDArray[float64], ("N_move_grid", 3)]
- tallies: list[TallySurfaceCrossing]
+ surface_crossing_tallies: list[TallySurfaceCrossing]
- def __init__(self, type_, name, boundary_condition):
+ def __init__(self, type_: int, name: str, boundary_condition: str) -> None:
super().__init__()
- # Type and name
self.type = type_
- if name != "":
- self.name = name
- else:
- self.name = f"{self.label}_{self.ID}"
+ self.name = name or "(Unnamed surface)"
# Boundary condition
if boundary_condition == "none":
@@ -187,21 +177,12 @@ def __init__(self, type_, name, boundary_condition):
self.move_time_grid = np.array([0.0, INF])
self.move_translations = np.zeros((2, 3))
- # Surface tallies
- self.tallies = []
+ # Surface-crossing tallies
+ self.surface_crossing_tallies = []
- def __repr__(self):
- """
- Return a human-readable description including type-specific parameters.
+ def __repr__(self) -> str:
+ text = super().__repr__()
- Returns
- -------
- str
- Multi-line formatted string with ID, name, BC, and geometry details.
- """
- text = "\n"
- text += f"{decode_type(self.type)}\n"
- text += f" - ID: {self.ID}\n"
text += f" - Name: {self.name}\n"
text += f" - Boundary condition: {decode_BC_type(self.boundary_condition)}\n"
@@ -288,8 +269,8 @@ def __repr__(self):
text += f" - A, B, C: {self.A}, {self.B}, {self.C}\n"
text += f" - R: {self.R} cm\n"
text += f" - r: {self.r} cm\n"
- if len(self.tallies) > 0:
- text += f" - Tallies: {[x.ID for x in self.tallies]}\n"
+ if len(self.surface_crossing_tallies) > 0:
+ text += f" - Surface-crossing tallies: {[x.name for x in self.surface_crossing_tallies]}\n"
return text
@@ -298,23 +279,22 @@ def __repr__(self):
# ==================================================================================
@classmethod
- def PlaneX(cls, name: str = "", x: float = 0.0, boundary_condition: str = "none"):
- """
- Create a plane perpendicular to +x at x = constant.
+ def PlaneX(
+ cls,
+ name: str = "",
+ x: float = 0.0,
+ boundary_condition: str = "none",
+ ) -> "Surface":
+ """Create the plane ``x = constant``.
Parameters
----------
name : str, optional
- User label.
- x : float, default 0.0
- Plane location (cm).
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- Linear plane with normal ``(+1, 0, 0)``.
+ User-facing surface name.
+ x : float, optional
+ Plane position in cm.
+ boundary_condition : {"none", "vacuum", "reflective"}, optional
+ Boundary condition applied when a particle crosses the plane.
"""
type_ = SURFACE_PLANE_X
surface = cls(type_, name, boundary_condition)
@@ -330,23 +310,17 @@ def PlaneX(cls, name: str = "", x: float = 0.0, boundary_condition: str = "none"
return surface
@classmethod
- def PlaneY(cls, name: str = "", y: float = 0.0, boundary_condition: str = "none"):
- """
- Create a plane perpendicular to +y at y = constant.
+ def PlaneY(
+ cls,
+ name: str = "",
+ y: float = 0.0,
+ boundary_condition: str = "none",
+ ) -> "Surface":
+ """Create the plane ``y = constant``.
- Parameters
- ----------
- name : str, optional
- User label.
- y : float, default 0.0
- Plane location (cm).
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- Linear plane with normal ``(0, +1, 0)``.
+ Parameters are the surface ``name``, position ``y`` in cm, and a
+ ``boundary_condition`` of ``"none"``, ``"vacuum"``, or
+ ``"reflective"``.
"""
type_ = SURFACE_PLANE_Y
surface = cls(type_, name, boundary_condition)
@@ -362,23 +336,17 @@ def PlaneY(cls, name: str = "", y: float = 0.0, boundary_condition: str = "none"
return surface
@classmethod
- def PlaneZ(cls, name: str = "", z: float = 0.0, boundary_condition: str = "none"):
- """
- Create a plane perpendicular to +z at z = constant.
+ def PlaneZ(
+ cls,
+ name: str = "",
+ z: float = 0.0,
+ boundary_condition: str = "none",
+ ) -> "Surface":
+ """Create the plane ``z = constant``.
- Parameters
- ----------
- name : str, optional
- User label.
- z : float, default 0.0
- Plane location (cm).
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- Linear plane with normal ``(0, 0, +1)``.
+ Parameters are the surface ``name``, position ``z`` in cm, and a
+ ``boundary_condition`` of ``"none"``, ``"vacuum"``, or
+ ``"reflective"``.
"""
type_ = SURFACE_PLANE_Z
surface = cls(type_, name, boundary_condition)
@@ -402,25 +370,12 @@ def Plane(
C: float = 0.0,
D: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create a general plane defined by A x + B y + C z + D = 0.
+ ) -> "Surface":
+ """Create a general plane ``A*x + B*y + C*z + D = 0``.
- The normal is normalized to unit length and stored in ``(nx, ny, nz)``.
-
- Parameters
- ----------
- name : str, optional
- User label.
- A, B, C, D : float
- Plane coefficients.
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- Linear plane with normalized normal vector.
+ The coefficients are normalized internally. ``(A, B, C)`` must be a
+ nonzero normal vector. Coordinates are evaluated in cm, so ``D`` must
+ use the corresponding length scaling.
"""
type_ = SURFACE_PLANE
surface = cls(type_, name, boundary_condition)
@@ -455,25 +410,10 @@ def CylinderX(
center: Sequence[float] = [0.0, 0.0],
radius: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create an infinite cylinder aligned with the x-axis.
+ ) -> "Surface":
+ """Create an infinite cylinder parallel to the x axis.
- Parameters
- ----------
- name : str, optional
- User label.
- center : (2,) array_like of float, default (0, 0)
- Cylinder center in (y, z) (cm).
- radius : float, default 1.0
- Cylinder radius (cm).
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- Quadratic cylinder surface.
+ ``center`` gives ``[y, z]`` in cm and ``radius`` is in cm.
"""
type_ = SURFACE_CYLINDER_X
surface = cls(type_, name, boundary_condition)
@@ -501,25 +441,10 @@ def CylinderY(
center: Sequence[float] = [0.0, 0.0],
radius: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create an infinite cylinder aligned with the y-axis.
+ ) -> "Surface":
+ """Create an infinite cylinder parallel to the y axis.
- Parameters
- ----------
- name : str, optional
- User label.
- center : (2,) array_like of float
- Cylinder center in (x, z) (cm).
- radius : float
- Cylinder radius (cm).
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- Quadratic cylinder surface.
+ ``center`` gives ``[x, z]`` in cm and ``radius`` is in cm.
"""
type_ = SURFACE_CYLINDER_Y
surface = cls(type_, name, boundary_condition)
@@ -547,25 +472,10 @@ def CylinderZ(
center: Sequence[float] = [0.0, 0.0],
radius: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create an infinite cylinder aligned with the z-axis.
+ ) -> "Surface":
+ """Create an infinite cylinder parallel to the z axis.
- Parameters
- ----------
- name : str, optional
- User label.
- center : (2,) array_like of float
- Cylinder center in (x, y) (cm).
- radius : float
- Cylinder radius (cm).
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- Quadratic cylinder surface.
+ ``center`` gives ``[x, y]`` in cm and ``radius`` is in cm.
"""
type_ = SURFACE_CYLINDER_Z
surface = cls(type_, name, boundary_condition)
@@ -595,25 +505,17 @@ def Cylinder(
axis: Sequence[float] = [0.0, 0.0, 1.0],
point: Sequence[float] = [0.0, 0.0, 0.0],
boundary_condition: str = "none",
- ):
- """
- Create a general infinite cylinder with an arbitrary axis.
+ ) -> "Surface":
+ """Create an infinite cylinder with an arbitrary axis.
Parameters
----------
- name : str, optional
- radius : float
- Cylinder radius (cm).
- axis : (3,) array_like of float
- Direction vector of the cylinder axis (normalized automatically).
- point : (3,) array_like of float
- A point on the cylinder axis (cm).
- boundary_condition : {"none","vacuum","reflective"}, optional
-
- Returns
- -------
- Surface
- General cylinder surface.
+ radius : float, optional
+ Cylinder radius in cm.
+ axis : sequence of 3 float, optional
+ Nonzero vector parallel to the cylinder axis.
+ point : sequence of 3 float, optional
+ A point on the cylinder axis, in cm.
"""
type_ = SURFACE_CYLINDER
surface = cls(type_, name, boundary_condition)
@@ -654,26 +556,8 @@ def Sphere(
center: Sequence[float] = [0.0, 0.0, 0.0],
radius: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create a sphere.
-
- Parameters
- ----------
- name : str, optional
- User label.
- center : (3,) array_like of float
- Sphere center (x, y, z) in cm.
- radius : float
- Radius (cm).
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- Quadratic spherical surface.
- """
+ ) -> "Surface":
+ """Create a sphere from its center and radius in cm."""
type_ = SURFACE_SPHERE
surface = cls(type_, name, boundary_condition)
@@ -702,26 +586,11 @@ def ConeX(
apex: Sequence[float] = [0.0, 0.0, 0.0],
t_sq: float = 1.0,
boundary_condition: str = "none",
- ):
- """
- Create an infinite cone with axis along the x-axis.
+ ) -> "Surface":
+ """Create a double cone aligned with the x axis.
- Equation: (y - y0)^2 + (z - z0)^2 - t_sq * (x - x0)^2 = 0
-
- Parameters
- ----------
- name : str, optional
- apex : (3,) array_like of float
- Cone apex (x0, y0, z0) in cm.
- t_sq : float
- Squared tangent of the half-angle: t_sq = tan^2(theta).
- For a 45-degree half-angle use t_sq = 1.0.
- boundary_condition : {"none","vacuum","reflective"}, optional
-
- Returns
- -------
- Surface
- Cone-X surface.
+ ``apex`` is in cm and ``t_sq`` is the squared tangent of the opening
+ half-angle.
"""
type_ = SURFACE_CONE_X
surface = cls(type_, name, boundary_condition)
@@ -749,25 +618,11 @@ def ConeY(
apex: Sequence[float] = [0.0, 0.0, 0.0],
t_sq: float = 1.0,
boundary_condition: str = "none",
- ):
- """
- Create an infinite cone with axis along the y-axis.
-
- Equation: (x - x0)^2 + (z - z0)^2 - t_sq * (y - y0)^2 = 0
+ ) -> "Surface":
+ """Create a double cone aligned with the y axis.
- Parameters
- ----------
- name : str, optional
- apex : (3,) array_like of float
- Cone apex (x0, y0, z0) in cm.
- t_sq : float
- Squared tangent of the half-angle: t_sq = tan^2(theta).
- boundary_condition : {"none","vacuum","reflective"}, optional
-
- Returns
- -------
- Surface
- Cone-Y surface.
+ ``apex`` is in cm and ``t_sq`` is the squared tangent of the opening
+ half-angle.
"""
type_ = SURFACE_CONE_Y
surface = cls(type_, name, boundary_condition)
@@ -795,25 +650,11 @@ def ConeZ(
apex: Sequence[float] = [0.0, 0.0, 0.0],
t_sq: float = 1.0,
boundary_condition: str = "none",
- ):
- """
- Create an infinite cone with axis along the z-axis.
-
- Equation: (x - x0)^2 + (y - y0)^2 - t_sq * (z - z0)^2 = 0
+ ) -> "Surface":
+ """Create a double cone aligned with the z axis.
- Parameters
- ----------
- name : str, optional
- apex : (3,) array_like of float
- Cone apex (x0, y0, z0) in cm.
- t_sq : float
- Squared tangent of the half-angle: t_sq = tan^2(theta).
- boundary_condition : {"none","vacuum","reflective"}, optional
-
- Returns
- -------
- Surface
- Cone surface.
+ ``apex`` is in cm and ``t_sq`` is the squared tangent of the opening
+ half-angle.
"""
type_ = SURFACE_CONE_Z
surface = cls(type_, name, boundary_condition)
@@ -849,24 +690,13 @@ def Quadric(
I: float = 0.0,
J: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create a general quadric:
- A x^2 + B y^2 + C z^2 + D xy + E yz + F zx + G x + H y + I z + J = 0
+ ) -> "Surface":
+ """Create a general second-degree surface.
- Parameters
- ----------
- name : str, optional
- User label.
- A,B,C,D,E,F,G,H,I,J : float
- Quadric coefficients.
- boundary_condition : str, optional
- Boundary type (``"none"``, ``"vacuum"``, or ``"reflective"``).
-
- Returns
- -------
- Surface
- General quadratic surface.
+ The coefficients define
+ ``A*x**2 + B*y**2 + C*z**2 + D*x*y + E*x*z + F*y*z
+ + G*x + H*y + I*z + J = 0``. Coordinates are evaluated in cm; the
+ coefficients must therefore use mutually consistent units.
"""
type_ = SURFACE_QUADRIC
surface = cls(type_, name, boundary_condition)
@@ -898,24 +728,11 @@ def TorusX(
R: float = 0.0,
r: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create a torus on the y-z plane radially symmetric around the x axis:
- f(x, y, z) = ( sqrt[(y - B)^2 + (z - C)^2] - R )^2 + (x - A)^2 - r^2
+ ) -> "Surface":
+ """Create a torus centered at ``(A, B, C)`` and aligned with x.
- Parameters
- ----------
- name : str, optional
- A,B,C,R,r : float
- A, B, C are displacement values for the torus in the x, y, z directions respectively
- R is the radius around which a circle is revolved about the axis of revolution (parallel with the x-axis)
- r is the radius of the circle that is being revolved
- boundary_condition : {"none","vacuum","reflective"}, optional
-
- Returns
- -------
- Surface
- Torus surface.
+ ``(A, B, C)`` gives the center in cm. ``R`` is the major radius and
+ ``r`` the minor radius, both in cm.
"""
type_ = SURFACE_TORUS_X
surface = cls(type_, name, boundary_condition)
@@ -943,24 +760,11 @@ def TorusY(
R: float = 0.0,
r: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create a torus on the x-z plane radially symmetric around the y axis:
- f(x, y, z) = ( sqrt[(x - A)^2 + (z - C)^2] - R )^2 + (y - B)^2 - r^2
+ ) -> "Surface":
+ """Create a torus centered at ``(A, B, C)`` and aligned with y.
- Parameters
- ----------
- name : str, optional
- A,B,C,R,r : float
- A, B, C are displacement values for the torus in the x, y, z directions respectively
- R is the radius around which a circle is revolved about the axis of revolution (parallel with the y-axis)
- r is the radius of the circle that is being revolved
- boundary_condition : {"none","vacuum","reflective"}, optional
-
- Returns
- -------
- Surface
- Torus surface.
+ ``(A, B, C)`` gives the center in cm. ``R`` is the major radius and
+ ``r`` the minor radius, both in cm.
"""
type_ = SURFACE_TORUS_Y
surface = cls(type_, name, boundary_condition)
@@ -988,24 +792,11 @@ def TorusZ(
R: float = 0.0,
r: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create a torus on the x-y plane radially symmetric around the z axis:
- f(x, y, z) = ( sqrt[(x - A)^2 + (y - B)^2] - R )^2 + (z - C)^2 - r^2
+ ) -> "Surface":
+ """Create a torus centered at ``(A, B, C)`` and aligned with z.
- Parameters
- ----------
- name : str, optional
- A,B,C,R,r : float
- A, B, C are displacement values for the torus in the x, y, z directions respectively
- R is the radius around which a circle is revolved about the axis of revolution (parallel with the z-axis)
- r is the radius of the circle that is being revolved
- boundary_condition : {"none","vacuum","reflective"}, optional
-
- Returns
- -------
- Surface
- Torus surface.
+ ``(A, B, C)`` gives the center in cm. ``R`` is the major radius and
+ ``r`` the minor radius, both in cm.
"""
type_ = SURFACE_TORUS_Z
surface = cls(type_, name, boundary_condition)
@@ -1032,27 +823,19 @@ def Torus(
R: float = 0.0,
r: float = 0.0,
boundary_condition: str = "none",
- ):
- """
- Create a general torus with an arbitrary axis.
+ ) -> "Surface":
+ """Create a torus with an arbitrary axis.
Parameters
----------
- name : str, optional
- center : (3,) array_like of float
- Torus center (cm).
- axis : (3,) array_like of float
- Direction vector of the torus axis (normalized automatically).
- R : float
- Major radius.
- r : float
- Minor radius of the tube.
- boundary_condition : {"none","vacuum","reflective"}, optional
-
- Returns
- -------
- Surface
- General torus surface.
+ center : sequence of 3 float, optional
+ Torus center in cm.
+ axis : sequence of 3 float, optional
+ Nonzero symmetry-axis vector.
+ R : float, optional
+ Major radius in cm.
+ r : float, optional
+ Minor radius in cm.
"""
x, y, z = center
ax, ay, az = axis
@@ -1084,105 +867,33 @@ def Torus(
# Region building
# ==================================================================================
- def __pos__(self):
- """
- Half-space on the **outward** side of the surface.
-
- Returns
- -------
- Region
- Region representing ``n · r + J >= 0`` (sign convention per type).
- """
+ def __pos__(self) -> Region:
return Region.make_halfspace(self, +1)
- def __neg__(self):
- """
- Half-space on the **inward** side of the surface.
-
- Returns
- -------
- Region
- Region representing the complement half-space.
- """
+ def __neg__(self) -> Region:
return Region.make_halfspace(self, -1)
# ==================================================================================
# Surface moving
# ==================================================================================
- def move(self, velocities, durations):
- """
- Define piecewise-constant motion for the surface.
-
- Appends a final static segment (zero velocity, infinite duration) so that
- the motion covers the whole simulation time.
+ def move(self, velocities: ArrayLike, durations: ArrayLike) -> None:
+ """Define piecewise-constant translational motion.
Parameters
----------
- velocities : array_like, shape (N, 3) or list
- Per-segment velocity vectors [cm/s].
- durations : array_like, shape (N,) or list
- Per-segment durations [s].
-
- Notes
- -----
- - Internally converts lists to arrays and constructs
- ``move_time_grid`` and cumulative ``move_translations``.
- - Sets ``moving=True`` and ``N_move = len(durations) + 1``.
-
- Examples
- --------
- >>> s = Surface.PlaneZ(z=0.0)
- >>> s.move(velocities=[[0,0,1.0]], durations=[0.5]) # 0.5 s upward, then static
- >>> s.N_move
- 2
+ velocities : array_like, shape (N, 3)
+ Velocity vector for each segment in cm/s.
+ durations : array_like, shape (N,)
+ Segment durations in seconds. A final stationary segment is appended
+ automatically.
"""
move_object(self, velocities, durations)
-# ======================================================================================
-# Type decoder
-# ======================================================================================
-
-
-def decode_type(type_):
- if type_ == SURFACE_PLANE_X:
- return "Plane-X surface"
- elif type_ == SURFACE_PLANE_Y:
- return "Plane-Y surface"
- elif type_ == SURFACE_PLANE_Z:
- return "Plane-Z surface"
- elif type_ == SURFACE_PLANE:
- return "Plane surface"
- elif type_ == SURFACE_CYLINDER_X:
- return "Infinite cylinder-X surface"
- elif type_ == SURFACE_CYLINDER_Y:
- return "Infinite cylinder-Y surface"
- elif type_ == SURFACE_CYLINDER_Z:
- return "Infinite cylinder-Z surface"
- elif type_ == SURFACE_CYLINDER:
- return "General cylinder surface"
- elif type_ == SURFACE_SPHERE:
- return "Sphere surface"
- elif type_ == SURFACE_CONE_X:
- return "Infinite cone-X surface"
- elif type_ == SURFACE_CONE_Y:
- return "Infinite cone-Y surface"
- elif type_ == SURFACE_CONE_Z:
- return "Infinite cone-Z surface"
- elif type_ == SURFACE_QUADRIC:
- return "Quadric surface"
- elif type_ == SURFACE_TORUS_X:
- return "Torus-X surface"
- elif type_ == SURFACE_TORUS_Y:
- return "Torus-Y surface"
- elif type_ == SURFACE_TORUS_Z:
- return "Torus-Z surface"
- elif type_ == SURFACE_TORUS:
- return "General torus surface"
-
-
def decode_BC_type(type_):
+ """Return the display name for a packed boundary-condition code."""
+
if type_ == BC_NONE:
return "None"
elif type_ == BC_VACUUM:
diff --git a/mcdc/object_/tally.py b/mcdc/object_/tally.py
index 1dc337cb0..91717b7f3 100644
--- a/mcdc/object_/tally.py
+++ b/mcdc/object_/tally.py
@@ -18,13 +18,15 @@
####
-import mcdc.object_.mesh as mesh_module
-
from mcdc.constant import (
INF,
MESH_STRUCTURED,
MESH_UNIFORM,
PI,
+ PARTICLE_ANY,
+ PARTICLE_NEUTRON,
+ PARTICLE_ELECTRON,
+ PARTICLE_PROTON,
SCORE_FLUX,
SCORE_DENSITY,
SCORE_COLLISION,
@@ -43,20 +45,142 @@
TALLY_TRACKLENGTH,
)
from mcdc.object_.mesh import MeshBase, MeshStructured, MeshUniform
-from mcdc.object_.base import ObjectPolymorphic
-from mcdc.object_.simulation import simulation
+from mcdc.object_.base import MCDCPolymorphic
from mcdc.print_ import print_1d_array, print_error
-class Tally(ObjectPolymorphic):
- # Annotations for Numba mode
- label: str = "tally"
+class Tally(MCDCPolymorphic):
+ """Quantities measured during the simulation.
+
+ Parameters
+ ----------
+ name : str, optional
+ User-facing tally name.
+ scores : list of str, optional
+ Scores to accumulate. Track-length scores are ``"flux"``, ``"density"``,
+ ``"collision"``, ``"capture"``, and ``"fission"``; surface-crossing
+ scores are ``"current-net"``, ``"current-in"``, and ``"current-out"``;
+ the collision score is ``"energy_deposition"``, scored in eV. Scores
+ from different estimator families cannot be mixed.
+ surface : Surface, optional
+ Surface filter. Required for a surface-crossing tally unless ``cell`` is
+ provided.
+ cell : Cell, optional
+ Cell filter.
+ mesh : MeshBase, optional
+ Spatial mesh filter for track-length or collision tallies.
+ mu : sequence of float, optional
+ Polar-cosine bin boundaries.
+ azi : sequence of float, optional
+ Azimuthal-angle bin boundaries in radians.
+ polar_reference : sequence of 3 float, optional
+ Reference direction for the angular filters.
+ particle_type : {"neutron", "electron", "proton"}, optional
+ Particle type selected by the tally. If omitted, the tally accepts any
+ transported particle type.
+ energy : sequence of float or "all", optional
+ Physical energy-bin boundaries in eV. In standard neutron multigroup
+ transport, boundaries instead use the group-coordinate energy, and
+ ``"all"`` creates one tally bin per energy group during simulation
+ compilation.
+ time : sequence of float, optional
+ Time-bin boundaries in seconds.
+
+ Returns
+ -------
+ TallySurfaceCrossing, TallyTracklength, or TallyCollision
+ Concrete tally selected from ``scores``.
+
+ Examples
+ --------
+ Score flux and fission on a structured mesh:
+
+ >>> import numpy as np
+ >>> import mcdc
+ >>> mesh = mcdc.MeshStructured(z=np.linspace(0.0, 10.0, 101))
+ >>> tally = mcdc.Tally(
+ ... name="Axial flux",
+ ... mesh=mesh,
+ ... scores=["flux", "fission"],
+ ... energy=[0.0, 1.0e6, 20.0e6],
+ ... )
+
+ Score net current crossing a surface:
+
+ >>> boundary = mcdc.Surface.PlaneZ(z=10.0)
+ >>> current = mcdc.Tally(surface=boundary, scores=["current-net"])
+
+ Filter a track-length tally by cell, angle, and time:
+
+ >>> material = mcdc.Material.multigroup(capture=np.array([1.0]))
+ >>> lower = mcdc.Surface.PlaneZ(z=0.0)
+ >>> upper = mcdc.Surface.PlaneZ(z=10.0)
+ >>> cell = mcdc.Cell(region=+lower & -upper, fill=material)
+ >>> filtered_flux = mcdc.Tally(
+ ... cell=cell,
+ ... scores=["flux", "capture"],
+ ... mu=np.linspace(-1.0, 1.0, 11),
+ ... azi=np.linspace(-np.pi, np.pi, 17),
+ ... time=[0.0, 1.0e-6, 2.0e-6],
+ ... )
+
+ Compile the geometry to resolve the cell's bounding surfaces:
+
+ >>> model = mcdc.Simulation(name="Cell-current example")
+ >>> model.set_model([cell])
+ >>> model.compile()
+
+ Score current entering and leaving the cell through any of its boundaries:
+
+ >>> cell_current = mcdc.Tally(
+ ... cell=cell,
+ ... scores=["current-net", "current-in", "current-out"],
+ ... )
+
+ A cell-only surface-crossing filter scores genuine changes in cell membership.
+ Crossings of surfaces inside the cell that do not enter or leave the cell are
+ not scored.
+
+ Restrict the cell current to one particular boundary surface:
+
+ >>> upper_surface_current = mcdc.Tally(
+ ... surface=upper,
+ ... cell=cell,
+ ... scores=["current-net", "current-in", "current-out"],
+ ... )
+ >>> model.set_tallies([cell_current, upper_surface_current])
+
+ With both filters, the surface selects where crossings are scored, while the
+ cell determines whether each crossing is incoming or outgoing.
+
+ Score energy deposition with both cell and mesh filters:
+
+ >>> deposition = mcdc.Tally(
+ ... cell=cell,
+ ... mesh=mesh,
+ ... scores=["energy_deposition"],
+ ... )
+
+ Use one energy bin per group in standard neutron multigroup transport:
+
+ >>> multigroup_flux = mcdc.Tally(
+ ... cell=cell,
+ ... scores=["flux"],
+ ... energy="all",
+ ... )
+ """
+
+ # MC/DC framework metadata
+ label = "tally"
+ sub_type = -1 # Polymorphic base
+ non_numba = ["_energy_all"]
# Basic properties
name: str
scores: list[int]
# Non-spatial filters
+ particle_type: int
filter_direction: bool
filter_energy: bool
filter_time: bool
@@ -65,6 +189,7 @@ class Tally(ObjectPolymorphic):
polar_reference: Annotated[NDArray[float64], (3,)]
energy: NDArray[float64]
time: NDArray[float64]
+ _energy_all: bool # Non-numba
# Score bins
bin: NDArray[float64]
@@ -88,8 +213,9 @@ def __new__(
mu: Sequence[float] | NoneType = None,
azi: Sequence[float] | NoneType = None,
polar_reference: Sequence[float] | NoneType = None,
+ particle_type: str | NoneType = None,
energy: Sequence[float] | str | NoneType = None,
- time: Sequence[float] | NoneType = None,
+ time: Sequence[float] | NDArray[float64] | NoneType = None,
spatial_shape: tuple[int, ...] | NoneType = None,
) -> TallySurfaceCrossing | TallyTracklength | TallyCollision:
# Determine tally estimator type and create the instance based on the provided
@@ -147,15 +273,15 @@ def __init__(
mu: Sequence[float] | NoneType = None,
azi: Sequence[float] | NoneType = None,
polar_reference: Sequence[float] | NoneType = None,
+ particle_type: str | NoneType = None,
energy: Sequence[float] | str | NoneType = None,
time: Sequence[float] | NoneType = None,
spatial_shape: tuple[int, ...] | NoneType = None,
- ):
+ ) -> None:
+ super().__init__()
+
# Set name
- if name != "":
- self.name = name
- else:
- self.name = f"{self.label}_{self.child_ID}"
+ self.name = name or "(Unnamed tally)"
# Set scores
self.scores = []
@@ -181,6 +307,18 @@ def __init__(
else:
print_error(f"Unknown tally score: {score}")
+ # Particle filter
+ if particle_type is None:
+ self.particle_type = PARTICLE_ANY
+ elif particle_type == "neutron":
+ self.particle_type = PARTICLE_NEUTRON
+ elif particle_type == "electron":
+ self.particle_type = PARTICLE_ELECTRON
+ elif particle_type == "proton":
+ self.particle_type = PARTICLE_PROTON
+ else:
+ print_error(f"Unsupported tally particle type: {particle_type}")
+
# Phase-space filters
self.mu = np.array([-1.0, 1.0])
self.azi = np.array([-PI, PI])
@@ -190,6 +328,7 @@ def __init__(
self.filter_direction = False
self.filter_energy = False
self.filter_time = False
+ self._energy_all = False
if mu is not None:
self.mu = np.array(mu)
self.filter_direction = True
@@ -197,12 +336,16 @@ def __init__(
self.azi = np.array(azi)
self.filter_direction = True
if polar_reference is not None:
- polar_reference = np.array(polar_reference)
- self.polar_reference = polar_reference / np.linalg.norm(polar_reference)
+ polar_reference_arr = np.array(polar_reference)
+ self.polar_reference = polar_reference_arr / np.linalg.norm(
+ polar_reference_arr
+ )
if energy is not None:
- if type(energy) == str and energy == "all_groups":
- G = simulation.materials[0].G
- self.energy = np.linspace(0, G, G + 1) - 0.5
+ if isinstance(energy, str):
+ if energy != "all":
+ print_error(f"Unsupported tally energy filter: {energy}")
+ self._energy_all = True
+ self.energy = np.array([0.0]) # Compilation placeholder
else:
self.energy = np.array(energy)
self.filter_energy = True
@@ -225,7 +368,7 @@ def __init__(
# Set bins and strides
self._set_bin_shape_and_strides(shape)
- def _set_bin_shape_and_strides(self, shape):
+ def _set_bin_shape_and_strides(self, shape: tuple):
# Set bins
self.bin_shape = list(shape)
@@ -235,7 +378,7 @@ def _set_bin_shape_and_strides(self, shape):
self.stride_azi = reduce(operator.mul, shape[2:])
self.stride_mu = reduce(operator.mul, shape[1:])
- def _use_census_based_tally(self, frequency):
+ def _use_census_based_tally(self, frequency: int, simulation):
first_census = simulation.settings.census_time[0]
self.time = np.linspace(0.0, first_census, frequency + 1)
@@ -257,13 +400,23 @@ def _use_census_based_tally(self, frequency):
def _phasespace_filter_text(self):
text = ""
- text += f" - Scores: {[decode_score_type(x) for x in self.scores]}\n"
+ text += f" - Scores: {', '.join(decode_score_type(x) for x in self.scores)}\n"
+ particle_name = {
+ PARTICLE_ANY: "Any",
+ PARTICLE_NEUTRON: "Neutron",
+ PARTICLE_ELECTRON: "Electron",
+ PARTICLE_PROTON: "Proton",
+ }.get(self.particle_type, "Unspecified")
+ text += f" - Particle: {particle_name}\n"
if self.filter_time or self.filter_energy or self.filter_direction:
text += f" - Phase-space filters\n"
if self.filter_time:
text += f" - Time {print_1d_array(self.time)} s\n"
if self.filter_energy:
- text += f" - Energy {print_1d_array(self.energy)} eV\n"
+ if self._energy_all:
+ text += f" - Energy: All multigroup energy groups\n"
+ else:
+ text += f" - Energy {print_1d_array(self.energy)} eV\n"
if self.filter_direction:
text += f" - Direction\n"
text += f" - Polar reference: {self.polar_reference}\n"
@@ -271,24 +424,37 @@ def _phasespace_filter_text(self):
text += f" - Azimuthal angle {print_1d_array(self.azi)}\n"
return text
- def __repr__(self):
- text = "\n"
- text += f"{decode_type(self.type)}\n"
- text += f" - ID: {self.ID}\n"
+ def _compile_into_simulation(self, simulation) -> bool:
+ # Already compiled?
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ return True
+
+ def _resolve_energy_filter(self, simulation) -> None:
+ """Resolve energy filters that require the complete material model."""
+ if self._energy_all:
+ if simulation.technique.neutron_multigroup.hybrid:
+ print_error(
+ 'The energy="all" filter requires standard neutron multigroup '
+ "transport."
+ )
+ G = simulation.materials[0].neutron_multigroup.G
+ self.energy = np.linspace(0, G, G + 1) - 0.5
+ shape = list(self.bin_shape)
+ shape[2] = G
+ self._set_bin_shape_and_strides(tuple(shape))
+
+ def __repr__(self) -> str:
+ text = super().__repr__()
+
text += f" - Name: {self.name}\n"
return text
-def decode_type(type_):
- if type_ == TALLY_TRACKLENGTH:
- return "Tracklength tally"
- elif type_ == TALLY_SURFACE_CROSSING:
- return "Surface crossing tally"
- elif type_ == TALLY_COLLISION:
- return "Collision tally"
-
-
def decode_score_type(type_, lower_case=False):
+ """Return the display or input name for a packed tally-score code."""
+
if type_ == SCORE_FLUX:
return "Flux" if not lower_case else "flux"
elif type_ == SCORE_DENSITY:
@@ -307,6 +473,9 @@ def decode_score_type(type_, lower_case=False):
return "Current out" if not lower_case else "current-out"
elif type_ == SCORE_ENERGY_DEPOSITION:
return "Energy deposition" if not lower_case else "energy_deposition"
+ else:
+ print_error(f"Unknown tally score code: {type_}")
+ return "Unknown score"
# ======================================================================================
@@ -315,15 +484,22 @@ def decode_score_type(type_, lower_case=False):
class TallySurfaceCrossing(Tally):
- # Annotations for Numba mode
- label: str = "surface_crossing_tally"
- non_numba: list[str] = ["surface", "cell"]
+ """Surface-crossing current tally.
- # Spatial filters
- surface: Surface | NoneType
+ Instances are normally created through :class:`Tally`, which selects this
+ estimator for current scores.
+ """
+
+ # MC/DC framework metadata
+ label = "surface_crossing_tally"
+ sub_type = TALLY_SURFACE_CROSSING
+ non_numba = ["surface", "cell"]
+
+ surface: Surface | NoneType # Non-numba
surface_filtered: bool
surface_filter_ID: int
- cell: Cell | NoneType
+
+ cell: Cell | NoneType # Non-numba
cell_filtered: bool
cell_filter_ID: int
@@ -336,17 +512,17 @@ def __init__(
mu: Sequence[float] | NoneType = None,
azi: Sequence[float] | NoneType = None,
polar_reference: Sequence[float] | NoneType = None,
+ particle_type: str | NoneType = None,
energy: Sequence[float] | str | NoneType = None,
time: Sequence[float] | NoneType = None,
- ):
- type_ = TALLY_SURFACE_CROSSING
- super(Tally, self).__init__(type_)
+ ) -> None:
super().__init__(
name,
scores,
mu=mu,
azi=azi,
polar_reference=polar_reference,
+ particle_type=particle_type,
energy=energy,
time=time,
)
@@ -364,26 +540,44 @@ def __init__(
self.cell_filtered = False
self.cell_filter_ID = -1
- # Surface filter
- if surface is not None:
+ # Set surface filter
+ if surface:
self.surface_filtered = True
- self.surface_filter_ID = surface.ID
- # Attach to the surface
- surface.tallies.append(self)
+ # Attach to surface
+ surface.surface_crossing_tallies.append(self)
- # Cell filter
- if cell is not None:
+ # Set cell filter
+ if cell:
self.cell_filtered = True
- self.cell_filter_ID = cell.ID
- # Attach to all bounding surfaces of the cell if surface filter is not specified
- if surface is None:
+ # Attach to all bounding surfaces if surface filter is not specified
+ if not self.surface_filtered:
for boundary_surface in cell.surfaces:
- boundary_surface.tallies.append(self)
+ boundary_surface.surface_crossing_tallies.append(self)
+
+ def _compile_into_simulation(self, simulation) -> bool:
+ # Already compiled?
+ if not super()._compile_into_simulation(simulation):
+ return False
- def __repr__(self):
+ # Set surface ID
+ surface = self.surface
+ if surface:
+ surface._compile_into_simulation(simulation)
+ self.surface_filter_ID = surface.ID
+
+ # Set cell ID
+ cell = self.cell
+ if cell:
+ cell._compile_into_simulation(simulation)
+ self.cell_filter_ID = cell.ID
+
+ return True
+
+ def __repr__(self) -> str:
text = super().__repr__()
+
if isinstance(self.surface, Surface):
text += f" - Surface filter: {self.surface.name}\n"
if isinstance(self.cell, Cell):
@@ -399,9 +593,16 @@ def __repr__(self):
class TallyCollision(Tally):
- # Annotations for Numba mode
- label: str = "collision_tally"
- non_numba: list[str] = ["cell", "mesh"]
+ """Collision-estimator tally.
+
+ Instances are normally created through :class:`Tally`, which selects this
+ estimator for the ``"energy_deposition"`` score.
+ """
+
+ # MC/DC framework metadata
+ label = "collision_tally"
+ sub_type = TALLY_COLLISION
+ non_numba = ["cell", "mesh"]
# Spatial filters
cell: Cell | NoneType
@@ -426,21 +627,21 @@ def __init__(
mu: Sequence[float] | NoneType = None,
azi: Sequence[float] | NoneType = None,
polar_reference: Sequence[float] | NoneType = None,
+ particle_type: str | NoneType = None,
energy: Sequence[float] | str | NoneType = None,
time: Sequence[float] | NoneType = None,
- ):
- type_ = TALLY_COLLISION
+ ) -> None:
spatial_shape = None
if mesh is not None:
spatial_shape = (mesh.Nx, mesh.Ny, mesh.Nz)
- super(Tally, self).__init__(type_)
super().__init__(
name,
scores,
mu=mu,
azi=azi,
polar_reference=polar_reference,
+ particle_type=particle_type,
energy=energy,
time=time,
spatial_shape=spatial_shape,
@@ -463,24 +664,17 @@ def __init__(
self.mesh_stride_y = -1
self.mesh_stride_x = -1
- # Cell filter
- if cell is not None:
+ # Set cell filter
+ if cell:
self.cell_filtered = True
- self.cell_filter_ID = cell.ID
- # Attach to the cell
+ # Attach to cell
cell.collision_tallies.append(self)
# Mesh filter
- if mesh is not None:
+ if mesh:
self.mesh_filtered = True
- self.mesh_filter_ID = mesh.ID
-
- # Mesh type
- if isinstance(mesh, MeshStructured):
- self.mesh_filter_type = MESH_STRUCTURED
- elif isinstance(mesh, MeshUniform):
- self.mesh_filter_type = MESH_UNIFORM
+ self.mesh_filter_type = mesh.sub_type
# Mesh strides
N_score = len(self.scores)
@@ -488,17 +682,36 @@ def __init__(
self.mesh_stride_y = N_score * mesh.Nz
self.mesh_stride_x = N_score * mesh.Nz * mesh.Ny
+ def _compile_into_simulation(self, simulation) -> bool:
+ # Already compiled?
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ # Set cell ID
+ cell = self.cell
+ if cell:
+ cell._compile_into_simulation(simulation)
+ self.cell_filter_ID = cell.ID
+
+ # Set mesh ID
+ mesh = self.mesh
+ if mesh:
+ mesh._compile_into_simulation(simulation)
+ self.mesh_filter_ID = mesh.ID
+
# Attach to all cells if cell filter is not specified
- if cell is None:
- for cell_ in simulation.cells:
- cell_.collision_tallies.append(self)
+ if not self.cell_filtered:
+ for cell in simulation.cells:
+ cell.collision_tallies.append(self)
- def __repr__(self):
+ return True
+
+ def __repr__(self) -> str:
text = super().__repr__()
- if isinstance(self.cell, Cell):
+ if self.cell:
text += f" - Cell filter: {self.cell.name}\n"
- if isinstance(self.mesh, MeshBase):
- text += f" - Mesh: {mesh_module.decode_type(self.mesh.type)} (ID {self.mesh.ID})\n"
+ if self.mesh:
+ text += f" - Mesh: {self.mesh.name}\n"
text += super()._phasespace_filter_text()
text += f" - Bin shape [mu, azi, energy, time, score]: {self.bin_shape} \n"
return text
@@ -510,9 +723,16 @@ def __repr__(self):
class TallyTracklength(Tally):
- # Annotations for Numba mode
- label: str = "tracklength_tally"
- non_numba: list[str] = ["cell", "mesh"]
+ """Track-length estimator tally.
+
+ Instances are normally created through :class:`Tally`, which selects this
+ estimator for flux, density, reaction-rate, and collision scores.
+ """
+
+ # MC/DC framework metadata
+ label = "tracklength_tally"
+ sub_type = TALLY_TRACKLENGTH
+ non_numba = ["cell", "mesh"]
# Spatial filters
cell: Cell | NoneType
@@ -537,21 +757,21 @@ def __init__(
mu: Sequence[float] | NoneType = None,
azi: Sequence[float] | NoneType = None,
polar_reference: Sequence[float] | NoneType = None,
+ particle_type: str | NoneType = None,
energy: Sequence[float] | str | NoneType = None,
time: Sequence[float] | NoneType = None,
- ):
- type_ = TALLY_TRACKLENGTH
+ ) -> None:
spatial_shape = None
if mesh is not None:
spatial_shape = (mesh.Nx, mesh.Ny, mesh.Nz)
- super(Tally, self).__init__(type_)
super().__init__(
name,
scores,
mu=mu,
azi=azi,
polar_reference=polar_reference,
+ particle_type=particle_type,
energy=energy,
time=time,
spatial_shape=spatial_shape,
@@ -574,24 +794,17 @@ def __init__(
self.mesh_stride_y = -1
self.mesh_stride_x = -1
- # Cell filter
- if cell is not None:
+ # Set cell filter
+ if cell:
self.cell_filtered = True
- self.cell_filter_ID = cell.ID
- # Attach to the cell
+ # Attach to cell
cell.tracklength_tallies.append(self)
# Mesh filter
- if mesh is not None:
+ if mesh:
self.mesh_filtered = True
- self.mesh_filter_ID = mesh.ID
-
- # Mesh type
- if isinstance(mesh, MeshStructured):
- self.mesh_filter_type = MESH_STRUCTURED
- elif isinstance(mesh, MeshUniform):
- self.mesh_filter_type = MESH_UNIFORM
+ self.mesh_filter_type = mesh.sub_type
# Mesh strides
N_score = len(self.scores)
@@ -599,17 +812,37 @@ def __init__(
self.mesh_stride_y = N_score * mesh.Nz
self.mesh_stride_x = N_score * mesh.Nz * mesh.Ny
+ def _compile_into_simulation(self, simulation) -> bool:
+ # Already compiled?
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ # Set cell ID
+ cell = self.cell
+ if cell:
+ cell._compile_into_simulation(simulation)
+ self.cell_filter_ID = cell.ID
+
+ # Set mesh ID
+ mesh = self.mesh
+ if mesh:
+ mesh._compile_into_simulation(simulation)
+ self.mesh_filter_ID = mesh.ID
+
# Attach to all cells if cell filter is not specified
- if cell is None:
- for cell_ in simulation.cells:
- cell_.tracklength_tallies.append(self)
+ if not self.cell_filtered:
+ for cell in simulation.cells:
+ cell.tracklength_tallies.append(self)
- def __repr__(self):
+ return True
+
+ def __repr__(self) -> str:
text = super().__repr__()
- if isinstance(self.cell, Cell):
+
+ if self.cell:
text += f" - Cell filter: {self.cell.name}\n"
- if isinstance(self.mesh, MeshBase):
- text += f" - Mesh: {mesh_module.decode_type(self.mesh.type)} (ID {self.mesh.ID})\n"
+ if self.mesh:
+ text += f" - Mesh: {self.mesh.name}\n"
text += super()._phasespace_filter_text()
text += f" - Bin shape [mu, azi, energy, time, score]: {self.bin_shape} \n"
return text
diff --git a/mcdc/object_/technique.py b/mcdc/object_/technique.py
index 271a2f31d..0a8ebc342 100644
--- a/mcdc/object_/technique.py
+++ b/mcdc/object_/technique.py
@@ -1,26 +1,65 @@
import numpy as np
from mcdc.constant import INF
-from mcdc.object_.base import ObjectSingleton
+from mcdc.object_.base import MCDCBase
from mcdc.object_.mesh import MeshBase, MeshUniform
from mcdc.print_ import print_error
from numpy.typing import NDArray
from typing import Annotated
+# ======================================================================================
+# Neutron multigroup
+# ======================================================================================
+
+
+class NeutronMultigroup(MCDCBase):
+ """Describe whether neutron multigroup transport is standard or hybrid."""
+
+ # MC/DC framework metadata
+ label = "neutron_multigroup"
+
+ hybrid: bool # Whether neutron multigroup transport is hybrid
+
+ def __init__(self) -> None:
+ self.hybrid = True
+
+
# ======================================================================================
# Implicit capture
# ======================================================================================
-class ImplicitCapture(ObjectSingleton):
- # Annotations for Numba mode
- label: str = "implicit_capture"
+class ImplicitCapture(MCDCBase):
+ """Simulation-owned implicit-capture configuration."""
+
+ # MC/DC framework metadata
+ label = "implicit_capture"
+
active: bool
- def __init__(self):
+ def __init__(self) -> None:
self.active = False
- def __call__(self, active: bool = True):
+ def __call__(self, active: bool = True) -> None:
+ """Configure implicit capture.
+
+ Parameters
+ ----------
+ active : bool, optional
+ Whether implicit capture is enabled.
+
+ Examples
+ --------
+ Enable implicit capture:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> simulation.technique.implicit_capture()
+
+ Disable implicit capture:
+
+ >>> simulation.technique.implicit_capture(active=False)
+ """
self.active = active
@@ -29,18 +68,45 @@ def __call__(self, active: bool = True):
# ======================================================================================
-class WeightedEmission(ObjectSingleton):
- # Annotations for Numba mode
- label: str = "weighted_emission"
+class WeightedEmission(MCDCBase):
+ """Simulation-owned weighted-emission configuration."""
+
+ # MC/DC framework metadata
+ label = "weighted_emission"
active: bool
weight_target: float
- def __init__(self):
+ def __init__(self) -> None:
self.active = False
self.weight_target = 0.0
- def __call__(self, active: bool = True, weight_target: float = 1.0):
+ def __call__(self, active: bool = True, weight_target: float = 1.0) -> None:
+ """Configure weighted emission.
+
+ Parameters
+ ----------
+ active : bool, optional
+ Whether the technique is active.
+ weight_target : float, optional
+ Target statistical weight for emitted particles.
+
+ Examples
+ --------
+ Enable weighted emission with unit target weight:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> simulation.technique.weighted_emission(weight_target=1.0)
+
+ Select a different target weight:
+
+ >>> simulation.technique.weighted_emission(weight_target=0.5)
+
+ Disable weighted emission:
+
+ >>> simulation.technique.weighted_emission(active=False)
+ """
self.active = active
self.weight_target = weight_target
@@ -50,20 +116,53 @@ def __call__(self, active: bool = True, weight_target: float = 1.0):
# ======================================================================================
-class GlobalWeightRoulette(ObjectSingleton):
- # Annotations for Numba mode
- label: str = "global_weight_roulette"
+class GlobalWeightRoulette(MCDCBase):
+ """Simulation-owned global weight-roulette configuration."""
+
+ # MC/DC framework metadata
+ label = "global_weight_roulette"
active: bool
weight_threshold: float
weight_target: float
- def __init__(self):
+ def __init__(self) -> None:
self.active = False
self.weight_threshold = 0.0
self.weight_target = 1.0
- def __call__(self, weight_threshold: float = 0.0, weight_target: float = 1.0):
+ def __call__(
+ self, weight_threshold: float = 0.0, weight_target: float = 1.0
+ ) -> None:
+ """Enable roulette below a global weight threshold.
+
+ Parameters
+ ----------
+ weight_threshold : float, optional
+ Particle weight below which roulette is applied.
+ weight_target : float, optional
+ Statistical weight assigned to particles that survive roulette.
+ Must be greater than or equal to ``weight_threshold``.
+
+ Examples
+ --------
+ Apply roulette below a particle weight of 0.25 and raise surviving
+ particles to unit weight:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> simulation.technique.global_weight_roulette(
+ ... weight_threshold=0.25,
+ ... weight_target=1.0,
+ ... )
+
+ Use a lower target weight:
+
+ >>> simulation.technique.global_weight_roulette(
+ ... weight_threshold=0.1,
+ ... weight_target=0.5,
+ ... )
+ """
if weight_threshold > weight_target:
print_error(
"For weight roulette, weight threshold has to be smaller than the target"
@@ -78,8 +177,11 @@ def __call__(self, weight_threshold: float = 0.0, weight_target: float = 1.0):
# ======================================================================================
-class WeightWindows(ObjectSingleton):
- label: str = "weight_windows"
+class WeightWindows(MCDCBase):
+ """Simulation-owned particle weight-window configuration."""
+
+ # MC/DC framework metadata
+ label = "weight_windows"
active: bool
@@ -97,7 +199,7 @@ class WeightWindows(ObjectSingleton):
target_weights: Annotated[NDArray[np.float64], ("Ne", "Nx", "Ny", "Nz")]
upper_weights: Annotated[NDArray[np.float64], ("Ne", "Nx", "Ny", "Nz")]
- def __init__(self):
+ def __init__(self) -> None:
self.active = False
self.energy_bounds = np.array([0.0, 1.0])
self.Ne = 1
@@ -109,7 +211,52 @@ def __init__(self):
self.target_weights = np.array([1.0]).reshape(*shape)
self.upper_weights = np.array([1.0]).reshape(*shape)
- def __call__(self, weight_windows, mesh=None, energy=None):
+ def __call__(
+ self,
+ weight_windows: NDArray[np.float64],
+ mesh: MeshBase | None = None,
+ energy: NDArray[np.float64] | None = None,
+ ) -> None:
+ """Configure lower, target, and upper particle weights.
+
+ Parameters
+ ----------
+ weight_windows : ndarray, shape (Ne, Nx, Ny, Nz, 3)
+ Lower, target, and upper weights in the final dimension. Every
+ lower weight must be positive, and each window must satisfy
+ ``lower <= target <= upper``.
+ mesh : MeshUniform or MeshStructured, optional
+ Spatial mesh. The default is one unbounded uniform bin.
+ energy : ndarray, optional
+ Strictly increasing energy boundaries in eV. The default is one
+ all-energy bin.
+
+ Examples
+ --------
+ Apply one weight window over all space and energy:
+
+ >>> import numpy as np
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> windows = np.array([0.5, 1.0, 2.0]).reshape(1, 1, 1, 1, 3)
+ >>> simulation.technique.weight_windows(windows)
+
+ Configure weight windows on a uniform spatial mesh:
+
+ >>> mesh = mcdc.MeshUniform(x=(-5.0, 1.0, 10))
+ >>> windows = np.tile([0.25, 0.5, 1.0], (1, 10, 1, 1, 1))
+ >>> simulation.technique.weight_windows(windows, mesh=mesh)
+
+ Configure both energy- and space-dependent windows:
+
+ >>> energy = np.array([0.0, 0.625, 20.0e6])
+ >>> windows = np.tile([0.25, 0.5, 1.0], (2, 10, 1, 1, 1))
+ >>> simulation.technique.weight_windows(
+ ... windows,
+ ... mesh=mesh,
+ ... energy=energy,
+ ... )
+ """
# fill in defaults
if mesh is None:
mesh = MeshUniform()
@@ -180,13 +327,68 @@ def __call__(self, weight_windows, mesh=None, energy=None):
# ======================================================================================
-class PopulationControl(ObjectSingleton):
- # Annotations for Numba mode
- label: str = "population_control"
+class PopulationControl(MCDCBase):
+ """Simulation-owned source-bank population-control configuration."""
+
+ # MC/DC framework metadata
+ label = "population_control"
+
active: bool
- def __init__(self):
+ def __init__(self) -> None:
self.active = False
- def __call__(self, active: bool = True):
+ def __call__(self, active: bool = True) -> None:
+ """Configure source-bank population control.
+
+ Parameters
+ ----------
+ active : bool, optional
+ Whether source-bank population control is enabled.
+
+ Examples
+ --------
+ Enable source-bank population control:
+
+ >>> import mcdc
+ >>> simulation = mcdc.Simulation()
+ >>> simulation.technique.population_control()
+
+ Disable source-bank population control:
+
+ >>> simulation.technique.population_control(active=False)
+ """
self.active = active
+
+
+# ======================================================================================
+# Simulation technique collection
+# ======================================================================================
+
+
+class Technique(MCDCBase):
+ """Own all simulation-wide transport-technique configurations.
+
+ Access the individual callable configurations through
+ ``simulation.technique``. The same hierarchy is retained in the packed
+ runtime simulation.
+ """
+
+ # MC/DC framework metadata
+ label = "technique"
+
+ neutron_multigroup: NeutronMultigroup
+ implicit_capture: ImplicitCapture
+ weighted_emission: WeightedEmission
+ global_weight_roulette: GlobalWeightRoulette
+ weight_windows: WeightWindows
+ population_control: PopulationControl
+
+ def __init__(self) -> None:
+ # Construct every simulation-wide technique configuration
+ self.neutron_multigroup = NeutronMultigroup()
+ self.implicit_capture = ImplicitCapture()
+ self.weighted_emission = WeightedEmission()
+ self.global_weight_roulette = GlobalWeightRoulette()
+ self.weight_windows = WeightWindows()
+ self.population_control = PopulationControl()
diff --git a/mcdc/object_/transport_model_data.py b/mcdc/object_/transport_model_data.py
new file mode 100644
index 000000000..05ceb83fc
--- /dev/null
+++ b/mcdc/object_/transport_model_data.py
@@ -0,0 +1,472 @@
+from types import NoneType
+from typing import Annotated
+
+import numpy as np
+from numpy import float64
+from numpy.typing import ArrayLike, NDArray
+
+from mcdc.constant import (
+ NEUTRON_MULTIGROUP_ENERGY_MIDPOINT,
+ NEUTRON_MULTIGROUP_ENERGY_MIDPOINT_LOG,
+ NEUTRON_MULTIGROUP_ENERGY_UNIFORM,
+ NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG,
+)
+from mcdc.object_.base import MCDCObject
+from mcdc.print_ import print_1d_array, print_error
+
+_NEUTRON_MULTIGROUP_ENERGY_REPRESENTATIONS = {
+ "midpoint": NEUTRON_MULTIGROUP_ENERGY_MIDPOINT,
+ "log_midpoint": NEUTRON_MULTIGROUP_ENERGY_MIDPOINT_LOG,
+ "uniform": NEUTRON_MULTIGROUP_ENERGY_UNIFORM,
+ "log_uniform": NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG,
+}
+_NEUTRON_MULTIGROUP_LOG_ENERGY_REPRESENTATIONS = {
+ NEUTRON_MULTIGROUP_ENERGY_MIDPOINT_LOG,
+ NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG,
+}
+
+
+class NeutronMultigroupData(MCDCObject):
+ """Groupwise macroscopic interaction data for neutron multigroup transport.
+
+ Neutron multigroup transport represents neutron energy with discrete groups
+ and describes interactions using groupwise cross sections, production
+ spectra, speeds, and delayed-precursor data. For a multigroup-only material,
+ :meth:`mcdc.Material.multigroup` provides the convenient entry point.
+ Construct ``NeutronMultigroupData`` directly when a material needs an
+ explicit physical energy grid, including when attaching it alongside a
+ :ref:`native composition `.
+
+ Parameters
+ ----------
+ capture : array_like of float, optional
+ Macroscopic capture cross section in ``cm^-1`` for each incoming
+ energy group, with shape ``(G,)``.
+ scatter : array_like of float, optional
+ Macroscopic scattering-production matrix in ``cm^-1`` with shape
+ ``(G, G)``, indexed as ``scatter[g_out, g_in]``. Column sums define the
+ scattering cross section for each incoming group. Columns are
+ normalized internally to form the scattering spectrum.
+ fission : array_like of float, optional
+ Macroscopic fission cross section in ``cm^-1`` for each incoming
+ energy group, with shape ``(G,)``. Supplying fission requires at least
+ one of ``nu_p`` or ``nu_d``.
+ nu_s : array_like of float, optional
+ Mean number of neutrons produced per scattering event in each
+ incoming group, with shape ``(G,)``. Defaults to one.
+ nu_p : array_like of float, optional
+ Mean prompt-fission yield for each incoming group, with shape
+ ``(G,)``.
+ nu_d : array_like of float, optional
+ Mean delayed-fission yield with shape ``(J, G)``, indexed as
+ ``nu_d[j, g_in]``. Its first dimension determines the number ``J`` of
+ delayed precursor groups.
+ chi_p : array_like of float, optional
+ Prompt-fission spectrum. Shape ``(G,)`` applies one outgoing spectrum
+ to every incoming group. Shape ``(G, G)`` is indexed as
+ ``chi_p[g_out, g_in]``. Required when ``nu_p`` is supplied and
+ ``G > 1``.
+ chi_d : array_like of float, optional
+ Delayed-fission spectrum with shape ``(G, J)``, indexed as
+ ``chi_d[g_out, j]``. Required when ``nu_d`` is supplied and ``G > 1``.
+ speed : array_like of float, optional
+ Neutron speed in cm/s for each energy group, with shape ``(G,)``.
+ Defaults to one.
+ decay_rate : array_like of float, optional
+ Decay constant in ``s^-1`` for each delayed precursor group, with
+ shape ``(J,)``. Defaults to infinity.
+ energy_grid : array_like of float, optional
+ Physical energy-group boundaries in eV with shape ``(G + 1,)``.
+ User-supplied boundaries must be strictly increasing, and group ``g``
+ spans ``energy_grid[g] <= E < energy_grid[g + 1]``. The default is a
+ zero-valued placeholder used only when physical energy mapping is not
+ required.
+ energy_representation : str or int, optional
+ Policy used to reconstruct continuous energy from a group when a
+ physical energy grid participates in transport.
+ ``"midpoint"`` uses the arithmetic midpoint, ``"log_midpoint"`` uses
+ the geometric midpoint, ``"uniform"`` samples uniformly in energy,
+ and ``"log_uniform"`` samples uniformly in log-energy. The default is
+ ``"midpoint"``. Only the default ``"midpoint"`` placeholder is
+ accepted when ``energy_grid`` is omitted. Logarithmic policies require
+ positive boundaries.
+ The corresponding ``NEUTRON_MULTIGROUP_ENERGY_*`` integer constants
+ are also accepted.
+
+ Notes
+ -----
+ ``G`` is inferred from ``capture``, ``scatter``, or ``fission``. An
+ explicit energy grid is required whenever simulation transport needs to
+ map continuous neutron energy to material-local groups.
+
+ Examples
+ --------
+ Construct one-group data with capture, scattering, and prompt fission:
+
+ >>> import mcdc
+ >>> import numpy as np
+ >>> neutron_multigroup = mcdc.NeutronMultigroupData(
+ ... capture=np.array([1.0 / 3.0]),
+ ... scatter=np.array([[1.0 / 3.0]]),
+ ... fission=np.array([1.0 / 3.0]),
+ ... nu_p=np.array([2.3]),
+ ... energy_grid=np.array([1.0e-5, 20.0e6]),
+ ... )
+
+ Construct data with two energy groups. Scattering is indexed by outgoing
+ then incoming group:
+
+ >>> two_group = mcdc.NeutronMultigroupData(
+ ... capture=np.array([0.1, 0.2]),
+ ... scatter=np.array([
+ ... [1.0, 2.0],
+ ... [3.0, 0.0],
+ ... ]),
+ ... nu_s=np.array([1.1, 1.2]),
+ ... energy_grid=np.array([1.0e-5, 1.0, 20.0e6]),
+ ... )
+
+ Construct two-group fission data with two delayed precursor groups. The
+ delayed yield is indexed by precursor then incoming energy group, while
+ the delayed spectrum is indexed by outgoing energy then precursor group:
+
+ >>> multiple_precursors = mcdc.NeutronMultigroupData(
+ ... fission=np.array([0.2, 0.3]),
+ ... nu_d=np.array([
+ ... [0.1, 0.2],
+ ... [0.3, 0.4],
+ ... ]),
+ ... chi_d=np.array([
+ ... [1.0, 3.0],
+ ... [3.0, 1.0],
+ ... ]),
+ ... decay_rate=np.array([0.01, 0.02]),
+ ... )
+ """
+
+ # MC/DC framework metadata
+ label = "neutron_multigroup_data"
+
+ G: int
+ J: int
+
+ energy_grid: Annotated[NDArray[float64], ("G+1",)]
+ energy_representation: int
+
+ speed: Annotated[NDArray[float64], ("G",)]
+ decay_rate: Annotated[NDArray[float64], ("J",)]
+
+ capture: Annotated[NDArray[float64], ("G",)]
+ scatter: Annotated[NDArray[float64], ("G",)]
+ fission: Annotated[NDArray[float64], ("G",)]
+ total: Annotated[NDArray[float64], ("G",)]
+
+ nu_s: Annotated[NDArray[float64], ("G",)]
+ nu_p: Annotated[NDArray[float64], ("G",)]
+ nu_d: Annotated[NDArray[float64], ("G", "J")]
+ nu_d_total: Annotated[NDArray[float64], ("G",)]
+ nu_f: Annotated[NDArray[float64], ("G",)]
+
+ chi_s: Annotated[NDArray[float64], ("G", "G")]
+ chi_p: Annotated[NDArray[float64], ("G", "G")]
+ chi_d: Annotated[NDArray[float64], ("J", "G")]
+
+ fissionable: bool
+
+ def __init__(
+ self,
+ capture: ArrayLike | NoneType = None,
+ scatter: ArrayLike | NoneType = None,
+ fission: ArrayLike | NoneType = None,
+ nu_s: ArrayLike | NoneType = None,
+ nu_p: ArrayLike | NoneType = None,
+ nu_d: ArrayLike | NoneType = None,
+ chi_p: ArrayLike | NoneType = None,
+ chi_d: ArrayLike | NoneType = None,
+ speed: ArrayLike | NoneType = None,
+ decay_rate: ArrayLike | NoneType = None,
+ energy_grid: ArrayLike | NoneType = None,
+ energy_representation: str | int = "midpoint",
+ ) -> None:
+ super().__init__()
+
+ # Convert user inputs to the runtime array representation
+ capture = _as_array("capture", capture)
+ scatter = _as_array("scatter", scatter)
+ fission = _as_array("fission", fission)
+ nu_s = _as_array("nu_s", nu_s)
+ nu_p = _as_array("nu_p", nu_p)
+ nu_d = _as_array("nu_d", nu_d)
+ chi_p = _as_array("chi_p", chi_p)
+ chi_d = _as_array("chi_d", chi_d)
+ speed = _as_array("speed", speed)
+ decay_rate = _as_array("decay_rate", decay_rate)
+ energy_grid = _as_array("energy_grid", energy_grid)
+
+ # Infer dimensions from the defining cross sections and delayed yields
+ self.G = _infer_group_count(capture, scatter, fission)
+ self.J = _infer_delayed_group_count(nu_d, self.G)
+
+ # Validate all user-facing array shapes before deriving stored data
+ _validate_shape("capture", capture, (self.G,))
+ _validate_shape("scatter", scatter, (self.G, self.G))
+ _validate_shape("fission", fission, (self.G,))
+ _validate_shape("nu_s", nu_s, (self.G,))
+ _validate_shape("nu_p", nu_p, (self.G,))
+ _validate_shape("nu_d", nu_d, (self.J, self.G))
+ _validate_prompt_spectrum_shape(chi_p, self.G)
+ _validate_shape("chi_d", chi_d, (self.G, self.J))
+ _validate_shape("speed", speed, (self.G,))
+ _validate_shape("decay_rate", decay_rate, (self.J,))
+ _validate_shape("energy_grid", energy_grid, (self.G + 1,))
+
+ # Reject values that cannot represent physical groupwise data
+ for name, array in (
+ ("capture", capture),
+ ("scatter", scatter),
+ ("fission", fission),
+ ("nu_s", nu_s),
+ ("nu_p", nu_p),
+ ("nu_d", nu_d),
+ ("chi_p", chi_p),
+ ("chi_d", chi_d),
+ ):
+ _validate_nonnegative(name, array)
+ _validate_positive("speed", speed)
+ _validate_decay_rate(decay_rate)
+
+ # Validate relationships between fission yields and spectra
+ if fission is not None and nu_p is None and nu_d is None:
+ print_error("NeutronMultigroupData fission data requires nu_p or nu_d.")
+ if fission is None and (nu_p is not None or nu_d is not None):
+ print_error(
+ "NeutronMultigroupData fission yields require fission cross sections."
+ )
+ if chi_p is not None and nu_p is None:
+ print_error("NeutronMultigroupData chi_p requires nu_p.")
+ if chi_d is not None and nu_d is None:
+ print_error("NeutronMultigroupData chi_d requires nu_d.")
+ if decay_rate is not None and nu_d is None:
+ print_error("NeutronMultigroupData decay_rate requires nu_d.")
+
+ # Resolve the energy grid and continuous-energy reconstruction policy
+ self.energy_representation = _resolve_energy_representation(
+ energy_representation
+ )
+ if energy_grid is None:
+ if self.energy_representation != NEUTRON_MULTIGROUP_ENERGY_MIDPOINT:
+ print_error(
+ "NeutronMultigroupData requires an explicit energy_grid when "
+ "energy_representation is not 'midpoint'."
+ )
+ self.energy_grid = np.zeros(self.G + 1, dtype=float64)
+ else:
+ if not np.all(np.isfinite(energy_grid)):
+ print_error("NeutronMultigroupData energy grid entries must be finite.")
+ if np.any(np.diff(energy_grid) <= 0.0):
+ print_error(
+ "NeutronMultigroupData energy grid must be strictly increasing."
+ )
+ if (
+ self.energy_representation
+ in _NEUTRON_MULTIGROUP_LOG_ENERGY_REPRESENTATIONS
+ and energy_grid[0] <= 0.0
+ ):
+ print_error(
+ "NeutronMultigroupData logarithmic energy representation requires positive "
+ "energy boundaries."
+ )
+ self.energy_grid = energy_grid
+
+ # Apply transport defaults for group speed and precursor decay
+ self.speed = np.ones(self.G, dtype=float64) if speed is None else speed
+ self.decay_rate = (
+ np.full(self.J, np.inf, dtype=float64) if decay_rate is None else decay_rate
+ )
+
+ # Build cross sections and the normalized scattering spectrum
+ self.capture = np.zeros(self.G, dtype=float64) if capture is None else capture
+ self.chi_s = np.zeros((self.G, self.G), dtype=float64)
+ if scatter is None:
+ self.scatter = np.zeros(self.G, dtype=float64)
+ else:
+ self.scatter = np.sum(scatter, axis=0)
+ self.chi_s = np.swapaxes(scatter, 0, 1).copy()
+ _normalize_rows(self.chi_s, self.scatter > 0.0, "scatter")
+
+ self.fissionable = fission is not None
+ self.fission = np.zeros(self.G, dtype=float64) if fission is None else fission
+ self.total = self.capture + self.scatter + self.fission
+
+ # Build scattering and fission yields
+ self.nu_s = np.ones(self.G, dtype=float64) if nu_s is None else nu_s
+ self.nu_p = np.zeros(self.G, dtype=float64) if nu_p is None else nu_p
+ if nu_d is None:
+ self.nu_d = np.zeros((self.G, self.J), dtype=float64)
+ else:
+ self.nu_d = np.swapaxes(nu_d, 0, 1).copy()
+ self.nu_d_total = np.sum(self.nu_d, axis=1)
+ self.nu_f = self.nu_p + self.nu_d_total
+
+ # Build normalized prompt- and delayed-fission spectra
+ self.chi_p = np.zeros((self.G, self.G), dtype=float64)
+ if nu_p is not None:
+ if self.G == 1:
+ self.chi_p[:] = 1.0
+ elif chi_p is None:
+ print_error("NeutronMultigroupData with nu_p and G > 1 requires chi_p.")
+ else:
+ if chi_p.ndim == 1:
+ chi_p = np.tile(chi_p[:, np.newaxis], (1, self.G))
+ self.chi_p = np.swapaxes(chi_p, 0, 1).copy()
+ _normalize_rows(self.chi_p, self.nu_p > 0.0, "chi_p")
+
+ self.chi_d = np.zeros((self.J, self.G), dtype=float64)
+ if nu_d is not None:
+ if self.G == 1:
+ self.chi_d[:] = 1.0
+ elif chi_d is None:
+ print_error("NeutronMultigroupData with nu_d and G > 1 requires chi_d.")
+ else:
+ self.chi_d = np.swapaxes(chi_d, 0, 1).copy()
+ active_delayed_groups = np.any(nu_d > 0.0, axis=1)
+ _normalize_rows(self.chi_d, active_delayed_groups, "chi_d")
+
+ def __repr__(self) -> str:
+ text = super().__repr__()
+ text += f" - G: {self.G}\n"
+ text += f" - J: {self.J}\n"
+ text += f" - Energy grid {print_1d_array(self.energy_grid)}\n"
+ text += f" - Sigma_c {print_1d_array(self.capture)}\n"
+ text += f" - Sigma_s {print_1d_array(self.scatter)}\n"
+ text += f" - Sigma_f {print_1d_array(self.fission)}\n"
+ text += f" - nu_s {print_1d_array(self.nu_s)}\n"
+ text += f" - nu_p {print_1d_array(self.nu_p)}\n"
+ text += f" - nu_d {print_1d_array(self.nu_d.flatten())}\n"
+ text += f" - chi_s {print_1d_array(self.chi_s.flatten())}\n"
+ text += f" - chi_p {print_1d_array(self.chi_p.flatten())}\n"
+ text += f" - chi_d {print_1d_array(self.chi_d.flatten())}\n"
+ text += f" - speed {print_1d_array(self.speed)}\n"
+ text += f" - decay rate {print_1d_array(self.decay_rate)}\n"
+ return text
+
+
+def _as_array(name, value):
+ """Convert an optional user value to a float64 array."""
+ if value is None:
+ return None
+ try:
+ return np.asarray(value, dtype=float64)
+ except (TypeError, ValueError):
+ print_error(f"NeutronMultigroupData {name} must be numeric array-like data.")
+
+
+def _infer_group_count(capture, scatter, fission) -> int:
+ """Infer the energy-group count from the first supplied cross section."""
+ defining = capture if capture is not None else scatter
+ defining = fission if defining is None else defining
+ if defining is None:
+ return 0
+ if defining.ndim == 0:
+ print_error(
+ "NeutronMultigroupData cross sections must be arrays with an energy-group axis."
+ )
+ if defining.shape[0] == 0:
+ print_error(
+ "NeutronMultigroupData cross sections must define at least one energy group."
+ )
+ return defining.shape[0]
+
+
+def _infer_delayed_group_count(nu_d, G: int) -> int:
+ """Infer the delayed-group count from delayed-fission yields."""
+ if nu_d is None:
+ return 0
+ if nu_d.ndim != 2:
+ print_error(
+ f"NeutronMultigroupData nu_d must have shape (J, G); got {nu_d.shape}."
+ )
+ if nu_d.shape[1] != G:
+ print_error(
+ f"NeutronMultigroupData nu_d must have shape (J, G) with G = {G}; got {nu_d.shape}."
+ )
+ return nu_d.shape[0]
+
+
+def _validate_shape(name, array, expected) -> None:
+ """Require an optional array to have its declared NeutronMultigroupData shape."""
+ if array is not None and array.shape != expected:
+ print_error(
+ f"NeutronMultigroupData {name} must have shape {expected}; got {array.shape}."
+ )
+
+
+def _validate_prompt_spectrum_shape(chi_p, G: int) -> None:
+ """Accept either a shared vector or an incoming-group spectrum matrix."""
+ if chi_p is None:
+ return
+ if chi_p.shape not in ((G,), (G, G)):
+ print_error(
+ f"NeutronMultigroupData chi_p must have shape ({G},) or ({G}, {G}); got {chi_p.shape}."
+ )
+
+
+def _validate_nonnegative(name, array) -> None:
+ """Reject negative or non-finite physical data."""
+ if array is None:
+ return
+ if not np.all(np.isfinite(array)) or np.any(array < 0.0):
+ print_error(
+ f"NeutronMultigroupData {name} entries must be finite and nonnegative."
+ )
+
+
+def _validate_positive(name, array) -> None:
+ """Reject nonpositive or non-finite physical data."""
+ if array is None:
+ return
+ if not np.all(np.isfinite(array)) or np.any(array <= 0.0):
+ print_error(
+ f"NeutronMultigroupData {name} entries must be finite and positive."
+ )
+
+
+def _validate_decay_rate(array) -> None:
+ """Allow the infinite default sentinel while rejecting invalid rates."""
+ if array is None:
+ return
+ if np.any(np.isnan(array)) or np.any(array < 0.0):
+ print_error("NeutronMultigroupData decay_rate entries must be nonnegative.")
+
+
+def _resolve_energy_representation(policy) -> int:
+ """Resolve a public reconstruction policy name or integer code."""
+ if isinstance(policy, str):
+ if policy not in _NEUTRON_MULTIGROUP_ENERGY_REPRESENTATIONS:
+ expected = ", ".join(_NEUTRON_MULTIGROUP_ENERGY_REPRESENTATIONS)
+ print_error(
+ f"Unknown NeutronMultigroupData energy representation {policy!r}. "
+ f"Expected one of: {expected}."
+ )
+ return _NEUTRON_MULTIGROUP_ENERGY_REPRESENTATIONS[policy]
+
+ if (
+ not isinstance(policy, (bool, np.bool_))
+ and isinstance(policy, (int, np.integer))
+ and policy in _NEUTRON_MULTIGROUP_ENERGY_REPRESENTATIONS.values()
+ ):
+ return int(policy)
+
+ print_error(f"Unknown NeutronMultigroupData energy representation {policy!r}.")
+
+
+def _normalize_rows(array, required, name) -> None:
+ """Normalize spectra over outgoing groups and validate active rows."""
+ for index in range(len(array)):
+ norm = np.sum(array[index])
+ if required[index] and norm <= 0.0:
+ print_error(
+ f"NeutronMultigroupData {name} spectrum {index} must have positive mass."
+ )
+ if norm > 0.0:
+ array[index] /= norm
diff --git a/mcdc/object_/universe.py b/mcdc/object_/universe.py
index ad477525b..43477f6a8 100644
--- a/mcdc/object_/universe.py
+++ b/mcdc/object_/universe.py
@@ -1,6 +1,6 @@
from __future__ import annotations
from types import NoneType
-from typing import TYPE_CHECKING, Annotated
+from typing import TYPE_CHECKING, Annotated, TypeAlias
if TYPE_CHECKING:
from mcdc.object_.cell import Cell
@@ -15,7 +15,7 @@
####
from mcdc.constant import INF
-from mcdc.object_.base import ObjectNonSingleton
+from mcdc.object_.base import MCDCObject
from mcdc.util import flatten
# ======================================================================================
@@ -23,108 +23,156 @@
# ======================================================================================
-class Universe(ObjectNonSingleton):
- """
- Define a list of cells as a universe.
+class Universe(MCDCObject):
+ """Reusable collections of cells in the simulation geometry.
Parameters
----------
name : str, optional
- User label.
- cells : list of Cell
- List of cells that comprise the universe.
- root : bool, optional
- Flag to set as the root universe (ID = 0).
-
- Returns
- -------
- Universe
- The universe object.
-
- See Also
+ User-facing universe name.
+ cells : list of Cell, optional
+ Cells belonging to the universe. Cells are tested in list order by the
+ geometry search.
+
+ Examples
--------
- mcdc.Cell : Creates a cell that can be used to define a universe.
+ Group two cells into a reusable universe:
+
+ >>> import numpy as np
+ >>> import mcdc
+ >>> left = mcdc.Surface.PlaneX(x=-1.0)
+ >>> middle = mcdc.Surface.PlaneX(x=0.0)
+ >>> right = mcdc.Surface.PlaneX(x=1.0)
+ >>> material = mcdc.Material.multigroup(capture=np.array([1.0]))
+ >>> cells = [
+ ... mcdc.Cell(region=+left & -middle, fill=material),
+ ... mcdc.Cell(region=+middle & -right, fill=material),
+ ... ]
+ >>> universe = mcdc.Universe(name="Two regions", cells=cells)
+
+ Create a universe for a spherical inclusion and its surrounding material:
+
+ >>> sphere = mcdc.Surface.Sphere(radius=0.5)
+ >>> fuel = mcdc.Material.multigroup(
+ ... fission=np.array([0.2]), nu_p=np.array([2.5])
+ ... )
+ >>> water = mcdc.Material.multigroup(capture=np.array([0.01]))
+ >>> pin = mcdc.Universe(
+ ... name="Pin",
+ ... cells=[
+ ... mcdc.Cell(region=-sphere, fill=fuel),
+ ... mcdc.Cell(region=+sphere, fill=water),
+ ... ],
+ ... )
+
+ Use a universe as a cell fill:
+
+ >>> placed_pin = mcdc.Cell(fill=pin, translation=[1.0, 0.0, 0.0])
"""
- # Annotations for Numba mode
- label: str = "universe"
- #
+ # MC/DC framework metadata
+ label = "universe"
+
name: str
cells: list[Cell]
- def __init__(self, name: str = "", cells: list[Cell] = [], root: bool = False):
- # Custom treatment for root universe
- if root:
- super().__init__(register=False)
- self.ID = 0
- else:
- super().__init__()
-
- # Set name
- if name != "":
- self.name = name
- else:
- self.name = f"{self.label}_{self.ID}"
+ def __init__(self, name: str = "", cells: list[Cell] = []) -> None:
+ super().__init__()
+ self.name = name or "(Unnamed universe)"
self.cells = cells
- def __repr__(self):
- text = "\n"
- text += f"Universe\n"
- if self.ID == 0:
- text += f" - ID: {self.ID} (root)\n"
- else:
- text += f" - ID: {self.ID}\n"
+ def __repr__(self) -> str:
+ text = super().__repr__()
+
text += f" - Name: {self.name}\n"
- text += f"Cells: {[x.ID for x in self.cells]}"
+ text += f" - Cells: {', '.join(x.name for x in self.cells)}\n"
return text
+UniverseLayout: TypeAlias = (
+ list[Universe] | list[list[Universe]] | list[list[list[Universe]]]
+)
+
+
# ======================================================================================
# Lattice
# ======================================================================================
-class Lattice(ObjectNonSingleton):
- """
- Define a regular lattice of universes.
+class Lattice(MCDCObject):
+ """Repeated arrangements of universes in the simulation geometry.
Parameters
----------
name : str, optional
- User label.
- x : tuple of (float, float, int), optional
- Lattice specification along x: ``(x0, dx, Nx)``.
- y : tuple of (float, float, int), optional
- Lattice specification along y: ``(y0, dy, Ny)``.
- z : tuple of (float, float, int), optional
- Lattice specification along z: ``(z0, dz, Nz)``.
- universes : list of Universe
- Array of universes filling each lattice cell.
-
- Returns
- -------
- Lattice
- The lattice object.
-
- See Also
+ User-facing lattice name.
+ x, y, z : tuple of (float, float, int), optional
+ ``(origin, spacing, number_of_bins)`` for each finite lattice axis, in
+ cm. An omitted axis is treated as a single unbounded bin.
+ universes : nested list of Universe, optional
+ Universe layout supplied in ``[z][y][x]`` order. The y and z axes are
+ reversed internally to match MC/DC's Cartesian indexing convention.
+
+ Notes
+ -----
+ A lattice retains the supplied :class:`Universe` objects. When the owning
+ simulation is compiled, those universes are registered and the packed
+ lattice IDs are rebuilt from their simulation-local IDs.
+
+ Examples
--------
- mcdc.Universe : Creates a universe to place in a lattice.
+ Place two universes next to each other along x:
+
+ >>> import mcdc
+ >>> left = mcdc.Universe(name="Left")
+ >>> right = mcdc.Universe(name="Right")
+ >>> lattice = mcdc.Lattice(
+ ... x=(-1.0, 1.0, 2),
+ ... universes=[[left, right]],
+ ... )
+
+ Build a two-dimensional 2-by-2 lattice:
+
+ >>> u00 = mcdc.Universe(name="Lower left")
+ >>> u10 = mcdc.Universe(name="Lower right")
+ >>> u01 = mcdc.Universe(name="Upper left")
+ >>> u11 = mcdc.Universe(name="Upper right")
+ >>> lattice_xy = mcdc.Lattice(
+ ... x=(-1.0, 1.0, 2),
+ ... y=(-1.0, 1.0, 2),
+ ... universes=[
+ ... [u00, u10],
+ ... [u01, u11],
+ ... ],
+ ... )
+
+ Place the lattice inside a cell:
+
+ >>> lattice_cell = mcdc.Cell(fill=lattice_xy)
"""
- # Annotations for Numba mode
- label: str = "lattice"
- #
+ # MC/DC framework metadata
+ label = "lattice"
+ non_numba = ["universes"]
+
name: str
+
x0: float
dx: float
Nx: int
+
y0: float
dy: float
Ny: int
+
z0: float
dz: float
Nz: int
+
+ universes: ( # Non-numba
+ list[Universe] | list[list[Universe]] | list[list[list[Universe]]]
+ )
universe_IDs: Annotated[NDArray[int64], ("Nx", "Ny", "Nz")]
def __init__(
@@ -133,15 +181,12 @@ def __init__(
x: tuple[float, float, int] | NoneType = None,
y: tuple[float, float, int] | NoneType = None,
z: tuple[float, float, int] | NoneType = None,
- universes: list[Universe] = None,
- ):
+ universes: UniverseLayout = [],
+ ) -> None:
super().__init__()
- # Set name
- if name != "":
- self.name = name
- else:
- self.name = f"{self.label}_{self.ID}"
+ self.name = name or "(Unnamed lattice)"
+ self.universes = universes
# Default uniform grids
self.x0 = -INF
@@ -171,15 +216,30 @@ def __init__(
self.dz = z[1]
self.Nz = z[2]
+ self._set_universe_IDs()
+
+ def _compile_into_simulation(self, simulation) -> bool:
+ """Compile contained universes and rebuild their lattice IDs."""
+ if not super()._compile_into_simulation(simulation):
+ return False
+
+ for universe in flatten(self.universes):
+ universe._compile_into_simulation(simulation)
+
+ self._set_universe_IDs()
+ return True
+
+ def _set_universe_IDs(self) -> None:
+ """Build the packed universe-ID array from the universe layout."""
# Set universe IDs
get_ID = np.vectorize(lambda obj: obj.ID)
- universe_IDs = get_ID(universes)
+ universe_IDs = get_ID(self.universes)
ax_expand = []
- if x is None:
+ if self.dx == 2 * INF:
ax_expand.append(2)
- if y is None:
+ if self.dy == 2 * INF:
ax_expand.append(1)
- if z is None:
+ if self.dz == 2 * INF:
ax_expand.append(0)
for ax in ax_expand:
universe_IDs = np.expand_dims(universe_IDs, axis=ax)
@@ -190,13 +250,12 @@ def __init__(
universe_IDs = np.flip(universe_IDs, axis=2)
self.universe_IDs = np.array(universe_IDs)
- def __repr__(self):
- text = "\n"
- text += f"Lattice\n"
- text += f" - ID: {self.ID}\n"
+ def __repr__(self) -> str:
+ text = super().__repr__()
+
text += f" - Name: {self.name}\n"
text += f" - (x0, dx, Nx): ({self.x0}, {self.dx}, {self.Nx})\n"
text += f" - (y0, dy, Ny): ({self.y0}, {self.dy}, {self.Ny})\n"
text += f" - (z0, dz, Nz): ({self.z0}, {self.dz}, {self.Nz})\n"
- text += f"Universes: {set([x.ID for x in list(flatten(self.universes))])}"
+ text += f"Universes: {set([x.name for x in list(flatten(self.universes))])}"
return text
diff --git a/mcdc/object_/util.py b/mcdc/object_/util.py
index d706ef0d9..eec8ff919 100644
--- a/mcdc/object_/util.py
+++ b/mcdc/object_/util.py
@@ -1,11 +1,352 @@
-import numpy as np
+import re
+from typing import Annotated, Union, get_args, get_origin
-from mcdc.constant import INF
+import numpy as np
from numpy import float64
from numpy.typing import NDArray
+from mcdc.constant import INF
from mcdc.print_ import print_error
+# ======================================================================================
+# Runtime annotation checking
+# ======================================================================================
+
+_DIMENSION_EXPRESSION_RE = re.compile(r"^\s*([A-Za-z_]\w*)\s*(?:([+-])\s*(\d+))?\s*$")
+_ANNOTATED_RE = re.compile(r"^\s*(?:typing\.)?Annotated\[(.*)\]\s*$")
+
+
+def parse_dimension_expression(expression: str) -> tuple[str, int]:
+ """Parse a dimension name with an optional integer offset.
+
+ Expressions such as ``"G"``, ``"G+1"``, and ``"N - 2"`` resolve to an
+ object attribute name and a signed offset without evaluating arbitrary
+ Python code.
+ """
+ match = _DIMENSION_EXPRESSION_RE.fullmatch(expression)
+ if match is None:
+ raise ValueError(f"Invalid dimension expression: {expression!r}")
+
+ attribute, operator, magnitude = match.groups()
+ offset = 0
+ if magnitude is not None:
+ offset = int(magnitude)
+ if operator == "-":
+ offset *= -1
+
+ return attribute, offset
+
+
+def check_type(value, hint, cls, obj=None) -> bool:
+ """Check a value against an MC/DC runtime type annotation.
+
+ The checker supports structured and stringified annotations, shaped NumPy
+ arrays, containers, unions, and unresolved class names. Annotation strings
+ are parsed without evaluating arbitrary Python code.
+ """
+ # Check stringified annotations without resolving forward references
+ if isinstance(hint, str):
+ hint = hint.strip()
+
+ if _is_ndarray_string(hint):
+ if not isinstance(value, np.ndarray):
+ return False
+ return _dtype_matches(value, _ndarray_dtype_key_from_string(hint))
+
+ annotated = _parse_annotated_string(hint)
+ if annotated is not None:
+ base, metadata = annotated
+ if _is_ndarray_string(base) and metadata:
+ if not isinstance(value, np.ndarray):
+ return False
+ shape = _shape_from_string(metadata[0])
+ expected_shape = _resolve_shape(shape, obj)
+ if expected_shape is None or not _shape_matches(value, expected_shape):
+ return False
+ return _dtype_matches(value, _ndarray_dtype_key_from_string(base))
+ return _mro_name_matches(value, base)
+
+ if "|" in hint:
+ return any(
+ check_type(value, item.strip(), cls)
+ for item in _split_top_level(hint, separator="|")
+ )
+
+ if hint.startswith("list[") and hint.endswith("]"):
+ item_type = _type_name_from_string(hint[5:-1])
+ if not isinstance(value, list):
+ return False
+ if item_type == "str":
+ return all(isinstance(item, str) for item in value)
+ if item_type in ("float", "float32", "float64"):
+ return all(isinstance(item, (float, int)) for item in value)
+ return True
+
+ return _mro_name_matches(value, hint)
+
+ # Check structured typing annotations
+ origin = get_origin(hint)
+
+ if origin is Annotated:
+ base, *metadata = get_args(hint)
+ if (
+ isinstance(value, np.ndarray)
+ and metadata
+ and isinstance(metadata[0], tuple)
+ ):
+ expected_shape = _resolve_shape(metadata[0], obj)
+ if expected_shape is None:
+ return False
+ return _shape_matches(value, expected_shape) and _dtype_matches(
+ value, _ndarray_dtype_key(base)
+ )
+ return check_type(value, base, cls)
+
+ if origin is np.ndarray:
+ return isinstance(value, np.ndarray)
+
+ if origin is None:
+ try:
+ return isinstance(value, hint)
+ except TypeError:
+ return True
+
+ if origin is list:
+ (item_type,) = get_args(hint)
+ return isinstance(value, list) and all(
+ check_type(item, item_type, cls) for item in value
+ )
+
+ if origin is set:
+ (item_type,) = get_args(hint)
+ return isinstance(value, set) and all(
+ check_type(item, item_type, cls) for item in value
+ )
+
+ if origin is dict:
+ key_type, value_type = get_args(hint)
+ return isinstance(value, dict) and all(
+ check_type(key, key_type, cls) and check_type(item, value_type, cls)
+ for key, item in value.items()
+ )
+
+ if origin is tuple:
+ item_types = get_args(hint)
+ if len(item_types) == 2 and item_types[1] is Ellipsis:
+ return isinstance(value, tuple) and all(
+ check_type(item, item_types[0], cls) for item in value
+ )
+ return (
+ isinstance(value, tuple)
+ and len(value) == len(item_types)
+ and all(
+ check_type(item, item_type, cls)
+ for item, item_type in zip(value, item_types)
+ )
+ )
+
+ if origin is Union:
+ return any(check_type(value, item_type, cls) for item_type in get_args(hint))
+
+ try:
+ return isinstance(value, origin)
+ except TypeError:
+ return True
+
+
+# --------------------------------------------------------------------------------------
+# Stringified annotation helpers
+# --------------------------------------------------------------------------------------
+
+
+def _parse_annotated_string(hint: str):
+ """Split a stringified Annotated type into its base and metadata."""
+ match = _ANNOTATED_RE.fullmatch(_strip_annotation_prefixes(hint))
+ if match is None:
+ return None
+
+ parts = _split_top_level(match.group(1))
+ if not parts:
+ return None
+ return parts[0].strip(), [part.strip() for part in parts[1:]]
+
+
+def _shape_from_string(shape: str):
+ """Parse shape metadata such as ``(3,)`` or ``("G+1",)``."""
+ shape = shape.strip()
+ if not (shape.startswith("(") and shape.endswith(")")):
+ return None
+
+ body = shape[1:-1].strip()
+ if not body:
+ return ()
+
+ dimensions = []
+ for item in _split_top_level(body):
+ item = item.strip()
+ if not item:
+ continue
+ if item == "None":
+ dimensions.append(None)
+ continue
+ try:
+ dimensions.append(int(item))
+ continue
+ except ValueError:
+ pass
+
+ if len(item) >= 2 and item[0] == item[-1] and item[0] in ("'", '"'):
+ item = item[1:-1]
+ try:
+ parse_dimension_expression(item)
+ except ValueError:
+ return None
+ dimensions.append(item)
+
+ return tuple(dimensions)
+
+
+def _split_top_level(
+ value: str,
+ separator: str = ",",
+ brackets: str = "[]()",
+) -> list[str]:
+ """Split text at separators that are not nested inside brackets."""
+ parts = []
+ buffer = []
+ depth = 0
+ opening = set(brackets[::2])
+ closing = set(brackets[1::2])
+
+ for character in value:
+ if character in opening:
+ depth += 1
+ elif character in closing:
+ depth -= 1
+
+ if character == separator and depth == 0:
+ parts.append("".join(buffer).strip())
+ buffer = []
+ else:
+ buffer.append(character)
+
+ if buffer:
+ parts.append("".join(buffer).strip())
+ return parts
+
+
+def _strip_annotation_prefixes(value: str) -> str:
+ """Remove module prefixes accepted in stringified annotations."""
+ return (
+ value.replace("typing.", "")
+ .replace("numpy.typing.", "")
+ .replace("numpy.", "")
+ .replace("np.", "")
+ )
+
+
+def _type_name_from_string(value: str) -> str:
+ """Return the unqualified base name of a stringified type."""
+ value = _strip_annotation_prefixes(value)
+ value = value.split("[", 1)[0]
+ return value.split(".")[-1].strip()
+
+
+def _mro_name_matches(value, expected: str) -> bool:
+ """Match an unresolved class name against the value's inheritance tree."""
+ expected_name = _type_name_from_string(expected)
+ return any(base.__name__ == expected_name for base in value.__class__.mro())
+
+
+# --------------------------------------------------------------------------------------
+# NumPy array annotation helpers
+# --------------------------------------------------------------------------------------
+
+
+def _is_ndarray_string(value: str) -> bool:
+ value = _strip_annotation_prefixes(value)
+ return value.startswith("NDArray[") or value.startswith("ndarray[")
+
+
+def _ndarray_dtype_key_from_string(value: str) -> str | None:
+ value = _strip_annotation_prefixes(value)
+ if "[" not in value or "]" not in value:
+ return None
+ dtype = value[value.find("[") + 1 : value.rfind("]")].strip()
+ return _strip_annotation_prefixes(dtype)
+
+
+def _ndarray_dtype_key(hint) -> str | None:
+ """Return the logical dtype from a structured NDArray annotation."""
+ arguments = get_args(hint)
+ if not arguments:
+ return None
+
+ dtype = arguments[-1]
+ dtype_arguments = get_args(dtype)
+ if dtype_arguments:
+ dtype = dtype_arguments[-1]
+
+ if dtype is float:
+ return "float"
+ if hasattr(dtype, "name"):
+ return dtype.name
+ try:
+ return np.dtype(dtype).name
+ except TypeError:
+ return None
+
+
+def _resolve_shape(shape, obj):
+ """Resolve dimension expressions against the object being validated."""
+ if shape is None:
+ return None
+
+ resolved = []
+ for dimension in shape:
+ if not isinstance(dimension, str):
+ resolved.append(dimension)
+ continue
+
+ if obj is None:
+ return None
+ try:
+ attribute, offset = parse_dimension_expression(dimension)
+ resolved.append(getattr(obj, attribute) + offset)
+ except (AttributeError, TypeError, ValueError):
+ return None
+
+ return tuple(resolved)
+
+
+def _dtype_matches(array: np.ndarray, dtype_key: str | None) -> bool:
+ if dtype_key is None:
+ return True
+
+ dtype_key = dtype_key.lower()
+ if dtype_key == "float":
+ return np.issubdtype(array.dtype, np.floating)
+ if dtype_key == "int":
+ return np.issubdtype(array.dtype, np.integer)
+ try:
+ return array.dtype == np.dtype(dtype_key)
+ except TypeError:
+ return True
+
+
+def _shape_matches(array: np.ndarray, shape: tuple[int | None, ...]) -> bool:
+ if array.ndim != len(shape):
+ return False
+ return all(
+ expected is None or actual == expected
+ for actual, expected in zip(array.shape, shape)
+ )
+
+
+# ======================================================================================
+# Probability distribution conversions
+# ======================================================================================
+
def cmf_from_pmf(
pmf: NDArray[float64],
@@ -137,6 +478,21 @@ def pdf_from_cdf(
def multi_cdf_from_pdf(offset, value, pdf):
+ """Normalize multiple flattened PDFs and construct their CDFs.
+
+ Parameters
+ ----------
+ offset : array_like of int
+ Start index of each table.
+ value, pdf : ndarray
+ Flattened sample values and probability densities.
+
+ Returns
+ -------
+ pdf, cdf : ndarray
+ Per-table normalized densities and cumulative distributions.
+ """
+
cdf = np.zeros_like(pdf)
for i in range(len(offset)):
@@ -158,10 +514,20 @@ def multi_cdf_from_pdf(offset, value, pdf):
return pdf, cdf
+# ======================================================================================
+# Array helpers
+# ======================================================================================
+
+
def is_sorted(a):
+ """Return whether an array is monotonically nondecreasing."""
return np.all(a[:-1] <= a[1:])
+# ======================================================================================
+# Natural isotopic abundance
+# ======================================================================================
+
# Natural isotopic abundance data from
# https://www.nndc.bnl.gov/walletcards/search.html
ISOTOPIC_ABUNDANCE = {
@@ -624,7 +990,19 @@ def is_sorted(a):
}
+# ======================================================================================
+# Object helpers
+# ======================================================================================
+
+
def move_object(object_, velocities, durations):
+ """Populate the shared piecewise-constant motion representation.
+
+ A zero-velocity segment of infinite duration is appended after the supplied
+ segments. The input lists are therefore mutated; array inputs are converted
+ to lists first.
+ """
+
object_.moving = True
object_.N_move = len(durations) + 1
object_.N_move_grid = len(durations) + 2
@@ -656,4 +1034,5 @@ def move_object(object_, velocities, durations):
def subtype_size(main_list, subtype: str):
+ """Count objects whose framework label matches ``subtype``."""
return len([x for x in main_list if x.label == subtype])
diff --git a/mcdc/output.py b/mcdc/output.py
index a911b18cb..5453ca25a 100644
--- a/mcdc/output.py
+++ b/mcdc/output.py
@@ -17,9 +17,7 @@
# ======================================================================================
-def generate_output(mcdc, data):
- from mcdc import simulation
-
+def generate_output(mcdc, data, simulationPy):
if not mcdc["mpi_master"]:
return
@@ -37,10 +35,11 @@ def generate_output(mcdc, data):
file["version"] = importlib.metadata.version("mcdc")
# Settings
- create_object_dataset(file, "settings", simulation.settings)
+ create_object_dataset(file, "settings", simulationPy.settings)
# No need to output tally if time census-based tally is used
if mcdc["settings"]["use_census_based_tally"]:
+ file.close()
return
# Tallies
@@ -166,15 +165,15 @@ def create_tally_dataset(file, mcdc, data):
# Mesh grid (TODO: Make mesh dataset in a separate group)
mesh_filtered_tally = None
- if tally["child_type"] == TALLY_TRACKLENGTH:
- mesh_filtered_tally = mcdc["tracklength_tallies"][tally["child_ID"]]
- elif tally["child_type"] == TALLY_COLLISION:
- mesh_filtered_tally = mcdc["collision_tallies"][tally["child_ID"]]
+ if tally["sub_type"] == TALLY_TRACKLENGTH:
+ mesh_filtered_tally = mcdc["tracklength_tallies"][tally["sub_ID"]]
+ elif tally["sub_type"] == TALLY_COLLISION:
+ mesh_filtered_tally = mcdc["collision_tallies"][tally["sub_ID"]]
if mesh_filtered_tally is not None and mesh_filtered_tally["mesh_filtered"]:
mesh_base = mcdc["meshes"][mesh_filtered_tally["mesh_filter_ID"]]
- mesh_type = mesh_base["child_type"]
- mesh_ID = mesh_base["child_ID"]
+ mesh_type = mesh_base["sub_type"]
+ mesh_ID = mesh_base["sub_ID"]
if mesh_type == MESH_UNIFORM:
mesh = mcdc["uniform_meshes"][mesh_ID]
x = np.linspace(
@@ -241,21 +240,26 @@ def replace_dataset(file, field, data):
file.create_dataset(field, data=data)
-def recombine_tallies():
- """Combine the tally output into a single file"""
- import h5py
- from mpi4py import MPI
+def recombine_tallies(simulationPy, simulation):
+ """Combine census-based tally files into the main output file.
+
+ Parameters
+ ----------
+ simulationPy : mcdc.Simulation
+ Python simulation object containing tally definitions and settings.
+ simulation : numpy.void
+ Packed runtime simulation state. Recombination is performed only by
+ its designated MPI master rank.
+ """
from mcdc.object_.tally import decode_score_type
- if MPI.COMM_WORLD.Get_rank() > 0:
+ if not simulation["mpi_master"]:
return
- # Get simulation and settings
- from mcdc.object_.simulation import simulation
-
- settings = simulation.settings
+ settings = simulationPy.settings
if not settings.use_census_based_tally:
print("Census-based tally is not used, nothing to recombine.")
+ return
# Settings parameters
base_name = settings.output_name
@@ -265,65 +269,63 @@ def recombine_tallies():
Nt = frequency * (N_census - 1)
# Append the tally dataset structure to the main output
- main_file = h5py.File(f"{base_name}.h5", "a")
- reference_file = h5py.File(f"{base_name}-batch_0-census_0.h5", "r")
- tally_group = main_file.create_group("tallies")
- for tally in simulation.tallies:
- name = f"tallies/{tally.name}"
- reference_file.copy(name, tally_group)
- reference_file.close()
-
- # Set the time grid
- time_grid = np.zeros(Nt + 1)
- for i in range(N_census - 1):
- start = settings.census_time[i - 1] if i > 0 else 0.0
- end = settings.census_time[i]
- new_grid = np.linspace(start, end, frequency + 1)
- offset = i * frequency + 1
- time_grid[offset : offset + frequency] = new_grid[1:]
- for tally in simulation.tallies:
- name = f"tallies/{tally.name}/grid/time"
- replace_dataset(main_file, name, time_grid)
-
- # Combine the tallies
- for tally in simulation.tallies:
- # The combined shape
- shape = tally.bin_shape
- shape[3] = Nt
-
- for score in tally.scores:
- score_name = f"tallies/{tally.name}/{decode_score_type(score, True)}"
-
- mean = np.zeros(shape)
- sdev = np.zeros(shape)
-
- # Selective squeeze
- axes_to_squeeze = [x for x, size in enumerate(shape) if size == 1 and x > 3]
- mean = np.squeeze(mean, axis=tuple(axes_to_squeeze))
- sdev = np.squeeze(sdev, axis=tuple(axes_to_squeeze))
-
- for i_census in range(N_census - 1):
- # Accumulate sum and sum of square
- for i_batch in range(N_batch):
- file_name = f"{base_name}-batch_{i_batch}-census_{i_census}.h5"
- file = h5py.File(file_name, "r")
+ with h5py.File(f"{base_name}.h5", "a") as main_file:
+ if "tallies" in main_file:
+ del main_file["tallies"]
+ tally_group = main_file.create_group("tallies")
+
+ with h5py.File(f"{base_name}-batch_0-census_0.h5", "r") as reference_file:
+ for tally in simulationPy.tallies:
+ name = f"tallies/{tally.name}"
+ reference_file.copy(name, tally_group)
+
+ # Set the time grid
+ time_grid = np.zeros(Nt + 1)
+ for i_census in range(N_census - 1):
+ start = settings.census_time[i_census - 1] if i_census > 0 else 0.0
+ end = settings.census_time[i_census]
+ new_grid = np.linspace(start, end, frequency + 1)
+ offset = i_census * frequency + 1
+ time_grid[offset : offset + frequency] = new_grid[1:]
+ for tally in simulationPy.tallies:
+ name = f"tallies/{tally.name}/grid/time"
+ replace_dataset(main_file, name, time_grid)
+
+ # Combine the tallies
+ for tally in simulationPy.tallies:
+ census_shape = tuple(int(size) for size in tally.bin_shape[:-1])
+ combined_shape = list(census_shape)
+ combined_shape[3] = Nt
+ combined_shape = tuple(combined_shape)
+
+ for score in tally.scores:
+ score_name = f"tallies/{tally.name}/{decode_score_type(score, True)}"
+ mean = np.zeros(combined_shape)
+ second_moment = np.zeros(combined_shape)
+
+ for i_census in range(N_census - 1):
offset = i_census * frequency
-
- score = file[f"{score_name}/mean"][()]
- mean[:, :, :, offset : offset + frequency] += score
- sdev[:, :, :, offset : offset + frequency] += score * score
-
- file.close()
-
- # Squeeze
- mean = np.squeeze(mean)
- sdev = np.squeeze(sdev)
-
- # Compute statistics
- mean /= N_batch
- sdev = np.sqrt((sdev / N_batch - np.square(mean)) / (N_batch - 1))
-
- replace_dataset(main_file, f"{score_name}/mean", mean)
- replace_dataset(main_file, f"{score_name}/sdev", sdev)
-
- main_file.close()
+ time_slice = [slice(None)] * len(combined_shape)
+ time_slice[3] = slice(offset, offset + frequency)
+ time_slice = tuple(time_slice)
+
+ for i_batch in range(N_batch):
+ file_name = f"{base_name}-batch_{i_batch}-census_{i_census}.h5"
+ with h5py.File(file_name, "r") as file:
+ score_data = np.asarray(
+ file[f"{score_name}/mean"][()]
+ ).reshape(census_shape)
+ mean[time_slice] += score_data
+ second_moment[time_slice] += np.square(score_data)
+
+ mean /= N_batch
+ if N_batch > 1:
+ variance = (second_moment / N_batch - np.square(mean)) / (
+ N_batch - 1
+ )
+ sdev = np.sqrt(np.maximum(variance, 0.0))
+ else:
+ sdev = np.zeros_like(mean)
+
+ replace_dataset(main_file, f"{score_name}/mean", np.squeeze(mean))
+ replace_dataset(main_file, f"{score_name}/sdev", np.squeeze(sdev))
diff --git a/mcdc/print_.py b/mcdc/print_.py
index 7a381d0fd..17d319bf9 100644
--- a/mcdc/print_.py
+++ b/mcdc/print_.py
@@ -1,47 +1,89 @@
-import numba as nb
+"""Terminal diagnostics and progress reporting for MC/DC."""
+
import sys
+
+import numba as nb
+from colorama import Fore, Style
from mpi4py import MPI
-from colorama import Fore, Back, Style
-master = MPI.COMM_WORLD.Get_rank() == 0
+import mcdc.mcdc_get as mcdc_get
+_IS_MASTER = MPI.COMM_WORLD.Get_rank() == 0
-import numba as nb
-import sys
-from colorama import Fore, Style
+# ======================================================================================
+# General formatting and diagnostics
+# ======================================================================================
-import mcdc.mcdc_get as mcdc_get
+def print_1d_array(array):
+ """Return a compact representation of a one-dimensional array."""
+ size = len(array)
+ if size > 5:
+ return (
+ f"(size={size}): "
+ f"[{array[0]:.5g}, {array[1]:.5g}, ..., "
+ f"{array[-2]:.5g}, {array[-1]:.5g}]"
+ )
-def print_1d_array(arr):
- N = len(arr)
- if N > 5:
- return f"(size={len(arr)}): [{arr[0]:.5g}, {arr[1]:.5g}, ..., {arr[-2]:.5g}, {arr[-1]:.5g}]"
- else:
- text = f"(size={len(arr)}): ["
- for i in range(N):
- text += f"{arr[i]:.5g}, "
- if N > 0:
- text = text[:-2]
- text += "]"
- return text
-
-
-def print_error(text):
- print(Fore.RED + f"[ERROR]: {text}\n")
- print(Style.RESET_ALL)
+ values = ", ".join(f"{value:.5g}" for value in array)
+ return f"(size={size}): [{values}]"
+
+
+def print_msg(message):
+ """Print a framework message on the MPI master rank."""
+ if not _IS_MASTER:
+ return
+ print(message)
sys.stdout.flush()
- sys.exit()
-def print_warning(text):
- print(Fore.YELLOW + f"[WARNING]: {text}\n")
- print(Style.RESET_ALL)
+def print_error(message):
+ """Print a fatal error and terminate the current process unsuccessfully."""
+ print(f"{Fore.RED}[ERROR]: {message}{Style.RESET_ALL}")
sys.stdout.flush()
+ raise SystemExit(1)
+
+
+def print_warning(message):
+ """Print a warning on the MPI master rank."""
+ if not _IS_MASTER:
+ return
+ print(f"{Fore.YELLOW}[WARNING]: {message}{Style.RESET_ALL}")
+ sys.stdout.flush()
+
+
+def print_structure(structure):
+ """Print every field in a NumPy structured record."""
+ for name in structure.dtype.names:
+ print(f"{name} = {structure[name]}")
+
+
+def print_bank(bank, show_content=False):
+ """Print summary information for a particle bank."""
+ size_field = bank["size"]
+ size = int(size_field[0]) if getattr(size_field, "ndim", 0) else int(size_field)
+ particles = bank["particle_data"]
+
+ print("\n=============")
+ print("Particle bank")
+ print(" tag :", bank["tag"])
+ print(" size :", size, "of", len(particles))
+ if show_content:
+ for index in range(size):
+ print(" ", particles[index])
+ print()
+
+
+# ======================================================================================
+# Calculation headers
+# ======================================================================================
def print_banner():
+ """Print the MC/DC banner on the MPI master rank."""
+ if not _IS_MASTER:
+ return
print(
"\n"
+ r" __ __ ____ __ ____ ____ "
@@ -59,17 +101,19 @@ def print_banner():
def print_configuration():
+ """Print the active execution configuration on the MPI master rank."""
+ if not _IS_MASTER:
+ return
mode = "Python" if nb.config.DISABLE_JIT else "Numba"
mpi_size = MPI.COMM_WORLD.Get_size()
-
- text = ""
- text += f" Mode | {mode}\n"
- text += f" MPI Processes | {mpi_size}\n"
- print(text)
+ print(f" Mode | {mode}\n MPI Processes | {mpi_size}\n")
sys.stdout.flush()
def print_eigenvalue_header(simulation):
+ """Print the eigenvalue-cycle table header."""
+ if not _IS_MASTER:
+ return
if simulation["settings"]["use_gyration_radius"]:
print("\n # k GyRad. k (avg) ")
print(" ==== ======= ====== ===================")
@@ -79,170 +123,123 @@ def print_eigenvalue_header(simulation):
sys.stdout.flush()
-def print_batch_header(i, N):
- print(f"\nBatch {i}/{N}")
+def print_header_batch(index, size):
+ """Print a one-based batch header."""
+ if not _IS_MASTER:
+ return
+ print(f"\nBatch {index + 1}/{size}")
sys.stdout.flush()
-def print_time(tag, t, percent):
- if t >= 24 * 60 * 60:
- print(" %s | %.2f days (%.1f%%)" % (tag, t / 24 / 60 / 60), percent)
- elif t >= 60 * 60:
- print(" %s | %.2f hours (%.1f%%)" % (tag, t / 60 / 60, percent))
- elif t >= 60:
- print(" %s | %.2f minutes (%.1f%%)" % (tag, t / 60, percent))
- else:
- print(" %s | %.2f seconds (%.1f%%)" % (tag, t, percent))
-
-
-def print_runtime(simulation):
- total = simulation["runtime_total"]
- preparation = simulation["runtime_preparation"]
- simulation = simulation["runtime_simulation"]
- output = simulation["runtime_output"]
- print("\n Runtime report:")
- print_time("Total ", total, 100)
- print_time("Preparation", preparation, preparation / total * 100)
- print_time("Simulation ", simulation, simulation / total * 100)
- print_time("Output ", output, output / total * 100)
- print("\n")
- sys.stdout.flush()
-
-
-def print_structure(struct):
- dtype = struct.dtype
- for name in dtype.names:
- print(f"{name} = {struct[name]}")
-
-
-# TODO: below is not evaulated yet during the refactor
-
-
-def print_msg(msg):
- if master:
- print(msg)
- sys.stdout.flush()
-
-
-def print_error(msg):
- print("ERROR: %s\n" % msg)
- sys.stdout.flush()
- sys.exit()
-
-
-def print_warning(msg):
- if master:
- print(Fore.RED + "Warning: %s\n" % msg)
- print(Style.RESET_ALL)
- sys.stdout.flush()
+# ======================================================================================
+# Calculation progress
+# ======================================================================================
def print_progress(percent, simulation):
- if master:
- sys.stdout.write("\r")
- if not simulation["settings"]["neutron_eigenvalue_mode"]:
- if simulation["settings"]["N_census"] == 1:
- sys.stdout.write(
- " [%-28s] %d%%" % ("=" * int(percent * 28), percent * 100.0)
- )
- else:
- idx = simulation["idx_census"] + 1
- N = simulation["settings"]["N_census"]
- sys.stdout.write(
- " Census %i/%i: [%-28s] %d%%"
- % (idx, N, "=" * int(percent * 28), percent * 100.0)
- )
+ """Update the fixed-source or eigenvalue progress bar."""
+ if not _IS_MASTER:
+ return
+
+ sys.stdout.write("\r")
+ settings = simulation["settings"]
+ if not settings["neutron_eigenvalue_mode"]:
+ if settings["N_census"] == 1:
+ sys.stdout.write(
+ " [%-28s] %d%%" % ("=" * int(percent * 28), percent * 100.0)
+ )
else:
- if simulation["settings"]["use_gyration_radius"]:
- sys.stdout.write(
- " [%-40s] %d%%" % ("=" * int(percent * 40), percent * 100.0)
- )
- else:
- sys.stdout.write(
- " [%-32s] %d%%" % ("=" * int(percent * 32), percent * 100.0)
- )
- sys.stdout.flush()
+ index = simulation["idx_census"] + 1
+ size = settings["N_census"]
+ sys.stdout.write(
+ " Census %i/%i: [%-28s] %d%%"
+ % (index, size, "=" * int(percent * 28), percent * 100.0)
+ )
+ elif settings["use_gyration_radius"]:
+ sys.stdout.write(" [%-40s] %d%%" % ("=" * int(percent * 40), percent * 100.0))
+ else:
+ sys.stdout.write(" [%-32s] %d%%" % ("=" * int(percent * 32), percent * 100.0))
+ sys.stdout.flush()
-def print_header_eigenvalue(simulation):
- if master:
- if simulation["settings"]["use_gyration_radius"]:
- print("\n # k GyRad. k (avg) ")
- print(" ==== ======= ====== ===================")
+def print_progress_eigenvalue(simulation, data):
+ """Print one eigenvalue-cycle result."""
+ if not _IS_MASTER:
+ return
+
+ index = simulation["idx_cycle"]
+ k_effective = simulation["k_eff"]
+ k_average = simulation["k_avg_running"]
+ k_standard_deviation = simulation["k_sdv_running"]
+ settings = simulation["settings"]
+
+ if settings["use_progress_bar"]:
+ sys.stdout.write("\r\033[K")
+
+ if settings["use_gyration_radius"]:
+ gyration_radius = mcdc_get.simulation.gyration_radius(index, simulation, data)
+ if simulation["cycle_active"]:
+ print(
+ " %-4i %.5f %6.2f %.5f +/- %.5f"
+ % (
+ index + 1,
+ k_effective,
+ gyration_radius,
+ k_average,
+ k_standard_deviation,
+ )
+ )
else:
- print("\n # k k (avg) ")
- print(" ==== ======= ===================")
+ print(" %-4i %.5f %6.2f" % (index + 1, k_effective, gyration_radius))
+ elif simulation["cycle_active"]:
+ print(
+ " %-4i %.5f %.5f +/- %.5f"
+ % (index + 1, k_effective, k_average, k_standard_deviation)
+ )
+ else:
+ print(" %-4i %.5f" % (index + 1, k_effective))
+ sys.stdout.flush()
-def print_header_batch(i, N):
- if master:
- print(f"\nBatch {i+1}/{N}")
- sys.stdout.flush()
+# ======================================================================================
+# Runtime report
+# ======================================================================================
-def print_progress_eigenvalue(simulation, data):
- if master:
- idx_cycle = simulation["idx_cycle"]
- k_eff = simulation["k_eff"]
- k_avg = simulation["k_avg_running"]
- k_sdv = simulation["k_sdv_running"]
- gr = mcdc_get.simulation.gyration_radius(idx_cycle, simulation, data)
- if simulation["settings"]["use_progress_bar"]:
- sys.stdout.write("\r")
- sys.stdout.write("\033[K")
- if simulation["settings"]["use_gyration_radius"]:
- if not simulation["cycle_active"]:
- print(" %-4i %.5f %6.2f" % (idx_cycle + 1, k_eff, gr))
- else:
- print(
- " %-4i %.5f %6.2f %.5f +/- %.5f"
- % (idx_cycle + 1, k_eff, gr, k_avg, k_sdv)
- )
- else:
- if not simulation["cycle_active"]:
- print(" %-4i %.5f" % (idx_cycle + 1, k_eff))
- else:
- print(
- " %-4i %.5f %.5f +/- %.5f" % (idx_cycle + 1, k_eff, k_avg, k_sdv)
- )
+def print_time(label, duration, percent):
+ """Print one duration using an appropriate time unit."""
+ if duration >= 24 * 60 * 60:
+ value = duration / (24 * 60 * 60)
+ unit = "days"
+ elif duration >= 60 * 60:
+ value = duration / (60 * 60)
+ unit = "hours"
+ elif duration >= 60:
+ value = duration / 60
+ unit = "minutes"
+ else:
+ value = duration
+ unit = "seconds"
+ print(f" {label} | {value:.2f} {unit} ({percent:.1f}%)")
def print_runtime(simulation):
- t_total = simulation["runtime_total"]
- t_preparation = simulation["runtime_preparation"]
- t_simulation = simulation["runtime_simulation"]
- t_output = simulation["runtime_output"]
- if master:
- print("\n Runtime report:")
- print_time("Total ", t_total, 100)
- print_time("Preparation", t_preparation, t_preparation / t_total * 100)
- print_time("Simulation ", t_simulation, t_simulation / t_total * 100)
- print_time("Output ", t_output, t_output / t_total * 100)
- print("\n")
- sys.stdout.flush()
-
-
-def print_time(tag, t, percent):
- if t >= 24 * 60 * 60:
- print(" %s | %.2f days (%.1f%%)" % (tag, t / 24 / 60 / 60), percent)
- elif t >= 60 * 60:
- print(" %s | %.2f hours (%.1f%%)" % (tag, t / 60 / 60, percent))
- elif t >= 60:
- print(" %s | %.2f minutes (%.1f%%)" % (tag, t / 60, percent))
- else:
- print(" %s | %.2f seconds (%.1f%%)" % (tag, t, percent))
+ """Print preparation, transport, and output runtimes."""
+ if not _IS_MASTER:
+ return
+ total = simulation["runtime_total"]
+ preparation = simulation["runtime_preparation"]
+ transport = simulation["runtime_simulation"]
+ output = simulation["runtime_output"]
-def print_bank(bank, show_content=False):
- tag = bank["tag"]
- size = bank["size"]
- particles = bank["particles"]
+ def percentage(duration):
+ return duration / total * 100.0 if total > 0.0 else 0.0
- print("\n=============")
- print("Particle bank")
- print(" tag :", tag)
- print(" size :", size, "of", len(bank["particles"]))
- if show_content and size > 0:
- for i in range(size):
- print(" ", particles[i])
- print("\n")
+ print("\n Runtime report:")
+ print_time("Total ", total, 100.0)
+ print_time("Preparation", preparation, percentage(preparation))
+ print_time("Simulation ", transport, percentage(transport))
+ print_time("Output ", output, percentage(output))
+ print()
+ sys.stdout.flush()
diff --git a/mcdc/py.typed b/mcdc/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/mcdc/transport/data.py b/mcdc/transport/data.py
index 84e8f63d2..f0894d1c3 100644
--- a/mcdc/transport/data.py
+++ b/mcdc/transport/data.py
@@ -24,9 +24,9 @@
@njit
-def evaluate_data(x, data_base, simulation, data):
- data_type = data_base["child_type"]
- ID = data_base["child_ID"]
+def evaluate_data(x, data_, simulation, data):
+ data_type = data_["sub_type"]
+ ID = data_["sub_ID"]
if data_type == DATA_TABLE:
table = simulation["table_data"][ID]
return evaluate_table(x, table, data)
diff --git a/mcdc/transport/distribution.py b/mcdc/transport/distribution.py
index 6d8fb1fbe..190f3ae04 100644
--- a/mcdc/transport/distribution.py
+++ b/mcdc/transport/distribution.py
@@ -18,14 +18,10 @@
DISTRIBUTION_TABULATED_ENERGY_ANGLE,
INTERPOLATION_HISTOGRAM,
INTERPOLATION_LINEAR,
- INTERPOLATION_LOG,
- INTERPOLATION_SEMILOGX,
- INTERPOLATION_SEMILOGY,
- MAX_BISECTION_ITERATIONS,
PI,
)
-from mcdc.transport.data import evaluate_table, get_table_interpolation_law
-from mcdc.transport.util import find_bin, linear_interpolation
+from mcdc.transport.data import evaluate_data
+from mcdc.transport.util import find_bin
# ======================================================================================
# General distribution samplers
@@ -44,8 +40,8 @@ def sample_distribution_with_scale(E, distribution, rng_state, simulation, data)
@njit
def _sample_distribution(E, distribution, rng_state, simulation, data, scale):
- distribution_type = distribution["child_type"]
- ID = distribution["child_ID"]
+ distribution_type = distribution["sub_type"]
+ ID = distribution["sub_ID"]
if distribution_type == DISTRIBUTION_TABULATED:
table = simulation["tabulated_distributions"][ID]
@@ -92,8 +88,8 @@ def sample_correlated_distribution_with_scale(
def _sample_correlated_distribution(
E, distribution, rng_state, simulation, data, scale
):
- distribution_type = distribution["child_type"]
- ID = distribution["child_ID"]
+ distribution_type = distribution["sub_type"]
+ ID = distribution["sub_ID"]
if distribution_type == DISTRIBUTION_KALBACH_MANN:
kalbach_mann = simulation["kalbach_mann_distributions"][ID]
@@ -185,7 +181,8 @@ def sample_tabulated(table, rng_state, simulation, data):
Sample a value from a tabulated distribution.
"""
- pdf_table = simulation["table_data"][table["pdf_ID"]]
+ pdf_data = simulation["data"][table["pdf_ID"]]
+ pdf_table = simulation["table_data"][pdf_data["sub_ID"]]
cdf = mcdc_get.table_data.aux_vector(0, pdf_table, data)
@@ -335,8 +332,9 @@ def _sample_multi_table(E, rng_state, multi_table, simulation, data, scale):
use_next_table = True # For scaling later if needed
# Sample from the selected table
- ID = int(mcdc_get.multi_table_distribution.table_IDs(idx, multi_table, data))
- table_distribution = simulation["tabulated_distributions"][ID]
+ ID = mcdc_get.multi_table_distribution.table_IDs(idx, multi_table, data)
+ sub_ID = simulation["distributions"][ID]["sub_ID"]
+ table_distribution = simulation["tabulated_distributions"][sub_ID]
sample = sample_tabulated(table_distribution, rng_state, simulation, data)
# No scaling needed?
@@ -347,18 +345,27 @@ def _sample_multi_table(E, rng_state, multi_table, simulation, data, scale):
if use_next_table:
idx -= 1
- # PDF table indices
- ID0 = int(mcdc_get.multi_table_distribution.table_IDs(idx, multi_table, data))
- ID1 = int(mcdc_get.multi_table_distribution.table_IDs(idx + 1, multi_table, data))
+ # PDF tables
+ ID0 = mcdc_get.multi_table_distribution.table_IDs(idx, multi_table, data)
+ ID1 = mcdc_get.multi_table_distribution.table_IDs(idx + 1, multi_table, data)
#
- pdf_ID = table_distribution["pdf_ID"]
- pdf_ID0 = simulation["tabulated_distributions"][ID0]["pdf_ID"]
- pdf_ID1 = simulation["tabulated_distributions"][ID1]["pdf_ID"]
-
- # The tables
- table = simulation["table_data"][pdf_ID]
- table0 = simulation["table_data"][pdf_ID0]
- table1 = simulation["table_data"][pdf_ID1]
+ sub_ID0 = simulation["distributions"][ID0]["sub_ID"]
+ sub_ID1 = simulation["distributions"][ID1]["sub_ID"]
+ #
+ table_distribution0 = simulation["tabulated_distributions"][sub_ID0]
+ table_distribution1 = simulation["tabulated_distributions"][sub_ID1]
+ #
+ ID = table_distribution["pdf_ID"]
+ ID0 = table_distribution0["pdf_ID"]
+ ID1 = table_distribution1["pdf_ID"]
+ #
+ sub_ID = simulation["data"][ID]["sub_ID"]
+ sub_ID0 = simulation["data"][ID0]["sub_ID"]
+ sub_ID1 = simulation["data"][ID1]["sub_ID"]
+ #
+ table = simulation["table_data"][sub_ID]
+ table0 = simulation["table_data"][sub_ID0]
+ table1 = simulation["table_data"][sub_ID1]
# Table's min
val_min0 = mcdc_get.table_data.x(0, table0, data)
@@ -381,8 +388,8 @@ def _sample_multi_table(E, rng_state, multi_table, simulation, data, scale):
@njit
def sample_maxwellian(E, rng_state, maxwellian, simulation, data):
# Get nuclear temperature
- table = simulation["table_data"][maxwellian["nuclear_temperature_ID"]]
- nuclear_temperature = evaluate_table(E, table, data)
+ table = simulation["data"][maxwellian["nuclear_temperature_ID"]]
+ nuclear_temperature = evaluate_data(E, table, simulation, data)
restriction_energy = maxwellian["restriction_energy"]
# Rejection sampling
@@ -411,8 +418,8 @@ def sample_level_scattering(E, level_scattering):
@njit
def sample_evaporation(E, rng_state, evaporation, simulation, data):
# Get nuclear temperature
- table = simulation["table_data"][evaporation["nuclear_temperature_ID"]]
- nuclear_temperature = evaluate_table(E, table, data)
+ table = simulation["data"][evaporation["nuclear_temperature_ID"]]
+ nuclear_temperature = evaluate_data(E, table, simulation, data)
restriction_energy = evaporation["restriction_energy"]
w = (E - restriction_energy) / nuclear_temperature
@@ -455,8 +462,8 @@ def sample_kalbach_mann(E, rng_state, kalbach_mann, data):
# ==================================================================================
# First table
- start = int(mcdc_get.kalbach_mann_distribution.offset(idx, kalbach_mann, data))
- end = int(mcdc_get.kalbach_mann_distribution.offset(idx + 1, kalbach_mann, data))
+ start = mcdc_get.kalbach_mann_distribution.offset(idx, kalbach_mann, data)
+ end = mcdc_get.kalbach_mann_distribution.offset(idx + 1, kalbach_mann, data)
E0_min = mcdc_get.kalbach_mann_distribution.energy_out(start, kalbach_mann, data)
E0_max = mcdc_get.kalbach_mann_distribution.energy_out(end - 1, kalbach_mann, data)
@@ -465,9 +472,7 @@ def sample_kalbach_mann(E, rng_state, kalbach_mann, data):
if idx + 2 == len(grid):
end = kalbach_mann["energy_length"]
else:
- end = int(
- mcdc_get.kalbach_mann_distribution.offset(idx + 2, kalbach_mann, data)
- )
+ end = mcdc_get.kalbach_mann_distribution.offset(idx + 2, kalbach_mann, data)
E1_min = mcdc_get.kalbach_mann_distribution.energy_out(start, kalbach_mann, data)
E1_max = mcdc_get.kalbach_mann_distribution.energy_out(end - 1, kalbach_mann, data)
@@ -480,13 +485,11 @@ def sample_kalbach_mann(E, rng_state, kalbach_mann, data):
idx += 1
# Get the table range
- start = int(mcdc_get.kalbach_mann_distribution.offset(idx, kalbach_mann, data))
+ start = mcdc_get.kalbach_mann_distribution.offset(idx, kalbach_mann, data)
if idx + 1 == len(grid):
end = kalbach_mann["energy_length"]
else:
- end = int(
- mcdc_get.kalbach_mann_distribution.offset(idx + 1, kalbach_mann, data)
- )
+ end = mcdc_get.kalbach_mann_distribution.offset(idx + 1, kalbach_mann, data)
size = end - start
# The CDF
@@ -562,8 +565,8 @@ def sample_tabulated_energy_angle(E, rng_state, table, data):
# ==================================================================================
# First table
- start = int(mcdc_get.tabulated_energy_angle_distribution.offset(idx, table, data))
- end = int(mcdc_get.tabulated_energy_angle_distribution.offset(idx + 1, table, data))
+ start = mcdc_get.tabulated_energy_angle_distribution.offset(idx, table, data)
+ end = mcdc_get.tabulated_energy_angle_distribution.offset(idx + 1, table, data)
E0_min = mcdc_get.tabulated_energy_angle_distribution.energy_out(start, table, data)
E0_max = mcdc_get.tabulated_energy_angle_distribution.energy_out(
end - 1, table, data
@@ -574,9 +577,7 @@ def sample_tabulated_energy_angle(E, rng_state, table, data):
if idx + 2 == len(grid):
end = table["energy_length"]
else:
- end = int(
- mcdc_get.tabulated_energy_angle_distribution.offset(idx + 2, table, data)
- )
+ end = mcdc_get.tabulated_energy_angle_distribution.offset(idx + 2, table, data)
E1_min = mcdc_get.tabulated_energy_angle_distribution.energy_out(start, table, data)
E1_max = mcdc_get.tabulated_energy_angle_distribution.energy_out(
end - 1, table, data
@@ -591,13 +592,11 @@ def sample_tabulated_energy_angle(E, rng_state, table, data):
idx += 1
# Get the table range
- start = int(mcdc_get.tabulated_energy_angle_distribution.offset(idx, table, data))
+ start = mcdc_get.tabulated_energy_angle_distribution.offset(idx, table, data)
if idx + 1 == len(grid):
end = table["energy_length"]
else:
- end = int(
- mcdc_get.tabulated_energy_angle_distribution.offset(idx + 1, table, data)
- )
+ end = mcdc_get.tabulated_energy_angle_distribution.offset(idx + 1, table, data)
size = end - start
# The CDF
@@ -642,16 +641,14 @@ def sample_tabulated_energy_angle(E, rng_state, table, data):
idx += 1
# Get the angular table range
- start = int(
- mcdc_get.tabulated_energy_angle_distribution.cosine_offset_(idx, table, data)
+ start = mcdc_get.tabulated_energy_angle_distribution.cosine_offset_(
+ idx, table, data
)
if idx + 1 == len(grid):
end = table["cosine_length"]
else:
- end = int(
- mcdc_get.tabulated_energy_angle_distribution.cosine_offset_(
- idx + 1, table, data
- )
+ end = mcdc_get.tabulated_energy_angle_distribution.cosine_offset_(
+ idx + 1, table, data
)
size = end - start
diff --git a/mcdc/transport/geometry/interface.py b/mcdc/transport/geometry/interface.py
index b225064de..b904dbe3f 100644
--- a/mcdc/transport/geometry/interface.py
+++ b/mcdc/transport/geometry/interface.py
@@ -15,7 +15,7 @@
from mcdc.transport.geometry.surface import get_distance, check_sense
# ======================================================================================
-# Geometry inspection
+# Geometry traversal
# ======================================================================================
@@ -30,36 +30,23 @@ def inspect_geometry(particle_container, simulation, data):
"""
particle = particle_container[0]
- # Store particle global coordinate
- # (particle will be temporarily translated and rotated)
- x_global = particle["x"]
- y_global = particle["y"]
- z_global = particle["z"]
- t_global = particle["t"]
- ux_global = particle["ux"]
- uy_global = particle["uy"]
- uz_global = particle["uz"]
+ # Preserve global coordinates while traversing nested geometry.
+ global_coordinates = _save_global_coordinates(particle_container)
speed = physics.particle_speed(particle_container, simulation, data)
# Default returns
distance = INF
event = EVENT_NONE
- # Find top cell from root universe if unknown
- if particle["cell_ID"] == -1:
- particle["cell_ID"] = get_cell(
- particle_container, UNIVERSE_ROOT, simulation, data
- )
-
- # Particle is lost?
- if particle["cell_ID"] == -1:
- event = EVENT_LOST
-
- # The top cell
- cell = simulation["cells"][particle["cell_ID"]]
+ # Find the top cell from the root universe if it is unknown.
+ cell_ID = _get_top_cell_ID(particle_container, speed, simulation, data)
+ if cell_ID == -1:
+ event = EVENT_LOST
# Recursively check cells until material cell is found (or the particle is lost)
while event != EVENT_LOST:
+ cell = simulation["cells"][cell_ID]
+
# Distance to nearest surface
d_surface, surface_ID = distance_to_nearest_surface(
particle_container, cell, simulation, data
@@ -86,24 +73,10 @@ def inspect_geometry(particle_container, simulation, data):
else:
# Cell is filled with universe or lattice
-
- # Apply translation
- if cell["fill_translated"]:
- particle["x"] -= cell["translation"][0]
- particle["y"] -= cell["translation"][1]
- particle["z"] -= cell["translation"][2]
-
- # Apply rotation
- if cell["fill_rotated"]:
- _rotate_particle(particle_container, cell["rotation"])
-
- # Universe cell?
- if cell["fill_type"] == FILL_UNIVERSE:
- # Get universe ID
- universe_ID = cell["fill_ID"]
+ _apply_fill_transform(particle_container, cell)
# Lattice cell?
- elif cell["fill_type"] == FILL_LATTICE:
+ if cell["fill_type"] == FILL_LATTICE:
# Get lattice
lattice = simulation["lattices"][cell["fill_ID"]]
@@ -124,35 +97,21 @@ def inspect_geometry(particle_container, simulation, data):
if not event & EVENT_LATTICE_CROSSING:
event += EVENT_LATTICE_CROSSING
- # Get universe
- ix, iy, iz = mesh.uniform.get_indices(particle_container, lattice)
- if ix == -1 or iy == -1 or iz == -1:
- event = EVENT_LOST
- continue
- universe_ID = int(
- mcdc_get.lattice.universe_IDs(ix, iy, iz, lattice, data)
- )
-
- # Lattice-translate the particle
- particle["x"] -= lattice["x0"] + (ix + 0.5) * lattice["dx"]
- particle["y"] -= lattice["y0"] + (iy + 0.5) * lattice["dy"]
- particle["z"] -= lattice["z0"] + (iz + 0.5) * lattice["dz"]
+ # Find the filled universe and enter its local coordinates.
+ universe_ID = _enter_fill(particle_container, cell, simulation, data)
+ if universe_ID == -1:
+ event = EVENT_LOST
+ continue
# Get inner cell
- cell_ID = get_cell(particle_container, universe_ID, simulation, data)
- if cell_ID > -1:
- cell = simulation["cells"][cell_ID]
- else:
+ cell_ID = _get_cell(
+ particle_container, speed, universe_ID, simulation, data
+ )
+ if cell_ID == -1:
event = EVENT_LOST
- # Reassign the global coordinate
- particle["x"] = x_global
- particle["y"] = y_global
- particle["z"] = z_global
- particle["t"] = t_global
- particle["ux"] = ux_global
- particle["uy"] = uy_global
- particle["uz"] = uz_global
+ # Restore the particle after traversal through local coordinates.
+ _restore_global_coordinates(particle_container, global_coordinates)
# Report lost particle
if event == EVENT_LOST:
@@ -175,33 +134,25 @@ def locate_particle(particle_container, simulation, data):
"""
particle = particle_container[0]
- # Store particle global coordinate
- # (particle will be temporarily translated and rotated)
- x_global = particle["x"]
- y_global = particle["y"]
- z_global = particle["z"]
- t_global = particle["t"]
- ux_global = particle["ux"]
- uy_global = particle["uy"]
- uz_global = particle["uz"]
+ # Preserve global coordinates while traversing nested geometry.
+ global_coordinates = _save_global_coordinates(particle_container)
+ # Use direction alone to resolve surface coincidence during location.
+ # Material-dependent speed is unavailable until location is complete.
+ direction_only_speed = INF
particle_is_lost = False
- # Find top cell from root universe if unknown
- if particle["cell_ID"] == -1:
- particle["cell_ID"] = get_cell(
- particle_container, UNIVERSE_ROOT, simulation, data
- )
-
- # Particle is lost?
- if particle["cell_ID"] == -1:
- particle_is_lost = True
-
- # The top cell
- cell = simulation["cells"][particle["cell_ID"]]
+ # Find the top cell from the root universe if it is unknown.
+ cell_ID = _get_top_cell_ID(
+ particle_container, direction_only_speed, simulation, data
+ )
+ if cell_ID == -1:
+ particle_is_lost = True
# Recursively check cells until material cell is found (or the particle is lost)
while not particle_is_lost:
+ cell = simulation["cells"][cell_ID]
+
# Material cell?
if cell["fill_type"] == FILL_MATERIAL:
particle["material_ID"] = cell["fill_ID"]
@@ -209,56 +160,27 @@ def locate_particle(particle_container, simulation, data):
else:
# Cell is filled with universe or lattice
+ _apply_fill_transform(particle_container, cell)
- # Apply translation
- if cell["fill_translated"]:
- particle["x"] -= cell["translation"][0]
- particle["y"] -= cell["translation"][1]
- particle["z"] -= cell["translation"][2]
-
- # Apply rotation
- if cell["fill_rotated"]:
- _rotate_particle(particle_container, cell["rotation"])
-
- # Universe cell?
- if cell["fill_type"] == FILL_UNIVERSE:
- # Get universe ID
- universe_ID = cell["fill_ID"]
-
- # Lattice cell?
- elif cell["fill_type"] == FILL_LATTICE:
- # Get lattice
- lattice = simulation["lattices"][cell["fill_ID"]]
-
- # Get universe
- ix, iy, iz = mesh.uniform.get_indices(particle_container, lattice)
- if ix == -1 or iy == -1 or iz == -1:
- particle_is_lost = True
- continue
- universe_ID = int(
- mcdc_get.lattice.universe_IDs(ix, iy, iz, lattice, data)
- )
-
- # Lattice-translate the particle
- particle["x"] -= lattice["x0"] + (ix + 0.5) * lattice["dx"]
- particle["y"] -= lattice["y0"] + (iy + 0.5) * lattice["dy"]
- particle["z"] -= lattice["z0"] + (iz + 0.5) * lattice["dz"]
+ # Find the filled universe and enter its local coordinates.
+ universe_ID = _enter_fill(particle_container, cell, simulation, data)
+ if universe_ID == -1:
+ particle_is_lost = True
+ continue
# Get inner cell
- cell_ID = get_cell(particle_container, universe_ID, simulation, data)
- if cell_ID > -1:
- cell = simulation["cells"][cell_ID]
- else:
+ cell_ID = _get_cell(
+ particle_container,
+ direction_only_speed,
+ universe_ID,
+ simulation,
+ data,
+ )
+ if cell_ID == -1:
particle_is_lost = True
- # Reassign the global coordinate
- particle["x"] = x_global
- particle["y"] = y_global
- particle["z"] = z_global
- particle["t"] = t_global
- particle["ux"] = ux_global
- particle["uy"] = uy_global
- particle["uz"] = uz_global
+ # Restore the particle after traversal through local coordinates.
+ _restore_global_coordinates(particle_container, global_coordinates)
# Report lost particle
if particle_is_lost:
@@ -267,6 +189,81 @@ def locate_particle(particle_container, simulation, data):
return not particle_is_lost
+# ======================================================================================
+# Geometry traversal helpers
+# ======================================================================================
+
+
+@njit
+def _save_global_coordinates(particle_container):
+ particle = particle_container[0]
+ return (
+ particle["x"],
+ particle["y"],
+ particle["z"],
+ particle["t"],
+ particle["ux"],
+ particle["uy"],
+ particle["uz"],
+ )
+
+
+@njit
+def _restore_global_coordinates(particle_container, coordinates):
+ particle = particle_container[0]
+ particle["x"] = coordinates[0]
+ particle["y"] = coordinates[1]
+ particle["z"] = coordinates[2]
+ particle["t"] = coordinates[3]
+ particle["ux"] = coordinates[4]
+ particle["uy"] = coordinates[5]
+ particle["uz"] = coordinates[6]
+
+
+@njit
+def _get_top_cell_ID(particle_container, speed, simulation, data):
+ particle = particle_container[0]
+ if particle["cell_ID"] == -1:
+ particle["cell_ID"] = _get_cell(
+ particle_container, speed, UNIVERSE_ROOT, simulation, data
+ )
+ return particle["cell_ID"]
+
+
+@njit
+def _apply_fill_transform(particle_container, cell):
+ particle = particle_container[0]
+
+ if cell["fill_translated"]:
+ particle["x"] -= cell["translation"][0]
+ particle["y"] -= cell["translation"][1]
+ particle["z"] -= cell["translation"][2]
+
+ if cell["fill_rotated"]:
+ _rotate_particle(particle_container, cell["rotation"])
+
+
+@njit
+def _enter_fill(particle_container, cell, simulation, data):
+ if cell["fill_type"] == FILL_UNIVERSE:
+ return cell["fill_ID"]
+
+ if cell["fill_type"] == FILL_LATTICE:
+ particle = particle_container[0]
+ lattice = simulation["lattices"][cell["fill_ID"]]
+ ix, iy, iz = mesh.uniform.get_indices(particle_container, lattice)
+ if ix == -1 or iy == -1 or iz == -1:
+ return -1
+
+ universe_ID = mcdc_get.lattice.universe_IDs(ix, iy, iz, lattice, data)
+ particle["x"] -= lattice["x0"] + (ix + 0.5) * lattice["dx"]
+ particle["y"] -= lattice["y0"] + (iy + 0.5) * lattice["dy"]
+ particle["z"] -= lattice["z0"] + (iz + 0.5) * lattice["dz"]
+ return universe_ID
+
+ return -1
+
+
@njit
def _rotate_particle(particle_container, rotation):
# Particle initial coordinate
@@ -334,14 +331,20 @@ def get_cell(particle_container, universe_ID, simulation, data):
Find and return particle cell ID in the given universe
Return -1 if particle is lost
"""
- particle = particle_container[0]
+ speed = physics.particle_speed(particle_container, simulation, data)
+ return _get_cell(particle_container, speed, universe_ID, simulation, data)
+
+
+@njit
+def _get_cell(particle_container, speed, universe_ID, simulation, data):
+ """Find the particle cell using the supplied speed for coincidence checks."""
universe = simulation["universes"][universe_ID]
# Check over all cells in the universe
for i in range(universe["N_cell"]):
- cell_ID = int(mcdc_get.universe.cell_IDs(i, universe, data))
+ cell_ID = mcdc_get.universe.cell_IDs(i, universe, data)
cell = simulation["cells"][cell_ID]
- if check_cell(particle_container, cell, simulation, data):
+ if _check_cell(particle_container, speed, cell, simulation, data):
return cell_ID
# Particle is not found
@@ -353,8 +356,13 @@ def check_cell(particle_container, cell, simulation, data):
"""
Check if the particle is inside the cell
"""
- particle = particle_container[0]
+ speed = physics.particle_speed(particle_container, simulation, data)
+ return _check_cell(particle_container, speed, cell, simulation, data)
+
+@njit
+def _check_cell(particle_container, speed, cell, simulation, data):
+ """Check cell membership using the supplied speed for coincidence checks."""
# Access RPN data
N_token = cell["region_RPN_tokens_length"]
if N_token == 0:
@@ -364,12 +372,9 @@ def check_cell(particle_container, cell, simulation, data):
value = util.local_array(literals.rpn_evaluation_buffer_size(), np.bool_)
N_value = 0
- # Particle parameters
- speed = physics.particle_speed(particle_container, simulation, data)
-
# March forward through RPN tokens
for idx in range(N_token):
- token = int(mcdc_get.cell.region_RPN_tokens(idx, cell, data))
+ token = mcdc_get.cell.region_RPN_tokens(idx, cell, data)
if token >= 0:
surface = simulation["surfaces"][token]
@@ -427,7 +432,7 @@ def distance_to_nearest_surface(particle_container, cell, simulation, data):
# Iterate over all surfaces and find the minimum distance
for i in range(cell["N_surface"]):
- candidate_surface_ID = int(mcdc_get.cell.surface_IDs(i, cell, data))
+ candidate_surface_ID = mcdc_get.cell.surface_IDs(i, cell, data)
surface = simulation["surfaces"][candidate_surface_ID]
d = get_distance(particle_container, speed, surface, data)
if d < distance:
diff --git a/mcdc/transport/mesh/interface.py b/mcdc/transport/mesh/interface.py
index 2e4fda69f..0066fc422 100644
--- a/mcdc/transport/mesh/interface.py
+++ b/mcdc/transport/mesh/interface.py
@@ -7,57 +7,57 @@
@njit
-def get_indices(particle_container, mesh_base, simulation, data):
- mesh_type = mesh_base["child_type"]
- mesh_ID = mesh_base["child_ID"]
+def get_indices(particle_container, mesh, simulation, data):
+ mesh_type = mesh["sub_type"]
+ mesh_ID = mesh["sub_ID"]
if mesh_type == MESH_UNIFORM:
- mesh = simulation["uniform_meshes"][mesh_ID]
- return uniform.get_indices(particle_container, mesh)
+ uniform_mesh = simulation["uniform_meshes"][mesh_ID]
+ return uniform.get_indices(particle_container, uniform_mesh)
elif mesh_type == MESH_STRUCTURED:
- mesh = simulation["structured_meshes"][mesh_ID]
- return structured.get_indices(particle_container, mesh, data)
+ structured_mesh = simulation["structured_meshes"][mesh_ID]
+ return structured.get_indices(particle_container, structured_mesh, data)
return -1, -1, -1
@njit
-def get_x(index, mesh_base, simulation, data):
- mesh_type = mesh_base["child_type"]
- mesh_ID = mesh_base["child_ID"]
+def get_x(index, mesh, simulation, data):
+ mesh_type = mesh["sub_type"]
+ mesh_ID = mesh["sub_ID"]
if mesh_type == MESH_UNIFORM:
- mesh = simulation["uniform_meshes"][mesh_ID]
- return mesh["x0"] + mesh["dx"] * index
+ uniform_mesh = simulation["uniform_meshes"][mesh_ID]
+ return uniform_mesh["x0"] + uniform_mesh["dx"] * index
elif mesh_type == MESH_STRUCTURED:
- mesh = simulation["structured_meshes"][mesh_ID]
- return mcdc_get.structured_mesh.x(index, mesh, data)
+ structured_mesh = simulation["structured_meshes"][mesh_ID]
+ return mcdc_get.structured_mesh.x(index, structured_mesh, data)
return 0.0
@njit
-def get_y(index, mesh_base, simulation, data):
- mesh_type = mesh_base["child_type"]
- mesh_ID = mesh_base["child_ID"]
+def get_y(index, mesh, simulation, data):
+ mesh_type = mesh["sub_type"]
+ mesh_ID = mesh["sub_ID"]
if mesh_type == MESH_UNIFORM:
- mesh = simulation["uniform_meshes"][mesh_ID]
- return mesh["y0"] + mesh["dy"] * index
+ uniform_mesh = simulation["uniform_meshes"][mesh_ID]
+ return uniform_mesh["y0"] + uniform_mesh["dy"] * index
elif mesh_type == MESH_STRUCTURED:
- mesh = simulation["structured_meshes"][mesh_ID]
- return mcdc_get.structured_mesh.y(index, mesh, data)
+ structured_mesh = simulation["structured_meshes"][mesh_ID]
+ return mcdc_get.structured_mesh.y(index, structured_mesh, data)
return 0.0
@njit
-def get_z(index, mesh_base, simulation, data):
- mesh_type = mesh_base["child_type"]
- mesh_ID = mesh_base["child_ID"]
+def get_z(index, mesh, simulation, data):
+ mesh_type = mesh["sub_type"]
+ mesh_ID = mesh["sub_ID"]
if mesh_type == MESH_UNIFORM:
- mesh = simulation["uniform_meshes"][mesh_ID]
- return mesh["z0"] + mesh["dz"] * index
+ uniform_mesh = simulation["uniform_meshes"][mesh_ID]
+ return uniform_mesh["z0"] + uniform_mesh["dz"] * index
elif mesh_type == MESH_STRUCTURED:
- mesh = simulation["structured_meshes"][mesh_ID]
- return mcdc_get.structured_mesh.z(index, mesh, data)
+ structured_mesh = simulation["structured_meshes"][mesh_ID]
+ return mcdc_get.structured_mesh.z(index, structured_mesh, data)
return 0.0
diff --git a/mcdc/transport/mesh/structured.py b/mcdc/transport/mesh/structured.py
index 9afe50a8a..7e3ad989f 100644
--- a/mcdc/transport/mesh/structured.py
+++ b/mcdc/transport/mesh/structured.py
@@ -2,16 +2,14 @@
####
-import mcdc.mcdc_get as mcdc_get
-
-from mcdc.constant import COINCIDENCE_TOLERANCE, COINCIDENCE_TOLERANCE_TIME, INF
+from mcdc.constant import COINCIDENCE_TOLERANCE, INF
from mcdc.transport.util import find_bin_with_rules
@njit
-def get_indices(particle_container, mesh, data):
+def get_indices(particle_container, structured_mesh, data):
"""
- Get mesh indices given the particle coordinate
+ Get structured_mesh indices given the particle coordinate
"""
particle = particle_container[0]
@@ -23,12 +21,24 @@ def get_indices(particle_container, mesh, data):
uy = particle["uy"]
uz = particle["uz"]
- grid_x = data[mesh["x_offset"] : (mesh["x_offset"] + mesh["x_length"])]
- # Above is equivalent to: grid_x = mcdc_get.structured_mesh.x_all(mesh, data)
- grid_y = data[mesh["y_offset"] : (mesh["y_offset"] + mesh["y_length"])]
- # Above is equivalent to: grid_y = mcdc_get.structured_mesh.y_all(mesh, data)
- grid_z = data[mesh["z_offset"] : (mesh["z_offset"] + mesh["z_length"])]
- # Above is equivalent to: grid_z = mcdc_get.structured_mesh.z_all(mesh, data)
+ grid_x = data[
+ structured_mesh["x_offset"] : (
+ structured_mesh["x_offset"] + structured_mesh["x_length"]
+ )
+ ]
+ # Above is equivalent to: grid_x = mcdc_get.structured_mesh.x_all(structured_mesh, data)
+ grid_y = data[
+ structured_mesh["y_offset"] : (
+ structured_mesh["y_offset"] + structured_mesh["y_length"]
+ )
+ ]
+ # Above is equivalent to: grid_y = mcdc_get.structured_structured_mesh.y_all(structured_mesh, data)
+ grid_z = data[
+ structured_mesh["z_offset"] : (
+ structured_mesh["z_offset"] + structured_mesh["z_length"]
+ )
+ ]
+ # Above is equivalent to: grid_z = mcdc_get.structured_structured_mesh.z_all(structured_mesh, data)
tolerance = COINCIDENCE_TOLERANCE
ux_go_lower = ux < 0.0
@@ -43,10 +53,10 @@ def get_indices(particle_container, mesh, data):
@njit
-def get_crossing_distance(particle_arr, speed, mesh):
+def get_crossing_distance(particle_arr, speed, structured_mesh):
"""
Get distance for the particle, moving with the given speed,
- to cross the nearest grid of the mesh
+ to cross the nearest grid of the structured_mesh
"""
particle = particle_arr[0]
@@ -59,25 +69,31 @@ def get_crossing_distance(particle_arr, speed, mesh):
uz = particle["uz"]
# Mesh parameters
- Nx = mesh["Nx"]
- Ny = mesh["Ny"]
- Nz = mesh["Nz"]
+ Nx = structured_mesh["Nx"]
+ Ny = structured_mesh["Ny"]
+ Nz = structured_mesh["Nz"]
- # Check if particle is outside the mesh grid and moving away
+ # Check if particle is outside the structured_mesh grid and moving away
if (
- (x < mesh["x"][0] + COINCIDENCE_TOLERANCE and ux < 0.0)
- or (x > mesh["x"][Nx] - COINCIDENCE_TOLERANCE and ux > 0.0)
- or (y < mesh["y"][0] + COINCIDENCE_TOLERANCE and uy < 0.0)
- or (y > mesh["y"][Ny] - COINCIDENCE_TOLERANCE and uy > 0.0)
- or (z < mesh["z"][0] + COINCIDENCE_TOLERANCE and uz < 0.0)
- or (z > mesh["z"][Nz] - COINCIDENCE_TOLERANCE and uz > 0.0)
+ (x < structured_mesh["x"][0] + COINCIDENCE_TOLERANCE and ux < 0.0)
+ or (x > structured_mesh["x"][Nx] - COINCIDENCE_TOLERANCE and ux > 0.0)
+ or (y < structured_mesh["y"][0] + COINCIDENCE_TOLERANCE and uy < 0.0)
+ or (y > structured_mesh["y"][Ny] - COINCIDENCE_TOLERANCE and uy > 0.0)
+ or (z < structured_mesh["z"][0] + COINCIDENCE_TOLERANCE and uz < 0.0)
+ or (z > structured_mesh["z"][Nz] - COINCIDENCE_TOLERANCE and uz > 0.0)
):
return INF
d = INF
- d = min(d, _grid_distance(x, ux, mesh["x"], Nx + 1, COINCIDENCE_TOLERANCE))
- d = min(d, _grid_distance(y, uy, mesh["y"], Ny + 1, COINCIDENCE_TOLERANCE))
- d = min(d, _grid_distance(z, uz, mesh["z"], Nz + 1, COINCIDENCE_TOLERANCE))
+ d = min(
+ d, _grid_distance(x, ux, structured_mesh["x"], Nx + 1, COINCIDENCE_TOLERANCE)
+ )
+ d = min(
+ d, _grid_distance(y, uy, structured_mesh["y"], Ny + 1, COINCIDENCE_TOLERANCE)
+ )
+ d = min(
+ d, _grid_distance(z, uz, structured_mesh["z"], Nz + 1, COINCIDENCE_TOLERANCE)
+ )
return d
diff --git a/mcdc/transport/mesh/uniform.py b/mcdc/transport/mesh/uniform.py
index 0384fabe6..7c62cd3ca 100644
--- a/mcdc/transport/mesh/uniform.py
+++ b/mcdc/transport/mesh/uniform.py
@@ -6,9 +6,9 @@
@njit
-def get_indices(particle_container, mesh):
+def get_indices(particle_container, uniform_mesh):
"""
- Get mesh indices given the particle coordinate
+ Get uniform_mesh indices given the particle coordinate
"""
particle = particle_container[0]
@@ -21,15 +21,15 @@ def get_indices(particle_container, mesh):
uz = particle["uz"]
# Mesh parameters
- x0 = mesh["x0"]
- y0 = mesh["y0"]
- z0 = mesh["z0"]
- dx = mesh["dx"]
- dy = mesh["dy"]
- dz = mesh["dz"]
- Nx = mesh["Nx"]
- Ny = mesh["Ny"]
- Nz = mesh["Nz"]
+ x0 = uniform_mesh["x0"]
+ y0 = uniform_mesh["y0"]
+ z0 = uniform_mesh["z0"]
+ dx = uniform_mesh["dx"]
+ dy = uniform_mesh["dy"]
+ dz = uniform_mesh["dz"]
+ Nx = uniform_mesh["Nx"]
+ Ny = uniform_mesh["Ny"]
+ Nz = uniform_mesh["Nz"]
x_last = x0 + Nx * dx
y_last = y0 + Ny * dy
z_last = z0 + Nz * dz
@@ -77,10 +77,10 @@ def get_indices(particle_container, mesh):
@njit
-def get_crossing_distance(particle_container, speed, mesh):
+def get_crossing_distance(particle_container, speed, uniform_mesh):
"""
Get distance for the particle, moving with the given speed,
- to cross the nearest grid of the mesh
+ to cross the nearest grid of the uniform_mesh
"""
particle = particle_container[0]
@@ -93,15 +93,15 @@ def get_crossing_distance(particle_container, speed, mesh):
uz = particle["uz"]
# Mesh parameters
- x0 = mesh["x0"]
- y0 = mesh["y0"]
- z0 = mesh["z0"]
- dx = mesh["dx"]
- dy = mesh["dy"]
- dz = mesh["dz"]
- Nx = mesh["Nx"]
- Ny = mesh["Ny"]
- Nz = mesh["Nz"]
+ x0 = uniform_mesh["x0"]
+ y0 = uniform_mesh["y0"]
+ z0 = uniform_mesh["z0"]
+ dx = uniform_mesh["dx"]
+ dy = uniform_mesh["dy"]
+ dz = uniform_mesh["dz"]
+ Nx = uniform_mesh["Nx"]
+ Ny = uniform_mesh["Ny"]
+ Nz = uniform_mesh["Nz"]
x_last = x0 + Nx * dx
y_last = y0 + Ny * dy
z_last = z0 + Nz * dz
diff --git a/mcdc/transport/particle.py b/mcdc/transport/particle.py
index e524e1c21..21c8f4b51 100644
--- a/mcdc/transport/particle.py
+++ b/mcdc/transport/particle.py
@@ -29,7 +29,6 @@ def copy(target_particle_container, source_particle_container):
target_particle["ux"] = source_particle["ux"]
target_particle["uy"] = source_particle["uy"]
target_particle["uz"] = source_particle["uz"]
- target_particle["g"] = source_particle["g"]
target_particle["E"] = source_particle["E"]
target_particle["w"] = source_particle["w"]
target_particle["particle_type"] = source_particle["particle_type"]
diff --git a/mcdc/transport/particle_bank.py b/mcdc/transport/particle_bank.py
index 9b32f9cd8..9736c2aa2 100644
--- a/mcdc/transport/particle_bank.py
+++ b/mcdc/transport/particle_bank.py
@@ -200,7 +200,7 @@ def manage_particle_banks(simulation):
)
# Population control
- if simulation["population_control"]["active"]:
+ if simulation["technique"]["population_control"]["active"]:
technique.population_control(simulation)
else:
# Swap census and source bank
diff --git a/mcdc/transport/physics/electron/native.py b/mcdc/transport/physics/electron/native.py
index f975865a2..1f3bb8aef 100644
--- a/mcdc/transport/physics/electron/native.py
+++ b/mcdc/transport/physics/electron/native.py
@@ -25,7 +25,6 @@
from mcdc.transport.data import evaluate_data
from mcdc.transport.distribution import (
sample_distribution,
- sample_multi_table,
)
from mcdc.transport.physics.util import (
evaluate_electron_xs_energy_grid,
@@ -54,15 +53,15 @@ def particle_speed(particle_container):
@njit
def macro_xs(reaction_type, particle_container, simulation, data):
particle = particle_container[0]
- material = simulation["native_materials"][particle["material_ID"]]
+ material = simulation["materials"][particle["material_ID"]]
E = particle["E"]
total = 0.0
for i in range(material["N_element"]):
- element_ID = int(mcdc_get.native_material.element_IDs(i, material, data))
+ element_ID = mcdc_get.material.element_IDs(i, material, data)
element = simulation["elements"][element_ID]
- element_density = mcdc_get.native_material.element_densities(i, material, data)
+ element_density = mcdc_get.material.element_densities(i, material, data)
xs = total_micro_xs(reaction_type, E, element, data)
total += element_density * xs
@@ -91,18 +90,18 @@ def total_micro_xs(reaction_type, E, element, data):
@njit
-def reaction_micro_xs(E, reaction_base, element, data):
+def reaction_micro_xs(E, reaction, element, data):
idx, E0, E1 = evaluate_electron_xs_energy_grid(E, element, data)
# Apply offset
- offset = reaction_base["xs_offset_"]
+ offset = reaction["xs_offset_"]
if idx < offset:
return 0.0
else:
idx -= offset
- xs0 = mcdc_get.electron_reaction.xs(idx, reaction_base, data)
- xs1 = mcdc_get.electron_reaction.xs(idx + 1, reaction_base, data)
+ xs0 = mcdc_get.electron_reaction.xs(idx, reaction, data)
+ xs1 = mcdc_get.electron_reaction.xs(idx + 1, reaction, data)
return linear_interpolation(E, E0, E1, xs0, xs1)
@@ -116,7 +115,7 @@ def collision(particle_container, collision_data_container, program, data):
simulation = util.access_simulation(program)
particle = particle_container[0]
collision_data = collision_data_container[0]
- material = simulation["native_materials"][particle["material_ID"]]
+ material = simulation["materials"][particle["material_ID"]]
# Particle properties
E = particle["E"]
@@ -137,10 +136,10 @@ def collision(particle_container, collision_data_container, program, data):
xi = rng.lcg(particle_container) * SigmaT
total = 0.0
for i in range(material["N_element"]):
- element_ID = int(mcdc_get.native_material.element_IDs(i, material, data))
+ element_ID = mcdc_get.material.element_IDs(i, material, data)
element = simulation["elements"][element_ID]
- element_density = mcdc_get.native_material.element_densities(i, material, data)
+ element_density = mcdc_get.material.element_densities(i, material, data)
sigmaT = total_micro_xs(ELECTRON_REACTION_TOTAL, E, element, data)
total += element_density * sigmaT
@@ -169,16 +168,14 @@ def collision(particle_container, collision_data_container, program, data):
if xi < total:
total -= sigma_ionization
for i in range(element["N_electron_ionization_reaction"]):
- reaction_ID = int(
- mcdc_get.element.electron_ionization_reaction_IDs(i, element, data)
+ reaction_ID = mcdc_get.element.electron_ionization_reaction_IDs(
+ i, element, data
)
- reaction = simulation["electron_ionization_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["electron_reactions"][reaction_base_ID]
- total += reaction_micro_xs(E, reaction_base, element, data)
+ reaction = simulation["electron_reactions"][reaction_ID]
+ total += reaction_micro_xs(E, reaction, element, data)
if xi < total:
- ionization(
+ sample_ionization(
reaction,
particle_container,
collision_data_container,
@@ -193,18 +190,14 @@ def collision(particle_container, collision_data_container, program, data):
if xi < total:
total -= sigma_elastic
for i in range(element["N_electron_elastic_scattering_reaction"]):
- reaction_ID = int(
- mcdc_get.element.electron_elastic_scattering_reaction_IDs(
- i, element, data
- )
+ reaction_ID = mcdc_get.element.electron_elastic_scattering_reaction_IDs(
+ i, element, data
)
- reaction = simulation["electron_elastic_scattering_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["electron_reactions"][reaction_base_ID]
- total += reaction_micro_xs(E, reaction_base, element, data)
+ reaction = simulation["electron_reactions"][reaction_ID]
+ total += reaction_micro_xs(E, reaction, element, data)
if xi < total:
- elastic_scattering(
+ sample_elastic_scattering(
reaction, particle_container, element, simulation, data
)
return
@@ -214,16 +207,14 @@ def collision(particle_container, collision_data_container, program, data):
if xi < total:
total -= sigma_bremsstrahlung
for i in range(element["N_electron_bremsstrahlung_reaction"]):
- reaction_ID = int(
- mcdc_get.element.electron_bremsstrahlung_reaction_IDs(i, element, data)
+ reaction_ID = mcdc_get.element.electron_bremsstrahlung_reaction_IDs(
+ i, element, data
)
- reaction = simulation["electron_bremsstrahlung_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["electron_reactions"][reaction_base_ID]
- total += reaction_micro_xs(E, reaction_base, element, data)
+ reaction = simulation["electron_reactions"][reaction_ID]
+ total += reaction_micro_xs(E, reaction, element, data)
if xi < total:
- bremsstrahlung(
+ sample_bremsstrahlung(
reaction,
particle_container,
collision_data_container,
@@ -237,16 +228,14 @@ def collision(particle_container, collision_data_container, program, data):
if xi < total:
total -= sigma_excitation
for i in range(element["N_electron_excitation_reaction"]):
- reaction_ID = int(
- mcdc_get.element.electron_excitation_reaction_IDs(i, element, data)
+ reaction_ID = mcdc_get.element.electron_excitation_reaction_IDs(
+ i, element, data
)
- reaction = simulation["electron_excitation_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["electron_reactions"][reaction_base_ID]
- total += reaction_micro_xs(E, reaction_base, element, data)
+ reaction = simulation["electron_reactions"][reaction_ID]
+ total += reaction_micro_xs(E, reaction, element, data)
if xi < total:
- excitation(
+ sample_excitation(
reaction,
particle_container,
collision_data_container,
@@ -262,9 +251,12 @@ def collision(particle_container, collision_data_container, program, data):
@njit
-def elastic_scattering(reaction, particle_container, element, simulation, data):
+def sample_elastic_scattering(reaction, particle_container, element, simulation, data):
particle = particle_container[0]
+ sub_ID = reaction["sub_ID"]
+ elastic_scattering = simulation["electron_elastic_scattering_reactions"][sub_ID]
+
# Current energy
E = particle["E"]
@@ -272,12 +264,10 @@ def elastic_scattering(reaction, particle_container, element, simulation, data):
# Total elastic xs
# -------------------------------------------------------------------------
- reaction_base_ID = int(reaction["parent_ID"])
- reaction_base = simulation["electron_reactions"][reaction_base_ID]
- xs_total = reaction_micro_xs(E, reaction_base, element, data)
+ xs_total = reaction_micro_xs(E, reaction, element, data)
# If large-angle, xs from data table
- xs_large = elastic_large_xs(E, reaction, simulation, data)
+ xs_large = elastic_large_xs(E, elastic_scattering, simulation, data)
# Important to check because of numerical issues
if xs_large < 0.0:
@@ -286,7 +276,7 @@ def elastic_scattering(reaction, particle_container, element, simulation, data):
xs_large = xs_total
prob_large = xs_large / xs_total
- mu_cut = float(reaction["mu_cut"])
+ mu_cut = float(elastic_scattering["mu_cut"])
xi = rng.lcg(particle_container)
@@ -295,8 +285,10 @@ def elastic_scattering(reaction, particle_container, element, simulation, data):
# Large-angle elastic scattering
# ---------------------------------------------------------------------
- multi_table = simulation["multi_table_distributions"][reaction["mu_ID"]]
- mu0 = sample_multi_table(E, particle_container, multi_table, simulation, data)
+ mu_distribution = simulation["distributions"][elastic_scattering["mu_ID"]]
+ mu0 = sample_distribution(
+ E, mu_distribution, particle_container, simulation, data
+ )
else:
# ---------------------------------------------------------------------
@@ -348,9 +340,9 @@ def sample_small_angle_mu_coulomb(E, Z, rng_state, mu_cut):
@njit
-def elastic_large_xs(E, reaction, simulation, data):
- data_base = simulation["data"][int(reaction["xs_large_ID"])]
- return evaluate_data(E, data_base, simulation, data)
+def elastic_large_xs(E, elastic_scattering, simulation, data):
+ reaction_data = simulation["data"][int(elastic_scattering["xs_large_ID"])]
+ return evaluate_data(E, reaction_data, simulation, data)
# ======================================================================================
@@ -359,16 +351,19 @@ def elastic_large_xs(E, reaction, simulation, data):
@njit
-def excitation(
+def sample_excitation(
reaction, particle_container, collision_data_container, simulation, data
):
particle = particle_container[0]
collision_data = collision_data_container[0]
+ sub_ID = reaction["sub_ID"]
+ excitation = simulation["electron_excitation_reactions"][sub_ID]
+
# Current energy
E = particle["E"]
- dE = evaluate_eloss(E, reaction, simulation, data)
+ dE = evaluate_eloss(E, excitation, simulation, data)
# Calculate outgoing energy
E_out = E - dE
@@ -387,8 +382,8 @@ def excitation(
@njit
def evaluate_eloss(E, reaction, simulation, data):
- data_base = simulation["data"][int(reaction["eloss_ID"])]
- return evaluate_data(E, data_base, simulation, data)
+ reaction_data = simulation["data"][int(reaction["eloss_ID"])]
+ return evaluate_data(E, reaction_data, simulation, data)
# ======================================================================================
@@ -397,16 +392,19 @@ def evaluate_eloss(E, reaction, simulation, data):
@njit
-def bremsstrahlung(
+def sample_bremsstrahlung(
reaction, particle_container, collision_data_container, simulation, data
):
particle = particle_container[0]
collision_data = collision_data_container[0]
+ sub_ID = reaction["sub_ID"]
+ bremsstrahlung = simulation["electron_bremsstrahlung_reactions"][sub_ID]
+
# Current energy
E = particle["E"]
- dE = evaluate_eloss(E, reaction, simulation, data)
+ dE = evaluate_eloss(E, bremsstrahlung, simulation, data)
E_out = E - dE
# Check for cutoff
@@ -426,22 +424,25 @@ def bremsstrahlung(
@njit
-def ionization(
+def sample_ionization(
reaction, particle_container, collision_data_container, element, program, data
):
simulation = util.access_simulation(program)
particle = particle_container[0]
collision_data = collision_data_container[0]
+ sub_ID = reaction["sub_ID"]
+ ionization = simulation["electron_ionization_reactions"][sub_ID]
+
# Current energy
E = particle["E"]
# Sample subshell
- N = int(reaction["N_subshell"])
+ N = int(ionization["N_subshell"])
total = 0.0
for i in range(N):
- xs_sub_ID = int(
- mcdc_get.electron_ionization_reaction.subshell_x_IDs(i, reaction, data)
+ xs_sub_ID = mcdc_get.electron_ionization_reaction.subshell_x_IDs(
+ i, ionization, data
)
xs_sub_table = simulation["data"][xs_sub_ID]
total += evaluate_data(E, xs_sub_table, simulation, data)
@@ -450,8 +451,8 @@ def ionization(
total_acc = 0.0
chosen = 0
for i in range(N):
- xs_sub_ID = int(
- mcdc_get.electron_ionization_reaction.subshell_x_IDs(i, reaction, data)
+ xs_sub_ID = mcdc_get.electron_ionization_reaction.subshell_x_IDs(
+ i, ionization, data
)
xs_sub_table = simulation["data"][xs_sub_ID]
total_acc += evaluate_data(E, xs_sub_table, simulation, data)
@@ -470,13 +471,11 @@ def ionization(
return
# Sample secondary energy
- dist_ID = int(
- mcdc_get.electron_ionization_reaction.subshell_product_IDs(
- chosen, reaction, data
- )
+ dist_ID = mcdc_get.electron_ionization_reaction.subshell_product_IDs(
+ chosen, ionization, data
)
- dist_base = simulation["distributions"][dist_ID]
- T_delta = sample_distribution(E, dist_base, particle_container, simulation, data)
+ T_dist = simulation["distributions"][dist_ID]
+ T_delta = sample_distribution(E, T_dist, particle_container, simulation, data)
# Primary outgoing energy
E_out = E - B - T_delta
diff --git a/mcdc/transport/physics/neutron/interface.py b/mcdc/transport/physics/neutron/interface.py
index 29f78602f..f9186efae 100644
--- a/mcdc/transport/physics/neutron/interface.py
+++ b/mcdc/transport/physics/neutron/interface.py
@@ -13,7 +13,7 @@
@njit
def particle_speed(particle_container, simulation, data):
- if simulation["settings"]["neutron_multigroup_mode"]:
+ if multigroup.applicable(particle_container, simulation, data):
return multigroup.particle_speed(particle_container, simulation, data)
else:
return native.particle_speed(particle_container)
@@ -26,7 +26,7 @@ def particle_speed(particle_container, simulation, data):
@njit
def macro_xs(reaction_type, particle_container, simulation, data):
- if simulation["settings"]["neutron_multigroup_mode"]:
+ if multigroup.applicable(particle_container, simulation, data):
return multigroup.macro_xs(reaction_type, particle_container, simulation, data)
else:
return native.macro_xs(reaction_type, particle_container, simulation, data)
@@ -34,7 +34,7 @@ def macro_xs(reaction_type, particle_container, simulation, data):
@njit
def neutron_production_xs(reaction_type, particle_container, simulation, data):
- if simulation["settings"]["neutron_multigroup_mode"]:
+ if multigroup.applicable(particle_container, simulation, data):
return multigroup.neutron_production_xs(
reaction_type, particle_container, simulation, data
)
@@ -53,7 +53,7 @@ def neutron_production_xs(reaction_type, particle_container, simulation, data):
def collision(particle_container, collision_data_container, program, data):
simulation = util.access_simulation(program)
- if simulation["settings"]["neutron_multigroup_mode"]:
+ if multigroup.applicable(particle_container, simulation, data):
multigroup.collision(
particle_container, collision_data_container, program, data
)
diff --git a/mcdc/transport/physics/neutron/multigroup.py b/mcdc/transport/physics/neutron/multigroup.py
index d5b6c0a52..e1d48d495 100644
--- a/mcdc/transport/physics/neutron/multigroup.py
+++ b/mcdc/transport/physics/neutron/multigroup.py
@@ -1,4 +1,3 @@
-import numpy as np
import math
from numba import njit
@@ -14,6 +13,10 @@
from mcdc.constant import (
PI,
+ NEUTRON_MULTIGROUP_ENERGY_MIDPOINT,
+ NEUTRON_MULTIGROUP_ENERGY_MIDPOINT_LOG,
+ NEUTRON_MULTIGROUP_ENERGY_UNIFORM,
+ NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG,
NEUTRON_REACTION_TOTAL,
NEUTRON_REACTION_CAPTURE,
NEUTRON_REACTION_ELASTIC_SCATTERING,
@@ -24,6 +27,33 @@
from mcdc.transport.physics.util import scatter_direction
from mcdc.transport.distribution import sample_isotropic_direction
+# ======================================================================================
+# Applicability
+# ======================================================================================
+
+
+@njit
+def applicable(particle_container, simulation, data):
+ particle = particle_container[0]
+ material = simulation["materials"][particle["material_ID"]]
+
+ if not material["has_neutron_multigroup"]:
+ return False
+
+ if simulation["technique"]["neutron_multigroup"]["hybrid"]:
+ mgxs_ID = material["neutron_multigroup_ID"]
+ mgxs = simulation["neutron_multigroup_data"][mgxs_ID]
+
+ E = particle["E"]
+ E_min = mcdc_get.neutron_multigroup_data.energy_grid(0, mgxs, data)
+ E_max = mcdc_get.neutron_multigroup_data.energy_grid_last(mgxs, data)
+
+ if E < E_min or E >= E_max:
+ return False
+
+ return True
+
+
# ======================================================================================
# Particle attributes
# ======================================================================================
@@ -32,8 +62,14 @@
@njit
def particle_speed(particle_container, simulation, data):
particle = particle_container[0]
- material = simulation["multigroup_materials"][particle["material_ID"]]
- return mcdc_get.multigroup_material.mgxs_speed(particle["g"], material, data)
+ material = simulation["materials"][particle["material_ID"]]
+
+ mgxs_ID = material["neutron_multigroup_ID"]
+ mgxs = simulation["neutron_multigroup_data"][mgxs_ID]
+
+ group = _get_energy_group(particle["E"], mgxs, simulation, data)
+
+ return mcdc_get.neutron_multigroup_data.speed(group, mgxs, data)
# ======================================================================================
@@ -44,38 +80,46 @@ def particle_speed(particle_container, simulation, data):
@njit
def macro_xs(reaction_type, particle_container, simulation, data):
particle = particle_container[0]
- material = simulation["multigroup_materials"][particle["material_ID"]]
- g = particle["g"]
+ material = simulation["materials"][particle["material_ID"]]
+
+ mgxs_ID = material["neutron_multigroup_ID"]
+ mgxs = simulation["neutron_multigroup_data"][mgxs_ID]
+
+ group = _get_energy_group(particle["E"], mgxs, simulation, data)
if reaction_type == NEUTRON_REACTION_TOTAL:
- return mcdc_get.multigroup_material.mgxs_total(g, material, data)
+ return mcdc_get.neutron_multigroup_data.total(group, mgxs, data)
elif reaction_type == NEUTRON_REACTION_CAPTURE:
- return mcdc_get.multigroup_material.mgxs_capture(g, material, data)
+ return mcdc_get.neutron_multigroup_data.capture(group, mgxs, data)
elif reaction_type == NEUTRON_REACTION_ELASTIC_SCATTERING:
- return mcdc_get.multigroup_material.mgxs_scatter(g, material, data)
+ return mcdc_get.neutron_multigroup_data.scatter(group, mgxs, data)
elif reaction_type == NEUTRON_REACTION_FISSION:
- return mcdc_get.multigroup_material.mgxs_fission(g, material, data)
+ return mcdc_get.neutron_multigroup_data.fission(group, mgxs, data)
return 0.0
@njit
def neutron_production_xs(reaction_type, particle_container, simulation, data):
particle = particle_container[0]
- material = simulation["multigroup_materials"][particle["material_ID"]]
- g = particle["g"]
+ material = simulation["materials"][particle["material_ID"]]
+
+ mgxs_ID = material["neutron_multigroup_ID"]
+ mgxs = simulation["neutron_multigroup_data"][mgxs_ID]
+
+ group = _get_energy_group(particle["E"], mgxs, simulation, data)
# Total production
if reaction_type == NEUTRON_REACTION_TOTAL:
total = 0.0
# Scattering production
- nu = mcdc_get.multigroup_material.mgxs_nu_s(g, material, data)
- xs = mcdc_get.multigroup_material.mgxs_scatter(g, material, data)
+ nu = mcdc_get.neutron_multigroup_data.nu_s(group, mgxs, data)
+ xs = mcdc_get.neutron_multigroup_data.scatter(group, mgxs, data)
total += nu * xs
# Fission production
- nu = mcdc_get.multigroup_material.mgxs_nu_f(g, material, data)
- xs = mcdc_get.multigroup_material.mgxs_fission(g, material, data)
+ nu = mcdc_get.neutron_multigroup_data.nu_f(group, mgxs, data)
+ xs = mcdc_get.neutron_multigroup_data.fission(group, mgxs, data)
total += nu * xs
return total
@@ -85,26 +129,26 @@ def neutron_production_xs(reaction_type, particle_container, simulation, data):
# Scattering production
elif reaction_type == NEUTRON_REACTION_ELASTIC_SCATTERING:
- nu = mcdc_get.multigroup_material.mgxs_nu_s(g, material, data)
- xs = mcdc_get.multigroup_material.mgxs_scatter(g, material, data)
+ nu = mcdc_get.neutron_multigroup_data.nu_s(group, mgxs, data)
+ xs = mcdc_get.neutron_multigroup_data.scatter(group, mgxs, data)
return nu * xs
# Fission production
elif reaction_type == NEUTRON_REACTION_FISSION:
- nu = mcdc_get.multigroup_material.mgxs_nu_f(g, material, data)
- xs = mcdc_get.multigroup_material.mgxs_fission(g, material, data)
+ nu = mcdc_get.neutron_multigroup_data.nu_f(group, mgxs, data)
+ xs = mcdc_get.neutron_multigroup_data.fission(group, mgxs, data)
return nu * xs
# Prompt fission production
elif reaction_type == NEUTRON_REACTION_FISSION_PROMPT:
- nu = mcdc_get.multigroup_material.mgxs_nu_p(g, material, data)
- xs = mcdc_get.multigroup_material.mgxs_fission(g, material, data)
+ nu = mcdc_get.neutron_multigroup_data.nu_p(group, mgxs, data)
+ xs = mcdc_get.neutron_multigroup_data.fission(group, mgxs, data)
return nu * xs
# Delayed neutron production
elif reaction_type == NEUTRON_REACTION_FISSION_DELAYED:
- nu = mcdc_get.multigroup_material.mgxs_nu_d_total(g, material, data)
- xs = mcdc_get.multigroup_material.mgxs_fission(g, material, data)
+ nu = mcdc_get.neutron_multigroup_data.nu_d_total(group, mgxs, data)
+ xs = mcdc_get.neutron_multigroup_data.fission(group, mgxs, data)
return nu * xs
# Unsupported default
@@ -130,7 +174,7 @@ def collision(particle_container, collision_data_container, program, data):
SigmaF = macro_xs(NEUTRON_REACTION_FISSION, particle_container, simulation, data)
# Implicit capture
- if simulation["implicit_capture"]["active"]:
+ if simulation["technique"]["implicit_capture"]["active"]:
particle["w"] *= (SigmaT - SigmaC) / SigmaT
SigmaT -= SigmaC
@@ -155,17 +199,19 @@ def collision(particle_container, collision_data_container, program, data):
@njit
def scattering(particle_container, program, data):
simulation = util.access_simulation(program)
+ particle = particle_container[0]
+ material = simulation["materials"][particle["material_ID"]]
+
+ # Material attributes
+ mgxs_ID = material["neutron_multigroup_ID"]
+ mgxs = simulation["neutron_multigroup_data"][mgxs_ID]
+ G = mgxs["G"]
# Particle attributes
- particle = particle_container[0]
- g = particle["g"]
ux = particle["ux"]
uy = particle["uy"]
uz = particle["uz"]
-
- # Material attributes
- material = simulation["multigroup_materials"][particle["material_ID"]]
- G = material["G"]
+ group = _get_energy_group(particle["E"], mgxs, simulation, data)
# Kill the current particle
particle["alive"] = False
@@ -173,13 +219,13 @@ def scattering(particle_container, program, data):
# Adjust production and product weights if weighted emission
weight_production = 1.0
weight_product = particle["w"]
- if simulation["weighted_emission"]["active"]:
- weight_target = simulation["weighted_emission"]["weight_target"]
+ if simulation["technique"]["weighted_emission"]["active"]:
+ weight_target = simulation["technique"]["weighted_emission"]["weight_target"]
weight_production = particle["w"] / weight_target
weight_product = weight_target
# Get number of secondaries
- nu_s = mcdc_get.multigroup_material.mgxs_nu_s(g, material, data)
+ nu_s = mcdc_get.neutron_multigroup_data.nu_s(group, mgxs, data)
N = int(math.floor(weight_production * nu_s + rng.lcg(particle_container)))
# Set up secondary partice container
@@ -205,19 +251,22 @@ def scattering(particle_container, program, data):
particle_new["uz"] = uz_new
# Get outgoing spectrum
- stride = material["G"]
- start = material["mgxs_chi_s_offset"] + g * stride
+ stride = mgxs["G"]
+ start = mgxs["chi_s_offset"] + group * stride
chi_s = data[start : start + stride]
- # Above is equivalent to: chi_s = mcdc_get.multigroup_material.mgxs_chi_s_vector(g, material, data)
+ # Above is equivalent to: chi_s = mcdc_get.neutron_multigroup_data.chi_s_vector(group, mgxs, data)
# Sample outgoing energy
xi = rng.lcg(particle_container_new)
total = 0.0
- for g_out in range(G):
- total += chi_s[g_out]
+ group_out = 0
+ for group_out in range(G):
+ total += chi_s[group_out]
if total > xi:
break
- particle_new["g"] = g_out
+ particle_new["E"] = _get_group_energy(
+ group_out, particle_container_new, mgxs, simulation, data
+ )
# Bank, but keep it if it is the last particle
if n == N - 1:
@@ -225,7 +274,6 @@ def scattering(particle_container, program, data):
particle["ux"] = particle_new["ux"]
particle["uy"] = particle_new["uy"]
particle["uz"] = particle_new["uz"]
- particle["g"] = particle_new["g"]
particle["E"] = particle_new["E"]
particle["w"] = particle_new["w"]
else:
@@ -235,16 +283,18 @@ def scattering(particle_container, program, data):
@njit
def fission(particle_container, program, data):
simulation = util.access_simulation(program)
+ particle = particle_container[0]
+ material = simulation["materials"][particle["material_ID"]]
settings = simulation["settings"]
- # Particle properties
- particle = particle_container[0]
- g = particle["g"]
+ # Material attributes
+ mgxs_ID = material["neutron_multigroup_ID"]
+ mgxs = simulation["neutron_multigroup_data"][mgxs_ID]
+ G = mgxs["G"]
+ J = mgxs["J"]
- # Material properties
- material = simulation["multigroup_materials"][particle["material_ID"]]
- G = material["G"]
- J = material["J"]
+ # Particle attributes
+ group = _get_energy_group(particle["E"], mgxs, simulation, data)
# Kill the current particle
particle["alive"] = False
@@ -252,19 +302,19 @@ def fission(particle_container, program, data):
# Adjust production and product weights if weighted emission
weight_production = 1.0
weight_product = particle["w"]
- if simulation["weighted_emission"]["active"]:
- weight_target = simulation["weighted_emission"]["weight_target"]
+ if simulation["technique"]["weighted_emission"]["active"]:
+ weight_target = simulation["technique"]["weighted_emission"]["weight_target"]
weight_production = particle["w"] / weight_target
weight_product = weight_target
# Fission yields
- nu = mcdc_get.multigroup_material.mgxs_nu_f(g, material, data)
- nu_p = mcdc_get.multigroup_material.mgxs_nu_p(g, material, data)
+ nu = mcdc_get.neutron_multigroup_data.nu_f(group, mgxs, data)
+ nu_p = mcdc_get.neutron_multigroup_data.nu_p(group, mgxs, data)
if J > 0:
- stride = material["J"]
- start = material["mgxs_nu_d_offset"] + g * stride
+ stride = mgxs["J"]
+ start = mgxs["nu_d_offset"] + group * stride
nu_d = data[start : start + stride]
- # Above is equivalent to: nu_d = mcdc_get.multigroup_material.mgxs_nu_d_vector(g, material, data)
+ # Above is equivalent to: nu_d = mcdc_get.neutron_multigroup_data.nu_d_vector(group, mgxs, data)
# Get number of secondaries
N = int(
@@ -296,10 +346,10 @@ def fission(particle_container, program, data):
total = nu_p
if xi < total:
prompt = True
- stride = material["G"]
- start = material["mgxs_chi_p_offset"] + g * stride
+ stride = mgxs["G"]
+ start = mgxs["chi_p_offset"] + group * stride
spectrum = data[start : start + stride]
- # Above is equivalent to: spectrum = mcdc_get.multigroup_material.mgxs_chi_p_vector(g, material, data)
+ # Above is equivalent to: spectrum = mcdc_get.neutron_multigroup_data.chi_p_vector(group, mgxs, data)
else:
prompt = False
@@ -307,26 +357,26 @@ def fission(particle_container, program, data):
for j in range(J):
total += nu_d[j]
if xi < total:
- stride = material["G"]
- start = material["mgxs_chi_d_offset"] + j * stride
+ stride = mgxs["G"]
+ start = mgxs["chi_d_offset"] + j * stride
spectrum = data[start : start + stride]
# Above is equivalent to:
- # spectrum = mcdc_get.multigroup_material.mgxs_chi_d_vector(
- # j, material, data
+ # spectrum = mcdc_get.neutron_multigroup_data.chi_d_vector(
+ # j, mgxs, data
# )
- decay = mcdc_get.multigroup_material.mgxs_decay_rate(
- j, material, data
- )
+ decay = mcdc_get.neutron_multigroup_data.decay_rate(j, mgxs, data)
break
# Sample outgoing energy
xi = rng.lcg(particle_container_new)
tot = 0.0
- for g_out in range(G):
- tot += spectrum[g_out]
+ for group_out in range(G):
+ tot += spectrum[group_out]
if tot > xi:
break
- particle_new["g"] = g_out
+ particle_new["E"] = _get_group_energy(
+ group_out, particle_container_new, mgxs, simulation, data
+ )
# Sample emission time
if not prompt:
@@ -366,7 +416,6 @@ def fission(particle_container, program, data):
particle["uy"] = particle_new["uy"]
particle["uz"] = particle_new["uz"]
particle["t"] = particle_new["t"]
- particle["g"] = particle_new["g"]
particle["E"] = particle_new["E"]
particle["w"] = particle_new["w"]
else:
@@ -383,3 +432,50 @@ def fission(particle_container, program, data):
else:
# Particle will participate after the current census is completed
particle_bank_module.bank_census_particle(particle_container_new, program)
+
+
+# ======================================================================================
+# Helpers
+# ======================================================================================
+
+
+@njit
+def _get_energy_group(E, mgxs, simulation, data):
+ if simulation["technique"]["neutron_multigroup"]["hybrid"]:
+ offset = mgxs["energy_grid_offset"]
+ length = mgxs["energy_grid_length"]
+ E_grid = data[offset : offset + length]
+ group = util.find_bin_with_rules(E, E_grid, 0.0, False)
+
+ else:
+ group = int(E)
+
+ return group
+
+
+@njit
+def _get_group_energy(group, rng_state, mgxs, simulation, data):
+ if simulation["technique"]["neutron_multigroup"]["hybrid"]:
+ E_low = mcdc_get.neutron_multigroup_data.energy_grid(group, mgxs, data)
+ E_high = mcdc_get.neutron_multigroup_data.energy_grid(group + 1, mgxs, data)
+ representation = mgxs["energy_representation"]
+
+ if representation == NEUTRON_MULTIGROUP_ENERGY_MIDPOINT:
+ energy = 0.5 * (E_low + E_high)
+ elif representation == NEUTRON_MULTIGROUP_ENERGY_MIDPOINT_LOG:
+ energy = math.sqrt(E_low * E_high)
+ elif representation == NEUTRON_MULTIGROUP_ENERGY_UNIFORM:
+ xi = rng.lcg(rng_state)
+ energy = E_low + xi * (E_high - E_low)
+ elif representation == NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG:
+ xi = rng.lcg(rng_state)
+ log_E_low = math.log(E_low)
+ energy = math.exp(log_E_low + xi * (math.log(E_high) - log_E_low))
+ else:
+ # Unreachable
+ energy = -1.0
+
+ else:
+ energy = float(group)
+
+ return energy
diff --git a/mcdc/transport/physics/neutron/native.py b/mcdc/transport/physics/neutron/native.py
index 1e75a7a96..bb69d7e5e 100644
--- a/mcdc/transport/physics/neutron/native.py
+++ b/mcdc/transport/physics/neutron/native.py
@@ -32,10 +32,10 @@
from mcdc.transport.data import evaluate_data
from mcdc.transport.distribution import (
sample_correlated_distribution_with_scale,
+ sample_distribution,
sample_distribution_with_scale,
sample_isotropic_cosine,
sample_isotropic_direction,
- sample_multi_table,
)
from mcdc.transport.physics.util import (
evaluate_neutron_xs_energy_grid,
@@ -72,16 +72,16 @@ def particle_energy_from_speed(speed):
@njit
def macro_xs(reaction_type, particle_container, simulation, data):
particle = particle_container[0]
- material = simulation["native_materials"][particle["material_ID"]]
+ material = simulation["materials"][particle["material_ID"]]
E = particle["E"]
total = 0.0
for i in range(material["N_nuclide"]):
- nuclide_ID = int(mcdc_get.native_material.nuclide_IDs(i, material, data))
+ nuclide_ID = mcdc_get.material.nuclide_IDs(i, material, data)
nuclide = simulation["nuclides"][nuclide_ID]
- nuclide_density = mcdc_get.native_material.nuclide_densities(i, material, data)
+ nuclide_density = mcdc_get.material.nuclide_densities(i, material, data)
xs = total_micro_xs(reaction_type, E, nuclide, data)
total += nuclide_density * xs
@@ -115,18 +115,18 @@ def total_micro_xs(reaction_type, E, nuclide, data):
@njit
-def reaction_micro_xs(E, reaction_base, nuclide, data):
+def reaction_micro_xs(E, reaction, nuclide, data):
idx, E0, E1 = evaluate_neutron_xs_energy_grid(E, nuclide, data)
# Apply offset
- offset = reaction_base["xs_offset_"]
+ offset = reaction["xs_offset_"]
if idx < offset:
return 0.0
else:
idx -= offset
- xs0 = mcdc_get.neutron_reaction.xs(idx, reaction_base, data)
- xs1 = mcdc_get.neutron_reaction.xs(idx + 1, reaction_base, data)
+ xs0 = mcdc_get.neutron_reaction.xs(idx, reaction, data)
+ xs1 = mcdc_get.neutron_reaction.xs(idx + 1, reaction, data)
return linear_interpolation(E, E0, E1, xs0, xs1)
@@ -171,28 +171,27 @@ def neutron_production_xs(reaction_type, particle_container, simulation, data):
@njit
def _neutron_inelastic_scattering_production_xs(particle_container, simulation, data):
particle = particle_container[0]
- material_base = simulation["materials"][particle["material_ID"]]
- material = simulation["native_materials"][material_base["child_ID"]]
+ material = simulation["materials"][particle["material_ID"]]
total = 0.0
for i in range(material["N_nuclide"]):
- nuclide_ID = int(mcdc_get.native_material.nuclide_IDs(i, material, data))
+ nuclide_ID = mcdc_get.material.nuclide_IDs(i, material, data)
nuclide = simulation["nuclides"][nuclide_ID]
E = particle["E"]
- nuclide_density = mcdc_get.native_material.nuclide_densities(i, material, data)
+ nuclide_density = mcdc_get.material.nuclide_densities(i, material, data)
for j in range(nuclide["N_neutron_inelastic_scattering_reaction"]):
- reaction_ID = int(
- mcdc_get.nuclide.neutron_inelastic_scattering_reaction_IDs(
- j, nuclide, data
- )
+ reaction_ID = mcdc_get.nuclide.neutron_inelastic_scattering_reaction_IDs(
+ j, nuclide, data
)
- reaction = simulation["neutron_inelastic_scattering_reactions"][reaction_ID]
- reaction_base = simulation["neutron_reactions"][reaction["parent_ID"]]
+ reaction = simulation["neutron_reactions"][reaction_ID]
+ inelastic_scattering = simulation["neutron_inelastic_scattering_reactions"][
+ reaction["sub_ID"]
+ ]
- xs = reaction_micro_xs(E, reaction_base, nuclide, data)
- nu = reaction["multiplicity"]
+ xs = reaction_micro_xs(E, reaction, nuclide, data)
+ nu = inelastic_scattering["multiplicity"]
total += nuclide_density * nu * xs
return total
@@ -201,30 +200,28 @@ def _neutron_inelastic_scattering_production_xs(particle_container, simulation,
@njit
def _neutron_fission_production_xs(particle_container, simulation, data):
particle = particle_container[0]
- material_base = simulation["materials"][particle["material_ID"]]
- material = simulation["native_materials"][material_base["child_ID"]]
+ material = simulation["materials"][particle["material_ID"]]
- if not material_base["fissionable"]:
+ if not material["fissionable"]:
return 0.0
total = 0.0
for i in range(material["N_nuclide"]):
- nuclide_ID = int(mcdc_get.native_material.nuclide_IDs(i, material, data))
+ nuclide_ID = mcdc_get.material.nuclide_IDs(i, material, data)
nuclide = simulation["nuclides"][nuclide_ID]
if not nuclide["fissionable"]:
continue
E = particle["E"]
- nuclide_density = mcdc_get.native_material.nuclide_densities(i, material, data)
+ nuclide_density = mcdc_get.material.nuclide_densities(i, material, data)
for j in range(nuclide["N_neutron_fission_reaction"]):
- reaction_ID = int(
- mcdc_get.nuclide.neutron_fission_reaction_IDs(j, nuclide, data)
+ reaction_ID = mcdc_get.nuclide.neutron_fission_reaction_IDs(
+ j, nuclide, data
)
- reaction = simulation["neutron_fission_reactions"][reaction_ID]
- reaction_base = simulation["neutron_reactions"][reaction["parent_ID"]]
+ reaction = simulation["neutron_reactions"][reaction_ID]
- xs = reaction_micro_xs(E, reaction_base, nuclide, data)
+ xs = reaction_micro_xs(E, reaction, nuclide, data)
nu_p = neutron_fission_prompt_multiplicity(E, nuclide, simulation, data)
nu_d = neutron_fission_delayed_multiplicity(E, nuclide, simulation, data)
nu = nu_d + nu_p
@@ -243,7 +240,7 @@ def collision(particle_container, collision_data_container, program, data):
simulation = util.access_simulation(program)
particle = particle_container[0]
collision_data = collision_data_container[0]
- material = simulation["native_materials"][particle["material_ID"]]
+ material = simulation["materials"][particle["material_ID"]]
# Particle properties
E = particle["E"]
@@ -255,7 +252,7 @@ def collision(particle_container, collision_data_container, program, data):
SigmaT = macro_xs(NEUTRON_REACTION_TOTAL, particle_container, simulation, data)
# Implicit capture
- if simulation["implicit_capture"]["active"]:
+ if simulation["technique"]["implicit_capture"]["active"]:
# Calculate capture fraction
SigmaC = macro_xs(
NEUTRON_REACTION_CAPTURE, particle_container, simulation, data
@@ -267,22 +264,18 @@ def collision(particle_container, collision_data_container, program, data):
# Q-value: xs-weighted average over all nuclides and capture reactions
for i in range(material["N_nuclide"]):
- nuclide_ID = int(mcdc_get.native_material.nuclide_IDs(i, material, data))
+ nuclide_ID = mcdc_get.material.nuclide_IDs(i, material, data)
nuclide = simulation["nuclides"][nuclide_ID]
- nuclide_density = mcdc_get.native_material.nuclide_densities(
- i, material, data
- )
+ nuclide_density = mcdc_get.material.nuclide_densities(i, material, data)
for j in range(nuclide["N_neutron_capture_reaction"]):
- reaction_ID = int(
- mcdc_get.nuclide.neutron_capture_reaction_IDs(j, nuclide, data)
+ reaction_ID = mcdc_get.nuclide.neutron_capture_reaction_IDs(
+ j, nuclide, data
)
- reaction = simulation["neutron_capture_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["neutron_reactions"][reaction_base_ID]
- xs = reaction_micro_xs(E, reaction_base, nuclide, data)
+ reaction = simulation["neutron_reactions"][reaction_ID]
+ xs = reaction_micro_xs(E, reaction, nuclide, data)
Sigma_rx = nuclide_density * xs
collision_data["energy_deposition"] += (
- reaction_base["q_value"] * 1e6 * particle["w"] * Sigma_rx / SigmaT
+ reaction["q_value"] * 1e6 * particle["w"] * Sigma_rx / SigmaT
)
# Capture particle weight
@@ -294,13 +287,13 @@ def collision(particle_container, collision_data_container, program, data):
xi = rng.lcg(particle_container) * SigmaT
total = 0.0
for i in range(material["N_nuclide"]):
- nuclide_ID = int(mcdc_get.native_material.nuclide_IDs(i, material, data))
+ nuclide_ID = mcdc_get.material.nuclide_IDs(i, material, data)
nuclide = simulation["nuclides"][nuclide_ID]
- nuclide_density = mcdc_get.native_material.nuclide_densities(i, material, data)
+ nuclide_density = mcdc_get.material.nuclide_densities(i, material, data)
sigmaT = total_micro_xs(NEUTRON_REACTION_TOTAL, E, nuclide, data)
- if simulation["implicit_capture"]["active"]:
+ if simulation["technique"]["implicit_capture"]["active"]:
sigmaC = total_micro_xs(NEUTRON_REACTION_CAPTURE, E, nuclide, data)
sigmaT -= sigmaC
@@ -330,19 +323,15 @@ def collision(particle_container, collision_data_container, program, data):
# Sample the actual reaction from the group
total -= sigma_elastic
for i in range(nuclide["N_neutron_elastic_scattering_reaction"]):
- reaction_ID = int(
- mcdc_get.nuclide.neutron_elastic_scattering_reaction_IDs(
- i, nuclide, data
- )
+ reaction_ID = mcdc_get.nuclide.neutron_elastic_scattering_reaction_IDs(
+ i, nuclide, data
)
- reaction = simulation["neutron_elastic_scattering_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["neutron_reactions"][reaction_base_ID]
- total += reaction_micro_xs(E, reaction_base, nuclide, data)
+ reaction = simulation["neutron_reactions"][reaction_ID]
+ total += reaction_micro_xs(E, reaction, nuclide, data)
# Execute the reaction
if xi < total:
- elastic_scattering(
+ sample_elastic_scattering(
reaction,
particle_container,
collision_data_container,
@@ -353,20 +342,18 @@ def collision(particle_container, collision_data_container, program, data):
return
# Capture
- if not simulation["implicit_capture"]["active"]:
+ if not simulation["technique"]["implicit_capture"]["active"]:
sigma_capture = total_micro_xs(NEUTRON_REACTION_CAPTURE, E, nuclide, data)
total += sigma_capture
if xi < total:
# Sample the actual reaction from the group
total -= sigma_capture
for i in range(nuclide["N_neutron_capture_reaction"]):
- reaction_ID = int(
- mcdc_get.nuclide.neutron_capture_reaction_IDs(i, nuclide, data)
+ reaction_ID = mcdc_get.nuclide.neutron_capture_reaction_IDs(
+ i, nuclide, data
)
- reaction = simulation["neutron_capture_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["neutron_reactions"][reaction_base_ID]
- xs = reaction_micro_xs(E, reaction_base, nuclide, data)
+ reaction = simulation["neutron_reactions"][reaction_ID]
+ xs = reaction_micro_xs(E, reaction, nuclide, data)
total += xs
# Execute the reaction
@@ -387,20 +374,16 @@ def collision(particle_container, collision_data_container, program, data):
# Sample the actual reaction from the group
total -= sigma_inelastic
for i in range(nuclide["N_neutron_inelastic_scattering_reaction"]):
- reaction_ID = int(
- mcdc_get.nuclide.neutron_inelastic_scattering_reaction_IDs(
- i, nuclide, data
- )
+ reaction_ID = mcdc_get.nuclide.neutron_inelastic_scattering_reaction_IDs(
+ i, nuclide, data
)
- reaction = simulation["neutron_inelastic_scattering_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["neutron_reactions"][reaction_base_ID]
- xs = reaction_micro_xs(E, reaction_base, nuclide, data)
+ reaction = simulation["neutron_reactions"][reaction_ID]
+ xs = reaction_micro_xs(E, reaction, nuclide, data)
total += xs
# Execute the reaction
if xi < total:
- inelastic_scattering(
+ sample_inelastic_scattering(
reaction,
particle_container,
collision_data_container,
@@ -416,17 +399,15 @@ def collision(particle_container, collision_data_container, program, data):
# Sample the actual reaction from the group
total -= sigma_fission
for i in range(nuclide["N_neutron_fission_reaction"]):
- reaction_ID = int(
- mcdc_get.nuclide.neutron_fission_reaction_IDs(i, nuclide, data)
+ reaction_ID = mcdc_get.nuclide.neutron_fission_reaction_IDs(
+ i, nuclide, data
)
- reaction = simulation["neutron_fission_reactions"][reaction_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["neutron_reactions"][reaction_base_ID]
- total += reaction_micro_xs(E, reaction_base, nuclide, data)
+ reaction = simulation["neutron_reactions"][reaction_ID]
+ total += reaction_micro_xs(E, reaction, nuclide, data)
# Execute the reaction
if xi < total:
- fission(
+ sample_fission(
reaction,
particle_container,
collision_data_container,
@@ -449,15 +430,12 @@ def capture(
particle = particle_container[0]
collision_data = collision_data_container[0]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["neutron_reactions"][reaction_base_ID]
-
# Terminate the particle
particle["alive"] = False
# Energy deposition
E = particle["E"]
- q_value = reaction_base["q_value"] * 1e6
+ q_value = reaction["q_value"] * 1e6
collision_data["energy_deposition"] += (E + q_value) * particle["w"]
@@ -467,11 +445,13 @@ def capture(
@njit
-def elastic_scattering(
+def sample_elastic_scattering(
reaction, particle_container, collision_data_container, nuclide, simulation, data
):
particle = particle_container[0]
collision_data = collision_data_container[0]
+ sub_ID = reaction["sub_ID"]
+ elastic_scattering = simulation["neutron_elastic_scattering_reactions"][sub_ID]
# Particle attributes
E = particle["E"]
@@ -524,8 +504,8 @@ def elastic_scattering(
uz = vz / speed
# Sample the scattering cosine from the multi-PDF distribution
- multi_table = simulation["multi_table_distributions"][reaction["mu_table_ID"]]
- mu0 = sample_multi_table(E, particle_container, multi_table, simulation, data)
+ mu_distribution = simulation["distributions"][elastic_scattering["mu_table_ID"]]
+ mu0 = sample_distribution(E, mu_distribution, particle_container, simulation, data)
# Scatter the direction in COM
azi = 2.0 * PI * rng.lcg(particle_container)
@@ -612,15 +592,14 @@ def sample_nucleus_velocity(A, particle_container):
@njit
-def inelastic_scattering(
+def sample_inelastic_scattering(
reaction, particle_container, collision_data_container, nuclide, program, data
):
simulation = util.access_simulation(program)
particle = particle_container[0]
collision_data = collision_data_container[0]
-
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["neutron_reactions"][reaction_base_ID]
+ sub_ID = reaction["sub_ID"]
+ inelastic_scattering = simulation["neutron_inelastic_scattering_reactions"][sub_ID]
# Particle attributes
E = particle["E"]
@@ -632,12 +611,12 @@ def inelastic_scattering(
particle["alive"] = False
# Energy deposition
- q_value = reaction_base["q_value"] * 1e6
+ q_value = reaction["q_value"] * 1e6
collision_data["energy_deposition"] += (E + q_value) * particle["w"]
# Number of secondaries and spectra
- N = reaction["multiplicity"]
- N_spectrum = reaction["N_spectrum"]
+ N = inelastic_scattering["multiplicity"]
+ N_spectrum = inelastic_scattering["N_spectrum"]
use_all_spectrum = N == N_spectrum
# Set up secondary partice container
@@ -653,18 +632,15 @@ def inelastic_scattering(
# Sample angle (if not energy-correlated)
# ==============================================================================
- angle_type = reaction["angle_type"]
+ angle_type = inelastic_scattering["angle_type"]
if angle_type == ANGLE_ENERGY_CORRELATED:
pass
elif angle_type == ANGLE_ISOTROPIC:
mu = sample_isotropic_cosine(particle_container_new)
elif angle_type == ANGLE_DISTRIBUTED:
- distribution_base = simulation["distributions"][reaction["mu_ID"]]
- multi_table = simulation["multi_table_distributions"][
- distribution_base["child_ID"]
- ]
- mu = sample_multi_table(
- E, particle_container_new, multi_table, simulation, data
+ mu_distribution = simulation["distributions"][inelastic_scattering["mu_ID"]]
+ mu = sample_distribution(
+ E, mu_distribution, particle_container_new, simulation, data
)
# ==============================================================================
@@ -673,19 +649,17 @@ def inelastic_scattering(
# Get energy spectrum
if use_all_spectrum:
- ID = int(
- mcdc_get.neutron_inelastic_scattering_reaction.energy_spectrum_IDs(
- n, reaction, data
- )
+ ID = mcdc_get.neutron_inelastic_scattering_reaction.energy_spectrum_IDs(
+ n, inelastic_scattering, data
)
- spectrum_base = simulation["distributions"][ID]
+ spectrum = simulation["distributions"][ID]
else:
- offset = reaction["spectrum_probability_grid_offset"]
- length = reaction["spectrum_probability_grid_length"]
+ offset = inelastic_scattering["spectrum_probability_grid_offset"]
+ length = inelastic_scattering["spectrum_probability_grid_length"]
probability_grid = data[offset : offset + length]
# Above is equivalent to:
# probability_grid = mcdc_get.neutron_inelastic_scattering_reaction.spectrum_probability_grid_all(
- # reaction, data
+ # inelastic_scattering, data
# )
probability_idx = find_bin(E, probability_grid)
xi = rng.lcg(particle_container_new)
@@ -693,35 +667,32 @@ def inelastic_scattering(
for j in range(N_spectrum):
probability = (
mcdc_get.neutron_inelastic_scattering_reaction.spectrum_probability(
- probability_idx, j, reaction, data
+ probability_idx, j, inelastic_scattering, data
)
)
total += probability
if xi < total:
- ID = int(
- mcdc_get.neutron_inelastic_scattering_reaction.energy_spectrum_IDs(
- j, reaction, data
- )
+ ID = mcdc_get.neutron_inelastic_scattering_reaction.energy_spectrum_IDs(
+ j, inelastic_scattering, data
)
- spectrum_base = simulation["distributions"][ID]
+ spectrum = simulation["distributions"][ID]
break
# Sample energy
if not angle_type == ANGLE_ENERGY_CORRELATED:
E_new = sample_distribution_with_scale(
- E, spectrum_base, particle_container_new, simulation, data
+ E, spectrum, particle_container_new, simulation, data
)
else:
E_new, mu = sample_correlated_distribution_with_scale(
- E, spectrum_base, particle_container_new, simulation, data
+ E, spectrum, particle_container_new, simulation, data
)
# ==============================================================================
# Frame transformation
# ==============================================================================
- reaction_base = simulation["neutron_reactions"][int(reaction["parent_ID"])]
- reference_frame = reaction_base["reference_frame"]
+ reference_frame = reaction["reference_frame"]
if reference_frame == REFERENCE_FRAME_COM:
A = nuclide["atomic_weight_ratio"]
mu_COM = mu
@@ -765,17 +736,17 @@ def inelastic_scattering(
@njit
-def fission(
+def sample_fission(
reaction, particle_container, collision_data_container, nuclide, program, data
):
simulation = util.access_simulation(program)
particle = particle_container[0]
collision_data = collision_data_container[0]
- settings = simulation["settings"]
+ sub_ID = reaction["sub_ID"]
+ fission = simulation["neutron_fission_reactions"][sub_ID]
- reaction_base_ID = reaction["parent_ID"]
- reaction_base = simulation["neutron_reactions"][reaction_base_ID]
+ settings = simulation["settings"]
# Particle properties
E = particle["E"]
@@ -788,14 +759,14 @@ def fission(
# Energy deposition
# TODO: Use energy-dependent Q-value
- q_value = reaction_base["q_value"] * 1e6
+ q_value = reaction["q_value"] * 1e6
collision_data["energy_deposition"] += (E + q_value) * particle["w"]
# Adjust production and product weights if weighted emission
weight_production = 1.0
weight_product = particle["w"]
- if simulation["weighted_emission"]["active"]:
- weight_target = simulation["weighted_emission"]["weight_target"]
+ if simulation["technique"]["weighted_emission"]["active"]:
+ weight_target = simulation["technique"]["weighted_emission"]["weight_target"]
weight_production = particle["w"] / weight_target
weight_product = weight_target
@@ -847,26 +818,23 @@ def fission(
if prompt:
# Sample angle (if not energy-correlated)
- angle_type = reaction["angle_type"]
+ angle_type = fission["angle_type"]
if angle_type == ANGLE_ENERGY_CORRELATED:
pass
elif angle_type == ANGLE_ISOTROPIC:
mu = sample_isotropic_cosine(particle_container_new)
elif angle_type == ANGLE_DISTRIBUTED:
- distribution_base = simulation["distributions"][reaction["mu_ID"]]
- multi_table = simulation["multi_table_distributions"][
- distribution_base["child_ID"]
- ]
- mu = sample_multi_table(
- E, particle_container_new, multi_table, simulation, data
+ mu_distribution = simulation["distributions"][fission["mu_ID"]]
+ mu = sample_distribution(
+ E, mu_distribution, particle_container_new, simulation, data
)
# Sample energy (also angle if correlated)
- spectrum_base = simulation["distributions"][reaction["spectrum_ID"]]
+ spectrum = simulation["distributions"][fission["spectrum_ID"]]
if not angle_type == ANGLE_ENERGY_CORRELATED:
E_new = sample_distribution_with_scale(
E,
- spectrum_base,
+ spectrum,
particle_container_new,
simulation,
data,
@@ -874,15 +842,14 @@ def fission(
else:
E_new, mu = sample_correlated_distribution_with_scale(
E,
- spectrum_base,
+ spectrum,
particle_container_new,
simulation,
data,
)
# Frame transformation
- reaction_base = simulation["neutron_reactions"][int(reaction["parent_ID"])]
- reference_frame = reaction_base["reference_frame"]
+ reference_frame = reaction["reference_frame"]
if reference_frame == REFERENCE_FRAME_COM:
A = nuclide["atomic_weight_ratio"]
mu_COM = mu
@@ -959,7 +926,6 @@ def fission(
particle["uy"] = particle_new["uy"]
particle["uz"] = particle_new["uz"]
particle["t"] = particle_new["t"]
- particle["g"] = particle_new["g"]
particle["E"] = particle_new["E"]
particle["w"] = particle_new["w"]
else:
@@ -980,11 +946,15 @@ def fission(
@njit
def neutron_fission_prompt_multiplicity(E, nuclide, simulation, data):
- data_base = simulation["data"][nuclide["neutron_fission_prompt_multiplicity_ID"]]
- return evaluate_data(E, data_base, simulation, data)
+ reaction_data = simulation["data"][
+ nuclide["neutron_fission_prompt_multiplicity_ID"]
+ ]
+ return evaluate_data(E, reaction_data, simulation, data)
@njit
def neutron_fission_delayed_multiplicity(E, nuclide, simulation, data):
- data_base = simulation["data"][nuclide["neutron_fission_delayed_multiplicity_ID"]]
- return evaluate_data(E, data_base, simulation, data)
+ reaction_data = simulation["data"][
+ nuclide["neutron_fission_delayed_multiplicity_ID"]
+ ]
+ return evaluate_data(E, reaction_data, simulation, data)
diff --git a/mcdc/transport/simulation.py b/mcdc/transport/simulation.py
index aca16d88f..80f3055d2 100644
--- a/mcdc/transport/simulation.py
+++ b/mcdc/transport/simulation.py
@@ -1,5 +1,3 @@
-import numpy as np
-
from numba import njit, objmode, uint64
####
@@ -292,7 +290,7 @@ def step_particle(particle_container, program, data):
# Collision
if particle["event"] & EVENT_COLLISION:
- collision_data_container = np.zeros(1, type_.collision_data)
+ collision_data_container = util.local_array(1, type_.collision_data)
# Execute the physics
physics.collision(particle_container, collision_data_container, program, data)
@@ -301,9 +299,9 @@ def step_particle(particle_container, program, data):
if simulation["cycle_active"]:
cell = simulation["cells"][particle["cell_ID"]]
for i in range(cell["N_collision_tally"]):
- tally_ID = int(mcdc_get.cell.collision_tally_IDs(i, cell, data))
- tally = simulation["collision_tallies"][tally_ID]
- tally_module.score.collision_tally(
+ tally_ID = mcdc_get.cell.collision_tally_IDs(i, cell, data)
+ tally = simulation["tallies"][tally_ID]
+ tally_module.score.collision(
particle_container,
collision_data_container,
tally,
@@ -333,11 +331,11 @@ def step_particle(particle_container, program, data):
return
# Weight windows
- if simulation["weight_windows"]["active"]:
+ if simulation["technique"]["weight_windows"]["active"]:
technique.weight_windows(particle_container, program, data)
# Global weight roulette
- if simulation["global_weight_roulette"]["active"]:
+ if simulation["technique"]["global_weight_roulette"]["active"]:
technique.global_weight_roulette(particle_container, simulation)
@@ -348,17 +346,14 @@ def move_to_event(particle_container, simulation, data):
# ==================================================================================
# Preparation (as needed)
# ==================================================================================
+
particle = particle_container[0]
- # Multigroup preparation
- # In MG mode, particle speed is material-dependent.
- if settings["neutron_multigroup_mode"]:
- # If material is not identified yet, locate the particle
- if particle["material_ID"] == -1:
- if not geometry.locate_particle(particle_container, simulation, data):
- # Particle is lost
- particle["event"] = EVENT_LOST
- return
+ # Locate the material before evaluating material-dependent transport data.
+ if particle["material_ID"] == -1:
+ if not geometry.locate_particle(particle_container, simulation, data):
+ particle["event"] = EVENT_LOST
+ return
# ==================================================================================
# Geometry inspection
@@ -430,9 +425,9 @@ def move_to_event(particle_container, simulation, data):
if simulation["cycle_active"]:
cell = simulation["cells"][particle["cell_ID"]]
for i in range(cell["N_tracklength_tally"]):
- tally_ID = int(mcdc_get.cell.tracklength_tally_IDs(i, cell, data))
- tally = simulation["tracklength_tallies"][tally_ID]
- tally_module.score.tracklength_tally(
+ tally_ID = mcdc_get.cell.tracklength_tally_IDs(i, cell, data)
+ tally = simulation["tallies"][tally_ID]
+ tally_module.score.tracklength(
particle_container, distance, tally, simulation, data
)
@@ -446,29 +441,29 @@ def move_to_event(particle_container, simulation, data):
@njit
-def surface_crossing(P_arr, simulation, data):
- P = P_arr[0]
- crossed_surface_ID = P["surface_ID"]
+def surface_crossing(particle_container, simulation, data):
+ particle = particle_container[0]
+ crossed_surface_ID = particle["surface_ID"]
surface = simulation["surfaces"][crossed_surface_ID]
BC = surface["boundary_condition"]
# Apply BC
if BC == BC_VACUUM:
- P["alive"] = False
+ particle["alive"] = False
elif BC == BC_REFLECTIVE:
- surface_module.reflect(P_arr, surface)
+ surface_module.reflect(particle_container, surface)
return # No score
# Score tally
- for i in range(surface["N_tally"]):
- tally_ID = int(mcdc_get.surface.tally_IDs(i, surface, data))
- tally = simulation["surface_crossing_tallies"][tally_ID]
- tally_module.score.surface_crossing_tally(
- P_arr, surface, tally, simulation, data
+ for i in range(surface["N_surface_crossing_tally"]):
+ tally_ID = mcdc_get.surface.surface_crossing_tally_IDs(i, surface, data)
+ tally = simulation["tallies"][tally_ID]
+ tally_module.score.surface_crossing(
+ particle_container, surface, tally, simulation, data
)
# Flag to check new cell later
- if P["alive"]:
- P["cell_ID"] = -1
- P["material_ID"] = -1
+ if particle["alive"]:
+ particle["cell_ID"] = -1
+ particle["material_ID"] = -1
diff --git a/mcdc/transport/source.py b/mcdc/transport/source.py
index 0adbec6a7..51a4672cd 100644
--- a/mcdc/transport/source.py
+++ b/mcdc/transport/source.py
@@ -26,6 +26,7 @@ def source_particle(particle_container, seed, simulation, data):
# TODO: use cdf and binary search instead
xi = rng.lcg(particle_container)
tot = 0.0
+ source = simulation["sources"][0]
for source in simulation["sources"]:
tot += source["probability"]
if tot >= xi:
@@ -62,22 +63,18 @@ def source_particle(particle_container, seed, simulation, data):
)
# Energy
- if simulation["settings"]["neutron_multigroup_mode"]:
- E = 0.0
- if source["mono_energetic"]:
- g = source["energy_group"]
- else:
- ID = source["energy_group_pmf_ID"]
- pmf = simulation["pmf_distributions"][ID]
- g = sample_pmf(pmf, particle_container, data)
+ if source["mono_energetic"]:
+ E = source["energy"]
+ elif source["discrete_energy"]:
+ ID = source["energy_pmf_ID"]
+ sub_ID = simulation["distributions"][ID]["sub_ID"]
+ pmf = simulation["pmf_distributions"][sub_ID]
+ E = sample_pmf(pmf, particle_container, data)
else:
- g = 0
- if source["mono_energetic"]:
- E = source["energy"]
- else:
- ID = source["energy_pdf_ID"]
- table = simulation["tabulated_distributions"][ID]
- E = sample_tabulated(table, particle_container, simulation, data)
+ ID = source["energy_pdf_ID"]
+ sub_ID = simulation["distributions"][ID]["sub_ID"]
+ table = simulation["tabulated_distributions"][sub_ID]
+ E = sample_tabulated(table, particle_container, simulation, data)
# Time
if source["discrete_time"]:
@@ -132,7 +129,6 @@ def source_particle(particle_container, seed, simulation, data):
particle["ux"] = ux
particle["uy"] = uy
particle["uz"] = uz
- particle["g"] = g
particle["E"] = E
particle["w"] = 1.0
particle["particle_type"] = source["particle_type"]
diff --git a/mcdc/transport/tally/filter.py b/mcdc/transport/tally/filter.py
index a743f785f..d976b1bee 100644
--- a/mcdc/transport/tally/filter.py
+++ b/mcdc/transport/tally/filter.py
@@ -16,14 +16,14 @@
@njit
-def get_filter_indices(particle_container, tally, data, MG_mode):
+def get_filter_indices(particle_container, tally, data):
i_mu, i_azi, i_energy, i_time = 0, 0, 0, 0
if tally["filter_direction"]:
i_mu, i_azi = get_direction_index(particle_container, tally, data)
if tally["filter_energy"]:
- i_energy = get_energy_index(particle_container, tally, data, MG_mode)
+ i_energy = get_energy_index(particle_container, tally, data)
if tally["filter_time"]:
i_time = get_time_index(particle_container, tally, data)
@@ -67,13 +67,10 @@ def get_direction_index(particle_container, tally, data):
@njit
-def get_energy_index(particle_container, tally, data, neutron_multigroup_mode):
+def get_energy_index(particle_container, tally, data):
particle = particle_container[0]
- if neutron_multigroup_mode:
- E = particle["g"]
- else:
- E = particle["E"]
+ E = particle["E"]
tolerance = COINCIDENCE_TOLERANCE_ENERGY
grid_energy = data[
diff --git a/mcdc/transport/tally/score.py b/mcdc/transport/tally/score.py
index fe2fbdee4..9b7a228fa 100644
--- a/mcdc/transport/tally/score.py
+++ b/mcdc/transport/tally/score.py
@@ -33,12 +33,12 @@
from mcdc.transport.tally.filter import get_filter_indices
# ======================================================================================
-# Surface crossing tally
+# Surface-crossing
# ======================================================================================
@njit
-def surface_crossing_tally(
+def surface_crossing(
particle_container,
surface,
tally,
@@ -46,13 +46,11 @@ def surface_crossing_tally(
data,
):
particle = particle_container[0]
- tally_base = simulation["tallies"][tally["parent_ID"]]
+ sub_ID = tally["sub_ID"]
+ surface_crossing_tally = simulation["surface_crossing_tallies"][sub_ID]
# Get filter indices
- MG_mode = simulation["settings"]["neutron_multigroup_mode"]
- i_mu, i_azi, i_energy, i_time = get_filter_indices(
- particle_container, tally_base, data, MG_mode
- )
+ i_mu, i_azi, i_energy, i_time = get_filter_indices(particle_container, tally, data)
# No score if outside non-changing phase-space bins
if i_mu == -1 or i_azi == -1 or i_energy == -1 or i_time == -1:
@@ -60,11 +58,11 @@ def surface_crossing_tally(
# Tally index
idx_base = (
- tally_base["bin_offset"]
- + i_mu * tally_base["stride_mu"]
- + i_azi * tally_base["stride_azi"]
- + i_energy * tally_base["stride_energy"]
- + i_time * tally_base["stride_time"]
+ tally["bin_offset"]
+ + i_mu * tally["stride_mu"]
+ + i_azi * tally["stride_azi"]
+ + i_energy * tally["stride_energy"]
+ + i_time * tally["stride_time"]
)
# Flux
@@ -73,9 +71,9 @@ def surface_crossing_tally(
flux = particle["w"] / abs(mu)
# Non-cell-filtered score
- if not tally["cell_filtered"]:
- for i_score in range(tally_base["scores_length"]):
- score_type = mcdc_get.tally.scores(i_score, tally_base, data)
+ if not surface_crossing_tally["cell_filtered"]:
+ for i_score in range(tally["scores_length"]):
+ score_type = mcdc_get.tally.scores(i_score, tally, data)
score = 0.0
if score_type == SCORE_CURRENT_NET:
@@ -92,14 +90,14 @@ def surface_crossing_tally(
# Cell-filtered score
previous_cell_ID = particle["cell_ID"]
- filter_cell_ID = tally["cell_filter_ID"]
+ filter_cell_ID = surface_crossing_tally["cell_filter_ID"]
filter_cell = simulation["cells"][filter_cell_ID]
was_in_filter_cell = previous_cell_ID == filter_cell_ID
now_in_filter_cell = check_cell(particle_container, filter_cell, simulation, data)
entered_filter_cell = not was_in_filter_cell and now_in_filter_cell
exited_filter_cell = was_in_filter_cell and not now_in_filter_cell
- for i_score in range(tally_base["scores_length"]):
- score_type = mcdc_get.tally.scores(i_score, tally_base, data)
+ for i_score in range(tally["scores_length"]):
+ score_type = mcdc_get.tally.scores(i_score, tally, data)
score = 0.0
if score_type == SCORE_CURRENT_NET:
@@ -118,23 +116,19 @@ def surface_crossing_tally(
# ======================================================================================
-# Collision tally
+# Collision
# ======================================================================================
@njit
-def collision_tally(
- particle_container, collision_data_container, tally, simulation, data
-):
+def collision(particle_container, collision_data_container, tally, simulation, data):
particle = particle_container[0]
collision_data = collision_data_container[0]
- tally_base = simulation["tallies"][tally["parent_ID"]]
+ sub_ID = tally["sub_ID"]
+ collision_tally = simulation["collision_tallies"][sub_ID]
# Get filter indices
- MG_mode = simulation["settings"]["neutron_multigroup_mode"]
- i_mu, i_azi, i_energy, i_time = get_filter_indices(
- particle_container, tally_base, data, MG_mode
- )
+ i_mu, i_azi, i_energy, i_time = get_filter_indices(particle_container, tally, data)
# No score if outside non-changing phase-space bins
if i_mu == -1 or i_azi == -1 or i_energy == -1 or i_time == -1:
@@ -142,8 +136,8 @@ def collision_tally(
# Mesh tally indices if needed
i_x, i_y, i_z = 0, 0, 0
- if tally["mesh_filtered"]:
- mesh = simulation["meshes"][tally["mesh_filter_ID"]]
+ if collision_tally["mesh_filtered"]:
+ mesh = simulation["meshes"][collision_tally["mesh_filter_ID"]]
i_x, i_y, i_z = mesh_module.get_indices(
particle_container, mesh, simulation, data
)
@@ -154,22 +148,22 @@ def collision_tally(
# Tally index
idx_base = (
- tally_base["bin_offset"]
- + i_mu * tally_base["stride_mu"]
- + i_azi * tally_base["stride_azi"]
- + i_energy * tally_base["stride_energy"]
- + i_time * tally_base["stride_time"]
+ tally["bin_offset"]
+ + i_mu * tally["stride_mu"]
+ + i_azi * tally["stride_azi"]
+ + i_energy * tally["stride_energy"]
+ + i_time * tally["stride_time"]
)
- if tally["mesh_filtered"]:
+ if collision_tally["mesh_filtered"]:
idx_base += (
- +i_x * tally["mesh_stride_x"]
- + i_y * tally["mesh_stride_y"]
- + i_z * tally["mesh_stride_z"]
+ +i_x * collision_tally["mesh_stride_x"]
+ + i_y * collision_tally["mesh_stride_y"]
+ + i_z * collision_tally["mesh_stride_z"]
)
# Score
- for i_score in range(tally_base["scores_length"]):
- score_type = mcdc_get.tally.scores(i_score, tally_base, data)
+ for i_score in range(tally["scores_length"]):
+ score_type = mcdc_get.tally.scores(i_score, tally, data)
score = 0.0
if score_type == SCORE_ENERGY_DEPOSITION:
score = collision_data["energy_deposition"]
@@ -182,15 +176,13 @@ def collision_tally(
@njit
-def tracklength_tally(particle_container, distance, tally, simulation, data):
+def tracklength(particle_container, distance, tally, simulation, data):
particle = particle_container[0]
- tally_base = simulation["tallies"][tally["parent_ID"]]
+ sub_ID = tally["sub_ID"]
+ tracklength_tally = simulation["tracklength_tallies"][sub_ID]
# Get filter indices
- MG_mode = simulation["settings"]["neutron_multigroup_mode"]
- i_mu, i_azi, i_energy, i_time = get_filter_indices(
- particle_container, tally_base, data, MG_mode
- )
+ i_mu, i_azi, i_energy, i_time = get_filter_indices(particle_container, tally, data)
# No score if outside non-changing phase-space bins
if i_mu == -1 or i_azi == -1 or i_energy == -1:
@@ -211,8 +203,8 @@ def tracklength_tally(particle_container, distance, tally, simulation, data):
t_final = t + ut * distance
# No score if particle does not cross the time bins
- t_min = mcdc_get.tally.time(0, tally_base, data)
- t_max = mcdc_get.tally.time_last(tally_base, data)
+ t_min = mcdc_get.tally.time(0, tally, data)
+ t_max = mcdc_get.tally.time_last(tally, data)
if (
t_final < t_min + COINCIDENCE_TOLERANCE_TIME
or t > t_max - COINCIDENCE_TOLERANCE_TIME
@@ -230,8 +222,8 @@ def tracklength_tally(particle_container, distance, tally, simulation, data):
# Mesh axis indices
i_x, i_y, i_z = 0, 0, 0
- if tally["mesh_filtered"]:
- mesh = simulation["meshes"][tally["mesh_filter_ID"]]
+ if tracklength_tally["mesh_filtered"]:
+ mesh = simulation["meshes"][tracklength_tally["mesh_filter_ID"]]
# Mesh axis indices
i_x, i_y, i_z = mesh_module.get_indices(
@@ -302,24 +294,24 @@ def tracklength_tally(particle_container, distance, tally, simulation, data):
# Tally base index
idx_base = (
- tally_base["bin_offset"]
- + i_mu * tally_base["stride_mu"]
- + i_azi * tally_base["stride_azi"]
- + i_energy * tally_base["stride_energy"]
- + i_time * tally_base["stride_time"]
+ tally["bin_offset"]
+ + i_mu * tally["stride_mu"]
+ + i_azi * tally["stride_azi"]
+ + i_energy * tally["stride_energy"]
+ + i_time * tally["stride_time"]
)
- if tally["mesh_filtered"]:
+ if tracklength_tally["mesh_filtered"]:
idx_base += (
- i_x * tally["mesh_stride_x"]
- + i_y * tally["mesh_stride_y"]
- + i_z * tally["mesh_stride_z"]
+ i_x * tracklength_tally["mesh_stride_x"]
+ + i_y * tracklength_tally["mesh_stride_y"]
+ + i_z * tracklength_tally["mesh_stride_z"]
)
# Sweep through the distance
distance_swept = 0.0
while distance_swept < distance - COINCIDENCE_TOLERANCE:
# The next time grid
- t_next = mcdc_get.tally.time(i_time + 1, tally_base, data)
+ t_next = mcdc_get.tally.time(i_time + 1, tally, data)
# Get the distance to score in this segment
if t_final < t_next - COINCIDENCE_TOLERANCE_TIME:
@@ -333,8 +325,8 @@ def tracklength_tally(particle_container, distance, tally, simulation, data):
# axis is crossed
axis_crossed = AXIS_T
- if tally["mesh_filtered"]:
- mesh = simulation["meshes"][tally["mesh_filter_ID"]]
+ if tracklength_tally["mesh_filtered"]:
+ mesh = simulation["meshes"][tracklength_tally["mesh_filter_ID"]]
# x-direction
if ux == 0.0:
@@ -386,8 +378,8 @@ def tracklength_tally(particle_container, distance, tally, simulation, data):
# Score
flux = distance_scored * particle["w"]
- for i_score in range(tally_base["scores_length"]):
- score_type = mcdc_get.tally.scores(i_score, tally_base, data)
+ for i_score in range(tally["scores_length"]):
+ score_type = mcdc_get.tally.scores(i_score, tally, data)
score = 0.0
if score_type == SCORE_FLUX:
score = flux
@@ -412,7 +404,7 @@ def tracklength_tally(particle_container, distance, tally, simulation, data):
distance_swept += distance_scored
# Move the 4D position
- if tally["mesh_filtered"]:
+ if tracklength_tally["mesh_filtered"]:
x += distance_scored * ux
y += distance_scored * uy
z += distance_scored * uz
@@ -421,44 +413,44 @@ def tracklength_tally(particle_container, distance, tally, simulation, data):
# Increment index and heck if out of bounds
if axis_crossed == AXIS_T:
i_time += 1
- idx_base += tally_base["stride_time"]
- if i_time == tally_base["time_length"] - 1:
+ idx_base += tally["stride_time"]
+ if i_time == tally["time_length"] - 1:
return
- elif tally["mesh_filtered"]:
- mesh = simulation["meshes"][tally["mesh_filter_ID"]]
+ elif tracklength_tally["mesh_filtered"]:
+ mesh = simulation["meshes"][tracklength_tally["mesh_filter_ID"]]
if axis_crossed == AXIS_X:
if ux > 0.0:
i_x += 1
if i_x == mesh["Nx"]:
return
- idx_base += tally["mesh_stride_x"]
+ idx_base += tracklength_tally["mesh_stride_x"]
else:
i_x -= 1
if i_x == -1:
return
- idx_base -= tally["mesh_stride_x"]
+ idx_base -= tracklength_tally["mesh_stride_x"]
elif axis_crossed == AXIS_Y:
if uy > 0.0:
i_y += 1
if i_y == mesh["Ny"]:
return
- idx_base += tally["mesh_stride_y"]
+ idx_base += tracklength_tally["mesh_stride_y"]
else:
i_y -= 1
if i_y == -1:
return
- idx_base -= tally["mesh_stride_y"]
+ idx_base -= tracklength_tally["mesh_stride_y"]
elif axis_crossed == AXIS_Z:
if uz > 0.0:
i_z += 1
if i_z == mesh["Nz"]:
return
- idx_base += tally["mesh_stride_z"]
+ idx_base += tracklength_tally["mesh_stride_z"]
else:
i_z -= 1
if i_z == -1:
return
- idx_base -= tally["mesh_stride_z"]
+ idx_base -= tracklength_tally["mesh_stride_z"]
# =============================================================================
diff --git a/mcdc/transport/technique.py b/mcdc/transport/technique.py
index 5de637998..602912eae 100644
--- a/mcdc/transport/technique.py
+++ b/mcdc/transport/technique.py
@@ -60,8 +60,8 @@ def global_weight_roulette(particle_container, simulation):
simulation : object
Simulation state containing global weight roulette parameters.
"""
- w_threshold = simulation["global_weight_roulette"]["weight_threshold"]
- w_target = simulation["global_weight_roulette"]["weight_target"]
+ w_threshold = simulation["technique"]["global_weight_roulette"]["weight_threshold"]
+ w_target = simulation["technique"]["global_weight_roulette"]["weight_target"]
weight_roulette(particle_container, w_threshold, w_target)
@@ -116,7 +116,7 @@ def query_weight_window(particle_container, simulation, data):
Upper weight bound.
"""
# grab objects
- ww_obj = simulation["weight_windows"]
+ ww_obj = simulation["technique"]["weight_windows"]
indices = get_ww_indices(particle_container, ww_obj, simulation, data)
# grab the actual ww parameters
lower = ww_get.lower_weights(*indices, ww_obj, data)
@@ -150,10 +150,7 @@ def get_ww_indices(particle_container, ww_obj, simulation, data):
# get energy index
energy_bounds = ww_get.energy_bounds_all(ww_obj, data)
- if simulation["settings"]["neutron_multigroup_mode"]:
- energy = particle["g"]
- else:
- energy = particle["E"]
+ energy = particle["E"]
ie = util.find_bin(energy, energy_bounds)
# get spatial index
diff --git a/mcdc/visualize.py b/mcdc/visualize.py
index 25640041a..58db720bc 100644
--- a/mcdc/visualize.py
+++ b/mcdc/visualize.py
@@ -1,11 +1,10 @@
from numba import njit
import numpy as np
-from mcdc.main import preparation
+from mcdc.main import prepare
-_visualize_cache = None
-
-def visualize(
+def visualize_model(
+ simulationPy,
vis_type,
x=0.0,
y=0.0,
@@ -20,16 +19,19 @@ def visualize(
Parameters
----------
- vis_plane : {'xy', 'yz', 'xz', 'zx', 'yz', 'zy'}
+ vis_type : {'xy', 'xz', 'yz', 'yx', 'zx', 'zy'}
Axis plane to visualize
x : float or array_like
- Plane x-position (float) for 'yz' plot. Range of x-axis for 'xy' or 'xz' plot.
+ Plane x-position (float) for 'yz' plot. Range of x-axis for 'xy' or
+ 'xz' plot, in cm.
y : float or array_like
- Plane y-position (float) for 'xz' plot. Range of y-axis for 'xy' or 'yz' plot.
+ Plane y-position (float) for 'xz' plot. Range of y-axis for 'xy' or
+ 'yz' plot, in cm.
z : float or array_like
- Plane z-position (float) for 'xy' plot. Range of z-axis for 'xz' or 'yz' plot.
+ Plane z-position (float) for 'xy' plot. Range of z-axis for 'xz' or
+ 'yz' plot, in cm.
time : array_like
- Times at which the geometry snapshots are taken
+ Times in seconds at which the geometry snapshots are taken
pixels : array_like
Number of respective pixels in the two axes in vis_plane
colors : array_like
@@ -41,11 +43,7 @@ def visualize(
from matplotlib import colors as mpl_colors
- # Use cached preparation if available
- global _visualize_cache
- if _visualize_cache is None:
- _visualize_cache = preparation()
- simulation_container, data = _visualize_cache
+ simulation_container, data = prepare(simulationPy)
simulation = simulation_container[0]
# ==================================================================================
@@ -98,9 +96,8 @@ def _compute_material_row(
particle = particle_arr[0]
- # Set time and energy
+ # Set time, energy, and direction
particle["t"] = time_val
- particle["g"] = 0
particle["E"] = 1e6
particle["ux"] = 0.0
particle["uy"] = 0.0
diff --git a/pyproject.toml b/pyproject.toml
index 0ad749218..b370a26df 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,31 +10,17 @@ authors = [
{ name = "MC/DC Development Team" },
]
-maintainers = [
- { name = "Ilham Variansyah", email = "variansi@oregonstate.edu" },
- { name = "Joanna Piper Morgan", email = "morgan83@llnl.gov" },
- { name = "Melek Derman", email = "dermanm@oregonstate.edu" },
- { name = "Nathan Glaser", email = "glaserna@oregonstate.edu" },
- { name = "Massimo Larsen", email = "larsemas@oregonstate.edu" },
- { name = "Braxton Cuneo", email = "bcuneo@seattleu.edu" },
- { name = "Kyle E. Niemeyer", email = "kyle.niemeyer@oregonstate.edu" },
- { name = "Madicken Munk", email = "madicken.munk@oregonstate.edu" },
-]
-
-description = "A performant, scalable, and machine-portable Monte Carlo radiation transport code."
+description = "A performant, scalable, and machine-portable Python-based Monte Carlo radiation transport code."
readme = "README.md"
-requires-python = ">=3.10"
-license = { file = "LICENSE" }
+requires-python = ">=3.11"
+license = "BSD-3-Clause"
+license-files = ["LICENSE"]
keywords = [
"monte carlo",
+ "nuclear engineering",
+ "particle transport",
"radiation transport",
- "neutron transport",
- "high-performance computing",
- "hpc",
- "gpu",
- "numba",
- "mpi",
]
classifiers = [
@@ -48,12 +34,10 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
-
- "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python :: 3.14",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
@@ -64,7 +48,7 @@ classifiers = [
]
dependencies = [
- "numba>=0.60.0",
+ "numba>=0.61.0",
"numpy>=2.0.0",
"scipy",
"matplotlib",
@@ -76,20 +60,21 @@ dependencies = [
[project.optional-dependencies]
docs = [
- "sphinx==7.2.6",
- "furo",
- "sphinx_toolbox",
+ "sphinx>=8,<10",
+ "pydata-sphinx-theme>=0.20,<0.21",
+ "sphinx-design>=0.6",
]
dev = [
"black",
"pre-commit",
+ "pyright",
"pytest",
]
[project.urls]
Homepage = "https://mcdc.readthedocs.io/"
-Documentation = "https://mcdc.readthedocs.io/"
+Documentation = "https://mcdc.readthedocs.io/en/stable/"
Repository = "https://github.com/mcdc-project/mcdc"
Issues = "https://github.com/mcdc-project/mcdc/issues"
"Release Notes" = "https://github.com/mcdc-project/mcdc/releases"
@@ -97,10 +82,8 @@ Issues = "https://github.com/mcdc-project/mcdc/issues"
[tool.hatch.version]
source = "vcs"
-[tool.hatch.build.hooks.vcs]
-version-file = "mcdc/_version.py"
-
[tool.black]
+target-version = ["py311", "py312", "py313", "py314"]
force-exclude = '''
(
mcdc/numba_types\.py
@@ -108,3 +91,25 @@ force-exclude = '''
| mcdc/mcdc_set/
)
'''
+
+[tool.pyright]
+include = ["test/typecheck"]
+pythonVersion = "3.14"
+typeCheckingMode = "strict"
+
+[tool.pytest.ini_options]
+testpaths = ["test/unit"]
+# The slow suite is selected by path (`pytest test/regression`); keep the cloned
+# regression data out of pytest's collection without replacing pytest's defaults.
+norecursedirs = [
+ "*.egg",
+ ".*",
+ "_darcs",
+ "build",
+ "CVS",
+ "dist",
+ "node_modules",
+ "venv",
+ "{arch}",
+ "mcdc-regression_test_data",
+]
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 000000000..d3a634aab
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,35 @@
+def pytest_addoption(parser):
+ parser.addoption(
+ "--mode",
+ choices=["python", "numba"],
+ default="python",
+ help="MCDC execution mode.",
+ )
+ parser.addoption(
+ "--target",
+ choices=["cpu", "gpu"],
+ default="cpu",
+ help="MCDC regression target.",
+ )
+ parser.addoption(
+ "--mpiexec",
+ type=int,
+ default=0,
+ help="Run regression cases with mpiexec and the given number of ranks.",
+ )
+ parser.addoption(
+ "--srun",
+ type=int,
+ default=0,
+ help="Run regression cases with srun and the given number of ranks.",
+ )
+ parser.addoption(
+ "--name",
+ default="ALL",
+ help="Regression case name pattern to run.",
+ )
+ parser.addoption(
+ "--skip",
+ default="NONE",
+ help="Regression case name pattern to skip.",
+ )
diff --git a/test/regression/README.md b/test/regression/README.md
deleted file mode 100644
index b9f5e4436..000000000
--- a/test/regression/README.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# MC/DC - Regression Test
-
-To run all tests:
-
-```bash
-python run.py
-```
-
-To run a specific test (with wildcard `*` support):
-
-```bash
-python run.py --name=
-```
-
-To run in Numba mode:
-
-```bash
-python run.py --mode=numba
-```
-
-To run in multiple MPI ranks (currently support `mpiexec` and `srun`):
-
-```bash
-python run.py --mpiexec=
-```
-
-To add a new test:
-
-1. Create a folder. The name of the folder will be the test name.
-2. Add the input file named `input.py`.
-3. Add the answer key file named `answer.h5`.
-4. Make sure that the number of particles run is large enough for a good test.
-5. If the test runs longer than 10 seconds in serial Python mode, consider decreasing the number of particles.
diff --git a/test/regression/azurv1/input.py b/test/regression/azurv1/input.py
index 80eb28d39..4ff4453a6 100644
--- a/test/regression/azurv1/input.py
+++ b/test/regression/azurv1/input.py
@@ -1,51 +1,62 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("AZURV1")
+
# ======================================================================================
-# Set model
+# Set simulation model
# ======================================================================================
# Infinite medium with isotropic plane surface at the center
# Based on Ganapol LA-UR-01-1854 (AZURV1 benchmark)
# Effective scattering ratio c = 1.1
-# Set materials
-m = mcdc.MaterialMG(
+# Materials
+m = mcdc.Material.multigroup(
capture=np.array([1.0 / 3.0]),
scatter=np.array([[1.0 / 3.0]]),
fission=np.array([1.0 / 3.0]),
nu_p=np.array([2.3]),
)
-# Set surfaces
+# Surfaces
s1 = mcdc.Surface.PlaneX(x=-1e10, boundary_condition="reflective")
s2 = mcdc.Surface.PlaneX(x=1e10, boundary_condition="reflective")
-# Set cells
-mcdc.Cell(region=+s1 & -s2, fill=m)
+# Cells
+cell = mcdc.Cell(region=+s1 & -s2, fill=m)
+
+# Set model
+simulation.set_model([cell])
# ======================================================================================
-# Set source
+# Set simulation sources
# ======================================================================================
# Isotropic pulse at x=t=0
-mcdc.Source(
+source = mcdc.Source(
position=[0.0, 0.0, 0.0],
isotropic=True,
- energy_group=0,
+ energy=0,
time=0.0,
)
+simulation.set_sources([source])
# ======================================================================================
-# Set tallies, settings, and run MC/DC
+# Set simulation tallies
# ======================================================================================
-# Tallies
mesh = mcdc.MeshStructured(x=np.linspace(-20.5, 20.5, 202))
-mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0.0, 20.0, 21))
+tally = mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0.0, 20.0, 21))
+simulation.set_tallies([tally])
+
+# ======================================================================================
+# Simulation settings and run simulation
+# ======================================================================================
# Settings
-mcdc.settings.N_particle = 60
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 60
+simulation.settings.N_batch = 2
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/azurv1_census/input.py b/test/regression/azurv1_census/input.py
index 9a8098887..84ffbb07c 100644
--- a/test/regression/azurv1_census/input.py
+++ b/test/regression/azurv1_census/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("AZURV1 census")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -9,7 +12,7 @@
# Effective scattering ratio c = 1.1
# Set materials
-m = mcdc.MaterialMG(
+m = mcdc.Material.multigroup(
capture=np.array([1.0 / 3.0]),
scatter=np.array([[1.0 / 3.0]]),
fission=np.array([1.0 / 3.0]),
@@ -21,19 +24,21 @@
s2 = mcdc.Surface.PlaneX(x=1e10, boundary_condition="reflective")
# Set cells
-mcdc.Cell(region=+s1 & -s2, fill=m)
+cell = mcdc.Cell(region=+s1 & -s2, fill=m)
+simulation.set_model([cell])
# ======================================================================================
# Set source
# ======================================================================================
# Isotropic pulse at x=t=0
-mcdc.Source(
+source = mcdc.Source(
position=[0.0, 0.0, 0.0],
isotropic=True,
- energy_group=0,
+ energy=0,
time=0.0,
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -41,17 +46,18 @@
# Tallies
mesh = mcdc.MeshStructured(x=np.linspace(-20.5, 20.5, 202))
-mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0.0, 20.0, 21))
+tally = mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0.0, 20.0, 21))
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 50
-mcdc.settings.N_batch = 2
-mcdc.settings.census_bank_buffer_ratio = 5.0
-mcdc.settings.source_bank_buffer_ratio = 5.0
-mcdc.settings.set_time_census(np.linspace(0.0, 20.0, 21)[1:-1])
+simulation.settings.N_particle = 50
+simulation.settings.N_batch = 2
+simulation.settings.census_bank_buffer_ratio = 5.0
+simulation.settings.source_bank_buffer_ratio = 5.0
+simulation.settings.set_time_census(np.linspace(0.0, 20.0, 21)[1:-1])
-# Tecniques
-mcdc.simulation.population_control()
+# Techniques
+simulation.technique.population_control()
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/azurv1_census_tally/input.py b/test/regression/azurv1_census_tally/input.py
index ab15e958d..71957d0a1 100644
--- a/test/regression/azurv1_census_tally/input.py
+++ b/test/regression/azurv1_census_tally/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("AZURV1 census tally")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -9,7 +12,7 @@
# Effective scattering ratio c = 1.1
# Set materials
-m = mcdc.MaterialMG(
+m = mcdc.Material.multigroup(
capture=np.array([1.0 / 3.0]),
scatter=np.array([[1.0 / 3.0]]),
fission=np.array([1.0 / 3.0]),
@@ -21,19 +24,21 @@
s2 = mcdc.Surface.PlaneX(x=1e10, boundary_condition="reflective")
# Set cells
-mcdc.Cell(region=+s1 & -s2, fill=m)
+cell = mcdc.Cell(region=+s1 & -s2, fill=m)
+simulation.set_model([cell])
# ======================================================================================
# Set source
# ======================================================================================
# Isotropic pulse at x=t=0
-mcdc.Source(
+source = mcdc.Source(
position=[0.0, 0.0, 0.0],
isotropic=True,
- energy_group=0,
+ energy=0,
time=0.0,
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -41,20 +46,18 @@
# Tallies
mesh = mcdc.MeshStructured(x=np.linspace(-20.5, 20.5, 202))
-mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0.0, 20.0, 21))
+tally = mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0.0, 20.0, 21))
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 50
-mcdc.settings.N_batch = 2
-mcdc.settings.census_bank_buffer_ratio = 5.0
-mcdc.settings.source_bank_buffer_ratio = 5.0
-mcdc.settings.set_time_census(np.linspace(0.0, 20.0, 21)[1:], tally_frequency=5)
+simulation.settings.N_particle = 50
+simulation.settings.N_batch = 2
+simulation.settings.census_bank_buffer_ratio = 5.0
+simulation.settings.source_bank_buffer_ratio = 5.0
+simulation.settings.set_time_census(np.linspace(0.0, 20.0, 21)[1:], tally_frequency=5)
-# Tecniques
-mcdc.simulation.population_control()
+# Techniques
+simulation.technique.population_control()
# Run
-mcdc.run()
-
-# Post-processing
-mcdc.recombine_tallies()
+simulation.run()
diff --git a/test/regression/basic_weight_windows/input.py b/test/regression/basic_weight_windows/input.py
index 38d3e9805..7201320bd 100644
--- a/test/regression/basic_weight_windows/input.py
+++ b/test/regression/basic_weight_windows/input.py
@@ -5,6 +5,9 @@
os.environ["MCDC_LIB"] = "../mcdc-regression_test_data/"
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Basic weight windows")
+
# Uses Pincell model as the basis
# Material
@@ -31,17 +34,19 @@
y0 = mcdc.Surface.PlaneY(y=-pitch / 2, boundary_condition="reflective")
y1 = mcdc.Surface.PlaneY(y=pitch / 2, boundary_condition="reflective")
#
-mcdc.Cell(-cylinder, fill=fuel)
-mcdc.Cell(+x0 & -x1 & +y0 & -y1 & +cylinder, fill=moderator)
+fuel_cell = mcdc.Cell(-cylinder, fill=fuel)
+moderator_cell = mcdc.Cell(+x0 & -x1 & +y0 & -y1 & +cylinder, fill=moderator)
+simulation.set_model([fuel_cell, moderator_cell])
# Source
-mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True, time=0.0, energy=14.1e6)
+source = mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True, time=0.0, energy=14.1e6)
+simulation.set_sources([source])
# Setting
-mcdc.settings.N_particle = 20
-mcdc.settings.N_batch = 2
-mcdc.settings.time_boundary = 1.0
-mcdc.settings.active_bank_buffer = 1000
+simulation.settings.N_particle = 20
+simulation.settings.N_batch = 2
+simulation.settings.time_boundary = 1.0
+simulation.settings.active_bank_buffer = 1000
# Mesh
Nx, Ny = 20, 20
@@ -50,7 +55,8 @@
mesh = mcdc.MeshUniform(x=(x0, dx, Nx), y=(y0, dy, Ny))
# Tally
-mcdc.Tally(mesh=mesh, scores=["flux"])
+tally = mcdc.Tally(mesh=mesh, scores=["flux"])
+simulation.set_tallies([tally])
# Weight windows
ww_array = np.ones((1, 20, 20, 1, 3))
@@ -58,6 +64,6 @@
ww_array[..., 0] = 0.55 # Forces roulette on split particles from 1.0
ww_array[..., 1] = 0.7 # arbitrary in the middle
ww_array[..., 2] = 0.9 # forces splitting on all particles born with w=1.0
-mcdc.simulation.weight_windows(ww_array, mesh=mesh)
+simulation.technique.weight_windows(ww_array, mesh=mesh)
-mcdc.run()
+simulation.run()
diff --git a/test/regression/c5g7_2d_k_eigenvalue/input.py b/test/regression/c5g7_2d_k_eigenvalue/input.py
index 2b8a87d3c..ec4ffe8a5 100644
--- a/test/regression/c5g7_2d_k_eigenvalue/input.py
+++ b/test/regression/c5g7_2d_k_eigenvalue/input.py
@@ -3,6 +3,9 @@
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("C5G7 2-D k-eigenvalue")
+
# =============================================================================
# Materials
# =============================================================================
@@ -13,7 +16,7 @@
# Materials
def set_mat(mat):
- return mcdc.MaterialMG(
+ return mcdc.Material.multigroup(
capture=mat["capture"][:],
scatter=mat["scatter"][:],
fission=mat["fission"][:],
@@ -167,18 +170,19 @@ def set_mat(mat):
)
# Root universe
-mcdc.simulation.set_root_universe(cells=[core])
+simulation.set_model([core])
# =============================================================================
# Set source
# =============================================================================
-mcdc.Source(
+source = mcdc.Source(
x=[0.0, pitch * 17 * 2],
y=[-pitch * 17 * 2, 0.0],
isotropic=True,
- energy_group=6,
+ energy=6,
)
+simulation.set_sources([source])
# =============================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -189,19 +193,19 @@ def set_mat(mat):
x=np.linspace(0.0, pitch * 17 * 3, 17 * 3 + 1),
y=np.linspace(-pitch * 17 * 3, 0.0, 17 * 3 + 1),
)
-mcdc.Tally(mesh=mesh, scores=["flux"])
+tally = mcdc.Tally(mesh=mesh, scores=["flux"])
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 20
-mcdc.settings.census_bank_buffer_ratio = 4.0
-mcdc.settings.source_bank_buffer_ratio = 3.0
-mcdc.settings.set_eigenmode(N_inactive=1, N_active=2, gyration_radius="infinite-z")
+simulation.settings.N_particle = 20
+simulation.settings.census_bank_buffer_ratio = 4.0
+simulation.settings.source_bank_buffer_ratio = 3.0
+simulation.settings.set_eigenmode(
+ N_inactive=1, N_active=2, gyration_radius="infinite-z"
+)
# Techniques
-mcdc.simulation.population_control()
+simulation.technique.population_control()
# Run
-mcdc.settings.set_eigenmode(N_inactive=1, N_active=2, gyration_radius="infinite-z")
-
-
-mcdc.run()
+simulation.run()
diff --git a/test/regression/conftest.py b/test/regression/conftest.py
new file mode 100644
index 000000000..707d0f4ca
--- /dev/null
+++ b/test/regression/conftest.py
@@ -0,0 +1,221 @@
+import fnmatch
+import subprocess
+import sys
+import warnings
+from pathlib import Path
+
+import h5py
+import numpy as np
+import pytest
+
+REGRESSION_DIR = Path(__file__).parent
+REGRESSION_DATA_NAME = "mcdc-regression_test_data"
+RELATIVE_TOLERANCE = 1e-6
+ABSOLUTE_TOLERANCE = 1e-14
+NON_TEST_DIRS = {"__pycache__", REGRESSION_DATA_NAME}
+
+
+def regression_cases():
+ return sorted(
+ path
+ for path in REGRESSION_DIR.iterdir()
+ if path.is_dir() and path.name not in NON_TEST_DIRS
+ )
+
+
+def selected_cases(config):
+ name_pattern = config.getoption("--name")
+ skip_pattern = config.getoption("--skip")
+ target = config.getoption("--target")
+
+ cases = []
+ for case in regression_cases():
+ if name_pattern != "ALL" and not fnmatch.fnmatch(case.name, name_pattern):
+ continue
+ if skip_pattern != "NONE" and fnmatch.fnmatch(case.name, skip_pattern):
+ continue
+ if target == "gpu" and ("iqmc" in case.name or "eigenvalue" in case.name):
+ continue
+ cases.append(case)
+ return cases
+
+
+def pytest_configure(config):
+ name_pattern = config.getoption("--name")
+ if name_pattern == "ALL":
+ return
+ if not any(fnmatch.fnmatch(case.name, name_pattern) for case in regression_cases()):
+ raise pytest.UsageError(
+ f"--name={name_pattern} did not match any regression case"
+ )
+
+
+def pytest_generate_tests(metafunc):
+ if "case_path" in metafunc.fixturenames:
+ cases = selected_cases(metafunc.config)
+ metafunc.parametrize("case_path", cases, ids=[case.name for case in cases])
+
+
+@pytest.fixture(scope="session")
+def regression_data():
+ data_path = REGRESSION_DIR / REGRESSION_DATA_NAME
+ if data_path.is_dir():
+ result = subprocess.run(
+ ["git", "pull"],
+ cwd=data_path,
+ capture_output=True,
+ text=True,
+ )
+ # An out-of-date clone is still usable (e.g. when running offline), so warn
+ # rather than skip.
+ if result.returncode != 0:
+ warnings.warn(
+ "Could not update regression data repository.\n"
+ + format_subprocess_output(result.stdout, result.stderr)
+ )
+ return data_path
+
+ result = subprocess.run(
+ ["git", "clone", f"https://github.com/mcdc-project/{REGRESSION_DATA_NAME}.git"],
+ cwd=REGRESSION_DIR,
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode != 0:
+ pytest.skip(
+ "Could not clone regression data repository.\n"
+ + format_subprocess_output(result.stdout, result.stderr)
+ )
+ return data_path
+
+
+@pytest.fixture
+def run_regression_case(pytestconfig, regression_data):
+ def run(case_path):
+ run_case(pytestconfig, case_path)
+
+ return run
+
+
+def run_case(config, case_path):
+ input_path = case_path / "input.py"
+ answer_path = case_path / "answer.h5"
+ output_path = case_path / "output.h5"
+
+ if not input_path.exists():
+ pytest.fail("input.py is missing")
+ if not answer_path.exists():
+ pytest.fail("answer.h5 is missing")
+
+ if output_path.exists():
+ output_path.unlink()
+
+ result = subprocess.run(
+ build_command(config),
+ cwd=case_path,
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode != 0:
+ pytest.fail(
+ f"Run failed with return code {result.returncode}.\n"
+ + format_subprocess_output(result.stdout, result.stderr)
+ )
+
+ if not output_path.exists():
+ pytest.fail(
+ "Run did not produce output.h5.\n"
+ + format_subprocess_output(result.stdout, result.stderr)
+ )
+
+ compare_outputs(output_path, answer_path, config.getoption("--target"))
+
+
+def build_command(config):
+ mode = config.getoption("--mode")
+ target = config.getoption("--target")
+ mpiexec = config.getoption("--mpiexec")
+ srun = config.getoption("--srun")
+ command = [
+ sys.executable,
+ "input.py",
+ f"--mode={mode}",
+ f"--target={target}",
+ "--output=output",
+ "--no-progress-bar",
+ ]
+
+ if mpiexec > 1:
+ prefix = ["mpiexec", "-n", str(mpiexec)]
+ if target == "gpu":
+ prefix.append("--gpus-per-task=1")
+ return [*prefix, *command]
+
+ if srun > 1:
+ prefix = ["srun", "-n", str(srun)]
+ if target == "gpu":
+ prefix.append("--gpus-per-task=1")
+ return [*prefix, *command]
+
+ return command
+
+
+def compare_outputs(output_path, answer_path, target):
+ errors = []
+ with h5py.File(output_path, "r") as output, h5py.File(answer_path, "r") as answer:
+ if "iqmc" in output.keys():
+ compare_iqmc(output, answer, errors)
+ else:
+ compare_tallies(output, answer, target, errors)
+ compare_k_results(output, answer, errors)
+
+ if errors:
+ pytest.fail("\n\n".join(errors))
+
+
+def compare_tallies(output, answer, target, errors):
+ name_root = "tallies"
+ for tally in answer[name_root].keys():
+ name_tally = f"{name_root}/{tally}"
+ for score in answer[name_tally].keys():
+ if score == "grid":
+ continue
+ name_score = f"{name_tally}/{score}"
+ for result in answer[name_score].keys():
+ if "uq_var" in result and target == "gpu":
+ continue
+ name = f"{name_score}/{result}"
+ assert_allclose(output[name][()], answer[name][()], name, errors)
+
+
+def compare_k_results(output, answer, errors):
+ for name in ["k_mean", "k_sdev", "k_cycle", "k_eff"]:
+ if name in output.keys():
+ assert_allclose(output[name][()], answer[name][()], name, errors)
+
+
+def compare_iqmc(output, answer, errors):
+ for score in output["iqmc/tally/"].keys():
+ name = f"iqmc/tally/{score}/mean"
+ assert_allclose(
+ np.squeeze(output[name][()]),
+ np.squeeze(answer[name][()]),
+ name,
+ errors,
+ )
+
+
+def assert_allclose(actual, expected, name, errors):
+ try:
+ np.testing.assert_allclose(
+ actual,
+ expected,
+ rtol=RELATIVE_TOLERANCE,
+ atol=ABSOLUTE_TOLERANCE,
+ )
+ except AssertionError as error:
+ errors.append(f"Differences in {name}\n{error}")
+
+
+def format_subprocess_output(stdout, stderr):
+ return f"stdout:\n{stdout}\n\nstderr:\n{stderr}"
diff --git a/test/regression/cooper2/input.py b/test/regression/cooper2/input.py
index be89385db..066c2f4e6 100644
--- a/test/regression/cooper2/input.py
+++ b/test/regression/cooper2/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Cooper problem 2")
+
# =============================================================================
# Set model
# =============================================================================
@@ -10,11 +13,13 @@
# Set materials
SigmaT = 5.0
c = 0.8
-m_barrier = mcdc.MaterialMG(
+m_barrier = mcdc.Material.multigroup(
capture=np.array([SigmaT]), scatter=np.array([[SigmaT * c]])
)
SigmaT = 1.0
-m_room = mcdc.MaterialMG(capture=np.array([SigmaT]), scatter=np.array([[SigmaT * c]]))
+m_room = mcdc.Material.multigroup(
+ capture=np.array([SigmaT]), scatter=np.array([[SigmaT * c]])
+)
# Set surfaces
sx1 = mcdc.Surface.PlaneX(x=0.0, boundary_condition="reflective")
@@ -26,22 +31,24 @@
sy3 = mcdc.Surface.PlaneY(y=4.0, boundary_condition="vacuum")
# Set cells
-mcdc.Cell(region=+sx1 & -sx2 & +sy1 & -sy2, fill=m_room)
-mcdc.Cell(region=+sx1 & -sx4 & +sy2 & -sy3, fill=m_room)
-mcdc.Cell(region=+sx3 & -sx4 & +sy1 & -sy2, fill=m_room)
-mcdc.Cell(region=+sx2 & -sx3 & +sy1 & -sy2, fill=m_barrier)
+room_lower_left = mcdc.Cell(region=+sx1 & -sx2 & +sy1 & -sy2, fill=m_room)
+room_upper = mcdc.Cell(region=+sx1 & -sx4 & +sy2 & -sy3, fill=m_room)
+room_lower_right = mcdc.Cell(region=+sx3 & -sx4 & +sy1 & -sy2, fill=m_room)
+barrier = mcdc.Cell(region=+sx2 & -sx3 & +sy1 & -sy2, fill=m_barrier)
+simulation.set_model([room_lower_left, room_upper, room_lower_right, barrier])
# =============================================================================
# Set source
# =============================================================================
-mcdc.Source(
+source = mcdc.Source(
x=[0.0, 1.0],
y=[0.0, 1.0],
isotropic=True,
- energy_group=0,
+ energy=0,
time=0.0,
)
+simulation.set_sources([source])
# =============================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -49,15 +56,16 @@
# Tallies
mesh = mcdc.MeshUniform(x=(0.0, 0.1, 40), y=(0.0, 0.1, 40))
-mcdc.Tally(mesh=mesh, scores=["flux"])
+tally = mcdc.Tally(mesh=mesh, scores=["flux"])
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 1000
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 1000
+simulation.settings.N_batch = 2
# Techniques
-mcdc.simulation.implicit_capture()
-mcdc.simulation.global_weight_roulette(0.1, 1.0)
+simulation.technique.implicit_capture()
+simulation.technique.global_weight_roulette(0.1, 1.0)
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/fuel_array_packaged/input.py b/test/regression/fuel_array_packaged/input.py
index 551123012..d8d59d76a 100644
--- a/test/regression/fuel_array_packaged/input.py
+++ b/test/regression/fuel_array_packaged/input.py
@@ -1,22 +1,25 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Packaged fuel array")
+
# ======================================================================================
# Materials
# ======================================================================================
-fuel = mcdc.MaterialMG(
+fuel = mcdc.Material.multigroup(
capture=np.array([0.45]),
fission=np.array([0.55]),
nu_p=np.array([2.5]),
)
-cover = mcdc.MaterialMG(
+cover = mcdc.Material.multigroup(
capture=np.array([0.05]),
scatter=np.array([[0.95]]),
)
-water = mcdc.MaterialMG(
+water = mcdc.Material.multigroup(
capture=np.array([0.02]),
scatter=np.array([[0.08]]),
)
@@ -69,13 +72,14 @@
)
# Root universe
-mcdc.simulation.set_root_universe(cells=[assembly_left, assembly_right])
+simulation.set_model([assembly_left, assembly_right])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(x=[-0.1, 0.1], isotropic=True, energy_group=0)
+source = mcdc.Source(x=[-0.1, 0.1], isotropic=True, energy=0)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -86,23 +90,31 @@
x=np.linspace(-10, 10, 201),
z=np.linspace(-5, 5, 101),
)
-mcdc.Tally(mesh=mesh, scores=["fission"])
+tally = mcdc.Tally(mesh=mesh, scores=["fission"])
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 100
-mcdc.settings.N_batch = 2
-mcdc.settings.active_bank_buffer = 1000
+simulation.settings.N_particle = 100
+simulation.settings.N_batch = 2
+simulation.settings.active_bank_buffer = 1000
# Run (or visualize)
visualize = False
if not visualize:
- mcdc.run()
+ simulation.run()
else:
colors = {
fuel: "red",
cover: "gray",
water: "blue",
}
- mcdc.visualize(
- "xz", y=0.0, x=[-11.0, 11.0], z=[-6, 6], pixels=(400, 400), colors=colors
+ simulation.visualize_model(
+ vis_plane="xz",
+ y=0.0,
+ x=[-11.0, 11.0],
+ z=[-6, 6],
+ pixels=(400, 400),
+ colors=colors,
+ time=[0.0],
+ save_as=None,
)
diff --git a/test/regression/hybrid_multigroup/answer.h5 b/test/regression/hybrid_multigroup/answer.h5
new file mode 100644
index 000000000..9bd8a3817
Binary files /dev/null and b/test/regression/hybrid_multigroup/answer.h5 differ
diff --git a/test/regression/hybrid_multigroup/input.py b/test/regression/hybrid_multigroup/input.py
new file mode 100644
index 000000000..d3a8e1198
--- /dev/null
+++ b/test/regression/hybrid_multigroup/input.py
@@ -0,0 +1,53 @@
+import numpy as np
+import os
+
+import mcdc
+
+os.environ["MCDC_LIB"] = "../mcdc-regression_test_data/"
+
+simulation = mcdc.Simulation("Hybrid multigroup regression")
+
+# Native composition combined with multigroup data selects hybrid transport
+material = mcdc.Material(
+ nuclide_composition={"H1": 5.0e-2},
+ neutron_multigroup=mcdc.NeutronMultigroupData(
+ capture=np.array([0.2]),
+ scatter=np.array([[0.8]]),
+ speed=np.array([1.383e6]),
+ energy_grid=np.array([0.1, 10.0]),
+ energy_representation="midpoint",
+ ),
+)
+
+boundary = mcdc.Surface.Sphere(radius=5.0, boundary_condition="vacuum")
+cell = mcdc.Cell(region=-boundary, fill=material)
+simulation.set_model([cell])
+
+multigroup_source = mcdc.Source(
+ position=[0.0, 0.0, 1.0],
+ isotropic=True,
+ energy=1.0,
+ probability=0.5,
+)
+native_source = mcdc.Source(
+ position=[0.0, 0.0, -1.0],
+ isotropic=True,
+ energy=1.0e6,
+ probability=0.5,
+)
+simulation.set_sources([multigroup_source, native_source])
+
+mesh = mcdc.MeshStructured(z=np.array([-5.0, 0.0, 5.0]))
+tally = mcdc.Tally(
+ name="hybrid_flux",
+ mesh=mesh,
+ particle_type="neutron",
+ energy=np.array([0.0, 0.1, 10.0, 1.0e5, 20.0e6]),
+ scores=["flux", "collision"],
+)
+simulation.set_tallies([tally])
+
+simulation.settings.N_particle = 40
+simulation.settings.N_batch = 2
+
+simulation.run()
diff --git a/test/regression/inf_shem361/input.py b/test/regression/inf_shem361/input.py
index 60b97b20c..b762b197c 100644
--- a/test/regression/inf_shem361/input.py
+++ b/test/regression/inf_shem361/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Infinite SHEM-361")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -20,7 +23,7 @@
lamd = data["lamd"]
# Set material
-m = mcdc.MaterialMG(
+m = mcdc.Material.multigroup(
capture=SigmaC,
scatter=SigmaS,
fission=SigmaF,
@@ -36,26 +39,29 @@
# Set cells
c = mcdc.Cell(region=+s1 & -s2, fill=m)
+simulation.set_model([c])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(
- position=(0.0, 0.0, 0.0), isotropic=True, energy_group=np.array([[360], [1.0]])
+source = mcdc.Source(
+ position=(0.0, 0.0, 0.0), isotropic=True, discrete_energy=np.array([[360], [1.0]])
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run MC/DC
# ======================================================================================
# Tallies
-mcdc.Tally(scores=["flux"], energy="all_groups")
+tally = mcdc.Tally(scores=["flux"], energy="all")
+simulation.set_tallies([tally])
-# Swttings
-mcdc.settings.N_particle = 25
-mcdc.settings.N_batch = 2
-mcdc.settings.active_bank_buffer = 1000
+# Settings
+simulation.settings.N_particle = 25
+simulation.settings.N_batch = 2
+simulation.settings.active_bank_buffer = 1000
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/inf_shem361_k_eigenvalue/input.py b/test/regression/inf_shem361_k_eigenvalue/input.py
index e57e0b691..28380ed5f 100644
--- a/test/regression/inf_shem361_k_eigenvalue/input.py
+++ b/test/regression/inf_shem361_k_eigenvalue/input.py
@@ -2,6 +2,9 @@
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Infinite SHEM-361 k-eigenvalue")
+
# =============================================================================
# Set model
# =============================================================================
@@ -19,7 +22,7 @@
G = data["G"]
# Set material
-m = mcdc.MaterialMG(
+m = mcdc.Material.multigroup(
capture=SigmaC,
scatter=SigmaS,
fission=SigmaF,
@@ -35,30 +38,33 @@
# Set cells
c = mcdc.Cell(region=+s1 & -s2, fill=m)
+simulation.set_model([c])
# =============================================================================
# Set initial source
# =============================================================================
-mcdc.Source(
- position=(0.0, 0.0, 0.0), isotropic=True, energy_group=np.array([[360], [1.0]])
+source = mcdc.Source(
+ position=(0.0, 0.0, 0.0), isotropic=True, discrete_energy=np.array([[360], [1.0]])
)
+simulation.set_sources([source])
# =============================================================================
# Set tallies, settings, techniques, and run MC/DC
# =============================================================================
# Tallies
-mcdc.Tally(scores=["flux"], energy="all_groups")
+tally = mcdc.Tally(scores=["flux"], energy="all")
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 70
-mcdc.settings.source_bank_buffer_ratio = 2.0
-mcdc.settings.census_bank_buffer_ratio = 3.0
-mcdc.settings.set_eigenmode(N_inactive=1, N_active=2)
+simulation.settings.N_particle = 70
+simulation.settings.source_bank_buffer_ratio = 2.0
+simulation.settings.census_bank_buffer_ratio = 3.0
+simulation.settings.set_eigenmode(N_inactive=1, N_active=2)
# Techniques
-mcdc.simulation.population_control()
+simulation.technique.population_control()
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/inf_shem361_td/input.py b/test/regression/inf_shem361_td/input.py
index 58b54f997..975fd993d 100644
--- a/test/regression/inf_shem361_td/input.py
+++ b/test/regression/inf_shem361_td/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Infinite SHEM-361 time dependent")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -20,7 +23,7 @@
lamd = data["lamd"]
# Set material
-m = mcdc.MaterialMG(
+m = mcdc.Material.multigroup(
capture=SigmaC,
scatter=SigmaS,
fission=SigmaF,
@@ -38,30 +41,33 @@
# Set cells
c = mcdc.Cell(region=+s1 & -s2, fill=m)
+simulation.set_model([c])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(
- position=(0.0, 0.0, 0.0), isotropic=True, energy_group=np.array([[360], [1.0]])
+source = mcdc.Source(
+ position=(0.0, 0.0, 0.0), isotropic=True, discrete_energy=np.array([[360], [1.0]])
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run MC/DC
# ======================================================================================
# Tallies
-mcdc.Tally(
+tally = mcdc.Tally(
scores=["flux"],
time=np.insert(np.logspace(-8, 1, 100), 0, 0.0),
- energy="all_groups",
+ energy="all",
)
+simulation.set_tallies([tally])
-# Swttings
-mcdc.settings.N_particle = 50
-mcdc.settings.N_batch = 2
-mcdc.settings.active_bank_buffer = 1000
+# Settings
+simulation.settings.N_particle = 50
+simulation.settings.N_batch = 2
+simulation.settings.active_bank_buffer = 1000
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/inf_shem361_td_census/input.py b/test/regression/inf_shem361_td_census/input.py
index 525615b0e..c571df021 100644
--- a/test/regression/inf_shem361_td_census/input.py
+++ b/test/regression/inf_shem361_td_census/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Infinite SHEM-361 time-dependent census")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -20,7 +23,7 @@
lamd = data["lamd"]
# Set material
-m = mcdc.MaterialMG(
+m = mcdc.Material.multigroup(
capture=SigmaC,
scatter=SigmaS,
fission=SigmaF,
@@ -38,36 +41,39 @@
# Set cells
c = mcdc.Cell(region=+s1 & -s2, fill=m)
+simulation.set_model([c])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(
- position=(0.0, 0.0, 0.0), isotropic=True, energy_group=np.array([[360], [1.0]])
+source = mcdc.Source(
+ position=(0.0, 0.0, 0.0), isotropic=True, discrete_energy=np.array([[360], [1.0]])
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, techniques, and run MC/DC
# ======================================================================================
# Tallies
-mcdc.Tally(
+tally = mcdc.Tally(
scores=["flux"],
time=np.insert(np.logspace(-8, 1, 100), 0, 0.0),
- energy="all_groups",
+ energy="all",
)
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 40
-mcdc.settings.N_batch = 2
-mcdc.settings.set_time_census(np.logspace(-5, 1, 6))
-mcdc.settings.active_bank_buffer = 1000
-mcdc.settings.census_bank_buffer_ratio = 5.0
-mcdc.settings.source_bank_buffer_ratio = 5.0
+simulation.settings.N_particle = 40
+simulation.settings.N_batch = 2
+simulation.settings.set_time_census(np.logspace(-5, 1, 6))
+simulation.settings.active_bank_buffer = 1000
+simulation.settings.census_bank_buffer_ratio = 5.0
+simulation.settings.source_bank_buffer_ratio = 5.0
# Techniques
-mcdc.simulation.population_control()
+simulation.technique.population_control()
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/kobayashi3-TD/input.py b/test/regression/kobayashi3-TD/input.py
index 09776f81c..717affda9 100644
--- a/test/regression/kobayashi3-TD/input.py
+++ b/test/regression/kobayashi3-TD/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Kobayashi 3-D time dependent")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -8,8 +11,8 @@
# (PNE 2001, https://doi.org/10.1016/S0149-1970(01)00007-5)
# Set materials
-m = mcdc.MaterialMG(capture=np.array([0.05]), scatter=np.array([[0.05]]))
-m_void = mcdc.MaterialMG(capture=np.array([5e-5]), scatter=np.array([[5e-5]]))
+m = mcdc.Material.multigroup(capture=np.array([0.05]), scatter=np.array([[0.05]]))
+m_void = mcdc.Material.multigroup(capture=np.array([5e-5]), scatter=np.array([[5e-5]]))
# Set surfaces
sx1 = mcdc.Surface.PlaneX(x=0.0, boundary_condition="reflective")
@@ -41,20 +44,22 @@
# Shield
box = +sx1 & -sx5 & +sy1 & -sy5 & +sz1 & -sz5
shield_cell = mcdc.Cell(region=box & ~void_channel, fill=m)
+simulation.set_model([source_cell, void_cell, shield_cell])
# ======================================================================================
# Set source
# ======================================================================================
# The source pulses in t=[0,5]
-mcdc.Source(
+source = mcdc.Source(
x=[0.0, 10.0],
y=[0.0, 10.0],
z=[0.0, 10.0],
isotropic=True,
- energy_group=0,
+ energy=0,
time=[0.0, 50.0],
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -62,18 +67,21 @@
# Tallies
time_grid = np.linspace(0.0, 200.0, 21)
-mcdc.Tally(cell=source_cell, scores=["flux"], time=time_grid)
-mcdc.Tally(cell=void_cell, scores=["flux"], time=time_grid)
-mcdc.Tally(cell=shield_cell, scores=["flux"], time=time_grid)
+source_tally = mcdc.Tally(cell=source_cell, scores=["flux"], time=time_grid)
+void_tally = mcdc.Tally(cell=void_cell, scores=["flux"], time=time_grid)
+shield_tally = mcdc.Tally(cell=shield_cell, scores=["flux"], time=time_grid)
mesh = mcdc.MeshUniform(x=(0.0, 1.0, 60), y=(0.0, 1.0, 100))
-mcdc.Tally(mesh=mesh, scores=["flux"], time=time_grid)
-mcdc.Tally(scores=["density"], time=time_grid)
+mesh_tally = mcdc.Tally(mesh=mesh, scores=["flux"], time=time_grid)
+density_tally = mcdc.Tally(scores=["density"], time=time_grid)
+simulation.set_tallies(
+ [source_tally, void_tally, shield_tally, mesh_tally, density_tally]
+)
# Settings
-mcdc.settings.N_particle = 25
+simulation.settings.N_particle = 25
# Techniques
-mcdc.simulation.implicit_capture()
+simulation.technique.implicit_capture()
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/kornreich/input.py b/test/regression/kornreich/input.py
index 309bcc431..4ae57c201 100644
--- a/test/regression/kornreich/input.py
+++ b/test/regression/kornreich/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Kornreich")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -8,13 +11,13 @@
# DOI: 10.1016/j.anucene.2004.03.012
# Set materials
-m1 = mcdc.MaterialMG(
+m1 = mcdc.Material.multigroup(
capture=np.array([0.0]),
scatter=np.array([[0.9]]),
fission=np.array([0.1]),
nu_p=np.array([6.0]),
)
-m2 = mcdc.MaterialMG(
+m2 = mcdc.Material.multigroup(
capture=np.array([0.68]),
scatter=np.array([[0.2]]),
fission=np.array([0.12]),
@@ -27,14 +30,16 @@
s3 = mcdc.Surface.PlaneX(x=2.5, boundary_condition="vacuum")
# Set cells
-mcdc.Cell(region=+s1 & -s2, fill=m1)
-mcdc.Cell(region=+s2 & -s3, fill=m2)
+cell_1 = mcdc.Cell(region=+s1 & -s2, fill=m1)
+cell_2 = mcdc.Cell(region=+s2 & -s3, fill=m2)
+simulation.set_model([cell_1, cell_2])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(x=[0.0, 2.5], isotropic=True, energy_group=0)
+source = mcdc.Source(x=[0.0, 2.5], isotropic=True, energy=0)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -68,13 +73,14 @@
]
)
)
-mcdc.Tally(mesh=mesh, scores=["flux"])
+tally = mcdc.Tally(mesh=mesh, scores=["flux"])
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 100
-mcdc.settings.census_bank_buffer_ratio = 3.0
-mcdc.settings.source_bank_buffer_ratio = 3.0
-mcdc.settings.set_eigenmode(N_inactive=1, N_active=2, gyration_radius="only-x")
+simulation.settings.N_particle = 100
+simulation.settings.census_bank_buffer_ratio = 3.0
+simulation.settings.source_bank_buffer_ratio = 3.0
+simulation.settings.set_eigenmode(N_inactive=1, N_active=2, gyration_radius="only-x")
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/lockwood/input.py b/test/regression/lockwood/input.py
index 4784ed17b..cf407b54b 100644
--- a/test/regression/lockwood/input.py
+++ b/test/regression/lockwood/input.py
@@ -7,6 +7,9 @@
# Set the XS library directory
os.environ["MCDC_LIB"] = "../mcdc-regression_test_data/"
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Lockwood")
+
# =============================================================================
# Set problem parameters
# =============================================================================
@@ -56,19 +59,21 @@
s2 = mcdc.Surface.PlaneZ(z=L, boundary_condition="vacuum")
-mcdc.Cell(region=+s1 & -s2, fill=mat)
+cell = mcdc.Cell(region=+s1 & -s2, fill=mat)
+simulation.set_model([cell])
# =============================================================================
# Set source
# =============================================================================
# Parallel beam of 1 MeV electrons entering at z=0
-mcdc.Source(
+source = mcdc.Source(
z=[z0 + TINY, z0 + TINY],
particle_type="electron",
energy=np.array([[ENERGY - 1, ENERGY + 1], [0.5, 0.5]]),
direction=[math.sin(THETA), 0.0 + TINY, math.cos(THETA)],
)
+simulation.set_sources([source])
# =============================================================================
# Set tally
@@ -77,20 +82,18 @@
z_bins = np.linspace(0.0, L, N_LAYERS + 1)
mesh = mcdc.MeshStructured(z=z_bins)
-mcdc.Tally(name="edep", mesh=mesh, scores=["energy_deposition"])
-
-mcdc.Tally(name="flux", scores=["flux"], mesh=mesh)
-
-mcdc.Tally(name="s1_current", surface=s1, scores=["current-net"])
-
-mcdc.Tally(name="s2_current", surface=s2, scores=["current-net"])
+edep_tally = mcdc.Tally(name="edep", mesh=mesh, scores=["energy_deposition"])
+flux_tally = mcdc.Tally(name="flux", scores=["flux"], mesh=mesh)
+s1_current = mcdc.Tally(name="s1_current", surface=s1, scores=["current-net"])
+s2_current = mcdc.Tally(name="s2_current", surface=s2, scores=["current-net"])
+simulation.set_tallies([edep_tally, flux_tally, s1_current, s2_current])
# =============================================================================
# Settings and run
# =============================================================================
-mcdc.settings.set_transported_particles(["electron"])
-mcdc.settings.N_particle = N_PARTICLES
-mcdc.settings.active_bank_buffer = N_PARTICLES * 10
+simulation.settings.set_transported_particles(["electron"])
+simulation.settings.N_particle = N_PARTICLES
+simulation.settings.active_bank_buffer = N_PARTICLES * 10
-mcdc.run()
+simulation.run()
diff --git a/test/regression/moving_pellet/input.py b/test/regression/moving_pellet/input.py
index 77148b19f..10f8774d4 100644
--- a/test/regression/moving_pellet/input.py
+++ b/test/regression/moving_pellet/input.py
@@ -2,18 +2,21 @@
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Moving pellet")
+
# ======================================================================================
# Set model
# ======================================================================================
# Set materials
-fuel = mcdc.MaterialMG(
+fuel = mcdc.Material.multigroup(
capture=np.array([0.5]),
fission=np.array([0.25]),
nu_p=np.array([1.5]),
speed=np.array([200000.0]),
)
-air = mcdc.MaterialMG(
+air = mcdc.Material.multigroup(
capture=np.array([0.002]),
scatter=np.array([[0.008]]),
speed=np.array([200000.0]),
@@ -39,24 +42,26 @@
# Make cells
fuel_pellet_region = +bot_z & -top_z & -cylinder_z
-mcdc.Cell(region=fuel_pellet_region, fill=fuel)
-mcdc.Cell(
+fuel_cell = mcdc.Cell(region=fuel_pellet_region, fill=fuel)
+air_cell = mcdc.Cell(
region=~fuel_pellet_region & +min_x & -max_x & +min_y & -max_y & +min_z & -max_z,
fill=air,
)
+simulation.set_model([fuel_cell, air_cell])
# ======================================================================================
# Set source
# ======================================================================================
-mcdc.Source(
+source = mcdc.Source(
x=[2.0, 3.0],
y=[-0.5, 0.5],
z=[-0.5, 0.5],
isotropic=True,
- energy_group=0,
+ energy=0,
time=[0.0, 9.0],
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -67,24 +72,25 @@
x=np.linspace(-5, 5, 101),
z=np.linspace(-10, 10, 101),
)
-mcdc.Tally(mesh=mesh, scores=["fission"], time=np.linspace(0, 9, 46))
+tally = mcdc.Tally(mesh=mesh, scores=["fission"], time=np.linspace(0, 9, 46))
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 50
-mcdc.settings.N_batch = 2
-mcdc.settings.active_bank_buffer = 1000
+simulation.settings.N_particle = 50
+simulation.settings.N_batch = 2
+simulation.settings.active_bank_buffer = 1000
# Run (or visualize)
visualize = False
if not visualize:
- mcdc.run()
+ simulation.run()
else:
colors = {
fuel: "red",
air: "blue",
}
- mcdc.visualize(
- "xz",
+ simulation.visualize_model(
+ vis_plane="xz",
y=0.0,
x=[-5.0, 5.0],
z=[-10, 10],
diff --git a/test/regression/moving_source/input.py b/test/regression/moving_source/input.py
index 9fa587501..2291e4863 100644
--- a/test/regression/moving_source/input.py
+++ b/test/regression/moving_source/input.py
@@ -2,12 +2,15 @@
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Moving source")
+
# ======================================================================================
# Set model
# ======================================================================================
# Set materials
-air = mcdc.MaterialMG(
+air = mcdc.Material.multigroup(
capture=np.array([0.002]),
scatter=np.array([[0.008]]),
speed=np.array([200000.0]),
@@ -22,7 +25,8 @@
max_z = mcdc.Surface.PlaneZ(z=10.0, boundary_condition="vacuum")
# Make cells
-mcdc.Cell(region=+min_x & -max_x & +min_y & -max_y & +min_z & -max_z, fill=air)
+cell = mcdc.Cell(region=+min_x & -max_x & +min_y & -max_y & +min_z & -max_z, fill=air)
+simulation.set_model([cell])
# ======================================================================================
# Set source
@@ -34,10 +38,11 @@
z=[-0.5, 0.5],
direction=[1.0, 1.0, 0.0],
polar_cosine=[-1.0, -0.9],
- energy_group=0,
+ energy=0,
time=[0.0, 10.0],
)
src.move([[1.0, 0.0, 0.0], [-0.5, 2.0, 0.0], [0.0, -3.0, 0.0]], [7.0, 2.0, 1.0])
+simulation.set_sources([src])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -48,12 +53,13 @@
x=np.linspace(-5.0, 5.0, 21),
y=np.linspace(-5.0, 5.0, 21),
)
-mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0, 10, 11))
+tally = mcdc.Tally(mesh=mesh, scores=["flux"], time=np.linspace(0, 10, 11))
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 1000
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 1000
+simulation.settings.N_batch = 2
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/pincell-energy_deposition/input.py b/test/regression/pincell-energy_deposition/input.py
index 9504e18ff..8fdbaf569 100644
--- a/test/regression/pincell-energy_deposition/input.py
+++ b/test/regression/pincell-energy_deposition/input.py
@@ -4,6 +4,9 @@
os.environ["MCDC_LIB"] = "../mcdc-regression_test_data/"
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Pincell energy deposition")
+
# Material
fuel = mcdc.Material(
nuclide_composition={
@@ -28,17 +31,19 @@
y0 = mcdc.Surface.PlaneY(y=-pitch / 2, boundary_condition="reflective")
y1 = mcdc.Surface.PlaneY(y=pitch / 2, boundary_condition="reflective")
-mcdc.Cell(-cylinder, fill=fuel)
-mcdc.Cell(+x0 & -x1 & +y0 & -y1 & +cylinder, fill=moderator)
+fuel_cell = mcdc.Cell(-cylinder, fill=fuel)
+moderator_cell = mcdc.Cell(+x0 & -x1 & +y0 & -y1 & +cylinder, fill=moderator)
+simulation.set_model([fuel_cell, moderator_cell])
# Source
-mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True, time=0.0, energy=14.1e6)
+source = mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True, time=0.0, energy=14.1e6)
+simulation.set_sources([source])
# Settings
-mcdc.settings.N_particle = 20
-mcdc.settings.N_batch = 2
-mcdc.settings.time_boundary = 1.0
-mcdc.settings.active_bank_buffer = 1000
+simulation.settings.N_particle = 20
+simulation.settings.N_batch = 2
+simulation.settings.time_boundary = 1.0
+simulation.settings.active_bank_buffer = 1000
# Edep tally
mesh = mcdc.MeshUniform(
@@ -46,6 +51,7 @@
y=(-pitch / 2, pitch / 8, 8),
)
-mcdc.Tally(name="edep_mesh", mesh=mesh, scores=["energy_deposition"])
+tally = mcdc.Tally(name="edep_mesh", mesh=mesh, scores=["energy_deposition"])
+simulation.set_tallies([tally])
-mcdc.run()
+simulation.run()
diff --git a/test/regression/pincell-k_eigenvalue/input.py b/test/regression/pincell-k_eigenvalue/input.py
index b497d7662..79f29aaf2 100644
--- a/test/regression/pincell-k_eigenvalue/input.py
+++ b/test/regression/pincell-k_eigenvalue/input.py
@@ -4,6 +4,9 @@
os.environ["MCDC_LIB"] = "../mcdc-regression_test_data/"
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Pincell k-eigenvalue")
+
# Material
fuel = mcdc.Material(
nuclide_composition={
@@ -28,24 +31,27 @@
y0 = mcdc.Surface.PlaneY(y=-pitch / 2, boundary_condition="reflective")
y1 = mcdc.Surface.PlaneY(y=pitch / 2, boundary_condition="reflective")
#
-mcdc.Cell(-cylinder, fill=fuel)
-mcdc.Cell(+x0 & -x1 & +y0 & -y1 & +cylinder, fill=moderator)
+fuel_cell = mcdc.Cell(-cylinder, fill=fuel)
+moderator_cell = mcdc.Cell(+x0 & -x1 & +y0 & -y1 & +cylinder, fill=moderator)
+simulation.set_model([fuel_cell, moderator_cell])
# Source
-mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True, energy=14.1e6)
+source = mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True, energy=14.1e6)
+simulation.set_sources([source])
# Setting
-mcdc.settings.N_particle = 30
-mcdc.settings.active_bank_buffer = 1000
-mcdc.settings.census_bank_buffer_ratio = 3.0
-mcdc.settings.source_bank_buffer_ratio = 3.0
-mcdc.settings.set_eigenmode(N_inactive=1, N_active=2)
+simulation.settings.N_particle = 30
+simulation.settings.active_bank_buffer = 1000
+simulation.settings.census_bank_buffer_ratio = 3.0
+simulation.settings.source_bank_buffer_ratio = 3.0
+simulation.settings.set_eigenmode(N_inactive=1, N_active=2)
# Tally
e_min, e_max = 1e-5, 20.0e6
groups = 500
energies = np.logspace(np.log10(e_min), np.log10(e_max), groups + 1)
-mcdc.Tally(scores=["flux"], energy=energies)
+tally = mcdc.Tally(scores=["flux"], energy=energies)
+simulation.set_tallies([tally])
-mcdc.run()
+simulation.run()
diff --git a/test/regression/pincell/input.py b/test/regression/pincell/input.py
index 5b762177d..bbc8c9726 100644
--- a/test/regression/pincell/input.py
+++ b/test/regression/pincell/input.py
@@ -4,6 +4,9 @@
os.environ["MCDC_LIB"] = "../mcdc-regression_test_data/"
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Pincell")
+
# Material
fuel = mcdc.Material(
nuclide_composition={
@@ -28,17 +31,19 @@
y0 = mcdc.Surface.PlaneY(y=-pitch / 2, boundary_condition="reflective")
y1 = mcdc.Surface.PlaneY(y=pitch / 2, boundary_condition="reflective")
#
-mcdc.Cell(-cylinder, fill=fuel)
-mcdc.Cell(+x0 & -x1 & +y0 & -y1 & +cylinder, fill=moderator)
+fuel_cell = mcdc.Cell(-cylinder, fill=fuel)
+moderator_cell = mcdc.Cell(+x0 & -x1 & +y0 & -y1 & +cylinder, fill=moderator)
+simulation.set_model([fuel_cell, moderator_cell])
# Source
-mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True, time=0.0, energy=14.1e6)
+source = mcdc.Source(position=[0.0, 0.0, 0.0], isotropic=True, time=0.0, energy=14.1e6)
+simulation.set_sources([source])
# Setting
-mcdc.settings.N_particle = 20
-mcdc.settings.N_batch = 2
-mcdc.settings.time_boundary = 1.0
-mcdc.settings.active_bank_buffer = 1000
+simulation.settings.N_particle = 20
+simulation.settings.N_batch = 2
+simulation.settings.time_boundary = 1.0
+simulation.settings.active_bank_buffer = 1000
# Tally
t_grid = np.insert(np.logspace(-9, -4, 200), 0, 0.0)
@@ -46,6 +51,7 @@
groups = 500
energies = np.logspace(np.log10(e_min), np.log10(e_max), groups + 1)
-mcdc.Tally(scores=["flux"], time=t_grid, energy=energies)
+tally = mcdc.Tally(scores=["flux"], time=t_grid, energy=energies)
+simulation.set_tallies([tally])
-mcdc.run()
+simulation.run()
diff --git a/test/regression/run.py b/test/regression/run.py
deleted file mode 100644
index 9602c11d6..000000000
--- a/test/regression/run.py
+++ /dev/null
@@ -1,259 +0,0 @@
-import h5py, os, sys, argparse, fnmatch
-import numpy as np
-from colorama import Fore, Style
-
-RELATIVE_TOLERANCE = 1e-6
-
-# Option parser
-parser = argparse.ArgumentParser(description="MC/DC regression test")
-parser.add_argument("--mode", type=str, choices=["python", "numba"], default="python")
-parser.add_argument("--target", type=str, choices=["cpu", "gpu"], default="cpu")
-parser.add_argument("--mpiexec", type=int, default=0)
-parser.add_argument("--srun", type=int, default=0)
-parser.add_argument("--name", type=str, default="ALL")
-parser.add_argument("--skip", type=str, default="NONE")
-args, unargs = parser.parse_known_args()
-
-# Parse
-mode = args.mode
-target = args.target
-mpiexec = args.mpiexec
-srun = args.srun
-name = args.name
-skip = args.skip
-
-regtest_data_name = "mcdc-regression_test_data"
-non_test_files = ["__pycache__", regtest_data_name, "tmp"]
-
-# Clone and update regression test data if needed
-if not os.path.isdir(regtest_data_name):
- os.system(f"git clone https://github.com/mcdc-project/{regtest_data_name}.git")
-else:
- os.chdir(regtest_data_name)
- os.system("git pull")
- os.chdir("..")
-
-# Get test names
-if name == "ALL":
- names = []
- for item in os.listdir():
- if os.path.isdir(item) and item not in non_test_files:
- names.append(item)
-else:
- names = [item for item in os.listdir() if fnmatch.fnmatch(item, name)]
-names.sort()
-
-# Remove skipped if specified
-if skip != "NONE":
- skips = [item for item in os.listdir() if fnmatch.fnmatch(item, skip)]
- for name in skips:
- print(Fore.YELLOW + "Note: Skipping %s" % name + Style.RESET_ALL)
- names.remove(name)
-
-# Skip domain decomp tests unless there are 4 MPI processes
-temp = names.copy()
-parallel_run = mpiexec > 0 or srun > 0
-for name in names:
- if name == "slab_reed_dd" and (
- not parallel_run or not (mpiexec % 4 == 0 and srun % 4 == 0)
- ):
- temp.remove(name)
- print(
- Fore.YELLOW
- + "Note: Skipping %s (require multiple of 4 MPI ranks)" % name
- + Style.RESET_ALL
- )
- elif name == "slab_reed_dd_3d" and (
- not parallel_run or not (mpiexec % 16 == 0 and srun % 16 == 0)
- ):
- temp.remove(name)
- print(
- Fore.YELLOW
- + "Note: Skipping %s (require multiple of 16 MPI ranks)" % name
- + Style.RESET_ALL
- )
-
-names = temp
-
-# Skip iqmc if GPU run
-if target == "gpu":
- temp = names.copy()
- for name in names:
- if ("iqmc" in name) or ("eigenvalue" in name):
- temp.remove(name)
- print(
- Fore.YELLOW + "Note: Skipping %s (GPU target)" % name + Style.RESET_ALL
- )
-names = temp
-
-# Data for each test
-printouts = []
-runtimes = []
-flags = []
-error_msgs = []
-crashes = []
-all_pass = True
-
-# Run all tests
-for i, name in enumerate(names):
- print("\n[%i/%i] " % (i + 1, len(names)) + name)
- error_msgs.append([])
- crashes.append(False)
- runtimes.append([0])
-
- # Change directory
- os.chdir(name)
-
- # Check test setup
- if not os.path.exists("input.py"):
- print(Fore.RED + " input.py is missing\n" + Style.RESET_ALL)
- sys.exit()
- if not os.path.exists("answer.h5"):
- print(Fore.RED + " answer.h5 is missing\n" + Style.RESET_ALL)
- sys.exit()
-
- # Delete output if exists
- if os.path.exists("output.h5"):
- os.remove("output.h5")
-
- # Run the test problem (redirect the stdout)
- if mpiexec > 1:
- gpus_per_task = ""
- if target == "gpu":
- gpus_per_task = f"--gpus-per-task=1 "
- os.system(
- "mpiexec -n %i %s python input.py --mode=%s --target=%s --output=output --no-progress-bar> tmp 2>&1"
- % (mpiexec, gpus_per_task, mode, target)
- )
- elif srun > 1:
- gpus_per_task = ""
- if target == "gpu":
- gpus_per_task = f"--gpus-per-task=1 "
- os.system(
- "srun -n %i %s python input.py --mode=%s --target=%s --output=output --no-progress-bar> tmp 2>&1"
- % (srun, gpus_per_task, mode, target)
- )
- else:
- os.system(
- "python input.py --mode=%s --target=%s --output=output --no-progress-bar > tmp 2>&1"
- % (mode, target)
- )
- with open("tmp") as f:
- printouts.append(f.read())
- os.remove("tmp")
-
- # Check if crashed
- if not os.path.exists("output.h5"):
- print(Fore.RED + " Failed: Run crashed" + Style.RESET_ALL)
- all_pass = False
- crashes[-1] = True
- os.chdir("..")
- continue
-
- # Get the output and the answer key
- output = h5py.File("output.h5", "r")
- answer = h5py.File("answer.h5", "r")
-
- runtimes[-1] = output["runtime/total"][()]
- print(" (%.2f seconds)" % runtimes[-1][0])
-
- # Compare mean, sdev, and uq_var (if available)
- if "iqmc" not in output.keys():
- name_root = "tallies"
- for tally in [key for key in answer[name_root].keys()]:
- name_tally = name_root + "/" + tally
- for score in [key for key in answer[name_tally].keys()]:
- if score in ["grid"]:
- continue
- name_score = name_tally + "/" + score
- for result in [key for key in answer[name_score].keys()]:
- name = name_score + "/" + result
- a = output[name][()]
- b = answer[name][()]
-
- # if (("sdev" in result) or ("uq_var" in result)) and (
- if ("uq_var" in result) and (args.target == "gpu"):
- continue
- # Passed?
- try:
- np.testing.assert_allclose(a, b, rtol=RELATIVE_TOLERANCE)
- print(
- Fore.GREEN + " {}: Passed".format(name) + Style.RESET_ALL
- )
- except AssertionError as error:
- all_pass = False
- error_msgs[-1].append(
- "Differences in %s"
- % (name + "/" + result + "\n" + "{}\n".format(error))
- )
- print(Fore.RED + " {}: Failed".format(name) + Style.RESET_ALL)
-
- # Other quantities
- for result_name in ["k_mean", "k_sdev", "k_cycle", "k_eff"]:
- if result_name not in output.keys():
- continue
-
- a = output[result_name][()]
- b = answer[result_name][()]
-
- # Passed?
- try:
- np.testing.assert_allclose(a, b, rtol=RELATIVE_TOLERANCE)
- print(Fore.GREEN + " {}: Passed".format(result_name) + Style.RESET_ALL)
- except AssertionError as error:
- all_pass = False
- error_msgs[-1].append(
- "Differences in {}\n{}".format(result_name, error)
- )
- print(Fore.RED + " {}: Failed".format(result_name) + Style.RESET_ALL)
-
- # iQMC flux
- if "iqmc" in output.keys():
- for score in [key for key in output["iqmc/tally/"].keys()]:
- name = f"iqmc/tally/{score}/mean"
- a = np.squeeze(output[name][()])
- b = np.squeeze(answer[name][()])
- # Passed?
- try:
- np.testing.assert_allclose(a, b, rtol=RELATIVE_TOLERANCE)
- print(Fore.GREEN + " {}: Passed".format(score) + Style.RESET_ALL)
- except AssertionError as error:
- all_pass = False
- error_msgs[-1].append("Differences in {}\n{}".format(score, a - b))
- print(Fore.RED + " {}: Failed".format(score) + Style.RESET_ALL)
-
- # Close files
- output.close()
- answer.close()
-
- # Move back up
- os.chdir("..")
-
-# Report test results
-N_fails = 0
-for i in range(len(names)):
- if crashes[i] or len(error_msgs[i]) > 0:
- N_fails += 1
-
-print(
- "\nTests passed: "
- + Fore.GREEN
- + "%i/%i" % (len(names) - N_fails, len(names))
- + Style.RESET_ALL
-)
-print("Tests failed: " + Fore.RED + "%i/%i" % (N_fails, len(names)) + Style.RESET_ALL)
-print(" (%.2f seconds)\n" % np.sum(np.array(runtimes)))
-for i in range(len(names)):
- if crashes[i]:
- print("\n" + "=" * 80)
- print("\n## {} crashed:".format(names[i]))
- print(printouts[i])
- if len(error_msgs[i]) > 0:
- print("\n" + "=" * 80)
- print("\n## {} failed:".format(names[i]))
- print(printouts[i])
- print("\n===\n")
- for msg in error_msgs[i]:
- print("\n# " + msg + "\n")
-
-assert all_pass
diff --git a/test/regression/slab_absorbium/input.py b/test/regression/slab_absorbium/input.py
index e46df04da..b715ff539 100644
--- a/test/regression/slab_absorbium/input.py
+++ b/test/regression/slab_absorbium/input.py
@@ -1,15 +1,18 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Slab absorbium")
+
# ======================================================================================
# Set model
# ======================================================================================
# Three slab layers with different purely-absorbing materials
# Set materials
-m1 = mcdc.MaterialMG(capture=np.array([1.0]))
-m2 = mcdc.MaterialMG(capture=np.array([1.5]))
-m3 = mcdc.MaterialMG(capture=np.array([2.0]))
+m1 = mcdc.Material.multigroup(capture=np.array([1.0]))
+m2 = mcdc.Material.multigroup(capture=np.array([1.5]))
+m3 = mcdc.Material.multigroup(capture=np.array([2.0]))
# Set surfaces
s1 = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
@@ -18,29 +21,36 @@
s4 = mcdc.Surface.PlaneZ(z=6.0, boundary_condition="vacuum")
# Set cells
-mcdc.Cell(region=+s1 & -s2, fill=m2)
-mcdc.Cell(region=+s2 & -s3, fill=m3)
-mcdc.Cell(region=+s3 & -s4, fill=m1)
+cell_1 = mcdc.Cell(region=+s1 & -s2, fill=m2)
+cell_2 = mcdc.Cell(region=+s2 & -s3, fill=m3)
+cell_3 = mcdc.Cell(region=+s3 & -s4, fill=m1)
+simulation.set_model([cell_1, cell_2, cell_3])
# ======================================================================================
# Set source
# ======================================================================================
# Uniform isotropic source throughout the domain
-mcdc.Source(z=[0.0, 6.0], isotropic=True, energy_group=0)
+source = mcdc.Source(z=[0.0, 6.0], isotropic=True, energy=0)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run mcdc
# ======================================================================================
# Tallies
-mcdc.Tally(surface=s4, scores=["current-net"])
+surface_tally = mcdc.Tally(surface=s4, scores=["current-net"])
mesh = mcdc.MeshStructured(z=np.linspace(0.0, 6.0, 61))
-mcdc.Tally(mesh=mesh, mu=np.linspace(-1.0, 1.0, 32 + 1), scores=["flux", "collision"])
+mesh_tally = mcdc.Tally(
+ mesh=mesh,
+ mu=np.linspace(-1.0, 1.0, 32 + 1),
+ scores=["flux", "collision"],
+)
+simulation.set_tallies([surface_tally, mesh_tally])
# Settings
-mcdc.settings.N_particle = 100
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 100
+simulation.settings.N_batch = 2
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/slab_isobeam_td/input.py b/test/regression/slab_isobeam_td/input.py
index 63f1927b9..a66da41f9 100644
--- a/test/regression/slab_isobeam_td/input.py
+++ b/test/regression/slab_isobeam_td/input.py
@@ -2,32 +2,37 @@
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Slab isotropic beam time dependent")
+
# ======================================================================================
# Set model
# ======================================================================================
# Finite homogeneous pure-absorbing slab
# Set materials
-m = mcdc.MaterialMG(capture=np.array([1.0]))
+m = mcdc.Material.multigroup(capture=np.array([1.0]))
# Set surfaces
s1 = mcdc.Surface.PlaneX(x=0.0, boundary_condition="vacuum")
s2 = mcdc.Surface.PlaneX(x=5.0, boundary_condition="vacuum")
# Set cells
-mcdc.Cell(region=+s1 & -s2, fill=m)
+cell = mcdc.Cell(region=+s1 & -s2, fill=m)
+simulation.set_model([cell])
# ======================================================================================
# Set source
# ======================================================================================
# Isotropic beam from left-end
-mcdc.Source(
+source = mcdc.Source(
position=(0.0, 0.0, 0.0),
white_direction=(1.0, 0.0, 0.0),
- energy_group=0,
+ energy=0,
time=[0.0, 5.0],
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -35,15 +40,16 @@
# Tallies
mesh = mcdc.MeshUniform(x=(0.0, 0.1, 50))
-mcdc.Tally(
+tally = mcdc.Tally(
mesh=mesh,
scores=["flux"],
time=np.linspace(0.0, 5.0, 51),
)
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 100
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 100
+simulation.settings.N_batch = 2
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/slab_isobeam_td_census/input.py b/test/regression/slab_isobeam_td_census/input.py
index f665e5c78..b2a4ccfb1 100644
--- a/test/regression/slab_isobeam_td_census/input.py
+++ b/test/regression/slab_isobeam_td_census/input.py
@@ -2,32 +2,37 @@
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Slab isotropic beam time-dependent census")
+
# ======================================================================================
# Set model
# ======================================================================================
# Finite homogeneous pure-absorbing slab
# Set materials
-m = mcdc.MaterialMG(capture=np.array([1.0]))
+m = mcdc.Material.multigroup(capture=np.array([1.0]))
# Set surfaces
s1 = mcdc.Surface.PlaneX(x=0.0, boundary_condition="vacuum")
s2 = mcdc.Surface.PlaneX(x=5.0, boundary_condition="vacuum")
# Set cells
-mcdc.Cell(region=+s1 & -s2, fill=m)
+cell = mcdc.Cell(region=+s1 & -s2, fill=m)
+simulation.set_model([cell])
# ======================================================================================
# Set source
# ======================================================================================
# Isotropic beam from left-end
-mcdc.Source(
+source = mcdc.Source(
position=(1e-10, 0.0, 0.0),
white_direction=(1.0, 0.0, 0.0),
- energy_group=0,
+ energy=0,
time=[0.0, 5.0],
)
+simulation.set_sources([source])
# ======================================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -35,20 +40,21 @@
# Tallies
mesh = mcdc.MeshUniform(x=(0.0, 0.1, 50))
-mcdc.Tally(
+tally = mcdc.Tally(
mesh=mesh,
scores=["flux"],
time=np.linspace(0.0, 5.0, 51),
)
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 100
-mcdc.settings.N_batch = 2
-mcdc.settings.source_bank_buffer_ratio = 5.0
-mcdc.settings.set_time_census(np.linspace(0.0, 5.0, 6)[1:])
+simulation.settings.N_particle = 100
+simulation.settings.N_batch = 2
+simulation.settings.source_bank_buffer_ratio = 5.0
+simulation.settings.set_time_census(np.linspace(0.0, 5.0, 6)[1:])
# Techniques
-mcdc.simulation.population_control()
+simulation.technique.population_control()
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/slab_reed/input.py b/test/regression/slab_reed/input.py
index e2dcd71b6..69cf5cb82 100644
--- a/test/regression/slab_reed/input.py
+++ b/test/regression/slab_reed/input.py
@@ -1,6 +1,9 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Reed slab")
+
# ======================================================================================
# Set model
# ======================================================================================
@@ -8,10 +11,10 @@
# Based on William H. Reed, NSE (1971), 46:2, 309-314, DOI: 10.13182/NSE46-309
# Set materials
-m1 = mcdc.MaterialMG(capture=np.array([50.0]))
-m2 = mcdc.MaterialMG(capture=np.array([5.0]))
-m3 = mcdc.MaterialMG(capture=np.array([0.0])) # Vacuum
-m4 = mcdc.MaterialMG(capture=np.array([0.1]), scatter=np.array([[0.9]]))
+m1 = mcdc.Material.multigroup(capture=np.array([50.0]))
+m2 = mcdc.Material.multigroup(capture=np.array([5.0]))
+m3 = mcdc.Material.multigroup(capture=np.array([0.0])) # Vacuum
+m4 = mcdc.Material.multigroup(capture=np.array([0.1]), scatter=np.array([[0.9]]))
# Set surfaces
s1 = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="reflective")
@@ -21,21 +24,23 @@
s5 = mcdc.Surface.PlaneZ(z=8.0, boundary_condition="vacuum")
# Set cells
-mcdc.Cell(region=+s1 & -s2, fill=m1)
-mcdc.Cell(region=+s2 & -s3, fill=m2)
-mcdc.Cell(region=+s3 & -s4, fill=m3)
-mcdc.Cell(region=+s4 & -s5, fill=m4)
+cell_1 = mcdc.Cell(region=+s1 & -s2, fill=m1)
+cell_2 = mcdc.Cell(region=+s2 & -s3, fill=m2)
+cell_3 = mcdc.Cell(region=+s3 & -s4, fill=m3)
+cell_4 = mcdc.Cell(region=+s4 & -s5, fill=m4)
+simulation.set_model([cell_1, cell_2, cell_3, cell_4])
# ======================================================================================
# Set source
# ======================================================================================
# Isotropic source in the absorbing medium
-mcdc.Source(z=[0.0, 2.0], isotropic=True, energy_group=0, probability=50.0)
+source_1 = mcdc.Source(z=[0.0, 2.0], isotropic=True, energy=0, probability=50.0)
# Isotropic source in the first half of the outermost medium,
# with 1/100 strength
-mcdc.Source(z=[5.0, 6.0], isotropic=True, energy_group=0, probability=0.5)
+source_2 = mcdc.Source(z=[5.0, 6.0], isotropic=True, energy=0, probability=0.5)
+simulation.set_sources([source_1, source_2])
# ======================================================================================
# Set tallies, settings, and run MC/DC
@@ -43,11 +48,12 @@
# Tallies
mesh = mcdc.MeshStructured(z=np.linspace(0.0, 8.0, 81))
-mcdc.Tally(mesh=mesh, scores=["flux"])
+tally = mcdc.Tally(mesh=mesh, scores=["flux"])
+simulation.set_tallies([tally])
# Settings
-mcdc.settings.N_particle = 4000
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 4000
+simulation.settings.N_batch = 2
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/sphere_in_cube/input.py b/test/regression/sphere_in_cube/input.py
index 8bedb7bbc..e16b46b41 100644
--- a/test/regression/sphere_in_cube/input.py
+++ b/test/regression/sphere_in_cube/input.py
@@ -1,14 +1,17 @@
import numpy as np
import mcdc
+# Create MC/DC simulation
+simulation = mcdc.Simulation("Sphere in cube")
+
# ======================================================================================
# Set model
# ======================================================================================
# Homogeneous pure-fission sphere inside a pure-scattering cube
# Set materials
-pure_f = mcdc.MaterialMG(fission=np.array([1.0]), nu_p=np.array([1.2]))
-pure_s = mcdc.MaterialMG(scatter=np.array([[1.0]]))
+pure_f = mcdc.Material.multigroup(fission=np.array([1.0]), nu_p=np.array([1.2]))
+pure_s = mcdc.Material.multigroup(scatter=np.array([[1.0]]))
# Set surfaces
sx1 = mcdc.Surface.PlaneX(x=0.0, boundary_condition="vacuum")
@@ -23,23 +26,25 @@
# Set cells
# Source
-mcdc.Cell(region=inside_box & ~inside_sphere, fill=pure_s)
+cube_cell = mcdc.Cell(region=inside_box & ~inside_sphere, fill=pure_s)
# Sphere
sphere_cell = mcdc.Cell(region=inside_sphere, fill=pure_f)
+simulation.set_model([cube_cell, sphere_cell])
# =============================================================================
# Set source
# =============================================================================
-mcdc.Source(
+source = mcdc.Source(
x=[0.0, 4.0],
y=[0.0, 4.0],
z=[0.0, 4.0],
isotropic=True,
- energy_group=0,
+ energy=0,
time=[0.0, 50.0],
)
+simulation.set_sources([source])
# =============================================================================
# Set tallies, settings, techniques, and run MC/DC
@@ -47,18 +52,19 @@
# Tallies
mesh = mcdc.MeshUniform(x=(0.0, 4.0, 1), y=(0.0, 4.0, 1), z=(0.0, 4.0, 1))
-mcdc.Tally(
+mesh_tally = mcdc.Tally(
mesh=mesh,
scores=["fission"],
)
-mcdc.Tally(cell=sphere_cell, scores=["fission"])
+cell_tally = mcdc.Tally(cell=sphere_cell, scores=["fission"])
+simulation.set_tallies([mesh_tally, cell_tally])
# Settings
-mcdc.settings.N_particle = 100
-mcdc.settings.N_batch = 2
+simulation.settings.N_particle = 100
+simulation.settings.N_batch = 2
# Techniques
-mcdc.simulation.implicit_capture()
+simulation.technique.implicit_capture()
# Run
-mcdc.run()
+simulation.run()
diff --git a/test/regression/test_regression.py b/test/regression/test_regression.py
new file mode 100644
index 000000000..1064fbef4
--- /dev/null
+++ b/test/regression/test_regression.py
@@ -0,0 +1,2 @@
+def test_regression(case_path, run_regression_case):
+ run_regression_case(case_path)
diff --git a/test/typecheck/public_api.py b/test/typecheck/public_api.py
new file mode 100644
index 000000000..b8f56f955
--- /dev/null
+++ b/test/typecheck/public_api.py
@@ -0,0 +1,56 @@
+"""Static checks for representative use of the public MC/DC API."""
+
+from pathlib import Path
+
+import numpy as np
+
+import mcdc
+
+
+def build_typed_simulation(output: Path) -> mcdc.Simulation:
+ neutron_data = mcdc.NeutronMultigroupData(capture=[0.1], scatter=[0.9])
+ material = mcdc.Material(name="Material", neutron_multigroup=neutron_data)
+
+ left = mcdc.Surface.PlaneX(x=-1.0, boundary_condition="vacuum")
+ right = mcdc.Surface.PlaneX(x=1.0, boundary_condition="vacuum")
+ cell = mcdc.Cell(region=+left & -right, fill=material)
+
+ universe = mcdc.Universe(name="Universe", cells=[cell])
+ lattice = mcdc.Lattice(x=(-1.0, 2.0, 1), universes=[[universe]])
+ root_cell = mcdc.Cell(fill=lattice)
+
+ source = mcdc.Source(position=[0.0, 0.0, 0.0], energy=1.0e6)
+ source.move(velocities=[[1.0, 0.0, 0.0]], durations=[1.0])
+ left.move(velocities=[[0.5, 0.0, 0.0]], durations=[1.0])
+
+ uniform_mesh = mcdc.MeshUniform(x=(-1.0, 0.1, 20))
+ structured_mesh = mcdc.MeshStructured(x=np.linspace(-1.0, 1.0, 21))
+ flux = mcdc.Tally(mesh=uniform_mesh, scores=["flux"])
+ density = mcdc.Tally(mesh=structured_mesh, scores=["density"])
+
+ simulation = mcdc.Simulation(name="Typed simulation")
+ simulation.set_model([root_cell])
+ simulation.set_sources([source])
+ simulation.set_tallies([flux, density])
+
+ simulation.settings.N_particle = 1_000
+ simulation.settings.set_time_census([1.0, 2.0])
+ simulation.settings.set_transported_particles(["neutron"])
+ simulation.technique.implicit_capture()
+ simulation.technique.weight_windows(
+ np.ones((1, 20, 1, 1, 3)),
+ mesh=uniform_mesh,
+ energy=np.array([0.0, 20.0e6]),
+ )
+
+ simulation.visualize_model(
+ vis_plane="xy",
+ x=[-1.0, 1.0],
+ y=[-1.0, 1.0],
+ z=0.0,
+ pixels=[100, 100],
+ colors={material: "red"},
+ time=[0.0],
+ save_as=output,
+ )
+ return simulation
diff --git a/test/unit/conftest.py b/test/unit/conftest.py
index 339f2e7f9..adbd9ed61 100644
--- a/test/unit/conftest.py
+++ b/test/unit/conftest.py
@@ -5,13 +5,35 @@
import pytest
-def pytest_addoption(parser):
- parser.addoption(
- "--mode",
- choices=["python", "numba"],
- default="python",
- help="MCDC execution mode for unit tests.",
- )
+@pytest.fixture
+def prepare_simulation():
+ """Compile and pack an explicit simulation for kernel-level unit tests."""
+ from mcdc.main import prepare
+ from mcdc.object_.cell import Cell
+ from mcdc.object_.simulation import Simulation
+
+ def _prepare(
+ *,
+ cells=(),
+ tallies=(),
+ sources=(),
+ objects=(),
+ configure=None,
+ ):
+ simulation = Simulation()
+ simulation.set_model(cells or (Cell(),))
+ simulation.set_tallies(tallies)
+ simulation.set_sources(sources)
+ if configure is not None:
+ configure(simulation)
+
+ simulation.compile()
+ for object_ in objects:
+ object_._compile_into_simulation(simulation)
+
+ return prepare(simulation)
+
+ return _prepare
@pytest.hookimpl(tryfirst=True)
diff --git a/test/unit/distributions/test_evaporation.py b/test/unit/distributions/test_evaporation.py
index f3b71c102..8b6f65084 100644
--- a/test/unit/distributions/test_evaporation.py
+++ b/test/unit/distributions/test_evaporation.py
@@ -4,6 +4,7 @@
import mcdc.numba_types as type_
import mcdc.transport.distribution as dist
+from mcdc.constant import DATA_TABLE
from .test_data import make_test_table_data_constant
@@ -17,9 +18,17 @@ def test_evaporation_sample(mock_rng_sequence, make_distribution_record):
)
data = np.asarray(data, dtype=np.float64)
- simulation_dtype = np.dtype([("table_data", type_.table_data, (1,))])
+ simulation_dtype = np.dtype(
+ [
+ ("data", type_.data, (1,)),
+ ("polynomial_data", type_.polynomial_data, (1,)),
+ ("table_data", type_.table_data, (1,)),
+ ]
+ )
simulation_container = np.zeros(1, dtype=simulation_dtype)
simulation = simulation_container[0]
+ simulation["data"][0]["sub_type"] = DATA_TABLE
+ simulation["data"][0]["sub_ID"] = 0
simulation["table_data"][0] = table
xi1, xi2 = 0.1, 0.2
diff --git a/test/unit/distributions/test_maxwellian.py b/test/unit/distributions/test_maxwellian.py
index 6d51f0c3d..78a05ad88 100644
--- a/test/unit/distributions/test_maxwellian.py
+++ b/test/unit/distributions/test_maxwellian.py
@@ -3,7 +3,7 @@
import mcdc.numba_types as type_
import mcdc.transport.distribution as dist
-from mcdc.constant import PI
+from mcdc.constant import DATA_TABLE, PI
from .test_data import make_test_table_data_constant
@@ -18,9 +18,17 @@ def test_maxwellian_sample(mock_rng_sequence, make_distribution_record):
)
data = np.asarray(data, dtype=np.float64)
- simulation_dtype = np.dtype([("table_data", type_.table_data, (1,))])
+ simulation_dtype = np.dtype(
+ [
+ ("data", type_.data, (1,)),
+ ("polynomial_data", type_.polynomial_data, (1,)),
+ ("table_data", type_.table_data, (1,)),
+ ]
+ )
simulation_container = np.zeros(1, dtype=simulation_dtype)
simulation = simulation_container[0]
+ simulation["data"][0]["sub_type"] = DATA_TABLE
+ simulation["data"][0]["sub_ID"] = 0
simulation["table_data"][0] = table
xi1, xi2, xi3 = 0.9, 0.9, 0.0
diff --git a/test/unit/distributions/test_multi_table.py b/test/unit/distributions/test_multi_table.py
index 6c335da26..2f63bf794 100644
--- a/test/unit/distributions/test_multi_table.py
+++ b/test/unit/distributions/test_multi_table.py
@@ -1,32 +1,38 @@
-import math
import numpy as np
-import mcdc.numba_types as type_
import mcdc.transport.distribution as dist
+from mcdc.object_.distribution import DistributionMultiTable
-from .test_data import make_test_multi_table_data
-
-def test_multi_table_distribution_sample(mock_rng_sequence, make_distribution_record):
- """
+def test_multi_table_distribution_sample(mock_rng_sequence, prepare_simulation):
# MCNP Theory & User Manual §2.4.3.5.4.4 (Law 4: Tabular Distribution)
- multi_table_dict, data = make_test_multi_table_data()
- multi_table = make_distribution_record(
- type_.multi_table_distribution, multi_table_dict
+ distribution = DistributionMultiTable(
+ grid=[1.0, 3.0],
+ offset=[0, 3],
+ value=[10.0, 20.0, 30.0, 100.0, 200.0, 300.0],
+ cdf=[0.0, 0.5, 1.0, 0.0, 0.6, 1.0],
)
- data = np.asarray(data, dtype=np.float64)
+ simulation_container, data = prepare_simulation(objects=[distribution])
+ simulation = simulation_container[0]
+ multi_table = simulation["multi_table_distributions"][distribution.sub_ID]
+
# For E_in = 2.0 on the grid [1, 3], Eq. (2.62) gives r = 0.5.
# xi_1 = 0.3 < r, so Eq. (2.64) selects l = i + 1, i.e. the second table.
xi1, xi2 = 0.3, 0.2
mock_rng = mock_rng_sequence(xi1, xi2)
- sampled_E = dist._sample_multi_table(2.0, mock_rng, multi_table, data, scale=True)
+ sampled_E = dist._sample_multi_table(
+ 2.0,
+ mock_rng,
+ multi_table,
+ simulation,
+ data,
+ scale=True,
+ )
# In the selected table, xi_2 = 0.2 falls in the first continuous bin.
- # Eq. (2.65) gives E' = E_l,k + (xi_2 - c_l,k) / p_l,k = 100 + 0.2 / 0.01 = 120.
- # The test data use constant p within the bin, so the linear-linear form in
- # Eq. (2.66) reduces to the same result.
- E_prime = 100.0 + (xi2 - 0.0) / 0.01
+ # The CDF rises from 0.0 to 0.6 over [100, 200], giving p = 0.006.
+ E_prime = 100.0 + (xi2 - 0.0) / 0.006
# Eq. (2.67) and Eq. (2.68) give the scaled bounds:
# E_1 = 10 + 0.5 * (100 - 10) = 55
# E_K = 30 + 0.5 * (300 - 30) = 165
@@ -36,4 +42,3 @@ def test_multi_table_distribution_sample(mock_rng_sequence, make_distribution_re
expected_E = 55.0 + (E_prime - 100.0) * (165.0 - 55.0) / (300.0 - 100.0)
np.testing.assert_allclose(sampled_E, expected_E, rtol=0.0, atol=1e-12)
- """
diff --git a/test/unit/distributions/test_nbody_correlated.py b/test/unit/distributions/test_nbody_correlated.py
index 65718bf13..b3ae02c39 100644
--- a/test/unit/distributions/test_nbody_correlated.py
+++ b/test/unit/distributions/test_nbody_correlated.py
@@ -1,39 +1,18 @@
-import math
import numpy as np
-import mcdc.numba_types as type_
import mcdc.transport.distribution as dist
-from mcdc.constant import DISTRIBUTION_N_BODY
+from mcdc.object_.distribution import DistributionNBody
-from .test_data import make_test_tabulated_data
-
-def test_nbody_sample_correlated(mock_rng_sequence, make_distribution_record):
- """
+def test_nbody_sample_correlated(mock_rng_sequence, prepare_simulation):
# MCNP Theory & User Manual §2.4.3.5.4.13 (Law 66: N-body Phase Space Distribution)
- table_dict, data = make_test_tabulated_data([2.0, 4.0, 6.0], [0.0, 0.4, 1.0])
- nbody = make_distribution_record(type_.nbody_distribution, table_dict)
- distribution = make_distribution_record(
- type_.distribution, {"child_type": DISTRIBUTION_N_BODY, "child_ID": 0}
- )
- data = np.asarray(data, dtype=np.float64)
-
- # Numba compiles all correlated-branch field accesses, so this container needs
- # the three correlated distribution arrays even though this test uses N-body only.
- simulation_dtype = np.dtype(
- [
- ("kalbach_mann_distributions", type_.kalbach_mann_distribution, (1,)),
- (
- "tabulated_energy_angle_distributions",
- type_.tabulated_energy_angle_distribution,
- (1,),
- ),
- ("nbody_distributions", type_.nbody_distribution, (1,)),
- ]
+ distribution = DistributionNBody(
+ values=[2.0, 4.0, 6.0],
+ probabilities=[1.0, 1.0, 1.0],
)
- simulation_container = np.zeros(1, dtype=simulation_dtype)
+ simulation_container, data = prepare_simulation(objects=[distribution])
simulation = simulation_container[0]
- simulation["nbody_distributions"][0] = nbody
+ distribution_base = simulation["distributions"][distribution.ID]
# First value samples energy, second value samples isotropic cosine.
xi1, xi2 = 0.2, 0.75
@@ -41,7 +20,7 @@ def test_nbody_sample_correlated(mock_rng_sequence, make_distribution_record):
sampled_E, sampled_mu = dist.sample_correlated_distribution(
2.0,
- distribution,
+ distribution_base,
mock_rng,
simulation,
data,
@@ -51,12 +30,12 @@ def test_nbody_sample_correlated(mock_rng_sequence, make_distribution_record):
# samples the cosine isotropically. This test is therefore checking the current
# reduced implementation, not reconstructing the full Law 66 rejection sampler
# from Eq. (2.103) through Eq. (2.106).
- # For the tabulated-energy part, xi_1 = 0.2 gives linear interpolation in the first
- # bin. For the angular part, MCNP Eq. (2.107) gives mu = 2 * xi_10 - 1 for
- # isotropic center-of-mass sampling.
- expected_E = 2.0 + (xi1 - 0.0) * (4.0 - 2.0) / (0.4 - 0.0)
+ # The constant PDF is normalized to 0.25 over [2, 6], so inverse-CDF sampling
+ # in the first bin gives E_out = 2 + xi_1 / 0.25.
+ # For the angular part, MCNP Eq. (2.107) gives mu = 2 * xi_10 - 1 for isotropic
+ # center-of-mass sampling.
+ expected_E = 2.0 + xi1 / 0.25
expected_mu = 2.0 * xi2 - 1.0
np.testing.assert_allclose(sampled_E, expected_E, rtol=0.0, atol=1e-12)
np.testing.assert_allclose(sampled_mu, expected_mu, rtol=0.0, atol=1e-12)
- """
diff --git a/test/unit/distributions/test_tabulated_distribution.py b/test/unit/distributions/test_tabulated_distribution.py
index 4733ee5ac..7fab4df59 100644
--- a/test/unit/distributions/test_tabulated_distribution.py
+++ b/test/unit/distributions/test_tabulated_distribution.py
@@ -1,26 +1,27 @@
-import math
import numpy as np
-import mcdc.numba_types as type_
import mcdc.transport.distribution as dist
+from mcdc.object_.distribution import DistributionTabulated
-from .test_data import make_test_tabulated_data
-
-def test_tabulated_distribution_sample(mock_rng_sequence, make_distribution_record):
- """
+def test_tabulated_distribution_sample(mock_rng_sequence, prepare_simulation):
# MCNP Theory & User Manual §2.4.3.5.4.4 (Law 4: Tabular Distribution)
- table_dict, data = make_test_tabulated_data([1.0, 3.0, 7.0], [0.0, 0.4, 1.0])
- table = make_distribution_record(type_.tabulated_distribution, table_dict)
- data = np.asarray(data, dtype=np.float64)
+ distribution = DistributionTabulated(
+ value=[1.0, 3.0, 7.0],
+ cdf=[0.0, 0.4, 1.0],
+ )
+ simulation_container, data = prepare_simulation(objects=[distribution])
+ simulation = simulation_container[0]
+ table = simulation["tabulated_distributions"][distribution.sub_ID]
+
xi1 = 0.2
mock_rng = mock_rng_sequence(xi1)
- sampled_E = dist.sample_tabulated(table, mock_rng, data)
+ sampled_E = dist.sample_tabulated(table, mock_rng, simulation, data)
+
# This is the single-table inverse-CDF interpolation used by the tabulated sampler:
# xi_1 = 0.2 lies in the first bin, so linear interpolation between
# (c_0, E_0) = (0.0, 1.0) and (c_1, E_1) = (0.4, 3.0) gives the expected value.
expected_E = 1.0 + (xi1 - 0.0) * (3.0 - 1.0) / (0.4 - 0.0)
np.testing.assert_allclose(sampled_E, expected_E, rtol=0.0, atol=1e-12)
- """
diff --git a/test/unit/geometry/surface/conftest.py b/test/unit/geometry/surface/conftest.py
index 97263e989..3a03147f3 100644
--- a/test/unit/geometry/surface/conftest.py
+++ b/test/unit/geometry/surface/conftest.py
@@ -1,21 +1,12 @@
import pytest
-@pytest.fixture(autouse=True)
-def reset_simulation():
- from mcdc.object_.simulation import simulation
-
- simulation.__init__()
- yield
- simulation.__init__()
-
-
@pytest.fixture
-def compile_surfaces():
+def compile_surfaces(prepare_simulation):
def _compile(static_surface_obj, moving_surface_obj):
- from mcdc.main import preparation
-
- structure_container, data = preparation()
+ structure_container, data = prepare_simulation(
+ objects=[static_surface_obj, moving_surface_obj]
+ )
structure = structure_container[0]
static_surface = structure["surfaces"][static_surface_obj.ID]
moving_surface = structure["surfaces"][moving_surface_obj.ID]
diff --git a/test/unit/geometry/surface/test_torus.py b/test/unit/geometry/surface/test_torus.py
index 75e2981f3..d31332a1c 100644
--- a/test/unit/geometry/surface/test_torus.py
+++ b/test/unit/geometry/surface/test_torus.py
@@ -26,7 +26,7 @@
@pytest.fixture(autouse=True)
-def setup_geometry_case():
+def setup_geometry_case(prepare_simulation):
global axis_aligned_surface, reference_surface, oblique_surface
global data, particle_container, particle
@@ -47,9 +47,13 @@ def setup_geometry_case():
r=r,
)
- from mcdc.main import preparation
-
- structure_container, data = preparation()
+ structure_container, data = prepare_simulation(
+ objects=[
+ axis_aligned_surface_obj,
+ reference_surface_obj,
+ oblique_surface_obj,
+ ]
+ )
structure = structure_container[0]
axis_aligned_surface = structure["surfaces"][axis_aligned_surface_obj.ID]
reference_surface = structure["surfaces"][reference_surface_obj.ID]
diff --git a/test/unit/geometry/test_interface.py b/test/unit/geometry/test_interface.py
new file mode 100644
index 000000000..126adc636
--- /dev/null
+++ b/test/unit/geometry/test_interface.py
@@ -0,0 +1,34 @@
+import numpy as np
+
+import mcdc
+import mcdc.numba_types as type_
+from mcdc.transport.geometry import locate_particle
+
+
+def test_locate_particle_without_material_speed(prepare_simulation):
+ material = mcdc.Material.multigroup(capture=np.array([1.0]))
+ boundary = mcdc.Surface.PlaneX(x=0.0)
+ universe = mcdc.Universe(cells=[mcdc.Cell(region=+boundary, fill=material)])
+ root_cell = mcdc.Cell(fill=universe, translation=[5.0, 0.0, 0.0])
+ simulation_container, data = prepare_simulation(cells=[root_cell])
+ simulation = simulation_container[0]
+
+ particle_container = np.zeros(1, dtype=type_.particle)
+ particle = particle_container[0]
+ particle["x"] = 6.0
+ particle["ux"] = 1.0
+ particle["cell_ID"] = -1
+ particle["material_ID"] = -1
+ original_coordinates = tuple(
+ particle[field] for field in ("x", "y", "z", "t", "ux", "uy", "uz")
+ )
+
+ found = locate_particle(particle_container, simulation, data)
+
+ assert found
+ assert particle["cell_ID"] == root_cell.ID
+ assert particle["material_ID"] == material.ID
+ assert (
+ tuple(particle[field] for field in ("x", "y", "z", "t", "ux", "uy", "uz"))
+ == original_coordinates
+ )
diff --git a/test/unit/geometry/test_lattice.py b/test/unit/geometry/test_lattice.py
new file mode 100644
index 000000000..d2331153e
--- /dev/null
+++ b/test/unit/geometry/test_lattice.py
@@ -0,0 +1,35 @@
+import numpy as np
+
+import mcdc
+
+
+def test_lattice_compiles_contained_universes():
+ lower_left = mcdc.Universe(name="Lower left")
+ lower_right = mcdc.Universe(name="Lower right")
+ upper_left = mcdc.Universe(name="Upper left")
+ upper_right = mcdc.Universe(name="Upper right")
+
+ lattice = mcdc.Lattice(
+ x=(-1.0, 1.0, 2),
+ y=(-1.0, 1.0, 2),
+ universes=[
+ [lower_left, lower_right],
+ [upper_left, upper_right],
+ ],
+ )
+
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell(fill=lattice)])
+ simulation.compile()
+
+ assert simulation.universes == [
+ simulation.root_universe,
+ lower_left,
+ lower_right,
+ upper_left,
+ upper_right,
+ ]
+ np.testing.assert_array_equal(
+ lattice.universe_IDs,
+ np.array([[[3], [1]], [[4], [2]]]),
+ )
diff --git a/test/unit/tally/conftest.py b/test/unit/tally/conftest.py
index cb276f87d..e330a3b92 100644
--- a/test/unit/tally/conftest.py
+++ b/test/unit/tally/conftest.py
@@ -4,22 +4,12 @@
import mcdc
import mcdc.numba_types as type_
from mcdc.constant import PARTICLE_NEUTRON
-from mcdc.main import preparation
-from mcdc.object_.simulation import simulation
-
-
-@pytest.fixture(autouse=True)
-def reset_simulation():
- # Keep simulation state isolated per test.
- simulation.__init__()
- yield
- simulation.__init__()
@pytest.fixture
def material_mg():
# Minimal multigroup material so particle speed is defined.
- return mcdc.MaterialMG(capture=np.array([1.0]))
+ return mcdc.Material.multigroup(capture=np.array([1.0]))
@pytest.fixture
@@ -32,7 +22,7 @@ def _particle(surface_ID, x, ux, cell_ID=-1, w=2.0):
particle["surface_ID"] = surface_ID
particle["cell_ID"] = cell_ID
particle["material_ID"] = 0
- particle["g"] = 0
+ particle["E"] = 0.0
particle["x"] = x
particle["y"] = 0.0
particle["z"] = 0.0
@@ -54,7 +44,11 @@ def slab_plane_x(material_mg):
s_right = mcdc.Surface.PlaneX(x=1.0, boundary_condition="vacuum")
c_left = mcdc.Cell(region=+s_left & -s_mid, fill=material_mg)
c_right = mcdc.Cell(region=+s_mid & -s_right, fill=material_mg)
+ simulation = mcdc.Simulation()
+ simulation.set_model([c_left, c_right])
+ simulation.compile()
return {
+ "simulation": simulation,
"s_left": s_left,
"s_mid": s_mid,
"s_right": s_right,
@@ -64,18 +58,21 @@ def slab_plane_x(material_mg):
@pytest.fixture
-def surface_crossing_tally_context(slab_plane_x):
+def surface_crossing_tally_context(slab_plane_x, prepare_simulation):
s_mid = slab_plane_x["s_mid"]
unbounded_tally_obj = mcdc.Tally(surface=s_mid, scores=["current-net"])
# Build compiled structures.
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[slab_plane_x["c_left"], slab_plane_x["c_right"]],
+ tallies=[unbounded_tally_obj],
+ )
mcdc_struct = mcdc_container[0]
# Compiled tally handles.
unbounded_tally = mcdc_struct["surface_crossing_tallies"][
- unbounded_tally_obj.child_ID
+ unbounded_tally_obj.sub_ID
]
# Particle for direct crossing-based tally-kernel testing.
@@ -83,7 +80,7 @@ def surface_crossing_tally_context(slab_plane_x):
particle = particle_container[0]
particle["particle_type"] = PARTICLE_NEUTRON
particle["material_ID"] = 0
- particle["g"] = 0
+ particle["E"] = 0.0
particle["x"] = 0.0
particle["y"] = 0.0
particle["z"] = 0.0
@@ -106,7 +103,7 @@ def surface_crossing_tally_context(slab_plane_x):
@pytest.fixture
def bin_value():
def _value(surface_crossing_tally, mcdc_struct, data):
- tally_base = mcdc_struct["tallies"][surface_crossing_tally["parent_ID"]]
+ tally_base = mcdc_struct["tallies"][surface_crossing_tally["base_ID"]]
return data[tally_base["bin_offset"]]
return _value
diff --git a/test/unit/tally/test_cell_filter_interior_plane.py b/test/unit/tally/test_cell_filter_interior_plane.py
index 09968dd10..74b93bb79 100644
--- a/test/unit/tally/test_cell_filter_interior_plane.py
+++ b/test/unit/tally/test_cell_filter_interior_plane.py
@@ -1,32 +1,37 @@
import numpy as np
import mcdc
-from mcdc.main import preparation
from mcdc.transport.simulation import surface_crossing
def _bin_value_score(tally, mcdc_struct, data, score_idx):
- tally_base = mcdc_struct["tallies"][tally["parent_ID"]]
+ tally_base = mcdc_struct["tallies"][tally["base_ID"]]
return data[tally_base["bin_offset"] + score_idx]
def test_cell_filter_ignores_redundant_interior_surface_crossing(
- material_mg, crossing_particle
+ material_mg, crossing_particle, prepare_simulation
):
s3 = mcdc.Surface.PlaneX(x=3.0)
s5 = mcdc.Surface.PlaneX(x=5.0)
# This region is equivalent to x < 5, but the expression includes a surface at s3.
c = mcdc.Cell(region=(-s5) | (-s3), fill=material_mg)
- mcdc.Cell(region=+s5, fill=material_mg)
+ outside = mcdc.Cell(region=+s5, fill=material_mg)
+ simulation = mcdc.Simulation()
+ simulation.set_model([c, outside])
+ simulation.compile()
tally_obj = mcdc.Tally(
cell=c,
scores=["current-net", "current-in", "current-out"],
)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[c, outside],
+ tallies=[tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- tally = mcdc_struct["surface_crossing_tallies"][tally_obj.child_ID]
+ tally = mcdc_struct["surface_crossing_tallies"][tally_obj.sub_ID]
# The particle crosses s3, but it remains inside c before and after crossing
particle_container = crossing_particle(s3.ID, x=3.0, ux=0.5, cell_ID=c.ID)
@@ -59,21 +64,30 @@ def _build_cube_with_interior_plane(material_mg):
outside_region = -x_min | +x_max | -y_min | +y_max | -z_min | +z_max
outside = mcdc.Cell(region=outside_region, fill=material_mg)
+ simulation = mcdc.Simulation()
+ simulation.set_model([cube, outside])
+ simulation.compile()
+
return {"cube": cube, "outside": outside, "x_min": x_min, "x_mid": x_mid}
-def _cube_current_tally(geom):
+def _cube_current_tally(geom, prepare_simulation):
tally_obj = mcdc.Tally(
cell=geom["cube"],
scores=["current-net", "current-in", "current-out"],
)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[geom["cube"], geom["outside"]],
+ tallies=[tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- tally = mcdc_struct["surface_crossing_tallies"][tally_obj.child_ID]
+ tally = mcdc_struct["surface_crossing_tallies"][tally_obj.sub_ID]
return tally, mcdc_struct, data
-def test_cell_filter_ignores_interior_plane_crossing(material_mg, crossing_particle):
+def test_cell_filter_ignores_interior_plane_crossing(
+ material_mg, crossing_particle, prepare_simulation
+):
"""
A particle crossing the cube's interior plane while remaining inside the
cube does not cross the cell boundary, so current-net/in/out must all be 0.
@@ -84,7 +98,7 @@ def test_cell_filter_ignores_interior_plane_crossing(material_mg, crossing_parti
(net = +w, in = +w, out = +w instead of 0).
"""
geom = _build_cube_with_interior_plane(material_mg)
- tally, mcdc_struct, data = _cube_current_tally(geom)
+ tally, mcdc_struct, data = _cube_current_tally(geom, prepare_simulation)
# The particle crosses the cube's interior plane but remains in the cube.
particle_container = crossing_particle(
@@ -106,7 +120,9 @@ def test_cell_filter_ignores_interior_plane_crossing(material_mg, crossing_parti
)
-def test_cell_filter_scores_real_face_entry(material_mg, crossing_particle):
+def test_cell_filter_scores_real_face_entry(
+ material_mg, crossing_particle, prepare_simulation
+):
"""
Contrast case on the same geometry: entering the cube through a real outer
face (x_min) from the outside cell is a genuine boundary crossing, so it
@@ -114,7 +130,7 @@ def test_cell_filter_scores_real_face_entry(material_mg, crossing_particle):
convention).
"""
geom = _build_cube_with_interior_plane(material_mg)
- tally, mcdc_struct, data = _cube_current_tally(geom)
+ tally, mcdc_struct, data = _cube_current_tally(geom, prepare_simulation)
# Entering through a real outer face should score incoming current.
particle_container = crossing_particle(
diff --git a/test/unit/tally/test_cell_tally.py b/test/unit/tally/test_cell_tally.py
index 2bd1ad350..9353d2197 100644
--- a/test/unit/tally/test_cell_tally.py
+++ b/test/unit/tally/test_cell_tally.py
@@ -2,27 +2,29 @@
import mcdc
-from mcdc.main import preparation
from mcdc.transport.simulation import surface_crossing
def _bin_value(surface_crossing_tally, mcdc_struct, data):
- tally_base = mcdc_struct["tallies"][surface_crossing_tally["parent_ID"]]
+ tally_base = mcdc_struct["tallies"][surface_crossing_tally["base_ID"]]
return data[tally_base["bin_offset"]]
def _bin_value_score(surface_crossing_tally, mcdc_struct, data, score_idx):
- tally_base = mcdc_struct["tallies"][surface_crossing_tally["parent_ID"]]
+ tally_base = mcdc_struct["tallies"][surface_crossing_tally["base_ID"]]
return data[tally_base["bin_offset"] + score_idx]
def test_surface_cell_filter_current_net_is_incoming_negative_outgoing_positive(
- slab_plane_x, crossing_particle
+ slab_plane_x, crossing_particle, prepare_simulation
):
current_tally_obj = mcdc.Tally(cell=slab_plane_x["c_right"], scores=["current-net"])
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[slab_plane_x["c_left"], slab_plane_x["c_right"]],
+ tallies=[current_tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.child_ID]
+ current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.sub_ID]
# Left -> right across the shared interior surface: incoming to c_right (-)
particle_container = crossing_particle(
@@ -50,15 +52,18 @@ def test_surface_cell_filter_current_net_is_incoming_negative_outgoing_positive(
def test_surface_cell_filter_current_records_in_and_out_separately(
- slab_plane_x, crossing_particle
+ slab_plane_x, crossing_particle, prepare_simulation
):
current_tally_obj = mcdc.Tally(
cell=slab_plane_x["c_right"],
scores=["current-net", "current-in", "current-out"],
)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[slab_plane_x["c_left"], slab_plane_x["c_right"]],
+ tallies=[current_tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.child_ID]
+ current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.sub_ID]
# One incoming and one outgoing crossing.
particle_container = crossing_particle(
@@ -90,15 +95,18 @@ def test_surface_cell_filter_current_records_in_and_out_separately(
def test_surface_cell_filter_current_counts_outgoing_to_vacuum(
- slab_plane_x, crossing_particle
+ slab_plane_x, crossing_particle, prepare_simulation
):
current_tally_obj = mcdc.Tally(
cell=slab_plane_x["c_right"],
scores=["current-net", "current-in", "current-out"],
)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[slab_plane_x["c_left"], slab_plane_x["c_right"]],
+ tallies=[current_tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.child_ID]
+ current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.sub_ID]
# c_right -> vacuum across the right boundary: outgoing (+)
particle_container = crossing_particle(
@@ -122,19 +130,25 @@ def test_surface_cell_filter_current_counts_outgoing_to_vacuum(
def test_surface_cell_filter_current_ignores_reflective_crossing(
- material_mg, crossing_particle
+ material_mg, crossing_particle, prepare_simulation
):
s_left = mcdc.Surface.PlaneX(x=-1.0, boundary_condition="vacuum")
s_right = mcdc.Surface.PlaneX(x=1.0, boundary_condition="reflective")
c_mid = mcdc.Cell(region=+s_left & -s_right, fill=material_mg)
+ simulation = mcdc.Simulation()
+ simulation.set_model([c_mid])
+ simulation.compile()
current_tally_obj = mcdc.Tally(
cell=c_mid,
scores=["current-net", "current-in", "current-out"],
)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[c_mid],
+ tallies=[current_tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.child_ID]
+ current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.sub_ID]
particle_container = crossing_particle(s_right.ID, x=1.0, ux=0.5)
particle = particle_container[0]
@@ -153,19 +167,25 @@ def test_surface_cell_filter_current_ignores_reflective_crossing(
def test_surface_cell_filter_current_scores_curved_boundary(
- material_mg, crossing_particle
+ material_mg, crossing_particle, prepare_simulation
):
s_cyl = mcdc.Surface.CylinderZ(center=(0.0, 0.0), radius=1.0)
c_inner = mcdc.Cell(region=-s_cyl, fill=material_mg)
c_outer = mcdc.Cell(region=+s_cyl, fill=material_mg)
+ simulation = mcdc.Simulation()
+ simulation.set_model([c_inner, c_outer])
+ simulation.compile()
current_tally_obj = mcdc.Tally(
cell=c_inner,
scores=["current-net", "current-in", "current-out"],
)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[c_inner, c_outer],
+ tallies=[current_tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.child_ID]
+ current_tally = mcdc_struct["surface_crossing_tallies"][current_tally_obj.sub_ID]
# Inner -> outer across the cylindrical surface: outgoing from c_inner (+)
particle_container = crossing_particle(
@@ -207,7 +227,7 @@ def test_surface_cell_filter_current_scores_curved_boundary(
def test_surface_cell_filter_scores_only_selected_surface(
- slab_plane_x, crossing_particle
+ slab_plane_x, crossing_particle, prepare_simulation
):
tally_obj = mcdc.Tally(
surface=slab_plane_x["s_mid"],
@@ -215,9 +235,12 @@ def test_surface_cell_filter_scores_only_selected_surface(
scores=["current-net", "current-in", "current-out"],
)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[slab_plane_x["c_left"], slab_plane_x["c_right"]],
+ tallies=[tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- tally = mcdc_struct["surface_crossing_tallies"][tally_obj.child_ID]
+ tally = mcdc_struct["surface_crossing_tallies"][tally_obj.sub_ID]
particle_container = crossing_particle(
slab_plane_x["s_right"].ID,
diff --git a/test/unit/tally/test_collision_tally.py b/test/unit/tally/test_collision_tally.py
index fcb4ca857..2e5b53a6b 100644
--- a/test/unit/tally/test_collision_tally.py
+++ b/test/unit/tally/test_collision_tally.py
@@ -10,6 +10,10 @@ def test_collision_tally_with_mesh_filter():
z=(-1.0, 1.0, 1),
)
tally = mcdc.Tally(mesh=mesh, scores=["energy_deposition"])
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.set_tallies([tally])
+ simulation.compile()
assert isinstance(tally, TallyCollision)
assert not tally.cell_filtered
diff --git a/test/unit/tally/test_nonbounded_surface_tally.py b/test/unit/tally/test_nonbounded_surface_tally.py
index 54671a92a..e0981957f 100644
--- a/test/unit/tally/test_nonbounded_surface_tally.py
+++ b/test/unit/tally/test_nonbounded_surface_tally.py
@@ -2,7 +2,6 @@
import pytest
import mcdc
-from mcdc.main import preparation
from mcdc.transport.simulation import surface_crossing
@@ -55,16 +54,19 @@ def test_unbounded_surface_crossing_tally_scoring(
def test_surface_crossing_tally_scores_vacuum_boundary(
- material_mg, bin_value, crossing_particle
+ material_mg, bin_value, crossing_particle, prepare_simulation
):
s_left = mcdc.Surface.PlaneX(x=-1.0, boundary_condition="vacuum")
s_right = mcdc.Surface.PlaneX(x=1.0, boundary_condition="vacuum")
- mcdc.Cell(region=+s_left & -s_right, fill=material_mg)
+ cell = mcdc.Cell(region=+s_left & -s_right, fill=material_mg)
tally_obj = mcdc.Tally(surface=s_right, scores=["current-net"])
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[cell],
+ tallies=[tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- tally = mcdc_struct["surface_crossing_tallies"][tally_obj.child_ID]
+ tally = mcdc_struct["surface_crossing_tallies"][tally_obj.sub_ID]
particle_container = crossing_particle(s_right.ID, x=1.0, ux=0.5)
particle = particle_container[0]
@@ -78,16 +80,19 @@ def test_surface_crossing_tally_scores_vacuum_boundary(
def test_surface_crossing_tally_scores_after_reflective_boundary(
- material_mg, bin_value, crossing_particle
+ material_mg, bin_value, crossing_particle, prepare_simulation
):
s_left = mcdc.Surface.PlaneX(x=-1.0, boundary_condition="vacuum")
s_right = mcdc.Surface.PlaneX(x=1.0, boundary_condition="reflective")
- mcdc.Cell(region=+s_left & -s_right, fill=material_mg)
+ cell = mcdc.Cell(region=+s_left & -s_right, fill=material_mg)
tally_obj = mcdc.Tally(surface=s_right, scores=["current-net"])
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(
+ cells=[cell],
+ tallies=[tally_obj],
+ )
mcdc_struct = mcdc_container[0]
- tally = mcdc_struct["surface_crossing_tallies"][tally_obj.child_ID]
+ tally = mcdc_struct["surface_crossing_tallies"][tally_obj.sub_ID]
particle_container = crossing_particle(s_right.ID, x=1.0, ux=0.5)
particle = particle_container[0]
diff --git a/test/unit/tally/test_tally_factory.py b/test/unit/tally/test_tally_factory.py
index be95c7af0..93e325762 100644
--- a/test/unit/tally/test_tally_factory.py
+++ b/test/unit/tally/test_tally_factory.py
@@ -1,6 +1,8 @@
+import numpy as np
import pytest
import mcdc
+from mcdc.constant import PARTICLE_ANY
from mcdc.object_.tally import (
TallyCollision,
@@ -75,22 +77,82 @@ def test_tally_factory_allows_combined_supported_filters(slab_plane_x):
cell=slab_plane_x["c_right"],
scores=["current-net"],
)
- assert surface_cell_tally.surface_filtered
- assert surface_cell_tally.surface_filter_ID == slab_plane_x["s_mid"].ID
- assert surface_cell_tally.cell_filtered
- assert surface_cell_tally.cell_filter_ID == slab_plane_x["c_right"].ID
-
cell_mesh_tally = mcdc.Tally(
cell=slab_plane_x["c_right"],
mesh=mesh,
scores=["flux"],
)
+
+ simulation = slab_plane_x["simulation"]
+ simulation.set_tallies([surface_cell_tally, cell_mesh_tally])
+ simulation.compile()
+
+ assert surface_cell_tally.surface_filtered
+ assert surface_cell_tally.surface_filter_ID == slab_plane_x["s_mid"].ID
+ assert surface_cell_tally.cell_filtered
+ assert surface_cell_tally.cell_filter_ID == slab_plane_x["c_right"].ID
assert cell_mesh_tally.cell_filtered
assert cell_mesh_tally.cell_filter_ID == slab_plane_x["c_right"].ID
assert cell_mesh_tally.mesh_filtered
assert cell_mesh_tally.mesh_filter_ID == mesh.ID
+def test_all_energy_filter_resizes_standard_multigroup_tally_bins(
+ prepare_simulation,
+):
+ material = mcdc.Material.multigroup(capture=np.ones(3))
+ cell = mcdc.Cell(fill=material)
+ tally = mcdc.Tally(scores=["flux"], energy="all")
+
+ simulation_container, data = prepare_simulation(cells=[cell], tallies=[tally])
+ simulation = simulation_container[0]
+ tally_record = simulation["tallies"][tally.ID]
+
+ assert tally.particle_type == PARTICLE_ANY
+ assert "_energy_all" not in tally_record.dtype.names
+ np.testing.assert_array_equal(tally.energy, [-0.5, 0.5, 1.5, 2.5])
+ assert tally.bin_shape == [1, 1, 3, 1, 1]
+ assert tally.stride_energy == 1
+ assert tally.stride_azi == 3
+ assert tally.stride_mu == 3
+ assert tally_record["bin_length"] == 3
+
+ bin_start = tally_record["bin_offset"]
+ bin_stop = bin_start + tally_record["bin_length"]
+ assert len(data[bin_start:bin_stop]) == 3
+
+
+def test_all_energy_filter_rejects_hybrid_multigroup_transport(
+ prepare_simulation,
+ capsys,
+):
+ material_a = mcdc.Material.multigroup(
+ capture=np.ones(2),
+ energy_grid=[0.0, 1.0, 2.0],
+ )
+ material_b = mcdc.Material.multigroup(
+ capture=np.ones(2),
+ energy_grid=[0.0, 2.0, 3.0],
+ )
+ cells = [mcdc.Cell(fill=material_a), mcdc.Cell(fill=material_b)]
+ tally = mcdc.Tally(scores=["flux"], energy="all")
+
+ with pytest.raises(SystemExit):
+ prepare_simulation(cells=cells, tallies=[tally])
+
+ assert (
+ 'The energy="all" filter requires standard neutron multigroup transport'
+ in capsys.readouterr().out
+ )
+
+
+def test_tally_factory_rejects_unsupported_energy_filter(capsys):
+ with pytest.raises(SystemExit):
+ mcdc.Tally(scores=["flux"], energy="groups")
+
+ assert "Unsupported tally energy filter: groups" in capsys.readouterr().out
+
+
def test_tally_factory_rejects_mixed_estimator_scores(capsys):
with pytest.raises(SystemExit):
mcdc.Tally(scores=["flux", "energy_deposition"])
diff --git a/test/unit/tally/test_tracklength_tally.py b/test/unit/tally/test_tracklength_tally.py
index 820d826a2..ded758486 100644
--- a/test/unit/tally/test_tracklength_tally.py
+++ b/test/unit/tally/test_tracklength_tally.py
@@ -4,6 +4,9 @@
def test_tracklength_tally_with_cell_filter(slab_plane_x):
tally = mcdc.Tally(cell=slab_plane_x["c_left"], scores=["flux", "capture"])
+ simulation = slab_plane_x["simulation"]
+ simulation.set_tallies([tally])
+ simulation.compile()
assert isinstance(tally, TallyTracklength)
assert tally.cell_filtered
@@ -20,6 +23,10 @@ def test_tracklength_tally_with_mesh_filter():
z=(-1.0, 1.0, 1),
)
tally = mcdc.Tally(mesh=mesh, scores=["flux"])
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.set_tallies([tally])
+ simulation.compile()
assert isinstance(tally, TallyTracklength)
assert not tally.cell_filtered
diff --git a/test/unit/technique/test_weight_windows.py b/test/unit/technique/test_weight_windows.py
index 5f0c64a00..d9a7b3439 100644
--- a/test/unit/technique/test_weight_windows.py
+++ b/test/unit/technique/test_weight_windows.py
@@ -2,7 +2,6 @@
import numpy as np
import pytest
-from mcdc.main import preparation
import mcdc.numba_types as type_
from mcdc.transport.technique import (
weight_roulette,
@@ -34,7 +33,13 @@ def make_mesh():
return mesh, N
-def make_ww_model_params(lower=0.1, target=1.0, upper=1.0, mess_up_size=False):
+def make_ww_model_params(
+ prepare_simulation,
+ lower=0.1,
+ target=1.0,
+ upper=1.0,
+ mess_up_size=False,
+):
mesh, N = make_mesh()
Ne = 1
@@ -47,13 +52,14 @@ def make_ww_model_params(lower=0.1, target=1.0, upper=1.0, mess_up_size=False):
ww_array[..., 1] = target
ww_array[..., 2] = upper
- mcdc.simulation.weight_windows(ww_array, mesh=mesh)
+ def configure(simulation):
+ simulation.technique.weight_windows(ww_array, mesh=mesh)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(configure=configure)
return mcdc_container[0], data
-def make_ww_model_distinct():
+def make_ww_model_distinct(prepare_simulation):
mesh, N = make_mesh()
energy = np.linspace(0.0, 6.0, 7)
Ne = 6
@@ -70,9 +76,10 @@ def make_ww_model_distinct():
ww_array[e, i, j, k, 1] = 10000 + val
ww_array[e, i, j, k, 2] = 20000 + val
- mcdc.simulation.weight_windows(ww_array, mesh=mesh, energy=energy)
+ def configure(simulation):
+ simulation.technique.weight_windows(ww_array, mesh=mesh, energy=energy)
- mcdc_container, data = preparation()
+ mcdc_container, data = prepare_simulation(configure=configure)
return mcdc_container[0], data
@@ -106,9 +113,9 @@ def make_ww_model_distinct():
),
],
)
-def test_error_throw(capsys, kwargs, expected_msg):
+def test_error_throw(prepare_simulation, capsys, kwargs, expected_msg):
with pytest.raises(SystemExit):
- make_ww_model_params(**kwargs)
+ make_ww_model_params(prepare_simulation, **kwargs)
out = capsys.readouterr().out
assert expected_msg in out
@@ -133,8 +140,8 @@ def test_roulette_from_weight_bounds():
assert p["w"] == target or not p["alive"]
-def test_split_from_weight_window():
- program, data = make_ww_model_distinct()
+def test_split_from_weight_window(prepare_simulation):
+ program, data = make_ww_model_distinct(prepare_simulation)
def run_split(initial_weight, w_upper=1.0, w_target=0.5, w_lower=0.0):
particles = np.zeros(1, type_.particle)
@@ -188,12 +195,11 @@ def run_split(initial_weight, w_upper=1.0, w_target=0.5, w_lower=0.0):
assert total_banked < maximum_bank
-def test_query_weight_window():
+def test_query_weight_window(prepare_simulation):
p = np.zeros(1, type_.particle_data)
- program, data = make_ww_model_distinct()
+ program, data = make_ww_model_distinct(prepare_simulation)
simulation = util.access_simulation(program)
- simulation["settings"]["neutron_multigroup_mode"] = False
# hardcode mesh params
pitch, height, N = 2.0, 10.0, 3
nx, ny, nz = N, N, N
diff --git a/test/unit/test_annotation_shape.py b/test/unit/test_annotation_shape.py
new file mode 100644
index 000000000..f2f058de5
--- /dev/null
+++ b/test/unit/test_annotation_shape.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+from typing import Annotated
+
+import numpy as np
+import pytest
+from numpy.typing import NDArray
+
+from mcdc.code_factory.numba_layers_generator import (
+ _accessor_1d_all,
+ _accessor_1d_last,
+)
+from mcdc.object_.base import MCDCBase
+from mcdc.object_.util import check_type, parse_dimension_expression
+
+
+class DimensionedObject(MCDCBase):
+ label = "dimensioned_object"
+
+ G: int
+ energy: Annotated[NDArray[np.float64], ("G+1",)]
+
+ def __init__(self, G, energy):
+ self.G = G
+ self.energy = energy
+
+
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ ("G", ("G", 0)),
+ ("G+1", ("G", 1)),
+ ("N - 2", ("N", -2)),
+ ],
+)
+def test_parse_dimension_expression(expression, expected):
+ assert parse_dimension_expression(expression) == expected
+
+
+@pytest.mark.parametrize("expression", ["1+G", "G*2", "G+1+2", "G + value"])
+def test_parse_dimension_expression_rejects_unsupported_syntax(expression):
+ with pytest.raises(ValueError, match="Invalid dimension expression"):
+ parse_dimension_expression(expression)
+
+
+def test_stringified_annotation_resolves_dimension_offset():
+ dimensioned = DimensionedObject(2, np.zeros(3))
+
+ assert dimensioned.energy.shape == (3,)
+
+
+def test_structured_annotation_resolves_dimension_offset():
+ dimensioned = DimensionedObject.__new__(DimensionedObject)
+ dimensioned.G = 2
+ hint = Annotated[NDArray[np.float64], ("G+1",)]
+
+ assert check_type(np.zeros(3), hint, DimensionedObject, dimensioned)
+ assert not check_type(np.zeros(2), hint, DimensionedObject, dimensioned)
+
+
+def test_stringified_annotation_rejects_incorrect_offset_shape(capsys):
+ with pytest.raises(SystemExit):
+ DimensionedObject(2, np.zeros(2))
+
+ assert "energy must be" in capsys.readouterr().out
+
+
+def test_generated_accessor_resolves_dimension_offset():
+ all_source = _accessor_1d_all("mgxs", "energy", "G+1")
+ last_source = _accessor_1d_last("mgxs", "energy", "N - 2")
+
+ assert 'size = mgxs["G"] + 1' in all_source
+ assert 'size = mgxs["N"] - 2' in last_source
diff --git a/test/unit/test_config.py b/test/unit/test_config.py
new file mode 100644
index 000000000..210d81150
--- /dev/null
+++ b/test/unit/test_config.py
@@ -0,0 +1,21 @@
+import mcdc
+import mcdc.config as config
+
+from mcdc.config import override_settings
+
+
+def test_compilation_applies_command_line_overrides(monkeypatch):
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ monkeypatch.setattr(config, "target", "cpu")
+ monkeypatch.setattr(config.args, "N_particle", 100)
+ monkeypatch.setattr(config.args, "N_batch", None)
+ monkeypatch.setattr(config.args, "output", None)
+ monkeypatch.setattr(
+ config.args, "progress_bar", simulation.settings.use_progress_bar
+ )
+
+ simulation.compile()
+
+ assert simulation.settings.N_particle == 100
+ assert not override_settings(simulation)
diff --git a/test/unit/test_example_inputs.py b/test/unit/test_example_inputs.py
new file mode 100644
index 000000000..d6c75ee13
--- /dev/null
+++ b/test/unit/test_example_inputs.py
@@ -0,0 +1,48 @@
+import runpy
+from pathlib import Path
+
+import pytest
+
+import mcdc
+
+EXAMPLES_ROOT = Path(__file__).parents[2] / "examples"
+EXCLUDED_EXAMPLES = ["hybrid_multigroup"]
+
+# Leave data-library-dependent examples to regression testing
+EXAMPLE_INPUTS = tuple(
+ input_path
+ for input_path in sorted(EXAMPLES_ROOT.glob("**/input.py"))
+ if input_path.parent.name not in EXCLUDED_EXAMPLES
+)
+assert EXAMPLE_INPUTS, "No example inputs found"
+
+
+def example_id(input_path):
+ return str(input_path.relative_to(EXAMPLES_ROOT).parent)
+
+
+@pytest.mark.parametrize("input_path", EXAMPLE_INPUTS, ids=example_id)
+def test_example_constructs_and_compiles_simulation(input_path, monkeypatch):
+ """Validate every example input without running particle transport."""
+ compiled_simulations = []
+
+ def compile_only(simulation, *args, **kwargs):
+ simulation.compile()
+ compiled_simulations.append(simulation)
+
+ monkeypatch.setattr(mcdc.Simulation, "run", compile_only)
+ monkeypatch.setattr(mcdc.Simulation, "visualize_model", compile_only)
+ monkeypatch.chdir(input_path.parent)
+
+ runpy.run_path(input_path.name, run_name="__main__")
+
+ assert compiled_simulations
+ unique_simulations = {
+ id(simulation): simulation for simulation in compiled_simulations
+ }
+ assert len(unique_simulations) == 1
+ simulation = next(iter(unique_simulations.values()))
+ assert simulation.compiled
+ assert simulation.cells
+ assert simulation.sources
+ assert simulation.tallies
diff --git a/test/unit/test_material.py b/test/unit/test_material.py
new file mode 100644
index 000000000..698b889c3
--- /dev/null
+++ b/test/unit/test_material.py
@@ -0,0 +1,160 @@
+import pytest
+
+import mcdc
+from mcdc.constant import FILL_MATERIAL
+from mcdc.object_.base import MCDCObject, MCDCPolymorphic
+from mcdc.object_.material import Material
+from mcdc.object_.nuclide import Nuclide
+
+
+def test_material_is_the_only_public_non_polymorphic_material_type():
+ assert mcdc.Material is Material
+ assert issubclass(Material, MCDCObject)
+ assert not issubclass(Material, MCDCPolymorphic)
+ assert not hasattr(mcdc, "MaterialMG")
+
+
+def test_multigroup_factory_returns_a_material_and_forwards_transport_data():
+ material = Material.multigroup(
+ name="Delayed fuel",
+ fission=[0.2, 0.3],
+ nu_d=[[0.1, 0.2], [0.3, 0.4]],
+ chi_d=[[1.0, 3.0], [3.0, 1.0]],
+ decay_rate=[0.01, 0.02],
+ energy_grid=[1.0e-5, 1.0, 20.0e6],
+ )
+
+ assert type(material) is Material
+ assert material.name == "Delayed fuel"
+ assert material.has_neutron_multigroup
+ assert material.neutron_multigroup.G == 2
+ assert material.neutron_multigroup.J == 2
+
+
+def test_material_accepts_native_multigroup_and_hybrid_data():
+ native = Material(nuclide_composition={"H1": 0.1})
+ neutron_multigroup = mcdc.NeutronMultigroupData(capture=[0.2])
+ multigroup = Material(neutron_multigroup=neutron_multigroup)
+ hybrid = Material(
+ element_composition={"H": 0.1},
+ neutron_multigroup=mcdc.NeutronMultigroupData(
+ scatter=[[0.3]],
+ energy_grid=[1.0e-5, 20.0e6],
+ ),
+ )
+
+ assert list(nuclide.name for nuclide in native.nuclides) == ["H1"]
+ assert not native.has_neutron_multigroup
+ assert native.neutron_multigroup.G == 0
+ assert multigroup.has_neutron_multigroup
+ assert multigroup.neutron_multigroup is neutron_multigroup
+ assert hybrid.has_neutron_multigroup
+ assert hybrid.elements[0].name == "H"
+ assert hybrid.neutron_multigroup.G == 1
+
+
+@pytest.mark.parametrize(
+ "kwargs, expected_message",
+ [
+ ({}, "Material requires"),
+ (
+ {
+ "nuclide_composition": {"H1": 0.1},
+ "element_composition": {"H": 0.1},
+ },
+ "Cannot specify both",
+ ),
+ (
+ {"neutron_multigroup": object()},
+ "neutron_multigroup must be a NeutronMultigroupData object",
+ ),
+ (
+ {"neutron_multigroup": mcdc.NeutronMultigroupData()},
+ "must define at least one energy group",
+ ),
+ (
+ {
+ "nuclide_composition": {"H1": 0.1},
+ "neutron_multigroup": mcdc.NeutronMultigroupData(capture=[0.2]),
+ },
+ "requires an explicit neutron multigroup energy_grid",
+ ),
+ (
+ {
+ "element_composition": {"H": 0.1},
+ "neutron_multigroup": mcdc.NeutronMultigroupData(capture=[0.2]),
+ },
+ "requires an explicit neutron multigroup energy_grid",
+ ),
+ ],
+)
+def test_material_rejects_invalid_representations(kwargs, expected_message, capsys):
+ with pytest.raises(SystemExit):
+ Material(**kwargs)
+
+ assert expected_message in capsys.readouterr().out
+
+
+def test_absent_mg_points_to_the_reserved_simulation_object(monkeypatch):
+ # Isolate object registration from native data-library loading
+ def compile_nuclide(nuclide, simulation):
+ nuclide.fissionable = False
+ return MCDCObject._compile_into_simulation(nuclide, simulation)
+
+ monkeypatch.setattr(Nuclide, "_compile_into_simulation", compile_nuclide)
+
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.compile()
+ material = Material(nuclide_composition={"H1": 0.1})
+
+ material._compile_into_simulation(simulation)
+
+ assert material.neutron_multigroup is simulation.neutron_multigroup_data[0]
+ assert material.neutron_multigroup.ID == 0
+ assert not material.has_neutron_multigroup
+ assert simulation.materials == [material]
+ assert len(simulation.neutron_multigroup_data) == 1
+
+
+def test_shared_mg_is_registered_once_for_multiple_materials():
+ neutron_multigroup = mcdc.NeutronMultigroupData(capture=[0.1])
+ material_a = Material(neutron_multigroup=neutron_multigroup)
+ material_b = Material(neutron_multigroup=neutron_multigroup)
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.compile()
+
+ material_a._compile_into_simulation(simulation)
+ material_b._compile_into_simulation(simulation)
+
+ assert simulation.materials == [material_a, material_b]
+ assert len(simulation.neutron_multigroup_data) == 2
+ assert simulation.neutron_multigroup_data[1] is neutron_multigroup
+ assert neutron_multigroup.ID == 1
+
+
+def test_material_mg_reference_is_packed(prepare_simulation):
+ neutron_multigroup = mcdc.NeutronMultigroupData(capture=[0.1, 0.2])
+ material = Material(neutron_multigroup=neutron_multigroup)
+ cell = mcdc.Cell(fill=material)
+
+ simulation_container, _ = prepare_simulation(cells=[cell])
+ simulation = simulation_container[0]
+ packed = simulation["materials"][0]
+ packed_cell = simulation["cells"][0]
+
+ assert simulation["N_material"] == 1
+ assert simulation["N_neutron_multigroup_data"] == 2
+ assert packed["ID"] == 0
+ assert packed["has_neutron_multigroup"]
+ assert packed["neutron_multigroup_ID"] == 1
+ assert packed_cell["fill_type"] == FILL_MATERIAL
+ assert packed_cell["fill_ID"] == 0
+
+
+def test_multigroup_factory_rejects_zero_group_data(capsys):
+ with pytest.raises(SystemExit):
+ Material.multigroup()
+
+ assert "must define at least one energy group" in capsys.readouterr().out
diff --git a/test/unit/test_numba_layers_generator.py b/test/unit/test_numba_layers_generator.py
new file mode 100644
index 000000000..21531dced
--- /dev/null
+++ b/test/unit/test_numba_layers_generator.py
@@ -0,0 +1,171 @@
+from typing import Annotated
+
+import numpy as np
+import pytest
+from numpy.typing import NDArray
+
+from mcdc.code_factory.numba_layers_generator import (
+ AccessorTarget,
+ _accessor_1d_all,
+ _accessor_1d_element,
+ _accessor_1d_last,
+ _accessor_2d_element,
+ _accessor_3d_element,
+ _accessor_4d_element,
+ set_object,
+ set_structure,
+ validate_unique_class_labels,
+ validate_accessor_targets,
+)
+from mcdc.mcdc_get import cell as get_cell
+from mcdc.object_.base import MCDCBase
+from mcdc.object_.surface import Surface
+
+
+class EmbeddedLeaf(MCDCBase):
+ label = "child"
+
+ active: bool
+ values: NDArray[np.float64]
+
+ def __init__(self):
+ self.active = True
+ self.values = np.array([2.0, 3.0])
+
+
+class EmbeddedOwner(MCDCBase):
+ label = "embedded_owner_test"
+
+ child: EmbeddedLeaf
+
+ def __init__(self):
+ self.child = EmbeddedLeaf()
+
+
+class DuplicateLabelA(MCDCBase):
+ label = "duplicate_label"
+
+
+class DuplicateLabelB(MCDCBase):
+ label = "duplicate_label"
+
+
+def test_set_structure_tracks_logical_accessor_types():
+ annotations = {
+ "example": {
+ "integer_values": NDArray[np.int64],
+ "float_values": NDArray[np.float64],
+ "integer_grid": Annotated[NDArray[np.int64], ("Nx", "Ny", "Nz")],
+ "surfaces": list[Surface],
+ }
+ }
+ structures = {"example": []}
+ accessor_targets = {"example": []}
+
+ set_structure("example", structures, accessor_targets, annotations)
+
+ assert accessor_targets["example"] == [
+ ("integer_values", ("integer_values_length",), True),
+ ("float_values", ("float_values_length",), False),
+ ("integer_grid", ("Nx", "Ny", "Nz"), True),
+ ("surface_IDs", ("N_surface",), True),
+ ]
+
+
+def test_embedded_mcdc_base_structures_and_records_are_recursive():
+ annotations = {
+ EmbeddedOwner.label: {"child": EmbeddedLeaf},
+ EmbeddedLeaf.label: {
+ "active": bool,
+ "values": NDArray[np.float64],
+ },
+ }
+ structures = {label: [] for label in annotations}
+ accessor_targets = {label: [] for label in annotations}
+ records = {label: {} for label in annotations}
+ order = []
+
+ set_structure(
+ EmbeddedOwner.label,
+ structures,
+ accessor_targets,
+ annotations,
+ order=order,
+ )
+
+ owner = EmbeddedOwner()
+ data = {"size": 0}
+ set_object(owner, annotations, structures, records, data)
+ data = {"size": 0, "array": np.zeros(data["size"])}
+ set_object(owner, annotations, structures, records, data, set_data=True)
+
+ assert order == [EmbeddedLeaf.label, EmbeddedOwner.label]
+ assert structures[EmbeddedOwner.label][0][0] == "child"
+ assert records[EmbeddedOwner.label]["child"]["active"]
+ np.testing.assert_array_equal(data["array"], owner.child.values)
+
+
+def test_runtime_class_labels_must_be_unique(capsys):
+ with pytest.raises(SystemExit):
+ validate_unique_class_labels([DuplicateLabelA, DuplicateLabelB])
+
+ assert "Duplicate MC/DC class label 'duplicate_label'" in capsys.readouterr().out
+
+
+def test_scalar_integer_getters_cast_values_from_data():
+ assert "return int64(data[offset + index])" in _accessor_1d_element(
+ "example", "values", cast_to_int=True
+ )
+ assert "return int64(data[end - 1])" in _accessor_1d_last(
+ "example", "values", "values_length", cast_to_int=True
+ )
+ assert "return int64(data[offset + index_1 * stride + index_2])" in (
+ _accessor_2d_element("example", "values", "Ny", cast_to_int=True)
+ )
+ assert "return int64(data[offset + index_1 * stride_2 * stride_3" in (
+ _accessor_3d_element("example", "values", "Ny", "Nz", cast_to_int=True)
+ )
+
+
+def test_float_and_bulk_getters_remain_zero_copy_views():
+ assert "return data[offset + index]" in _accessor_1d_element("example", "values")
+ assert "return data[start:end]" in _accessor_1d_all(
+ "example", "values", "values_length"
+ )
+
+
+def test_mixed_literal_and_named_strides_are_generated():
+ getter_3d = _accessor_3d_element("example", "values", 3, "Nz")
+ getter_4d = _accessor_4d_element("example", "values", 2, "Nz", 4)
+
+ assert "stride_2 = 3" in getter_3d
+ assert 'stride_3 = example["Nz"]' in getter_3d
+ assert "stride_2 = 2" in getter_4d
+ assert 'stride_3 = example["Nz"]' in getter_4d
+ assert "stride_4 = 4" in getter_4d
+
+
+def test_unsupported_accessor_rank_is_rejected_before_generation():
+ targets = {
+ "example": [AccessorTarget("values", ("N1", "N2", "N3", "N4", "N5"), False)]
+ }
+
+ with pytest.raises(ValueError, match="one through four dimensions"):
+ validate_accessor_targets(targets)
+
+
+def test_generated_integer_getter_returns_int_and_bulk_getter_returns_view():
+ cell = np.zeros(
+ 1,
+ dtype=[("surface_IDs_offset", np.int64), ("N_surface", np.int64)],
+ )[0]
+ cell["N_surface"] = 2
+ data = np.array([3.0, 7.0])
+
+ surface_ID = get_cell.surface_IDs(1, cell, data)
+ surface_IDs = get_cell.surface_IDs_all(cell, data)
+
+ assert isinstance(surface_ID, (int, np.integer))
+ assert surface_ID == 7
+ assert surface_IDs.dtype == np.float64
+ assert np.shares_memory(surface_IDs, data)
diff --git a/test/unit/test_object_compilation.py b/test/unit/test_object_compilation.py
new file mode 100644
index 000000000..a0e9772be
--- /dev/null
+++ b/test/unit/test_object_compilation.py
@@ -0,0 +1,321 @@
+import numpy as np
+import pytest
+
+import mcdc
+
+from mcdc.object_.base import MCDCBase, MCDCObject
+from mcdc.object_.data import DataPolynomial
+from mcdc.object_.transport_model_data import NeutronMultigroupData
+from mcdc.object_.technique import Technique
+from mcdc.object_.nuclide import Nuclide
+from mcdc.object_.universe import Universe
+
+
+class ObjectOwner(Universe):
+ child: DataPolynomial
+ children: list[DataPolynomial]
+ ignored: DataPolynomial
+
+ non_numba = ["ignored"]
+
+ def __init__(self, child, children, ignored):
+ super().__init__()
+ self.child = child
+ self.children = children
+ self.ignored = ignored
+
+
+class EmbeddedConfiguration(MCDCBase):
+ label = "embedded_configuration"
+
+ def __init__(self):
+ self.member = None
+
+
+def test_simulation_reserves_zero_group_mg_as_id_zero():
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+
+ assert isinstance(simulation.technique, Technique)
+
+ simulation.compile()
+
+ assert len(simulation.neutron_multigroup_data) == 1
+ assert isinstance(simulation.neutron_multigroup_data[0], NeutronMultigroupData)
+ assert simulation.neutron_multigroup_data[0].ID == 0
+ assert simulation.neutron_multigroup_data[0].G == 0
+ assert simulation.neutron_multigroup_data[0].compile_ID == simulation.compile_ID
+
+
+def test_simulation_derives_hybrid_for_local_multigroup_grids():
+ material_a = mcdc.Material.multigroup(capture=[0.1], energy_grid=[1.0, 2.0])
+ material_b = mcdc.Material.multigroup(capture=[0.2], energy_grid=[2.0, 3.0])
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell(fill=material_a), mcdc.Cell(fill=material_b)])
+
+ simulation.compile()
+
+ assert simulation.technique.neutron_multigroup.hybrid
+
+
+@pytest.mark.parametrize("explicit_grid", [False, True])
+def test_simulation_derives_standard_multigroup_for_shared_grid(
+ explicit_grid, prepare_simulation
+):
+ kwargs = {"energy_grid": [1.0, 2.0]} if explicit_grid else {}
+ material_a = mcdc.Material.multigroup(capture=[0.1], **kwargs)
+ material_b = mcdc.Material.multigroup(capture=[0.2], **kwargs)
+
+ simulation_container, _ = prepare_simulation(
+ cells=[mcdc.Cell(fill=material_a), mcdc.Cell(fill=material_b)]
+ )
+
+ simulation = simulation_container[0]
+ assert not simulation["technique"]["neutron_multigroup"]["hybrid"]
+
+
+def test_hybrid_multigroup_requires_explicit_energy_grids(capsys):
+ material_default = mcdc.Material.multigroup(capture=[0.1])
+ material_explicit = mcdc.Material.multigroup(capture=[0.2], energy_grid=[2.0, 3.0])
+ simulation = mcdc.Simulation()
+ simulation.set_model(
+ [mcdc.Cell(fill=material_default), mcdc.Cell(fill=material_explicit)]
+ )
+
+ with pytest.raises(SystemExit):
+ simulation.compile()
+
+ assert "requires an explicit energy_grid" in capsys.readouterr().out
+
+
+@pytest.mark.parametrize(
+ "source",
+ [
+ mcdc.Source(energy=1),
+ mcdc.Source(energy=1.0),
+ mcdc.Source(discrete_energy=([0, 1], [0.25, 0.75])),
+ ],
+)
+def test_standard_multigroup_accepts_integer_group_coordinates(source):
+ material = mcdc.Material.multigroup(capture=[0.1, 0.2])
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell(fill=material)])
+ simulation.set_sources([source])
+
+ simulation.compile()
+
+ assert not simulation.technique.neutron_multigroup.hybrid
+
+
+@pytest.mark.parametrize(
+ "source, expected_message",
+ [
+ (
+ mcdc.Source(energy=0.5),
+ "must be finite, integer-valued group coordinates",
+ ),
+ (
+ mcdc.Source(discrete_energy=([0, 1.5], [0.25, 0.75])),
+ "must be finite, integer-valued group coordinates",
+ ),
+ (
+ mcdc.Source(energy=np.nan),
+ "must be finite, integer-valued group coordinates",
+ ),
+ (
+ mcdc.Source(energy=-1),
+ "must satisfy 0 <= energy < G",
+ ),
+ (
+ mcdc.Source(energy=2),
+ "must satisfy 0 <= energy < G",
+ ),
+ (
+ mcdc.Source(energy=([0.0, 1.0], [1.0, 1.0])),
+ "requires neutron sources to use a scalar energy or discrete_energy",
+ ),
+ ],
+)
+def test_standard_multigroup_rejects_invalid_source_energy(
+ source,
+ expected_message,
+ capsys,
+):
+ material = mcdc.Material.multigroup(capture=[0.1, 0.2])
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell(fill=material)])
+ simulation.set_sources([source])
+
+ with pytest.raises(SystemExit):
+ simulation.compile()
+
+ assert expected_message in capsys.readouterr().out
+
+
+def test_standard_neutron_multigroup_does_not_validate_electron_source_energy():
+ material = mcdc.Material.multigroup(capture=[0.1, 0.2])
+ source = mcdc.Source(
+ particle_type="electron",
+ energy=([1.0e3, 2.0e3], [1.0, 1.0]),
+ )
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell(fill=material)])
+ simulation.set_sources([source])
+
+ simulation.compile()
+
+ assert not simulation.technique.neutron_multigroup.hybrid
+
+
+def test_native_only_simulation_remains_hybrid(monkeypatch):
+ # Isolate mode finalization from native data-library loading.
+ def compile_nuclide(nuclide, simulation):
+ nuclide.fissionable = False
+ return MCDCObject._compile_into_simulation(nuclide, simulation)
+
+ monkeypatch.setattr(Nuclide, "_compile_into_simulation", compile_nuclide)
+ monkeypatch.setattr(Nuclide, "set_neutron_data", lambda self, simulation: None)
+ material = mcdc.Material(nuclide_composition={"H1": 0.1})
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell(fill=material)])
+
+ simulation.compile()
+
+ assert simulation.technique.neutron_multigroup.hybrid
+
+
+def test_local_multigroup_grids_pack_hybrid(prepare_simulation):
+ material_a = mcdc.Material.multigroup(capture=[0.1], energy_grid=[1.0, 2.0])
+ material_b = mcdc.Material.multigroup(capture=[0.2], energy_grid=[2.0, 3.0])
+
+ simulation_container, _ = prepare_simulation(
+ cells=[mcdc.Cell(fill=material_a), mcdc.Cell(fill=material_b)]
+ )
+
+ simulation = simulation_container[0]
+ assert simulation["technique"]["neutron_multigroup"]["hybrid"]
+
+
+def test_mcdc_object_compiles_object_members_and_lists():
+ child = DataPolynomial(np.array([1.0]))
+ children = [
+ DataPolynomial(np.array([2.0])),
+ DataPolynomial(np.array([3.0])),
+ ]
+ ignored = DataPolynomial(np.array([4.0]))
+ simulation = mcdc.Simulation()
+ simulation.root_universe = ObjectOwner(child, children, ignored)
+ simulation.root_universe.cells = [mcdc.Cell()]
+
+ simulation.compile()
+
+ assert simulation.data[1:] == [child, *children]
+ assert ignored.compile_ID == 0
+
+
+def test_simulation_compiles_objects_owned_by_embedded_configuration():
+ mesh = mcdc.MeshUniform()
+ weight_windows = np.ones((1, 1, 1, 1, 3))
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.technique.weight_windows(weight_windows, mesh=mesh)
+
+ simulation.compile()
+
+ assert simulation.meshes == [mesh]
+ assert simulation.technique.compile_ID == simulation.compile_ID
+ assert simulation.technique.weight_windows.compile_ID == simulation.compile_ID
+
+
+def test_embedded_compile_id_prevents_cycles_and_supports_recompilation():
+ configuration_a = EmbeddedConfiguration()
+ configuration_b = EmbeddedConfiguration()
+ configuration_a.member = configuration_b
+ configuration_b.member = configuration_a
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.configuration = configuration_a
+
+ simulation.compile()
+ first_compile_ID = simulation.compile_ID
+
+ assert configuration_a.compile_ID == first_compile_ID
+ assert configuration_b.compile_ID == first_compile_ID
+
+ simulation.compile()
+
+ assert simulation.compile_ID != first_compile_ID
+ assert configuration_a.compile_ID == simulation.compile_ID
+ assert configuration_b.compile_ID == simulation.compile_ID
+
+
+def test_material_canonicalizes_composition_before_member_compilation(monkeypatch):
+ # Replace library loading with the minimal state needed for this unit test
+ def compile_nuclide(nuclide, simulation):
+ nuclide.fissionable = nuclide.name == "U235"
+ return MCDCObject._compile_into_simulation(nuclide, simulation)
+
+ monkeypatch.setattr(Nuclide, "_compile_into_simulation", compile_nuclide)
+
+ material_a = mcdc.Material(nuclide_composition={"U235": 1.0})
+ material_b = mcdc.Material(nuclide_composition={"U235": 2.0})
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.compile()
+
+ material_a._compile_into_simulation(simulation)
+ material_b._compile_into_simulation(simulation)
+
+ assert len(simulation.nuclides) == 1
+ assert material_a.nuclides == simulation.nuclides
+ assert material_b.nuclides == simulation.nuclides
+ assert material_a.fissionable
+ assert material_b.fissionable
+
+
+def test_simulation_compilation_finalizes_model_wide_state():
+ source_a = mcdc.Source(probability=1.0)
+ source_b = mcdc.Source(probability=3.0)
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.set_sources([source_a, source_b])
+ simulation.settings.neutron_eigenvalue_mode = True
+ simulation.settings.N_inactive = 2
+ simulation.settings.N_cycle = 4
+ simulation.settings.k_init = 1.25
+
+ simulation.compile()
+
+ assert np.allclose([source_a.probability, source_b.probability], [0.25, 0.75])
+ assert simulation.k_eff == 1.25
+ assert not simulation.cycle_active
+ assert simulation.k_cycle.shape == (4,)
+ assert simulation.gyration_radius.shape == (4,)
+
+
+def test_simulation_compilation_sets_particle_bank_capacities():
+ simulation = mcdc.Simulation()
+ simulation.set_model([mcdc.Cell()])
+ simulation.settings.N_particle = 100
+ simulation.settings.N_census = 2
+ simulation.settings.active_bank_buffer = 11
+ simulation.settings.census_bank_buffer_ratio = 2.0
+ simulation.settings.source_bank_buffer_ratio = 3.0
+ simulation.settings.future_bank_buffer_ratio = 4.0
+
+ simulation.compile()
+
+ N_work = int(np.ceil(100 / simulation.mpi_size))
+ assert simulation.bank_active.size[0] == 11
+ assert simulation.bank_census.size[0] == 2 * N_work
+ assert simulation.bank_source.size[0] == 3 * N_work
+ assert simulation.bank_future.size[0] == 4 * N_work
+
+
+def test_simulation_rejects_an_empty_root_universe(capsys):
+ simulation = mcdc.Simulation()
+
+ with pytest.raises(SystemExit):
+ simulation.compile()
+
+ assert "root universe is empty" in capsys.readouterr().out
diff --git a/test/unit/test_particle.py b/test/unit/test_particle.py
new file mode 100644
index 000000000..438075c9f
--- /dev/null
+++ b/test/unit/test_particle.py
@@ -0,0 +1,30 @@
+import numpy as np
+
+import mcdc.numba_types as type_
+from mcdc.transport.particle import copy
+
+
+def test_copy_particle_data_to_active_particle():
+ source = np.zeros(1, dtype=type_.particle_data)
+ target = np.zeros(1, dtype=type_.particle)
+
+ values = {
+ "x": 1.0,
+ "y": 2.0,
+ "z": 3.0,
+ "t": 4.0,
+ "ux": 0.1,
+ "uy": 0.2,
+ "uz": 0.3,
+ "E": 8.0,
+ "w": 9.0,
+ "particle_type": 10,
+ "rng_seed": 11,
+ }
+ for field, value in values.items():
+ source[0][field] = value
+
+ copy(target, source)
+
+ for field, value in values.items():
+ assert target[0][field] == value
diff --git a/test/unit/test_print.py b/test/unit/test_print.py
new file mode 100644
index 000000000..3b50de23d
--- /dev/null
+++ b/test/unit/test_print.py
@@ -0,0 +1,72 @@
+import numpy as np
+import pytest
+
+import mcdc.print_ as print_module
+
+
+def test_print_1d_array():
+ assert print_module.print_1d_array(np.array([])) == "(size=0): []"
+ assert print_module.print_1d_array(np.array([1.0, 2.0])) == "(size=2): [1, 2]"
+ assert print_module.print_1d_array(np.arange(6.0)) == "(size=6): [0, 1, ..., 4, 5]"
+
+
+def test_print_bank_uses_runtime_particle_data(capsys):
+ bank = {
+ "size": np.array([1]),
+ "tag": "source",
+ "particle_data": np.array([1.0, 2.0]),
+ }
+
+ print_module.print_bank(bank, show_content=True)
+
+ output = capsys.readouterr().out
+ assert "size : 1 of 2" in output
+ assert "1.0" in output
+
+
+@pytest.mark.parametrize(
+ ("duration", "expected"),
+ [
+ (2.0, "2.00 seconds"),
+ (120.0, "2.00 minutes"),
+ (7_200.0, "2.00 hours"),
+ (172_800.0, "2.00 days"),
+ ],
+)
+def test_print_time(duration, expected, capsys):
+ print_module.print_time("Stage", duration, 25.0)
+
+ assert capsys.readouterr().out == f" Stage | {expected} (25.0%)\n"
+
+
+def test_print_runtime_handles_zero_total(monkeypatch, capsys):
+ monkeypatch.setattr(print_module, "_IS_MASTER", True)
+ simulation = {
+ "runtime_total": 0.0,
+ "runtime_preparation": 0.0,
+ "runtime_simulation": 0.0,
+ "runtime_output": 0.0,
+ }
+
+ print_module.print_runtime(simulation)
+
+ output = capsys.readouterr().out
+ assert "Runtime report:" in output
+ assert "Preparation | 0.00 seconds (0.0%)" in output
+
+
+def test_master_only_message(monkeypatch, capsys):
+ monkeypatch.setattr(print_module, "_IS_MASTER", False)
+
+ print_module.print_msg("hidden")
+ print_module.print_warning("hidden")
+
+ assert capsys.readouterr().out == ""
+
+
+def test_print_error_exits_unsuccessfully(capsys):
+ with pytest.raises(SystemExit) as error:
+ print_module.print_error("invalid model")
+
+ assert error.value.code == 1
+ assert "[ERROR]: invalid model" in capsys.readouterr().out
diff --git a/test/unit/test_source.py b/test/unit/test_source.py
new file mode 100644
index 000000000..c8f174ef4
--- /dev/null
+++ b/test/unit/test_source.py
@@ -0,0 +1,202 @@
+import numpy as np
+import pytest
+
+import mcdc
+import mcdc.numba_types as type_
+from mcdc.transport.source import source_particle
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y", "z"])
+def test_position_and_box_bounds_are_mutually_exclusive(coordinate, capsys):
+ with pytest.raises(SystemExit):
+ mcdc.Source(position=[0.0, 0.0, 0.0], **{coordinate: [-1.0, 1.0]})
+
+ assert "Cannot specify position together with x, y, or z" in capsys.readouterr().out
+
+
+@pytest.mark.parametrize(
+ "kwargs",
+ [
+ {"isotropic": True, "direction": [0.0, 0.0, 1.0]},
+ {"isotropic": True, "white_direction": [0.0, 0.0, 1.0]},
+ {
+ "direction": [0.0, 0.0, 1.0],
+ "white_direction": [0.0, 0.0, 1.0],
+ },
+ ],
+)
+def test_direction_representations_are_mutually_exclusive(kwargs, capsys):
+ with pytest.raises(SystemExit):
+ mcdc.Source(**kwargs)
+
+ assert "Cannot specify more than one" in capsys.readouterr().out
+
+
+@pytest.mark.parametrize(
+ "kwargs",
+ [
+ {"polar_cosine": [0.8, 1.0]},
+ {"azimuthal": [0.0, np.pi]},
+ {
+ "white_direction": [0.0, 0.0, 1.0],
+ "polar_cosine": [0.8, 1.0],
+ },
+ ],
+)
+def test_angular_bounds_require_direction(kwargs, capsys):
+ with pytest.raises(SystemExit):
+ mcdc.Source(**kwargs)
+
+ assert "polar_cosine and azimuthal require direction" in capsys.readouterr().out
+
+
+def test_direction_accepts_angular_bounds():
+ source = mcdc.Source(
+ direction=[0.0, 0.0, 1.0],
+ polar_cosine=[0.8, 1.0],
+ azimuthal=[0.0, np.pi],
+ )
+
+ assert not source.isotropic_direction
+ assert not source.mono_direction
+ np.testing.assert_array_equal(source.polar_cosine, [0.8, 1.0])
+ np.testing.assert_array_equal(source.azimuthal, [0.0, np.pi])
+
+
+@pytest.mark.parametrize(
+ "energy",
+ [
+ 10_000,
+ 10_000.0,
+ np.int64(10_000),
+ np.float64(10_000.0),
+ ],
+)
+def test_scalar_energy(energy):
+ source = mcdc.Source(energy=energy)
+
+ assert source.mono_energetic
+ assert isinstance(source.energy, float)
+ assert source.energy == 10_000.0
+
+
+@pytest.mark.parametrize(
+ "energy",
+ [
+ [[9_999.0, 10_001.0], [0.5, 0.5]],
+ ([9_999.0, 10_001.0], [0.5, 0.5]),
+ np.array([[9_999.0, 10_001.0], [0.5, 0.5]]),
+ ],
+)
+def test_energy_distribution(energy):
+ source = mcdc.Source(energy=energy)
+
+ assert not source.mono_energetic
+ assert not source.discrete_energy
+ np.testing.assert_array_equal(source.energy_pdf.pdf.x, [9_999.0, 10_001.0])
+
+
+@pytest.mark.parametrize(
+ "discrete_energy",
+ [
+ [[9_999.0, 10_001.0], [0.25, 0.75]],
+ ([9_999.0, 10_001.0], [0.25, 0.75]),
+ np.array([[9_999.0, 10_001.0], [0.25, 0.75]]),
+ ],
+)
+def test_discrete_energy_distribution(discrete_energy):
+ source = mcdc.Source(discrete_energy=discrete_energy)
+
+ assert not source.mono_energetic
+ assert source.discrete_energy
+ np.testing.assert_array_equal(source.energy_pmf.value, [9_999.0, 10_001.0])
+ assert "Energy: PMF" in repr(source)
+
+
+def test_energy_and_discrete_energy_are_mutually_exclusive(capsys):
+ with pytest.raises(SystemExit):
+ mcdc.Source(
+ energy=10_000.0,
+ discrete_energy=[[9_999.0, 10_001.0], [0.25, 0.75]],
+ )
+
+ assert "Cannot specify both energy and discrete_energy" in capsys.readouterr().out
+
+
+def test_transport_source_sets_mono_energy(prepare_simulation):
+ source = mcdc.Source(
+ position=[0.0, 0.0, 0.0],
+ direction=[0.0, 0.0, 1.0],
+ energy=10_000.0,
+ )
+ simulation_container, data = prepare_simulation(sources=[source])
+ particle_container = np.zeros(1, dtype=type_.particle)
+
+ source_particle(
+ particle_container,
+ np.uint64(1),
+ simulation_container[0],
+ data,
+ )
+
+ assert particle_container[0]["E"] == 10_000.0
+
+
+def test_transport_source_samples_discrete_energy(prepare_simulation):
+ source = mcdc.Source(
+ position=[0.0, 0.0, 0.0],
+ direction=[0.0, 0.0, 1.0],
+ discrete_energy=([10_001.0], [1.0]),
+ )
+ simulation_container, data = prepare_simulation(sources=[source])
+ particle_container = np.zeros(1, dtype=type_.particle)
+
+ source_particle(
+ particle_container,
+ np.uint64(1),
+ simulation_container[0],
+ data,
+ )
+
+ assert particle_container[0]["E"] == 10_001.0
+
+
+@pytest.mark.parametrize("time", [2, 2.0, np.int64(2), np.float64(2.0)])
+def test_scalar_time(time):
+ source = mcdc.Source(time=time)
+
+ assert source.discrete_time
+ assert source.time == 2.0
+
+
+@pytest.mark.parametrize(
+ "time",
+ [
+ [1.0, 2.0],
+ (1.0, 2.0),
+ np.array([1.0, 2.0]),
+ ],
+)
+def test_time_interval(time):
+ source = mcdc.Source(time=time)
+
+ assert not source.discrete_time
+ np.testing.assert_array_equal(source.time_range, [1.0, 2.0])
+
+
+@pytest.mark.parametrize(
+ "kwargs, expected_message",
+ [
+ ({"energy": [1.0, 2.0, 3.0]}, "Energy distribution must have shape (2, N)"),
+ (
+ {"discrete_energy": [1.0, 2.0, 3.0]},
+ "Discrete energy distribution must have shape (2, N)",
+ ),
+ ({"time": [1.0, 2.0, 3.0]}, "Source time interval must have shape (2,)"),
+ ],
+)
+def test_invalid_distribution_shape(kwargs, expected_message, capsys):
+ with pytest.raises(SystemExit):
+ mcdc.Source(**kwargs)
+
+ assert expected_message in capsys.readouterr().out
diff --git a/test/unit/test_transport_model_data.py b/test/unit/test_transport_model_data.py
new file mode 100644
index 000000000..b96ff1538
--- /dev/null
+++ b/test/unit/test_transport_model_data.py
@@ -0,0 +1,327 @@
+import numpy as np
+import pytest
+
+import mcdc
+from mcdc.constant import (
+ NEUTRON_MULTIGROUP_ENERGY_MIDPOINT,
+ NEUTRON_MULTIGROUP_ENERGY_MIDPOINT_LOG,
+ NEUTRON_MULTIGROUP_ENERGY_UNIFORM,
+ NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG,
+)
+from mcdc.mcdc_get import neutron_multigroup_data as get_neutron_multigroup_data
+from mcdc.object_.base import MCDCObject, MCDCPolymorphic
+from mcdc.object_.transport_model_data import NeutronMultigroupData
+
+
+def test_neutron_multigroup_is_public():
+ assert mcdc.NeutronMultigroupData is NeutronMultigroupData
+ assert not hasattr(mcdc, "NeutronMultigroup")
+ assert not hasattr(mcdc, "MGXS")
+ assert issubclass(NeutronMultigroupData, MCDCObject)
+ assert not issubclass(NeutronMultigroupData, MCDCPolymorphic)
+ assert NeutronMultigroupData.label == "neutron_multigroup_data"
+
+
+def test_zero_group_placeholder():
+ neutron_multigroup = NeutronMultigroupData()
+
+ assert neutron_multigroup.G == 0
+ assert neutron_multigroup.J == 0
+ assert not neutron_multigroup.fissionable
+ assert (
+ neutron_multigroup.energy_representation == NEUTRON_MULTIGROUP_ENERGY_MIDPOINT
+ )
+
+ np.testing.assert_array_equal(neutron_multigroup.energy_grid, [0.0])
+ np.testing.assert_array_equal(neutron_multigroup.speed, [])
+ np.testing.assert_array_equal(neutron_multigroup.decay_rate, [])
+ np.testing.assert_array_equal(neutron_multigroup.capture, [])
+ np.testing.assert_array_equal(neutron_multigroup.scatter, [])
+ np.testing.assert_array_equal(neutron_multigroup.fission, [])
+ np.testing.assert_array_equal(neutron_multigroup.total, [])
+ np.testing.assert_array_equal(neutron_multigroup.nu_s, [])
+ np.testing.assert_array_equal(neutron_multigroup.nu_p, [])
+ np.testing.assert_array_equal(neutron_multigroup.nu_d, np.empty((0, 0)))
+ np.testing.assert_array_equal(neutron_multigroup.nu_d_total, [])
+ np.testing.assert_array_equal(neutron_multigroup.nu_f, [])
+ np.testing.assert_array_equal(neutron_multigroup.chi_s, np.empty((0, 0)))
+ np.testing.assert_array_equal(neutron_multigroup.chi_p, np.empty((0, 0)))
+ np.testing.assert_array_equal(neutron_multigroup.chi_d, np.empty((0, 0)))
+
+
+def test_capture_only_defaults():
+ neutron_multigroup = NeutronMultigroupData(capture=[0.1, 0.2])
+
+ assert neutron_multigroup.G == 2
+ assert neutron_multigroup.J == 0
+ assert not neutron_multigroup.fissionable
+
+ np.testing.assert_array_equal(neutron_multigroup.energy_grid, [0.0, 0.0, 0.0])
+ np.testing.assert_array_equal(neutron_multigroup.speed, [1.0, 1.0])
+ np.testing.assert_array_equal(neutron_multigroup.capture, [0.1, 0.2])
+ np.testing.assert_array_equal(neutron_multigroup.scatter, [0.0, 0.0])
+ np.testing.assert_array_equal(neutron_multigroup.fission, [0.0, 0.0])
+ np.testing.assert_array_equal(neutron_multigroup.total, [0.1, 0.2])
+ np.testing.assert_array_equal(neutron_multigroup.nu_s, [1.0, 1.0])
+ np.testing.assert_array_equal(neutron_multigroup.nu_f, [0.0, 0.0])
+ assert neutron_multigroup.capture.dtype == np.float64
+
+
+def test_documented_two_group_example():
+ neutron_multigroup = NeutronMultigroupData(
+ capture=[0.1, 0.2],
+ scatter=[
+ [1.0, 2.0],
+ [3.0, 0.0],
+ ],
+ nu_s=[1.1, 1.2],
+ energy_grid=[1.0e-5, 1.0, 20.0e6],
+ )
+
+ assert neutron_multigroup.G == 2
+ np.testing.assert_array_equal(neutron_multigroup.capture, [0.1, 0.2])
+ np.testing.assert_array_equal(neutron_multigroup.scatter, [4.0, 2.0])
+ np.testing.assert_allclose(
+ neutron_multigroup.chi_s,
+ [
+ [0.25, 0.75],
+ [1.0, 0.0],
+ ],
+ )
+ np.testing.assert_array_equal(neutron_multigroup.total, [4.1, 2.2])
+ np.testing.assert_array_equal(neutron_multigroup.nu_s, [1.1, 1.2])
+ np.testing.assert_array_equal(neutron_multigroup.energy_grid, [1.0e-5, 1.0, 20.0e6])
+
+
+def test_prompt_fission_spectrum_is_shared_and_normalized():
+ neutron_multigroup = NeutronMultigroupData(
+ fission=[0.2, 0.3],
+ nu_p=[2.4, 2.5],
+ chi_p=[1.0, 3.0],
+ )
+
+ assert neutron_multigroup.fissionable
+ np.testing.assert_array_equal(neutron_multigroup.nu_f, [2.4, 2.5])
+ np.testing.assert_allclose(
+ neutron_multigroup.chi_p,
+ [
+ [0.25, 0.75],
+ [0.25, 0.75],
+ ],
+ )
+
+
+def test_documented_multiple_precursor_group_example():
+ neutron_multigroup = NeutronMultigroupData(
+ fission=[0.2, 0.3],
+ nu_d=[
+ [0.1, 0.2],
+ [0.3, 0.4],
+ ],
+ chi_d=[
+ [1.0, 3.0],
+ [3.0, 1.0],
+ ],
+ decay_rate=[0.01, 0.02],
+ )
+
+ assert neutron_multigroup.J == 2
+ np.testing.assert_array_equal(
+ neutron_multigroup.nu_d,
+ [
+ [0.1, 0.3],
+ [0.2, 0.4],
+ ],
+ )
+ np.testing.assert_allclose(neutron_multigroup.nu_d_total, [0.4, 0.6])
+ np.testing.assert_allclose(neutron_multigroup.nu_f, [0.4, 0.6])
+ np.testing.assert_allclose(
+ neutron_multigroup.chi_d,
+ [
+ [0.25, 0.75],
+ [0.75, 0.25],
+ ],
+ )
+ np.testing.assert_array_equal(neutron_multigroup.decay_rate, [0.01, 0.02])
+
+
+def test_one_group_fission_spectra_default_to_one():
+ neutron_multigroup = NeutronMultigroupData(
+ fission=[0.2],
+ nu_p=[2.4],
+ nu_d=[[0.1], [0.2]],
+ )
+
+ np.testing.assert_array_equal(neutron_multigroup.chi_p, [[1.0]])
+ np.testing.assert_array_equal(neutron_multigroup.chi_d, [[1.0], [1.0]])
+ np.testing.assert_array_equal(neutron_multigroup.nu_f, [2.7])
+ np.testing.assert_array_equal(neutron_multigroup.decay_rate, [np.inf, np.inf])
+
+
+def test_standalone_mg_registration_and_packing(prepare_simulation):
+ neutron_multigroup = NeutronMultigroupData(
+ capture=[0.1, 0.2],
+ scatter=[[1.0, 2.0], [3.0, 0.0]],
+ energy_grid=[1.0e-5, 1.0, 20.0e6],
+ )
+
+ simulation_container, data = prepare_simulation(
+ cells=[mcdc.Cell()], objects=[neutron_multigroup]
+ )
+ simulation = simulation_container[0]
+ reserved = simulation["neutron_multigroup_data"][0]
+ packed = simulation["neutron_multigroup_data"][1]
+
+ assert simulation["N_neutron_multigroup_data"] == 2
+ assert reserved["ID"] == 0
+ assert reserved["G"] == 0
+ assert neutron_multigroup.ID == 1
+ assert packed["ID"] == 1
+ assert packed["G"] == 2
+
+ np.testing.assert_array_equal(
+ get_neutron_multigroup_data.energy_grid_all(reserved, data), [0.0]
+ )
+ np.testing.assert_array_equal(
+ get_neutron_multigroup_data.energy_grid_all(packed, data),
+ [1.0e-5, 1.0, 20.0e6],
+ )
+ np.testing.assert_array_equal(
+ get_neutron_multigroup_data.capture_all(packed, data), [0.1, 0.2]
+ )
+ np.testing.assert_array_equal(
+ get_neutron_multigroup_data.scatter_all(packed, data), [4.0, 2.0]
+ )
+ np.testing.assert_allclose(
+ get_neutron_multigroup_data.chi_s_vector(0, packed, data),
+ [0.25, 0.75],
+ )
+
+
+@pytest.mark.parametrize(
+ "policy, expected",
+ [
+ ("midpoint", NEUTRON_MULTIGROUP_ENERGY_MIDPOINT),
+ ("log_midpoint", NEUTRON_MULTIGROUP_ENERGY_MIDPOINT_LOG),
+ ("uniform", NEUTRON_MULTIGROUP_ENERGY_UNIFORM),
+ ("log_uniform", NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG),
+ (NEUTRON_MULTIGROUP_ENERGY_MIDPOINT, NEUTRON_MULTIGROUP_ENERGY_MIDPOINT),
+ (
+ np.int64(NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG),
+ NEUTRON_MULTIGROUP_ENERGY_UNIFORM_LOG,
+ ),
+ ],
+)
+def test_energy_grid_and_representation(policy, expected):
+ neutron_multigroup = NeutronMultigroupData(
+ capture=[0.1, 0.2],
+ energy_grid=[1.0e-5, 1.0, 20.0e6],
+ energy_representation=policy,
+ )
+
+ assert neutron_multigroup.energy_representation == expected
+ np.testing.assert_array_equal(neutron_multigroup.energy_grid, [1.0e-5, 1.0, 20.0e6])
+
+
+@pytest.mark.parametrize(
+ "kwargs, expected_message",
+ [
+ (
+ {"capture": [0.1, 0.2], "energy_grid": [1.0, 2.0]},
+ "energy_grid must have shape (3,)",
+ ),
+ (
+ {"capture": [0.1, 0.2], "energy_grid": [1.0, 2.0, 2.0]},
+ "energy grid must be strictly increasing",
+ ),
+ (
+ {
+ "capture": [0.1],
+ "energy_grid": [0.0, 1.0],
+ "energy_representation": "log_midpoint",
+ },
+ "logarithmic energy representation requires positive",
+ ),
+ (
+ {"capture": [0.1], "energy_representation": "average"},
+ "Unknown NeutronMultigroupData energy representation",
+ ),
+ (
+ {"capture": [0.1], "energy_representation": "uniform"},
+ "requires an explicit energy_grid",
+ ),
+ (
+ {"fission": [0.1]},
+ "NeutronMultigroupData fission data requires nu_p or nu_d",
+ ),
+ (
+ {"nu_p": [2.4]},
+ "nu_p must have shape (0,)",
+ ),
+ (
+ {"fission": [0.1, 0.2], "nu_p": [2.4, 2.5]},
+ "NeutronMultigroupData with nu_p and G > 1 requires chi_p",
+ ),
+ (
+ {"fission": [0.1, 0.2], "nu_d": [[0.1, 0.2]]},
+ "NeutronMultigroupData with nu_d and G > 1 requires chi_d",
+ ),
+ (
+ {"capture": [-0.1]},
+ "capture entries must be finite and nonnegative",
+ ),
+ (
+ {"capture": [0.1], "speed": [0.0]},
+ "speed entries must be finite and positive",
+ ),
+ (
+ {
+ "fission": [0.1, 0.2],
+ "nu_p": [2.4, 2.5],
+ "chi_p": [0.0, 0.0],
+ },
+ "chi_p spectrum 0 must have positive mass",
+ ),
+ ],
+)
+def test_invalid_inputs_are_rejected(kwargs, expected_message, capsys):
+ with pytest.raises(SystemExit):
+ NeutronMultigroupData(**kwargs)
+
+ assert expected_message in capsys.readouterr().out
+
+
+@pytest.mark.parametrize(
+ "kwargs, expected_message",
+ [
+ (
+ {"capture": [0.1, 0.2], "fission": [0.3], "nu_p": [2.4, 2.5]},
+ "fission must have shape (2,)",
+ ),
+ (
+ {"scatter": [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]},
+ "scatter must have shape (2, 2)",
+ ),
+ (
+ {"capture": [0.1, 0.2], "speed": [1.0]},
+ "speed must have shape (2,)",
+ ),
+ (
+ {
+ "fission": [0.1, 0.2],
+ "nu_p": [2.4],
+ "chi_p": [0.5, 0.5],
+ },
+ "nu_p must have shape (2,)",
+ ),
+ (
+ {"fission": [0.1, 0.2], "nu_d": [[0.1]]},
+ "nu_d must have shape (J, G) with G = 2",
+ ),
+ ],
+)
+def test_inconsistent_shapes_are_rejected(kwargs, expected_message, capsys):
+ with pytest.raises(SystemExit):
+ NeutronMultigroupData(**kwargs)
+
+ assert expected_message in capsys.readouterr().out
diff --git a/test/unit/test_visualize.py b/test/unit/test_visualize.py
new file mode 100644
index 000000000..155bc711f
--- /dev/null
+++ b/test/unit/test_visualize.py
@@ -0,0 +1,30 @@
+import matplotlib
+import numpy as np
+
+import mcdc
+
+matplotlib.use("Agg")
+
+
+def test_visualize_model(tmp_path):
+ material = mcdc.Material.multigroup(capture=np.array([1.0]))
+ left = mcdc.Surface.PlaneZ(z=0.0, boundary_condition="vacuum")
+ right = mcdc.Surface.PlaneZ(z=1.0, boundary_condition="vacuum")
+ cell = mcdc.Cell(region=+left & -right, fill=material)
+
+ simulation = mcdc.Simulation()
+ simulation.set_model([cell])
+
+ output = tmp_path / "model"
+ simulation.visualize_model(
+ vis_plane="xz",
+ x=[-0.5, 0.5],
+ y=0.0,
+ z=[0.0, 1.0],
+ pixels=[2, 2],
+ colors=None,
+ time=[0.0],
+ save_as=output,
+ )
+
+ assert (tmp_path / "model.png").is_file()
diff --git a/test/unit/transport/test_multigroup.py b/test/unit/transport/test_multigroup.py
new file mode 100644
index 000000000..d4568b2b6
--- /dev/null
+++ b/test/unit/transport/test_multigroup.py
@@ -0,0 +1,157 @@
+import math
+
+import numpy as np
+import pytest
+
+import mcdc
+import mcdc.numba_types as type_
+import mcdc.transport.physics.neutron.multigroup as multigroup
+
+
+def _prepare_multigroup(prepare_simulation, **kwargs):
+ material = mcdc.Material.multigroup(
+ energy_grid=[1.0, 10.0, 100.0],
+ **kwargs,
+ )
+ simulation_container, data = prepare_simulation(cells=[mcdc.Cell(fill=material)])
+ simulation = simulation_container[0]
+ packed_material = simulation["materials"][0]
+ mgxs = simulation["neutron_multigroup_data"][
+ packed_material["neutron_multigroup_ID"]
+ ]
+ return simulation, mgxs, data
+
+
+@pytest.mark.parametrize(
+ "representation, expected",
+ [
+ ("midpoint", 55.0),
+ ("log_midpoint", math.sqrt(1000.0)),
+ ("uniform", None),
+ ("log_uniform", None),
+ ],
+)
+def test_group_energy_representation(representation, expected, prepare_simulation):
+ simulation, mgxs, data = _prepare_multigroup(
+ prepare_simulation,
+ capture=[1.0, 1.0],
+ energy_representation=representation,
+ )
+ simulation["technique"]["neutron_multigroup"]["hybrid"] = True
+
+ rng_state = np.zeros(1, dtype=type_.particle)
+ rng_state[0]["rng_seed"] = np.uint64(1)
+
+ energy = multigroup._get_group_energy(1, rng_state, mgxs, simulation, data)
+
+ if expected is None:
+ assert 10.0 <= energy < 100.0
+ else:
+ assert energy == pytest.approx(expected)
+
+
+def test_standard_multigroup_uses_group_coordinate_energy(prepare_simulation):
+ simulation, mgxs, data = _prepare_multigroup(
+ prepare_simulation,
+ capture=[1.0, 1.0],
+ )
+
+ rng_state = np.zeros(1, dtype=type_.particle)
+
+ energy = multigroup._get_group_energy(1, rng_state, mgxs, simulation, data)
+
+ assert energy == 1.0
+
+
+def test_hybrid_energy_groups_are_left_closed(prepare_simulation):
+ simulation, mgxs, data = _prepare_multigroup(
+ prepare_simulation,
+ capture=[1.0, 1.0],
+ )
+ simulation["technique"]["neutron_multigroup"]["hybrid"] = True
+
+ particle_container = np.zeros(1, dtype=type_.particle)
+ particle = particle_container[0]
+ particle["material_ID"] = 0
+
+ particle["E"] = 1.0
+ assert multigroup.applicable(particle_container, simulation, data)
+ assert multigroup._get_energy_group(particle["E"], mgxs, simulation, data) == 0
+
+ particle["E"] = 10.0
+ assert multigroup._get_energy_group(particle["E"], mgxs, simulation, data) == 1
+
+ particle["E"] = 100.0
+ assert not multigroup.applicable(particle_container, simulation, data)
+
+
+def _make_particle():
+ particle_container = np.zeros(1, dtype=type_.particle)
+ particle = particle_container[0]
+ particle["material_ID"] = 0
+ particle["alive"] = True
+ particle["E"] = 5.0
+ particle["w"] = 1.0
+ particle["uz"] = 1.0
+ particle["rng_seed"] = np.uint64(1)
+ return particle_container
+
+
+def _assert_fission_products(particle_container, simulation):
+ particle = particle_container[0]
+ assert particle["alive"]
+ assert particle["E"] == pytest.approx(55.0)
+
+ bank = simulation["bank_active"]
+ assert bank["size"][0] == 3
+ banked = bank["particle_data"][:3]
+ np.testing.assert_allclose(banked["E"], 55.0)
+
+
+def test_scattering_product_uses_multigroup_data(prepare_simulation):
+ simulation, _, data = _prepare_multigroup(
+ prepare_simulation,
+ scatter=[[0.0, 0.0], [1.0, 1.0]],
+ )
+ simulation["technique"]["neutron_multigroup"]["hybrid"] = True
+ particle_container = _make_particle()
+
+ multigroup.scattering(particle_container, simulation, data)
+
+ particle = particle_container[0]
+ assert particle["alive"]
+ assert particle["E"] == pytest.approx(55.0)
+
+
+def test_prompt_fission_products(prepare_simulation):
+ simulation, _, data = _prepare_multigroup(
+ prepare_simulation,
+ fission=[1.0, 1.0],
+ nu_p=[4.0, 4.0],
+ chi_p=[0.0, 1.0],
+ )
+ simulation["technique"]["neutron_multigroup"]["hybrid"] = True
+ particle_container = _make_particle()
+
+ multigroup.fission(particle_container, simulation, data)
+
+ _assert_fission_products(particle_container, simulation)
+ assert particle_container[0]["t"] == 0.0
+
+
+def test_delayed_fission_products(prepare_simulation):
+ simulation, _, data = _prepare_multigroup(
+ prepare_simulation,
+ fission=[1.0, 1.0],
+ nu_d=[[4.0, 4.0]],
+ chi_d=[[0.0], [1.0]],
+ decay_rate=[1.0],
+ )
+ simulation["technique"]["neutron_multigroup"]["hybrid"] = True
+ particle_container = _make_particle()
+
+ multigroup.fission(particle_container, simulation, data)
+
+ _assert_fission_products(particle_container, simulation)
+ assert particle_container[0]["t"] > 0.0
+ assert np.all(simulation["bank_active"]["particle_data"][:3]["t"] > 0.0)
diff --git a/tools/data_library_generator/electron/README.md b/tools/data_library_generator/electron/README.md
index 6a28bf7e2..f4f9d6b2c 100644
--- a/tools/data_library_generator/electron/README.md
+++ b/tools/data_library_generator/electron/README.md
@@ -100,5 +100,5 @@ only CDFs for EPRDATA14; sampling from the CDF is handled on the MC/DC side.
```
## See Also
-- [Continuous Energy Theory Guide](../../docs/source/theory/cont_energy.rst)
-- [Installation Guide — CE Library Configuration](../../docs/source/install.rst)
+- [Continuous Energy Theory Guide](../../../docs/source/theory/continuous_energy.rst)
+- [Installation — CE Library Configuration](../../../docs/source/user_guide/getting_started/installation.rst)
diff --git a/tools/data_library_generator/neutron/README.md b/tools/data_library_generator/neutron/README.md
index 6f133dd94..526fdacb2 100644
--- a/tools/data_library_generator/neutron/README.md
+++ b/tools/data_library_generator/neutron/README.md
@@ -76,5 +76,5 @@ For each ACE file in `$MCDC_ACELIB`, the generator:
## See Also
-- [Continuous Energy Theory Guide](../../docs/source/theory/cont_energy.rst)
-- [Installation Guide — CE Library Configuration](../../docs/source/install.rst)
+- [Continuous Energy Theory Guide](../../../docs/source/theory/continuous_energy.rst)
+- [Installation — CE Library Configuration](../../../docs/source/user_guide/getting_started/installation.rst)